1. 程式人生 > >Spring AOP造成的@Controller註冊失敗

Spring AOP造成的@Controller註冊失敗

昨天在重構專案時 ,調整了原有Controller的路徑 ,並抽取了部分方法形成AbstractController ,所有業務模組的Controller都繼承它 .然後啟動發現幾乎所有移動過的Controller都失效了.

主要物件

  • ControllerLogInterceptor : aop攔截器 ,攔截目標路徑下所有Controller
@Pointcut("execution(public * com.eddy..*.*Controller.*(..))")
public void handleController() {
}
  • AppController : 一個介面
public interface AppController {
    String getAppName();
}
  • AbstractController : 抽象的Controller ,包含一些公用方法 ;實現了AppController介面
@Slf4j
public abstract class AbstractController implements AppController {
    @Autowired
    protected HttpSession session;

    @Override
    public String getAppName
() { return OSC_APPNAME; } }
  • ContactController : 業務邏輯的Controller
@RestController
@RequestMapping("/eddy/contact")
public class ContactController extends AbstractOSCController {
...
}

原因分析

  • 因為是移動路徑過後出現的 ,首先考慮路徑影響
    • 使用的SpringBoot ,全註解配置的Controller ,路徑並不會影響Bean的解析
    • 同路徑下某一Controller可用 ,該Controller沒有繼承AbstractController ,繼承了另一個類
  • 考慮AbstractController的影響
    • 之前也是使用過繼承Controller的做法 ,沒有出現問題
    • 這個類也在AOP的匹配範圍內
  • 修改了AbstractController->AbstractAction ,依舊無效
    • AOP主要是代理原有Bean ,Abstract不會生成Spring Bean ,不會產生作用
  • 終極大招 ,原始碼斷點:
    • 列印Debug Log : ContactController註冊為Bean ,但沒有掃描到mapping路徑
    • 斷點檢視Spring如何掃描Controller並新增mapping的

RequestMapping

通過日誌 ,找到掃描Mapping的目標類RequestMappingHandlerMapping (父物件AbstractHandlerMethodMapping#initHandlerMethods方法)

//AbstractHandlerMethodMapping.class
protected void initHandlerMethods() {
        if (logger.isDebugEnabled()) {
            logger.debug("Looking for request mappings in application context: " + getApplicationContext());
        }
        String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
                getApplicationContext().getBeanNamesForType(Object.class));

        for (String beanName : beanNames) {
            if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
                Class<?> beanType = null;
                try {
                    beanType = getApplicationContext().getType(beanName);
                }
                catch (Throwable ex) {
                    // An unresolvable bean type, probably from a lazy bean - let's ignore it.
                    if (logger.isDebugEnabled()) {
                        logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
                    }
                }
                if (beanType != null && isHandler(beanType)) {
                    detectHandlerMethods(beanName);
                }
            }
        }
        handlerMethodsInitialized(getHandlerMethods());
    }

可以看到 ,通過迴圈已載入的Bean ,然後通過相應Handler (這裡是RequestMappingHandler) 判斷是否應該處理.

//RequestMappingHandlerMapping.class
@Override
protected boolean isHandler(Class<?> beanType) {
    //判斷對應class類是否 有Controller註解或繼承自Controller註解 (RestController) 
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }

這裡斷點除錯後 ,發現ContactController直接跳過 ,它沒有@Controller註解!!!! 但程式碼裡確實進行了配置 ,在經過多次重編譯過後 … 發現ContactController對應Bean的classType是 com.sun.Proxy…

也就是說SpringAOP代理將這個Bean本有的資訊抹除了 ,所以我修改了SpringAOP的匹配路徑 ,果然Controller可用了.

但是AOP部分的程式碼也很重要 ,我不希望丟掉.同時我發現
* 單一的Controller(無繼承關係) 和 AOP 工作的很好
* 繼承自其他類的Controller也工作的很好
那麼 ,Spring的AOP應該是不會破壞Bean的型別判斷的 : 他們的型別都是 com.eddy.ContactController$Spring..Cglib...這樣的 ,是能夠通過SpringMVC的檢查的

結論

最後的不同就在於那個簡單的介面…
* Spring預設使用JDK ,對介面進行動態代理 : ContactController extends AbstractController implement AppController ,所以將與介面有關的類都代理成了Proxy .
* 而繼承自單一類/沒有介面的 ,使用cglib代理 ,就不存在這個問題 ,他依舊保留了原有class的資訊.

增加配置 spring.aop.proxy-target-class=true ,預設統一使用cglib即可