问题
今天一个项目组在升级我们的基础平台之后,无法正常启动,报出如下错误:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'viewControllerHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/WebMvcAutoConfiguration$EnableWebMvcConfiguration.class]: Initialization of bean failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport$EmptyHandlerMapping]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport$EmptyHandlerMapping
.....
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport$EmptyHandlerMapping]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport$EmptyHandlerMapping
......
分析
看错误信息,是Spring在创建viewControllerHandlerMapping
对象的时候失败了,具体原因是创建内部类WebMvcConfigurationSupport$EmptyHandlerMapping
的CGLIB代理对象时由于是final类型导致无法创建从而失败(CGLIB通过继承对象来实现,final类无法被继承)。
那么为什么其它项目组正常而偏偏该项目不行呢?
找到创建viewControllerHandlerMapping
的地方,在类WebMvcConfigurationSupport
中,代码如下:
@Bean
public HandlerMapping viewControllerHandlerMapping() {
ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
addViewControllers(registry);
AbstractHandlerMapping handlerMapping = registry.buildHandlerMapping();
handlerMapping = (handlerMapping != null ? handlerMapping : new EmptyHandlerMapping());
handlerMapping.setPathMatcher(mvcPathMatcher());
handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
除了内部类EmptyHandlerMapping
确实是final之外没有任何特别的地方,一时间也没有头绪,只能用笨办法和其它正常的慢慢对比了。
通过对比我们发现,其它项目在该处创建EmptyHandlerMapping
后到Spring的ApplicationContext
中都是真实对象,而该项目在创建EmptyHandlerMapping
后会通过CGLIB创建代理对象,这也是导致失败的原因。
本着快速解决问题的原则,网上有说把proxy-target-class
改为false使用jdk动态代理就不会有该问题,可是改了之后发现依然无效。
看来不找到问题的源头是解决不了了,只能慢慢分析。
错误源头
折腾了半天,通过排除法,终于发现原来是有一个类在获取Spring的ApplicationContext对象时是通过继承ApplicationObjectSupport
来实现的:
@Component
@Transactional
public class InitManagerImpl extends ApplicationObjectSupport implements InitManager {
}
也就是这里导致了Spring在创建完EmptyHandlerMapping
对象之后会生成CGLIB代理对象,
把它改为注入的方式获取,该问题解决:
@Component
@Transactional
public class InitManagerImpl implements InitManager {
@Autowired
private ApplicationContext applicationContext;
}
问题原因
虽然解决了问题,但到底是什么原因使得项目中有ApplicationObjectSupport
的继承类就会导致Spring对创建EmptyHandlerMapping
区别对待呢?
空下来的时候专门研究了下,还是有些门道。
来简单看一下WebMvcConfigurationSupport.EmptyHandlerMapping
类的源代码:
private static final class EmptyHandlerMapping extends AbstractHandlerMapping {
@Override
protected Object getHandlerInternal(HttpServletRequest request) {
return null;
}
}
继承了AbstractHandlerMapping
,再来看一下AbstractHandlerMapping:
public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {
//......
}
继承了WebApplicationObjectSupport
,而WebApplicationObjectSupport同样是继承于ApplicationObjectSupport
的:
public abstract class WebApplicationObjectSupport extends ApplicationObjectSupport implements ServletContextAware {
//......
}
这里我们找到了和我们导致出错类的共同点,都是ApplicationObjectSupport
的子类,那这会有什么问题呢?
因为是创建代理对象,肯定是AOP方面的东西,再来分析一下Spring中AOP处理的部分源码。
这里比较重要的是AopUtils
中的canApply
方法:
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
//......
Set<Class<?>> classes = new LinkedHashSet<Class<?>>(ClassUtils.getAllInterfacesForClassAsSet(targetClass));
classes.add(targetClass);
for (Class<?> clazz : classes) {
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
for (Method method : methods) {
if ((introductionAwareMethodMatcher != null &&
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions)) ||
methodMatcher.matches(method, targetClass)) {
return true;
}
}
}
return false;
}
是否创建代理对象就是通过这个方法返回true或false来决定的。
这里会判断class实现的所有接口及自身的方法(包括父类方法)是否需要AOP,如果需要就会生成代理对象。
到这里其实路线已经很清晰了,因为同样继承ApplicationObjectSupport
类所以里面的方法一样冲突了。
照理说这两个类也不相干啊,为什么不同的类实例会相互影响呢?
再来看执行具体判断的AspectJExpressionPointcut
类中的matches
方法:
public boolean matches(Method method, Class<?> targetClass, boolean beanHasIntroductions) {
checkReadyToMatch();
Method targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
ShadowMatch shadowMatch = getShadowMatch(targetMethod, method);
if (shadowMatch.alwaysMatches()) {
return true;
}
else if (shadowMatch.neverMatches()) {
return false;
}
else {
if (beanHasIntroductions) {
return true;
}
RuntimeTestWalker walker = getRuntimeTestWalker(shadowMatch);
return (!walker.testsSubtypeSensitiveVars() || walker.testTargetInstanceOfResidue(targetClass));
}
}
问题就在这里面调用的getShadowMatch
方法,这里面用到了缓存:
private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) {
// Avoid lock contention for known Methods through concurrent access...
ShadowMatch shadowMatch = this.shadowMatchCache.get(targetMethod);
if (shadowMatch == null) {
//......
}
return shadowMatch;
}
可以看到缓存key是Method,这也就解释了上面为什么不同的类实例有相同的方法会相互影响。
我们的业务类因为有事务切面、日志拦截等所以肯定是AOP代理对象,而Spring的ApplicationContext对象初始化在Spring MVC的WebApplicationContext对象之前,这也就导致了同样继承ApplicationObjectSupport
后方法缓存的关系影响Spring判断是否为EmptyHandlerMapping
生成代理对象。
最后
到这问题也彻底分析清楚了,不知道这算不算是Spring的一个bug呢?