Could not generate CGLIB subclass of class WebMvcConfigurationSupport$EmptyHandlerMapping 踩坑记录

分类: WEB开发 0人评论 selfly 2月前发布

问题

今天一个项目组在升级我们的基础平台之后,无法正常启动,报出如下错误:

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呢?

上一篇:
没有了
下一篇:

你可能感兴趣的文章

0 条评论