Browse Source
ClassLoaderRule with @ClassLoaderConfiguration allows easy creation FilteringClassLoader. Changed usage pattern of ClassUtils.isPresent(…) in order to ease testing. Copied the ShadowingClassloader from SpringFramework to get a constructor that doesn't shadow anything by default. Original pull request: #202.pull/347/head
8 changed files with 460 additions and 40 deletions
@ -0,0 +1,60 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2017 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.classloadersupport; |
||||||
|
|
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
|
||||||
|
/** |
||||||
|
* configures the {@link ClassLoaderRule}. |
||||||
|
* |
||||||
|
* @author Jens Schauder |
||||||
|
*/ |
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
public @interface ClassLoaderConfiguration { |
||||||
|
|
||||||
|
/** |
||||||
|
* classes of which the package that contains them will be loaded by the {@link HidingClassLoader} not by |
||||||
|
* the normal {@code ClassLoader}. |
||||||
|
* |
||||||
|
* @return list of classes not to shadow. |
||||||
|
*/ |
||||||
|
Class[] shadowPackage() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* prefixes of class names that will be loaded by the {@link HidingClassLoader}. |
||||||
|
* |
||||||
|
* @return list of class prefixes which will not be shadowed. |
||||||
|
*/ |
||||||
|
String[] shadowByPrefix() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* classes of which the package that contains them will be hidden by the {@link HidingClassLoader}. |
||||||
|
* |
||||||
|
* @return list of classes to hidePackage. |
||||||
|
*/ |
||||||
|
Class[] hidePackage() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* classes from packages that will be hidden by the {@link HidingClassLoader}. |
||||||
|
* |
||||||
|
* @return list of classes of which the package will be hidden. |
||||||
|
*/ |
||||||
|
String[] hideByPrefix() default {}; |
||||||
|
} |
||||||
@ -0,0 +1,113 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2017 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.classloadersupport; |
||||||
|
|
||||||
|
import static java.util.Arrays.*; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import org.junit.rules.MethodRule; |
||||||
|
import org.junit.runners.model.FrameworkMethod; |
||||||
|
import org.junit.runners.model.Statement; |
||||||
|
|
||||||
|
/** |
||||||
|
* supports creation of tests that need to load classes with a {@link HidingClassLoader}. |
||||||
|
* |
||||||
|
* @author Jens Schauder |
||||||
|
*/ |
||||||
|
public class ClassLoaderRule implements MethodRule { |
||||||
|
|
||||||
|
public HidingClassLoader classLoader; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Statement apply(final Statement base, FrameworkMethod method, Object target) { |
||||||
|
|
||||||
|
CombinedClassLoaderConfiguration combinedConfiguration = new CombinedClassLoaderConfiguration( |
||||||
|
method.getAnnotation(ClassLoaderConfiguration.class), |
||||||
|
method.getDeclaringClass().getAnnotation(ClassLoaderConfiguration.class) |
||||||
|
); |
||||||
|
|
||||||
|
classLoader = createClassLoader(combinedConfiguration); |
||||||
|
|
||||||
|
return new Statement() { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void evaluate() throws Throwable { |
||||||
|
|
||||||
|
try { |
||||||
|
base.evaluate(); |
||||||
|
} finally { |
||||||
|
classLoader = null; |
||||||
|
} |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
private static HidingClassLoader createClassLoader(CombinedClassLoaderConfiguration configuration) { |
||||||
|
|
||||||
|
HidingClassLoader classLoader = new HidingClassLoader(mergeHidden(configuration)); |
||||||
|
|
||||||
|
for (Class shadow : configuration.shadowPackages) { |
||||||
|
classLoader.excludeClass(shadow.getPackage().getName()); |
||||||
|
} |
||||||
|
|
||||||
|
for (String shadow : configuration.shadowByPrefix) { |
||||||
|
classLoader.excludePackage(shadow); |
||||||
|
} |
||||||
|
|
||||||
|
return classLoader; |
||||||
|
} |
||||||
|
|
||||||
|
private static List<String> mergeHidden(CombinedClassLoaderConfiguration configuration) { |
||||||
|
|
||||||
|
List<String> hidden = new ArrayList<String>(); |
||||||
|
|
||||||
|
for (Class aClass : configuration.hidePackages) { |
||||||
|
hidden.add(aClass.getPackage().getName()); |
||||||
|
} |
||||||
|
|
||||||
|
for (String aPackage : configuration.hideByPrefix) { |
||||||
|
hidden.add(aPackage); |
||||||
|
} |
||||||
|
|
||||||
|
return hidden; |
||||||
|
} |
||||||
|
|
||||||
|
private static class CombinedClassLoaderConfiguration { |
||||||
|
|
||||||
|
final List<Class> shadowPackages = new ArrayList<Class>(); |
||||||
|
final List<String> shadowByPrefix = new ArrayList<String>(); |
||||||
|
final List<Class> hidePackages = new ArrayList<Class>(); |
||||||
|
final List<String> hideByPrefix = new ArrayList<String>(); |
||||||
|
|
||||||
|
CombinedClassLoaderConfiguration(ClassLoaderConfiguration methodAnnotation, ClassLoaderConfiguration classAnnotation) { |
||||||
|
|
||||||
|
mergeAnnotation(methodAnnotation); |
||||||
|
mergeAnnotation(classAnnotation); |
||||||
|
} |
||||||
|
|
||||||
|
private void mergeAnnotation(ClassLoaderConfiguration methodAnnotation) { |
||||||
|
|
||||||
|
if (methodAnnotation != null) { |
||||||
|
|
||||||
|
shadowPackages.addAll(asList(methodAnnotation.shadowPackage())); |
||||||
|
shadowByPrefix.addAll(asList(methodAnnotation.shadowByPrefix())); |
||||||
|
hidePackages.addAll(asList(methodAnnotation.hidePackage())); |
||||||
|
hideByPrefix.addAll(asList(methodAnnotation.hideByPrefix())); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2017 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
package org.springframework.data.classloadersupport; |
||||||
|
|
||||||
|
import java.net.URLClassLoader; |
||||||
|
import java.util.Collection; |
||||||
|
|
||||||
|
/** |
||||||
|
* is intended for testing code that depends on the presence/absence of certain classes. |
||||||
|
* |
||||||
|
* Classes can be: |
||||||
|
* <ul> |
||||||
|
* <li>shadowed: reloaded by this classloader no matter if they are loaded already by the SystemClassLoader</li> |
||||||
|
* <li>hidden: not loaded by this classloader no matter if they are loaded already by the SystemClassLoader. Trying to load these classes results in a {@link ClassNotFoundException}</li> |
||||||
|
* <li>all other classes get loaded by the SystemClassLoader</li> |
||||||
|
* </ul> |
||||||
|
* |
||||||
|
* @author Jens Schauder |
||||||
|
*/ |
||||||
|
public class HidingClassLoader extends ShadowingClassLoader { |
||||||
|
|
||||||
|
private final Collection<String> hidden; |
||||||
|
|
||||||
|
HidingClassLoader(Collection<String> hidden) { |
||||||
|
super(URLClassLoader.getSystemClassLoader()); |
||||||
|
this.hidden = hidden; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Class<?> loadClass(String name) throws ClassNotFoundException { |
||||||
|
|
||||||
|
checkIfHidden(name); |
||||||
|
return super.loadClass(name); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected Class<?> findClass(String name) throws ClassNotFoundException { |
||||||
|
|
||||||
|
checkIfHidden(name); |
||||||
|
return super.findClass(name); |
||||||
|
} |
||||||
|
|
||||||
|
private void checkIfHidden(String name) throws ClassNotFoundException { |
||||||
|
|
||||||
|
for (String prefix : hidden) { |
||||||
|
if (name.startsWith(prefix)) { |
||||||
|
throw new ClassNotFoundException(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,186 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2012 the original author or authors. |
||||||
|
* |
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||||
|
* you may not use this file except in compliance with the License. |
||||||
|
* You may obtain a copy of the License at |
||||||
|
* |
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* |
||||||
|
* Unless required by applicable law or agreed to in writing, software |
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||||
|
* See the License for the specific language governing permissions and |
||||||
|
* limitations under the License. |
||||||
|
*/ |
||||||
|
|
||||||
|
package org.springframework.data.classloadersupport; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.io.InputStream; |
||||||
|
import java.lang.instrument.ClassFileTransformer; |
||||||
|
import java.lang.instrument.IllegalClassFormatException; |
||||||
|
import java.net.URL; |
||||||
|
import java.util.Enumeration; |
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.LinkedList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
import org.springframework.core.DecoratingClassLoader; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.FileCopyUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* ClassLoader decorator that shadows an enclosing ClassLoader, |
||||||
|
* applying registered transformers to all affected classes. |
||||||
|
* |
||||||
|
* @author Rob Harrop |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @author Costin Leau |
||||||
|
* @since 2.0 |
||||||
|
* @see #addTransformer |
||||||
|
* @see org.springframework.core.OverridingClassLoader |
||||||
|
*/ |
||||||
|
public class ShadowingClassLoader extends DecoratingClassLoader { |
||||||
|
|
||||||
|
/** Packages that are excluded by default */ |
||||||
|
public static final String[] DEFAULT_EXCLUDED_PACKAGES = |
||||||
|
new String[] {"java.", "javax.", "sun.", "oracle.", "com.sun.", "com.ibm.", "COM.ibm.", |
||||||
|
"org.w3c.", "org.xml.", "org.dom4j.", "org.eclipse", "org.aspectj.", "net.sf.cglib", |
||||||
|
"org.springframework.cglib", "org.apache.xerces.", "org.apache.commons.logging."}; |
||||||
|
|
||||||
|
|
||||||
|
private final ClassLoader enclosingClassLoader; |
||||||
|
|
||||||
|
private final List<ClassFileTransformer> classFileTransformers = new LinkedList<ClassFileTransformer>(); |
||||||
|
|
||||||
|
private final Map<String, Class<?>> classCache = new HashMap<String, Class<?>>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a new ShadowingClassLoader, decorating the given ClassLoader. |
||||||
|
* @param enclosingClassLoader the ClassLoader to decorate |
||||||
|
*/ |
||||||
|
public ShadowingClassLoader(ClassLoader enclosingClassLoader) { |
||||||
|
Assert.notNull(enclosingClassLoader, "Enclosing ClassLoader must not be null"); |
||||||
|
this.enclosingClassLoader = enclosingClassLoader; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add the given ClassFileTransformer to the list of transformers that this |
||||||
|
* ClassLoader will apply. |
||||||
|
* @param transformer the ClassFileTransformer |
||||||
|
*/ |
||||||
|
public void addTransformer(ClassFileTransformer transformer) { |
||||||
|
Assert.notNull(transformer, "Transformer must not be null"); |
||||||
|
this.classFileTransformers.add(transformer); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Copy all ClassFileTransformers from the given ClassLoader to the list of |
||||||
|
* transformers that this ClassLoader will apply. |
||||||
|
* @param other the ClassLoader to copy from |
||||||
|
*/ |
||||||
|
public void copyTransformers(ShadowingClassLoader other) { |
||||||
|
Assert.notNull(other, "Other ClassLoader must not be null"); |
||||||
|
this.classFileTransformers.addAll(other.classFileTransformers); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public Class<?> loadClass(String name) throws ClassNotFoundException { |
||||||
|
if (shouldShadow(name)) { |
||||||
|
System.out.println("shadowing " + name); |
||||||
|
Class<?> cls = this.classCache.get(name); |
||||||
|
if (cls != null) { |
||||||
|
return cls; |
||||||
|
} |
||||||
|
return doLoadClass(name); |
||||||
|
} |
||||||
|
else { |
||||||
|
return this.enclosingClassLoader.loadClass(name); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine whether the given class should be excluded from shadowing. |
||||||
|
* @param className the name of the class
|
||||||
|
* @return whether the specified class should be shadowed |
||||||
|
*/ |
||||||
|
private boolean shouldShadow(String className) { |
||||||
|
return (!className.equals(getClass().getName()) && !className.endsWith("ShadowingClassLoader") && |
||||||
|
isEligibleForShadowing(className)); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine whether the specified class is eligible for shadowing |
||||||
|
* by this class loader. |
||||||
|
* @param className the class name to check |
||||||
|
* @return whether the specified class is eligible |
||||||
|
* @see #isExcluded |
||||||
|
*/ |
||||||
|
protected boolean isEligibleForShadowing(String className) { |
||||||
|
return isExcluded(className); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private Class<?> doLoadClass(String name) throws ClassNotFoundException { |
||||||
|
String internalName = StringUtils.replace(name, ".", "/") + ".class"; |
||||||
|
InputStream is = this.enclosingClassLoader.getResourceAsStream(internalName); |
||||||
|
if (is == null) { |
||||||
|
throw new ClassNotFoundException(name); |
||||||
|
} |
||||||
|
try { |
||||||
|
byte[] bytes = FileCopyUtils.copyToByteArray(is); |
||||||
|
bytes = applyTransformers(name, bytes); |
||||||
|
Class<?> cls = defineClass(name, bytes, 0, bytes.length); |
||||||
|
// Additional check for defining the package, if not defined yet.
|
||||||
|
if (cls.getPackage() == null) { |
||||||
|
int packageSeparator = name.lastIndexOf('.'); |
||||||
|
if (packageSeparator != -1) { |
||||||
|
String packageName = name.substring(0, packageSeparator); |
||||||
|
definePackage(packageName, null, null, null, null, null, null, null); |
||||||
|
} |
||||||
|
} |
||||||
|
this.classCache.put(name, cls); |
||||||
|
return cls; |
||||||
|
} |
||||||
|
catch (IOException ex) { |
||||||
|
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private byte[] applyTransformers(String name, byte[] bytes) { |
||||||
|
String internalName = StringUtils.replace(name, ".", "/"); |
||||||
|
try { |
||||||
|
for (ClassFileTransformer transformer : this.classFileTransformers) { |
||||||
|
byte[] transformed = transformer.transform(this, internalName, null, null, bytes); |
||||||
|
bytes = (transformed != null ? transformed : bytes); |
||||||
|
} |
||||||
|
return bytes; |
||||||
|
} |
||||||
|
catch (IllegalClassFormatException ex) { |
||||||
|
throw new IllegalStateException(ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public URL getResource(String name) { |
||||||
|
return this.enclosingClassLoader.getResource(name); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public InputStream getResourceAsStream(String name) { |
||||||
|
return this.enclosingClassLoader.getResourceAsStream(name); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Enumeration<URL> getResources(String name) throws IOException { |
||||||
|
return this.enclosingClassLoader.getResources(name); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue