diff --git a/src/main/java/org/springframework/data/jpa/repository/config/InspectionClassLoader.java b/src/main/java/org/springframework/data/jpa/repository/config/InspectionClassLoader.java new file mode 100644 index 000000000..d116bd3d8 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/repository/config/InspectionClassLoader.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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.jpa.repository.config; + +import org.springframework.instrument.classloading.ShadowingClassLoader; + +/** + * Disposable {@link ClassLoader} used to inspect user-code classes within an isolated class loader without preventing + * class transformation at a later time. + * + * @author Mark Paluch + * @since 2.1 + */ +class InspectionClassLoader extends ShadowingClassLoader { + + /** + * Create a new {@link InspectionClassLoader} instance. + * + * @param parent the parent classloader. + */ + InspectionClassLoader(ClassLoader parent) { + + super(parent, true); + + excludePackage("org.springframework."); + } +} diff --git a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java index 20cedffac..79128c49d 100644 --- a/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java +++ b/src/main/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtension.java @@ -17,12 +17,16 @@ package org.springframework.data.jpa.repository.config; import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; +import lombok.experimental.UtilityClass; + import java.lang.annotation.Annotation; import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.LinkedHashSet; import java.util.Locale; import java.util.Optional; +import java.util.Set; import javax.persistence.Entity; import javax.persistence.MappedSuperclass; @@ -35,6 +39,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.io.ResourceLoader; import org.springframework.dao.DataAccessException; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.data.jpa.repository.JpaRepository; @@ -47,6 +52,7 @@ import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; import org.springframework.lang.Nullable; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; /** @@ -183,6 +189,19 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi registerIfNotAlreadyRegistered(contextDefinition, registry, JPA_CONTEXT_BEAN_NAME, source); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport#getConfigurationInspectionClassLoader(org.springframework.core.io.ResourceLoader) + */ + protected ClassLoader getConfigurationInspectionClassLoader(ResourceLoader loader) { + + ClassLoader classLoader = loader.getClassLoader(); + + return classLoader != null && LazyJvmAgent.isActive(loader.getClassLoader()) + ? new InspectionClassLoader(loader.getClassLoader()) + : loader.getClassLoader(); + } + /** * Creates an anonymous factory to extract the actual {@link javax.persistence.EntityManager} from the * {@link javax.persistence.EntityManagerFactory} bean name reference. @@ -210,4 +229,37 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi Optional entityManagerFactoryRef = config.getAttribute("entityManagerFactoryRef"); return entityManagerFactoryRef.orElse("entityManagerFactory"); } + + /** + * Utility to determine if a lazy Java agent is being used that might transform classes at a later time. + * + * @author Mark Paluch + * @since 2.1 + */ + @UtilityClass + static class LazyJvmAgent { + + private static final Set AGENT_CLASSES; + + static { + + Set agentClasses = new LinkedHashSet<>(); + + agentClasses.add("org.springframework.instrument.InstrumentationSavingAgent"); + agentClasses.add("org.eclipse.persistence.internal.jpa.deployment.JavaSECMPInitializerAgent"); + + AGENT_CLASSES = Collections.unmodifiableSet(agentClasses); + } + + /** + * Determine if any agent is active. + * + * @return {@literal true} if an agent is active. + */ + static boolean isActive(@Nullable ClassLoader classLoader) { + + return AGENT_CLASSES.stream() // + .anyMatch(agentClass -> ClassUtils.isPresent(agentClass, classLoader)); + } + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/config/InspectionClassLoaderUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/config/InspectionClassLoaderUnitTests.java new file mode 100644 index 000000000..d90fde0ea --- /dev/null +++ b/src/test/java/org/springframework/data/jpa/repository/config/InspectionClassLoaderUnitTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018 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.jpa.repository.config; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.Test; + +/** + * Unit tests for {@link InspectionClassLoader}. + * + * @author Mark Paluch + */ +public class InspectionClassLoaderUnitTests { + + @Test // DATAJPA-1250 + public void shouldLoadExternalClass() throws ClassNotFoundException { + + InspectionClassLoader classLoader = new InspectionClassLoader(getClass().getClassLoader()); + + Class isolated = classLoader.loadClass("org.hsqldb.Database"); + Class included = getClass().getClassLoader().loadClass("org.hsqldb.Database"); + + assertThat(isolated.getClassLoader()).isSameAs(classLoader).isNotSameAs(getClass().getClassLoader()); + assertThat(isolated).isNotEqualTo(included); + } +} diff --git a/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java index 1b42b6419..ec6e0830a 100644 --- a/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/config/JpaRepositoryConfigExtensionUnitTests.java @@ -39,14 +39,17 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationSource; +import org.springframework.instrument.classloading.ShadowingClassLoader; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; /** * Unit tests for {@link JpaRepositoryConfigExtension}. * * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class JpaRepositoryConfigExtensionUnitTests { @@ -110,6 +113,40 @@ public class JpaRepositoryConfigExtensionUnitTests { factoryBean.createInstance().afterPropertiesSet(); } + @Test // DATAJPA-1250 + public void shouldUseInspectionClassLoader() { + + JpaRepositoryConfigExtension extension = new JpaRepositoryConfigExtension(); + ClassLoader classLoader = extension.getConfigurationInspectionClassLoader(new GenericApplicationContext()); + + assertThat(classLoader, is(instanceOf(InspectionClassLoader.class))); + } + + @Test // DATAJPA-1250 + public void shouldNotUseInspectionClassLoaderWithoutEclipseLink() { + + ShadowingClassLoader shadowingClassLoader = new ShadowingClassLoader(getClass().getClassLoader(), false) { + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + + if (name.startsWith("org.springframework.instrument.") || name.startsWith("org.eclipse.")) { + throw new ClassNotFoundException("Excluded: " + name); + } + + return getClass().getClassLoader().loadClass(name); + } + }; + + GenericApplicationContext context = new GenericApplicationContext(); + context.setClassLoader(shadowingClassLoader); + + JpaRepositoryConfigExtension extension = new JpaRepositoryConfigExtension(); + ClassLoader classLoader = extension.getConfigurationInspectionClassLoader(context); + + assertThat(classLoader, is(not(instanceOf(InspectionClassLoader.class)))); + } + private void assertOnlyOnePersistenceAnnotationBeanPostProcessorRegistered(DefaultListableBeanFactory factory, String expectedBeanName) {