From b91b89a9d550d010c1ae5707244ac44aa3a2e83c Mon Sep 17 00:00:00 2001 From: Alexander Kriegisch Date: Thu, 22 Jul 2021 11:57:44 +0700 Subject: [PATCH] [MSHADE-400] Self-minimisation with custom entry points See https://issues.apache.org/jira/browse/MSHADE-400. Solves https://issues.apache.org/jira/browse/MSHADE-366, too. --- .../maven/plugins/shade/DefaultShader.java | 20 ++-- .../plugins/shade/filter/MinijarFilter.java | 101 ++++++++++++++++-- .../maven/plugins/shade/mojo/ShadeMojo.java | 53 +++++++-- .../shade/filter/MinijarFilterTest.java | 19 ---- 4 files changed, 147 insertions(+), 46 deletions(-) diff --git a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java index 7f852e2d..0ff035ae 100644 --- a/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java +++ b/src/main/java/org/apache/maven/plugins/shade/DefaultShader.java @@ -237,17 +237,19 @@ private void shadeJars( ShadeRequest shadeRequest, Set resources, List jarFilters = getFilters( jar, shadeRequest.getFilters() ); if ( jar.isDirectory() ) { - shadeDir( shadeRequest, resources, transformers, remapper, jos, duplicates, jar, jar, "", jarFilters ); + shadeDir( shadeRequest, resources, transformers, packageMapper, jos, duplicates, + jar, jar, "", jarFilters ); } else { - shadeJar( shadeRequest, resources, transformers, remapper, jos, duplicates, jar, jarFilters ); + shadeJar( shadeRequest, resources, transformers, packageMapper, jos, duplicates, + jar, jarFilters ); } } } private void shadeDir( ShadeRequest shadeRequest, Set resources, - List transformers, RelocatorRemapper remapper, + List transformers, DefaultPackageMapper packageMapper, JarOutputStream jos, MultiValuedMap duplicates, File jar, File current, String prefix, List jarFilters ) throws IOException { @@ -264,7 +266,7 @@ private void shadeDir( ShadeRequest shadeRequest, Set resources, try { shadeDir( - shadeRequest, resources, transformers, remapper, jos, + shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, file, prefix + file.getName() + '/', jarFilters ); continue; @@ -283,7 +285,7 @@ private void shadeDir( ShadeRequest shadeRequest, Set resources, try { shadeJarEntry( - shadeRequest, resources, transformers, remapper, jos, duplicates, jar, + shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, new Callable() { @Override @@ -302,7 +304,7 @@ public InputStream call() throws Exception } private void shadeJar( ShadeRequest shadeRequest, Set resources, - List transformers, RelocatorRemapper remapper, + List transformers, DefaultPackageMapper packageMapper, JarOutputStream jos, MultiValuedMap duplicates, File jar, List jarFilters ) throws IOException { @@ -323,7 +325,7 @@ private void shadeJar( ShadeRequest shadeRequest, Set resources, try { shadeJarEntry( - shadeRequest, resources, transformers, remapper, jos, duplicates, jar, + shadeRequest, resources, transformers, packageMapper, jos, duplicates, jar, new Callable() { @Override @@ -617,7 +619,7 @@ private void addRemappedClass( JarOutputStream jos, File jar, String name, return; } - // Keep the original class in, in case nothing was relocated by RelocatorRemapper. This avoids binary + // Keep the original class, in case nothing was relocated by ShadeClassRemapper. This avoids binary // differences between classes, simply because they were rewritten and only details like constant pool or // stack map frames are slightly different. byte[] originalClass = IOUtil.toByteArray( is ); @@ -643,7 +645,7 @@ private void addRemappedClass( JarOutputStream jos, File jar, String name, throw new MojoExecutionException( "Error in ASM processing class " + name, ise ); } - // If nothing was relocated by RelocatorRemapper, write the original class, otherwise the transformed one + // If nothing was relocated by ShadeClassRemapper, write the original class, otherwise the transformed one final byte[] renamedClass; if ( cv.remapped ) { diff --git a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java index 23286788..84c88090 100644 --- a/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java +++ b/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java @@ -77,17 +77,30 @@ public class MinijarFilter public MinijarFilter( MavenProject project, Log log ) throws IOException { - this( project, log, Collections.emptyList() ); + this( project, log, Collections.emptyList(), Collections.emptySet() ); + } + + /** + * @param project {@link MavenProject} + * @param log {@link Log} + * @param entryPoints + * @throws IOException in case of error. + */ + public MinijarFilter( MavenProject project, Log log, Set entryPoints ) + throws IOException + { + this( project, log, Collections.emptyList(), entryPoints ); } /** * @param project {@link MavenProject} * @param log {@link Log} * @param simpleFilters {@link SimpleFilter} + * @param entryPoints * @throws IOException in case of errors. * @since 1.6 */ - public MinijarFilter( MavenProject project, Log log, List simpleFilters ) + public MinijarFilter( MavenProject project, Log log, List simpleFilters, Set entryPoints ) throws IOException { this.log = log; @@ -111,8 +124,44 @@ public MinijarFilter( MavenProject project, Log log, List simpleFi log.warn( "Removing module-info from " + artifactFile.getName() ); } removePackages( artifactUnit ); - removable.removeAll( artifactUnit.getClazzes() ); - removable.removeAll( artifactUnit.getTransitiveDependencies() ); + if ( entryPoints.isEmpty() ) + { + removable.removeAll( artifactUnit.getClazzes() ); + removable.removeAll( artifactUnit.getTransitiveDependencies() ); + } + else + { + Set artifactUnitClazzes = artifactUnit.getClazzes(); + Set entryPointsToKeep = new HashSet<>(); + for ( String entryPoint : entryPoints ) + { + Clazz entryPointFound = null; + for ( Clazz clazz : artifactUnitClazzes ) + { + if ( clazz.getName().equals( entryPoint ) ) + { + entryPointFound = clazz; + break; + } + } + if ( entryPointFound != null ) + { + entryPointsToKeep.add( entryPointFound ); + } + } + removable.removeAll( entryPointsToKeep ); + if ( entryPointsToKeep.isEmpty() ) + { + removable.removeAll( artifactUnit.getTransitiveDependencies() ); + } + else + { + for ( Clazz entryPoint : entryPointsToKeep ) + { + removable.removeAll( entryPoint.getTransitiveDependencies() ); + } + } + } removeSpecificallyIncludedClasses( project, simpleFilters == null ? Collections.emptyList() : simpleFilters ); removeServices( project, cp ); @@ -133,12 +182,11 @@ private void removeServices( final MavenProject project, final Clazzpath cp ) { if ( new File( fileName ).isDirectory() ) { - log.debug( "Not a JAR file candidate. Ignoring classpath element '" + fileName + "'." ); - continue; + repeatScan |= removeServicesFromDir( cp, neededClasses, fileName ); } - if ( removeServicesFromJar( cp, neededClasses, fileName ) ) + else { - repeatScan = true; + repeatScan |= removeServicesFromJar( cp, neededClasses, fileName ); } } } @@ -150,6 +198,43 @@ private void removeServices( final MavenProject project, final Clazzpath cp ) while ( repeatScan ); } + private boolean removeServicesFromDir( Clazzpath cp, Set neededClasses, String fileName ) + { + final File servicesDir = new File( fileName, "META-INF/services/" ); + if ( !servicesDir.isDirectory() ) + { + return false; + } + final File[] serviceProviderConfigFiles = servicesDir.listFiles(); + if ( serviceProviderConfigFiles == null || serviceProviderConfigFiles.length == 0 ) + { + return false; + } + + boolean repeatScan = false; + for ( File serviceProviderConfigFile : serviceProviderConfigFiles ) + { + final String serviceClassName = serviceProviderConfigFile.getName(); + final boolean isNeededClass = neededClasses.contains( cp.getClazz( serviceClassName ) ); + if ( !isNeededClass ) + { + continue; + } + + try ( final BufferedReader configFileReader = new BufferedReader( + new InputStreamReader( new FileInputStream( serviceProviderConfigFile ), UTF_8 ) ) ) + { + // check whether the found classes use services in turn + repeatScan |= scanServiceProviderConfigFile( cp, configFileReader ); + } + catch ( final IOException e ) + { + log.warn( e.getMessage() ); + } + } + return repeatScan; + } + private boolean removeServicesFromJar( Clazzpath cp, Set neededClasses, String fileName ) { boolean repeatScan = false; diff --git a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java index 6cc019cd..89c2b96f 100644 --- a/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java +++ b/src/main/java/org/apache/maven/plugins/shade/mojo/ShadeMojo.java @@ -147,7 +147,7 @@ public class ShadeMojo * syntax groupId is equivalent to groupId:*:*:*, groupId:artifactId is * equivalent to groupId:artifactId:*:* and groupId:artifactId:classifier is equivalent to * groupId:artifactId:*:classifier. For example: - * + * *
      * <artifactSet>
      *   <includes>
@@ -164,7 +164,7 @@ public class ShadeMojo
 
     /**
      * Packages to be relocated. For example:
-     * 
+     *
      * 
      * <relocations>
      *   <relocation>
@@ -179,7 +179,7 @@ public class ShadeMojo
      *   </relocation>
      * </relocations>
      * 
- * + * * Note: Support for includes exists only since version 1.4. */ @SuppressWarnings( "MismatchedReadAndWriteOfArray" ) @@ -200,7 +200,7 @@ public class ShadeMojo * to use an include to collect a set of files from the archive then use excludes to further reduce the set. By * default, all files are included and no files are excluded. If multiple filters apply to an artifact, the * intersection of the matched files will be included in the final JAR. For example: - * + * *
      * <filters>
      *   <filter>
@@ -336,13 +336,41 @@ public class ShadeMojo
 
     /**
      * When true, dependencies will be stripped down on the class level to only the transitive hull required for the
-     * artifact. Note: Usage of this feature requires Java 1.5 or higher.
+     * artifact. See also {@link #entryPoints}, if you wish to further optimize JAR minimization.
+     * 

+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. * * @since 1.4 */ @Parameter private boolean minimizeJar; + /** + * Use this option in order to fine-tune {@link #minimizeJar}: By default, all of the target module's classes are + * kept and used as entry points for JAR minimization. By explicitly limiting the set of entry points, you can + * further minimize the set of classes kept in the shaded JAR. This affects both classes in the module itself and + * dependency classes. If {@link #minimizeJar} is inactive, this option has no effect either. + *

+ * Note: This feature requires Java 1.8 or higher due to its use of + * jdependency. Its accuracy therefore also depends on + * jdependency's limitations. + *

+ * Configuration example: + *

{@code
+     * true
+     * 
+     *   org.acme.Application
+     *   org.acme.OtherEntryPoint
+     * 
+     * }
+ * + * @since 3.3.1 + */ + @Parameter + private Set entryPoints; + /** * The path to the output file for the shaded artifact. When this parameter is set, the created archive will neither * replace the project's main artifact nor will it be attached. Hence, this parameter causes the parameters @@ -391,7 +419,7 @@ public class ShadeMojo */ @Parameter( defaultValue = "false" ) private boolean skip; - + /** * @throws MojoExecutionException in case of an error. */ @@ -555,7 +583,7 @@ public void execute() replaceFile( finalFile, testSourcesJar ); testSourcesJar = finalFile; } - + renamed = true; } @@ -965,11 +993,16 @@ private List getFilters() if ( minimizeJar ) { - getLog().info( "Minimizing jar " + project.getArtifact() ); + if ( entryPoints == null ) + { + entryPoints = new HashSet<>(); + } + getLog().info( "Minimizing jar " + project.getArtifact() + + ( entryPoints.isEmpty() ? "" : ", entry points = " + entryPoints ) ); try { - filters.add( new MinijarFilter( project, getLog(), simpleFilters ) ); + filters.add( new MinijarFilter( project, getLog(), simpleFilters, entryPoints ) ); } catch ( IOException e ) { @@ -1149,7 +1182,7 @@ private void rewriteDependencyReducedPomIfWeHaveReduction( List depe } File f = dependencyReducedPomLocation; - // MSHADE-225 + // MSHADE-225 // Works for now, maybe there's a better algorithm where no for-loop is required if ( loopCounter == 0 ) { diff --git a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java index 874c93e7..ad198fc0 100644 --- a/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java +++ b/src/test/java/org/apache/maven/plugins/shade/filter/MinijarFilterTest.java @@ -165,23 +165,4 @@ public void finishedShouldProduceMessageForClassesTotalZero() } - /** - * Verify that directories are ignored when scanning the classpath for JARs containing services, - * but warnings are logged instead - * - * @see MSHADE-366 - */ - @Test - public void removeServicesShouldIgnoreDirectories() throws Exception { - String classPathElementToIgnore = tempFolder.getRoot().getAbsolutePath(); - MavenProject mockedProject = mockProject(emptyFile, classPathElementToIgnore); - - new MinijarFilter(mockedProject, log); - - verify(log, never()).warn(logCaptor.capture()); - verify(log, times(1)).debug( - "Not a JAR file candidate. Ignoring classpath element '" + classPathElementToIgnore + "'." - ); - } - }