I am trying to run a standalone MVC + JPA application with embedded tomcat
But the application fails with the following error, tho I dont see any obvious reason for this failure. I believe this has something to do with the way Tomcat deals with class loaders
I am not sure how spring boot gets around this problem
Appreciate if anyone can shed light on why this error is thrown
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userRepositoryImpl': Initialization of bean failed; nested exception is java.lang.IllegalAccessError: class com.acme.$Proxy53 cannot access its superinterface com.acme.UserRepository
... 36 more
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:584)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:846)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:863)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:546)
at org.springframework.web.context.ContextLoader.configureAndRefreshWebApplicationContext(ContextLoader.java:400)
at org.springframework.web.context.ContextLoader.initWebApplicationContext(ContextLoader.java:291)
at org.springframework.web.context.ContextLoaderListener.contextInitialized(ContextLoaderListener.java:103)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4661)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5131)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1382)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1372)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:907)
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:831)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1382)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1372)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134)
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:907)
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:262)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:423)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:933)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:398)
at com.acme.EmbeddedTomcatWebAppWithoutBoot.main(EmbeddedTomcatWebAppWithoutBoot.java:65)
Caused by: java.lang.IllegalAccessError: class com.acme.$Proxy53 cannot access its superinterface com.acme.UserRepository
at java.lang.reflect.Proxy.defineClass0(Native Method)
at java.lang.reflect.Proxy.access$300(Proxy.java:228)
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:642)
at java.lang.reflect.Proxy$ProxyClassFactory.apply(Proxy.java:557)
at java.lang.reflect.WeakCache$Factory.get(WeakCache.java:230)
at java.lang.reflect.WeakCache.get(WeakCache.java:127)
at java.lang.reflect.Proxy.getProxyClass0(Proxy.java:419)
at java.lang.reflect.Proxy.newProxyInstance(Proxy.java:719)
at org.springframework.aop.framework.JdkDynamicAopProxy.getProxy(JdkDynamicAopProxy.java:123)
at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:110)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:473)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:352)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:301)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:434)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1749)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576)
... 36 more
Sources used
EmbeddedTomcatWebAppWithoutBoot.java
package com.acme;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.data.repository.config.RepositoryConfiguration;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.SharedEntityManagerCreator;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import javax.persistence.Entity;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Properties;
import static java.lang.String.valueOf;
public class EmbeddedTomcatWebAppWithoutBoot {
private static final int PORT = 8080;
public static void main(String[] args) throws LifecycleException {
String appBase = ".";
Tomcat tomcat = new Tomcat();
tomcat.setBaseDir(createTempDir());
tomcat.setPort(PORT);
tomcat.getHost().setAppBase(appBase);
tomcat.addWebapp("", ".");
tomcat.getConnector(); // Trigger the creation of the default connector
tomcat.start();
ClassLoader classLoader = findContext(tomcat).getLoader().getClassLoader();
Thread.currentThread().setContextClassLoader(classLoader);
tomcat.getServer().await();
}
private static Context findContext(Tomcat tomcat) {
for (Container child : tomcat.getHost().findChildren()) {
if (child instanceof Context) {
return (Context) child;
}
}
throw new IllegalStateException("The host does not contain a Context");
}
// based on AbstractEmbeddedServletContainerFactory
private static String createTempDir() {
try {
File tempDir = File.createTempFile("tomcat.", "." + PORT);
tempDir.delete();
tempDir.mkdir();
tempDir.deleteOnExit();
return tempDir.getAbsolutePath();
} catch (IOException ex) {
throw new RuntimeException(
"Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"),
ex
);
}
}
}
class MainWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
rootContext.register(DataAccessConfiguration.class, UserRepositoryImpl.class);
servletContext.addListener(new ContextLoaderListener(rootContext));
AnnotationConfigWebApplicationContext servletAppContext = new AnnotationConfigWebApplicationContext();
servletAppContext.register(WebMvcConfiguration.class);
DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
ServletRegistration.Dynamic servletRegistration = servletContext.addServlet("dispatcher", dispatcherServlet);
servletRegistration.setLoadOnStartup(1);
servletRegistration.addMapping("/");
}
}
@EnableWebMvc
@Configuration
@Import(UserResource.class)
class WebMvcConfiguration {}
@RestController
class UserResource {
private final UserRepository userRepository;
public UserResource(UserRepository userRepository) {
this.userRepository = userRepository;
}
@GetMapping
ResponseEntity<List<User>> get() {
return ResponseEntity.ok().body(userRepository.findAllUsers());
}
}
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
class UserRequest {
private String name;
private String authorityName;
}
@Configuration
@EnableTransactionManagement
class DataAccessConfiguration {
@Bean
DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.setName("sample")
.build();
}
@Bean
LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
factoryBean.setDataSource(dataSource);
HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
factoryBean.setJpaVendorAdapter(vendorAdapter);
factoryBean.setPackagesToScan(EmbeddedTomcatWebAppWithoutBoot.class.getPackage().getName());
// set som extra properties hibernate
Properties jpaProperties = new Properties();
jpaProperties.setProperty("hibernate.show_sql", "true");
jpaProperties.setProperty("hibernate.format_sql", "true");
jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
jpaProperties.setProperty("hibernate.hbm2ddl.auto", "create-drop");
factoryBean.setJpaProperties(jpaProperties);
return factoryBean;
}
@Bean
JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
@Bean
EntityManager entityManager(EntityManagerFactory entityManagerFactory) {
return SharedEntityManagerCreator.createSharedEntityManager(entityManagerFactory);
}
@Bean
PersistenceExceptionTranslationPostProcessor postProcessor() {
return new PersistenceExceptionTranslationPostProcessor();
}
}
interface UserRepository {
List<User> findAllUsers();
}
@Repository
@Transactional
class UserRepositoryImpl implements UserRepository {
private final EntityManager entityManager;
public UserRepositoryImpl(EntityManager entityManager) {
this.entityManager = entityManager;
}
@Override
public List<User> findAllUsers() {
return entityManager.createQuery("from User", User.class)
.getResultList();
}
}
@Entity
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ToString
class User {
@Id
@GeneratedValue
private int id;
private String name;
}
build.gradle
ext {
set('spring-boot.version', '2.1.2.RELEASE')
// copied from above
set('spring.version', '5.1.4.RELEASE')
// async logging
set('lmax-disruptor.version', '3.4.2')
// misc utils
set('unexceptional.version', '1.0.0')
set('mapstruct.version', '1.3.0.Final')
// tooling
set('jetbrains-annotations.version', '16.0.2')
}
group 'com.acme'
version '1.0.0-SNAPSHOT'
apply plugin: 'java-library'
sourceCompatibility = 1.8
repositories {
mavenCentral()
jcenter()
maven {
url 'https://repo.spring.io/libs-milestone'
}
}
configurations {
springBom
developmentOnly
// we want to make sure the dependencies bom ia available everywhere so that the dependencies can be resolved across all configurations
compileOnly.extendsFrom(springBom)
annotationProcessor.extendsFrom(springBom)
testAnnotationProcessor.extendsFrom(springBom)
api.extendsFrom(springBom)
implementation.extendsFrom(springBom)
runtimeClasspath {
extendsFrom developmentOnly
}
// lets inherit everything
testCompileOnly.extendsFrom(compileOnly)
}
dependencies {
//*** bill of materials
springBom platform("org.springframework.boot:spring-boot-dependencies:${project.'spring-boot.version'}")
implementation 'org.springframework:spring-context'
implementation 'org.springframework.data:spring-data-jpa'
implementation 'org.hibernate:hibernate-core'
implementation 'com.h2database:h2'
// web
implementation 'org.springframework:spring-web'
implementation 'org.springframework:spring-webmvc'
// jackson for json serialization
implementation 'com.fasterxml.jackson.core:jackson-databind'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8' // new java 8 classes like stream, OPtional
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' // new java date & time api
implementation 'com.fasterxml.jackson.module:jackson-module-parameter-names' // custom modules within jackson
// embedded server
implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
// log4j2
implementation 'org.apache.logging.log4j:log4j-core'
implementation 'org.apache.logging.log4j:log4j-slf4j-impl'
implementation 'org.apache.logging.log4j:log4j-jcl'
implementation 'org.apache.logging.log4j:log4j-jul'
implementation "com.lmax:disruptor:${project.'lmax-disruptor.version'}"
// unexceptional for making sure code is not super polluted
implementation "io.earcam:io.earcam.unexceptional:${project.'unexceptional.version'}"
// junit
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testImplementation 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.mockito:mockito-junit-jupiter'
// test misc
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.hamcrest:hamcrest-library'
testImplementation 'org.mockito:mockito-junit-jupiter'
// NOTE: Due to a bug as of now mapstruct needs to go before lombok
// https://github.com/mapstruct/mapstruct/issues/1581
// maspstruct
implementation "org.mapstruct:mapstruct:${project.'mapstruct.version'}"
implementation "org.mapstruct:mapstruct:${project.'mapstruct.version'}"
annotationProcessor "org.mapstruct:mapstruct-processor:${project.'mapstruct.version'}"
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${project.'mapstruct.version'}"
// lombok
compileOnly('org.projectlombok:lombok')
annotationProcessor('org.projectlombok:lombok')
}
configurations {
all {
// we need to exclude logging to avoid keeping both slf4j & log4j2 both in the same location
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
// `spring-boot-starter-test` is pulling older version of junit. Lets just ignore it
exclude group: 'junit', module: 'junit'
}
}
The complete runnable gradle project can be be found here
The creation of the proxy is failing because the interface that defines the proxy's API, UserRepository
, is package-private, access to a package-private type is prohibited across class loaders and the class loader being used to create the proxy is not the same as the class loader that was used to load UserRepository
.
There are a few different ways that you can fix the problem, including:
UserRepository
publicMainWebAppInitializer
to call rootContext.setClassLoader(getClass().getClassLoader())
The second option is closer to what Spring Boot does and ensures that the class loader that loaded MainWebAppInitializer
(and UserRepository
) is used for proxy creation.