DATAJPA-1250 - Inspect repository interfaces in isolated classloader.

We now load repository interfaces for configuration inspection (i.e. for strict mode verification) within an isolated, throw-away classloader if Spring Instrumentation or EclipseLink are on the class path. Loading interfaces and hence domain classes in an isolated classloader does not prevent load-time weaving during EntityManagerFactoryBean initialization since the domain class wasn't loaded by the application classloader.
This commit is contained in:
Mark Paluch
2018-02-26 10:46:19 +01:00
committed by Oliver Gierke
parent 87e9bbc230
commit c1f61b1118
4 changed files with 169 additions and 0 deletions

View File

@@ -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.");
}
}

View File

@@ -17,12 +17,16 @@ package org.springframework.data.jpa.repository.config;
import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*; import static org.springframework.data.jpa.repository.config.BeanDefinitionNames.*;
import lombok.experimental.UtilityClass;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.MappedSuperclass; 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.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.AnnotationConfigUtils;
import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.io.ResourceLoader;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor; import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.JpaRepository; 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.data.repository.config.XmlRepositoryConfigurationSource;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
@@ -183,6 +189,19 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
registerIfNotAlreadyRegistered(contextDefinition, registry, JPA_CONTEXT_BEAN_NAME, source); 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 * Creates an anonymous factory to extract the actual {@link javax.persistence.EntityManager} from the
* {@link javax.persistence.EntityManagerFactory} bean name reference. * {@link javax.persistence.EntityManagerFactory} bean name reference.
@@ -210,4 +229,37 @@ public class JpaRepositoryConfigExtension extends RepositoryConfigurationExtensi
Optional<String> entityManagerFactoryRef = config.getAttribute("entityManagerFactoryRef"); Optional<String> entityManagerFactoryRef = config.getAttribute("entityManagerFactoryRef");
return entityManagerFactoryRef.orElse("entityManagerFactory"); 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<String> AGENT_CLASSES;
static {
Set<String> 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));
}
}
} }

View File

@@ -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);
}
}

View File

@@ -39,14 +39,17 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigUtils; 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.RepositoryConfigurationExtension;
import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationSource;
import org.springframework.instrument.classloading.ShadowingClassLoader;
import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor; import org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor;
/** /**
* Unit tests for {@link JpaRepositoryConfigExtension}. * Unit tests for {@link JpaRepositoryConfigExtension}.
* *
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch
*/ */
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class JpaRepositoryConfigExtensionUnitTests { public class JpaRepositoryConfigExtensionUnitTests {
@@ -110,6 +113,40 @@ public class JpaRepositoryConfigExtensionUnitTests {
factoryBean.createInstance().afterPropertiesSet(); 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, private void assertOnlyOnePersistenceAnnotationBeanPostProcessorRegistered(DefaultListableBeanFactory factory,
String expectedBeanName) { String expectedBeanName) {