Spring Boot 定義接口的方法是否能夠聲明爲 private?

咱們在 Controller 中定義接口的時候,通常都是像下面這樣:java

@GetMapping("/01")
public String hello(Map<String,Object> map) {
    map.put("name", "javaboy");
    return "forward:/index";
}

估計不多有人會把接口方法定義成 private 的吧?那咱們不由要問,若是非要定義成 private 的方法,那能運行起來嗎?web

帶着這個疑問,咱們開始今天的源碼解讀~數組

在咱們使用 Spring Boot 的時候,常常會看到 HandlerMethod 這個類型,例如咱們在定義攔截器的時候,若是攔截目標是一個方法,則 preHandle 的第三個參數就是 HandlerMethod(如下案例選自鬆哥以前的視頻:手把手教你 Spring Boot 自定義註解):app

@Component
public class IdempotentInterceptor implements HandlerInterceptor {
    @Autowired
    TokenService tokenService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        //省略...
        return true;
    }
    //...
}

咱們在閱讀 SpringMVC 源碼的時候,也會反覆看到這個 HandlerMethod,那麼它究竟是什麼意思?今天我想和小夥伴們捋一捋這個問題,把這個問題搞清楚了,前面的問題你們也就懂了。異步

1.概覽

能夠看到,HandlerMethod 體系下的類並很少:ide

HandlerMethod工具

封裝 Handler 和具體處理請求的 Method。開發工具

InvocableHandlerMethodui

在 HandlerMethod 的基礎上增長了調用的功能。this

ServletInvocableHandlerMethod

在 InvocableHandlerMethod 的基礎上增了對 @ResponseStatus 註解的支持、增長了對返回值的處理。

ConcurrentResultHandlerMethod

在 ServletInvocableHandlerMethod 的基礎上,增長了對異步結果的處理。

基本上就是這四個,接下來鬆哥就來詳細說一說這四個組件。

2.HandlerMethod

2.1 bridgedMethod

在正式開始介紹 HandlerMethod 以前,想先和你們聊聊 bridgedMethod,由於在 HandlerMethod 中將會涉及到這個東西,而有的小夥伴可能還沒據說過 bridgedMethod,所以鬆哥在這裏作一個簡單介紹。

首先考考你們,下面這段代碼編譯會報錯嗎?

public interface Animal<T> {
    void eat(T t);
}
public class Cat implements Animal<String> {
    @Override
    public void eat(String s) {
        System.out.println("cat eat " + s);
    }
}
public class Demo01 {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.eat(new Object());
    }
}

首先咱們定義了一個 Animal 接口,裏邊定義了一個 eat 方法,同時聲明瞭一個泛型。Cat 實現了 Animal 接口,將泛型也定義爲了 String。當我調用的時候,聲明類型是 Animal,實際類型是 Cat,這個時候調 eat 方法傳入了 Object 對象你們猜猜會怎麼樣?若是調用 eat 方法時傳入的是 String 類型那就確定沒問題,但若是不是 String 呢?

鬆哥先說結論:編譯沒問題,運行報錯。

若是小夥伴們在本身電腦上寫出上面這段代碼,你會發現這樣一個問題,開發工具中提示的參數類型居然是 Object,以鬆哥的 IDEA 爲例,以下:

你們看到,在我寫代碼的時候,開發工具會給我提示,這個參數類型是 Object,有的小夥伴會以爲奇怪,明明是泛型,怎麼變成 Object 了?

咱們能夠經過反射查看 Cat 類中到底有哪些方法,代碼以下:

public class Demo01 {
    public static void main(String[] args) {
        Method[] methods = Cat.class.getMethods();
        for (Method method : methods) {
            String name = method.getName();
            Class<?>[] parameterTypes = method.getParameterTypes();
            System.out.println(name+"("+ Arrays.toString(parameterTypes) +")");
        }
    }
}

運行結果以下:

能夠看到,在實際運行過程當中,居然有兩個 eat 方法,一個的參數爲 String 類型,另外一個參數爲 Object 類型,這是怎麼回事呢?

這個參數類型爲 Object 的方法實際上是 Java 虛擬機在運行時建立出來的,這個方法就是咱們所說的 bridge method。本節的小標題叫作 bridgedMethod,這是 HandlerMethod 源碼中的變量名,bridge 結尾多了一個 d,含義變成了被 bridge 的方法,也就是參數爲 String 的原方法,你們在接下來的源碼中看到了 bridgedMethod 就知道這表示參數類型不變的原方法。

2.2 HandlerMethod 介紹

接下來咱們來簡單看下 HandlerMethod。

在咱們前面分析 HandlerMapping 的時候(參見:SpringMVC 九大組件之 HandlerMapping 深刻分析),裏邊有涉及到 HandlerMethod,建立 HandlerMethod 的入口方法是 createWithResolvedBean,所以這裏咱們就從該方法開始看起:

public HandlerMethod createWithResolvedBean() {
    Object handler = this.bean;
    if (this.bean instanceof String) {
        String beanName = (String) this.bean;
        handler = this.beanFactory.getBean(beanName);
    }
    return new HandlerMethod(this, handler);
}

這個方法主要是確認了一下 handler 的類型,若是 handler 是 String 類型,則根據 beanName 從 Spring 容器中從新查找到 handler 對象,而後構建 HandlerMethod:

private HandlerMethod(HandlerMethod handlerMethod, Object handler) {
    this.bean = handler;
    this.beanFactory = handlerMethod.beanFactory;
    this.beanType = handlerMethod.beanType;
    this.method = handlerMethod.method;
    this.bridgedMethod = handlerMethod.bridgedMethod;
    this.parameters = handlerMethod.parameters;
    this.responseStatus = handlerMethod.responseStatus;
    this.responseStatusReason = handlerMethod.responseStatusReason;
    this.resolvedFromHandlerMethod = handlerMethod;
    this.description = handlerMethod.description;
}

這裏的參數都比較簡單,沒啥好說的,惟一值得介紹的地方有兩個:parameters 和 responseStatus。

parameters

parameters 實際上就是方法參數,對應的類型是 MethodParameter,這個類的源碼我這裏就不貼出來了,主要和你們說一下封裝的內容包括:參數的序號(parameterIndex),參數嵌套級別(nestingLevel),參數類型(parameterType),參數的註解(parameterAnnotations),參數名稱查找器(parameterNameDiscoverer),參數名稱(parameterName)等。

HandlerMethod 中還提供了兩個內部類來封裝 MethodParameter,分別是:

  • HandlerMethodParameter:這個封裝方法調用的參數。
  • ReturnValueMethodParameter:這個繼承自 HandlerMethodParameter,它封裝了方法的返回值,返回值裏邊的 parameterIndex 是 -1。

注意,這二者中的 method 都是 bridgedMethod。

responseStatus

這個主要是處理方法的 @ResponseStatus 註解,這個註解用來描述方法的響應狀態碼,使用方式像下面這樣:

@GetMapping("/04")
@ResponseBody
@ResponseStatus(code = HttpStatus.OK)
public void hello4(@SessionAttribute("name") String name) {
    System.out.println("name = " + name);
}

從這段代碼中你們能夠看到,其實 @ResponseStatus 註解靈活性不好,不實用,當咱們定義一個接口的時候,很難預知到該接口的響應狀態碼是 200。

在 handlerMethod 中,在調用其構造方法的時候,都會調用 evaluateResponseStatus 方法處理 @ResponseStatus 註解,以下:

private void evaluateResponseStatus() {
    ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class);
    if (annotation == null) {
        annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class);
    }
    if (annotation != null) {
        this.responseStatus = annotation.code();
        this.responseStatusReason = annotation.reason();
    }
}

能夠看到,這段代碼也比較簡單,找到註解,把裏邊的值解析出來,賦值給相應的變量。

這下小夥伴們應該明白了 HandlerMethod 大概是個怎麼回事。

3.InvocableHandlerMethod

看名字就知道,InvocableHandlerMethod 能夠調用 HandlerMethod 中的具體方法,也就是 bridgedMethod。咱們先來看下 InvocableHandlerMethod 中聲明的屬性:

private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
@Nullable
private WebDataBinderFactory dataBinderFactory;

主要就是這三個屬性:

  • resolvers:這個不用說,參數解析器,前面的文章中鬆哥已經和你們聊過這個問題了。
  • parameterNameDiscoverer:這個用來獲取參數名稱,在 MethodParameter 中會用到。
  • dataBinderFactory:這個用來建立 WebDataBinder,在參數解析器中會用到。

具體的請求調用方法是 invokeForRequest,咱們一塊兒來看下:

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
    return doInvoke(args);
}
@Nullable
protected Object doInvoke(Object... args) throws Exception {
    Method method = getBridgedMethod();
    ReflectionUtils.makeAccessible(method);
    try {
        if (KotlinDetector.isSuspendingFunction(method)) {
            return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
        }
        return method.invoke(getBean(), args);
    }
    catch (InvocationTargetException ex) {
        // 省略 ...
    }
}

首先調用 getMethodArgumentValues 方法按順序獲取到全部參數的值,這些參數值組成一個數組,而後調用 doInvoke 方法執行,在 doInvoke 方法中,首先獲取到 bridgedMethod,並設置其可見(意味着咱們在 Controller 中定義的接口方法也能夠是 private 的),而後直接經過反射調用便可。當咱們沒看 SpringMVC 源碼的時候,咱們就知道接口方法最終確定是經過反射調用的,如今,通過層層分析以後,終於在這裏找到了反射調用代碼。

最後鬆哥再來講一下負責參數解析的 getMethodArgumentValues 方法:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    MethodParameter[] parameters = getMethodParameters();
    if (ObjectUtils.isEmpty(parameters)) {
        return EMPTY_ARGS;
    }
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
        MethodParameter parameter = parameters[i];
        parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
        args[i] = findProvidedArgument(parameter, providedArgs);
        if (args[i] != null) {
            continue;
        }
        if (!this.resolvers.supportsParameter(parameter)) {
            throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
        }
        try {
            args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        }
        catch (Exception ex) {
            // 省略...
        }
    }
    return args;
}
  1. 首先調用 getMethodParameters 方法獲取到方法的全部參數。
  2. 建立 args 數組用來保存參數的值。
  3. 接下來一堆初始化配置。
  4. 若是 providedArgs 中提供了參數值,則直接賦值。
  5. 查看是否有參數解析器支持當前參數類型,若是沒有,直接拋出異常。
  6. 調用參數解析器對參數進行解析,解析完成後,賦值。

是否是,很 easy!

4.ServletInvocableHandlerMethod

ServletInvocableHandlerMethod 則是在 InvocableHandlerMethod 的基礎上,又增長了兩個功能:

  • @ResponseStatus 註解的處理
  • 對返回值的處理

Servlet 容器下 Controller 在查找適配器時發起調用的最終就是 ServletInvocableHandlerMethod。

這裏的處理核心方法是 invokeAndHandle,以下:

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
        Object... providedArgs) throws Exception {
    Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
    setResponseStatus(webRequest);
    if (returnValue == null) {
        if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
            disableContentCachingIfNecessary(webRequest);
            mavContainer.setRequestHandled(true);
            return;
        }
    }
    else if (StringUtils.hasText(getResponseStatusReason())) {
        mavContainer.setRequestHandled(true);
        return;
    }
    mavContainer.setRequestHandled(false);
    try {
        this.returnValueHandlers.handleReturnValue(
                returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
    }
    catch (Exception ex) {
        throw ex;
    }
}
  1. 首先調用父類的 invokeForRequest 方法對請求進行執行,拿到請求結果。
  2. 調用 setResponseStatus 方法處理 @ResponseStatus 註解,具體的處理邏輯是這樣:若是沒有添加 @ResponseStatus 註解,則什麼都不作;若是添加了該註解,而且 reason 屬性不爲空,則直接輸出錯誤,不然設置響應狀態碼。這裏須要注意一點,若是響應狀態碼是 200,就不要設置 reason,不然會按照 error 處理。
  3. 接下來就是對返回值的處理了,returnValueHandlers#handleReturnValue 方法鬆哥在以前的文章中和你們專門介紹過,這裏就再也不贅述,傳送門:Spring Boot 中如何統一 API 接口響應格式?

事實上,ServletInvocableHandlerMethod 還有一個子類 ConcurrentResultHandlerMethod,這個支持異步調用結果處理,由於使用場景較少,這裏就不作介紹啦。

5.小結

如今你們能夠回答文章標題提出的問題了吧?

相關文章
相關標籤/搜索