4141import java .util .SortedSet ;
4242import java .util .TreeMap ;
4343import java .util .TreeSet ;
44+ import java .util .regex .Pattern ;
4445
4546@ Name ("classloader" )
4647@ Summary ("Show classloader info" )
5455 " classloader -a -c 327a647b\n " +
5556 " classloader -c 659e0bfd --load demo.MathGame\n " +
5657 " classloader -u # url statistics\n " +
58+ " classloader -c 659e0bfd --url-classes\n " +
59+ " classloader -c 659e0bfd --url-classes -d\n " +
60+ " classloader -c 659e0bfd --url-classes --jar spring-core --class org.springframework\n " +
5761 Constants .WIKI + Constants .WIKI_HOME + "classloader" )
5862public class ClassLoaderCommand extends AnnotatedCommand {
5963
6064 private static Logger logger = LoggerFactory .getLogger (ClassLoaderCommand .class );
65+ private static final int DEFAULT_URL_CLASSES_LIMIT = 100 ;
66+ private static final String UNKNOWN_CODE_SOURCE = "<unknown>" ;
6167 private boolean isTree = false ;
6268 private String hashCode ;
6369 private String classLoaderClass ;
@@ -68,6 +74,13 @@ public class ClassLoaderCommand extends AnnotatedCommand {
6874
6975 private boolean urlStat = false ;
7076
77+ private boolean urlClasses = false ;
78+ private boolean urlClassesDetail = false ;
79+ private boolean urlClassesRegEx = false ;
80+ private int urlClassesLimit = DEFAULT_URL_CLASSES_LIMIT ;
81+ private String jarFilter ;
82+ private String classFilter ;
83+
7184 private String loadClass = null ;
7285
7386 private volatile boolean isInterrupted = false ;
@@ -126,6 +139,42 @@ public void setUrlStat(boolean urlStat) {
126139 this .urlStat = urlStat ;
127140 }
128141
142+ @ Option (longName = "url-classes" , flag = true )
143+ @ Description ("Display relationship between jar(URL) and loaded classes in the specified ClassLoader" )
144+ public void setUrlClasses (boolean urlClasses ) {
145+ this .urlClasses = urlClasses ;
146+ }
147+
148+ @ Option (shortName = "d" , longName = "details" , flag = true )
149+ @ Description ("Display class list for each jar(URL), only works with --url-classes" )
150+ public void setUrlClassesDetail (boolean urlClassesDetail ) {
151+ this .urlClassesDetail = urlClassesDetail ;
152+ }
153+
154+ @ Option (shortName = "E" , longName = "regex" , flag = true )
155+ @ Description ("Enable regular expression to match for --jar/--class, only works with --url-classes" )
156+ public void setUrlClassesRegEx (boolean urlClassesRegEx ) {
157+ this .urlClassesRegEx = urlClassesRegEx ;
158+ }
159+
160+ @ Option (shortName = "n" , longName = "limit" )
161+ @ Description ("Maximum number of classes to display per jar(URL) in details mode (100 by default), only works with --url-classes -d" )
162+ public void setUrlClassesLimit (int urlClassesLimit ) {
163+ this .urlClassesLimit = urlClassesLimit ;
164+ }
165+
166+ @ Option (longName = "jar" )
167+ @ Description ("Filter jar(URL) by keyword (or regex with -E), only works with --url-classes" )
168+ public void setJarFilter (String jarFilter ) {
169+ this .jarFilter = jarFilter ;
170+ }
171+
172+ @ Option (longName = "class" )
173+ @ Description ("Filter classes by keyword/package (or regex with -E), only works with --url-classes" )
174+ public void setClassFilter (String classFilter ) {
175+ this .classFilter = StringUtils .normalizeClassName (classFilter );
176+ }
177+
129178 @ Override
130179 public void process (CommandProcess process ) {
131180 // ctrl-C support
@@ -143,6 +192,12 @@ public void process(CommandProcess process) {
143192 process .end ();
144193 return ;
145194 }
195+
196+ if (!urlClasses && (urlClassesDetail || urlClassesRegEx || jarFilter != null || classFilter != null
197+ || urlClassesLimit != DEFAULT_URL_CLASSES_LIMIT )) {
198+ process .end (-1 , "Options -d/-E/-n/--jar/--class only work with --url-classes." );
199+ return ;
200+ }
146201
147202 if (hashCode != null || classLoaderClass != null ) {
148203 classLoaderSpecified = true ;
@@ -174,6 +229,19 @@ public void process(CommandProcess process) {
174229 }
175230 }
176231
232+ if (urlClasses ) {
233+ if (!classLoaderSpecified ) {
234+ process .end (-1 , "Please specify classloader with '-c <classloader hash>' or '--classLoaderClass <classloader class name>' for --url-classes." );
235+ return ;
236+ }
237+ if (targetClassLoader == null ) {
238+ process .end (-1 , "Can not find classloader by hashcode: " + hashCode + "." );
239+ return ;
240+ }
241+ processUrlClasses (process , inst , targetClassLoader );
242+ return ;
243+ }
244+
177245 if (all ) {
178246 String hashCode = this .hashCode ;
179247 if (StringUtils .isBlank (hashCode ) && targetClassLoader != null ) {
@@ -407,6 +475,172 @@ private boolean checkInterrupted(CommandProcess process) {
407475 }
408476 }
409477
478+ private void processUrlClasses (CommandProcess process , Instrumentation inst , ClassLoader targetClassLoader ) {
479+ if (!urlClassesDetail && urlClassesLimit != DEFAULT_URL_CLASSES_LIMIT ) {
480+ process .end (-1 , "Option -n/--limit only works with --url-classes -d." );
481+ return ;
482+ }
483+ if (urlClassesDetail && urlClassesLimit <= 0 ) {
484+ process .end (-1 , "Option -n/--limit must be greater than 0." );
485+ return ;
486+ }
487+
488+ Pattern jarPattern = null ;
489+ Pattern classPattern = null ;
490+ if (urlClassesRegEx ) {
491+ try {
492+ if (jarFilter != null ) {
493+ jarPattern = Pattern .compile (jarFilter );
494+ }
495+ if (classFilter != null ) {
496+ classPattern = Pattern .compile (classFilter );
497+ }
498+ } catch (Throwable e ) {
499+ process .end (-1 , "Regex compile error: " + e .getMessage ());
500+ return ;
501+ }
502+ }
503+
504+ Map <String , UrlClassStatBuilder > statsMap = new HashMap <String , UrlClassStatBuilder >();
505+ Class <?>[] allLoadedClasses = inst .getAllLoadedClasses ();
506+ for (int i = 0 ; i < allLoadedClasses .length ; i ++) {
507+ if ((i & 0x3FFF ) == 0 && checkInterrupted (process )) {
508+ return ;
509+ }
510+ Class <?> clazz = allLoadedClasses [i ];
511+ if (clazz == null ) {
512+ continue ;
513+ }
514+ if (clazz .getClassLoader () != targetClassLoader ) {
515+ continue ;
516+ }
517+
518+ String url = codeSourceLocation (clazz );
519+ if (!matchJarFilter (url , jarPattern )) {
520+ continue ;
521+ }
522+
523+ UrlClassStatBuilder builder = statsMap .get (url );
524+ if (builder == null ) {
525+ builder = new UrlClassStatBuilder (url , classFilter != null , urlClassesDetail ? urlClassesLimit : 0 );
526+ statsMap .put (url , builder );
527+ }
528+ builder .increaseLoadedCount ();
529+
530+ if (classFilter != null ) {
531+ if (matchClassFilter (clazz .getName (), classPattern )) {
532+ builder .increaseMatchedCount ();
533+ builder .tryAddClass (clazz .getName ());
534+ }
535+ } else {
536+ builder .tryAddClass (clazz .getName ());
537+ }
538+ }
539+
540+ boolean hasClassFilter = classFilter != null ;
541+ List <UrlClassStat > stats = new ArrayList <UrlClassStat >(statsMap .size ());
542+ for (UrlClassStatBuilder builder : statsMap .values ()) {
543+ if (hasClassFilter && builder .getMatchedClassCount () == 0 ) {
544+ continue ;
545+ }
546+ stats .add (builder .build ());
547+ }
548+
549+ Collections .sort (stats , new Comparator <UrlClassStat >() {
550+ @ Override
551+ public int compare (UrlClassStat o1 , UrlClassStat o2 ) {
552+ int c1 = hasClassFilter ? safeInt (o1 .getMatchedClassCount ()) : o1 .getLoadedClassCount ();
553+ int c2 = hasClassFilter ? safeInt (o2 .getMatchedClassCount ()) : o2 .getLoadedClassCount ();
554+ int diff = c2 - c1 ;
555+ if (diff != 0 ) {
556+ return diff ;
557+ }
558+ return o1 .getUrl ().compareTo (o2 .getUrl ());
559+ }
560+ });
561+
562+ RowAffect affect = new RowAffect ();
563+ affect .rCnt (stats .size ());
564+ ClassLoaderModel model = new ClassLoaderModel ()
565+ .setClassLoader (ClassUtils .createClassLoaderVO (targetClassLoader ))
566+ .setUrlClassStats (stats )
567+ .setUrlClassStatsDetail (urlClassesDetail );
568+ process .appendResult (model );
569+ process .appendResult (new RowAffectModel (affect ));
570+ process .end ();
571+ }
572+
573+ private static int safeInt (Integer v ) {
574+ return v == null ? 0 : v .intValue ();
575+ }
576+
577+ private boolean matchJarFilter (String url , Pattern jarPattern ) {
578+ if (jarFilter == null ) {
579+ return true ;
580+ }
581+ String jarName = guessJarName (url );
582+ if (urlClassesRegEx ) {
583+ return jarPattern != null && (jarPattern .matcher (url ).find () || jarPattern .matcher (jarName ).find ());
584+ }
585+ return containsIgnoreCase (url , jarFilter ) || containsIgnoreCase (jarName , jarFilter );
586+ }
587+
588+ private boolean matchClassFilter (String className , Pattern classPattern ) {
589+ if (classFilter == null ) {
590+ return true ;
591+ }
592+ if (urlClassesRegEx ) {
593+ return classPattern != null && classPattern .matcher (className ).find ();
594+ }
595+ return containsIgnoreCase (className , classFilter );
596+ }
597+
598+ static boolean containsIgnoreCase (String text , String keyword ) {
599+ if (text == null || keyword == null ) {
600+ return false ;
601+ }
602+ return text .toLowerCase ().contains (keyword .toLowerCase ());
603+ }
604+
605+ private static String codeSourceLocation (Class <?> clazz ) {
606+ try {
607+ ProtectionDomain protectionDomain = clazz .getProtectionDomain ();
608+ if (protectionDomain == null ) {
609+ return UNKNOWN_CODE_SOURCE ;
610+ }
611+ CodeSource codeSource = protectionDomain .getCodeSource ();
612+ if (codeSource == null ) {
613+ return UNKNOWN_CODE_SOURCE ;
614+ }
615+ URL location = codeSource .getLocation ();
616+ if (location == null ) {
617+ return UNKNOWN_CODE_SOURCE ;
618+ }
619+ return location .toString ();
620+ } catch (Throwable t ) {
621+ return UNKNOWN_CODE_SOURCE ;
622+ }
623+ }
624+
625+ static String guessJarName (String url ) {
626+ if (url == null ) {
627+ return com .taobao .arthas .core .util .Constants .EMPTY_STRING ;
628+ }
629+ String s = url ;
630+ int bangIndex = s .lastIndexOf ('!' );
631+ if (bangIndex >= 0 ) {
632+ s = s .substring (0 , bangIndex );
633+ }
634+ while (s .endsWith ("/" )) {
635+ s = s .substring (0 , s .length () - 1 );
636+ }
637+ int slash = Math .max (s .lastIndexOf ('/' ), s .lastIndexOf ('\\' ));
638+ if (slash >= 0 && slash < s .length () - 1 ) {
639+ s = s .substring (slash + 1 );
640+ }
641+ return s ;
642+ }
643+
410644 private Map <ClassLoaderVO , ClassLoaderUrlStat > urlStats (Instrumentation inst ) {
411645 Map <ClassLoaderVO , ClassLoaderUrlStat > urlStats = new HashMap <ClassLoaderVO , ClassLoaderUrlStat >();
412646 Map <ClassLoader , Set <String >> usedUrlsMap = new HashMap <ClassLoader , Set <String >>();
@@ -639,6 +873,110 @@ public boolean accept(ClassLoader classLoader) {
639873 }
640874 }
641875
876+ public static class UrlClassStat {
877+ private String url ;
878+ private int loadedClassCount ;
879+ private Integer matchedClassCount ;
880+ private List <String > classes ;
881+ private boolean truncated ;
882+
883+ public String getUrl () {
884+ return url ;
885+ }
886+
887+ public void setUrl (String url ) {
888+ this .url = url ;
889+ }
890+
891+ public int getLoadedClassCount () {
892+ return loadedClassCount ;
893+ }
894+
895+ public void setLoadedClassCount (int loadedClassCount ) {
896+ this .loadedClassCount = loadedClassCount ;
897+ }
898+
899+ public Integer getMatchedClassCount () {
900+ return matchedClassCount ;
901+ }
902+
903+ public void setMatchedClassCount (Integer matchedClassCount ) {
904+ this .matchedClassCount = matchedClassCount ;
905+ }
906+
907+ public List <String > getClasses () {
908+ return classes ;
909+ }
910+
911+ public void setClasses (List <String > classes ) {
912+ this .classes = classes ;
913+ }
914+
915+ public boolean isTruncated () {
916+ return truncated ;
917+ }
918+
919+ public void setTruncated (boolean truncated ) {
920+ this .truncated = truncated ;
921+ }
922+ }
923+
924+ private static class UrlClassStatBuilder {
925+ private final String url ;
926+ private final boolean hasClassFilter ;
927+ private final int limit ;
928+ private int loadedClassCount ;
929+ private int matchedClassCount ;
930+ private SortedSet <String > classNames ;
931+ private boolean truncated ;
932+
933+ UrlClassStatBuilder (String url , boolean hasClassFilter , int limit ) {
934+ this .url = url ;
935+ this .hasClassFilter = hasClassFilter ;
936+ this .limit = limit ;
937+ if (limit > 0 ) {
938+ this .classNames = new TreeSet <String >();
939+ }
940+ }
941+
942+ void increaseLoadedCount () {
943+ loadedClassCount ++;
944+ }
945+
946+ void increaseMatchedCount () {
947+ matchedClassCount ++;
948+ }
949+
950+ int getMatchedClassCount () {
951+ return matchedClassCount ;
952+ }
953+
954+ void tryAddClass (String className ) {
955+ if (classNames == null ) {
956+ return ;
957+ }
958+ if (classNames .size () >= limit ) {
959+ truncated = true ;
960+ return ;
961+ }
962+ classNames .add (className );
963+ }
964+
965+ UrlClassStat build () {
966+ UrlClassStat stat = new UrlClassStat ();
967+ stat .setUrl (url );
968+ stat .setLoadedClassCount (loadedClassCount );
969+ if (hasClassFilter ) {
970+ stat .setMatchedClassCount (matchedClassCount );
971+ }
972+ if (classNames != null ) {
973+ stat .setClasses (new ArrayList <String >(classNames ));
974+ }
975+ stat .setTruncated (truncated );
976+ return stat ;
977+ }
978+ }
979+
642980 public static class ClassLoaderUrlStat {
643981 private Collection <String > usedUrls ;
644982 private Collection <String > unUsedUrls ;
0 commit comments