从原理层面掌握HandlerMethod、InvocableHandlerMethod的使用【一块儿学Spring MVC】

每篇一句

想当火影的人没有近道可寻,当上火影的人一样无路可退

前言

HandlerMethod它做为Spring MVC的非公开API,可能绝大多数小伙伴都对它比较陌生,但我相信你对它又不是那么的生疏,由于你可能没用过但确定见过。
好比Spring MVC的拦截器HandlerInterceptor的拦截方法的第三个入参Object handler,虽然它是Object类型,但其实绝大部分状况下咱们都会看成HandlerMethod来使用;又好比我以前的这篇讲RequestMappingHandlerMapping的文章也大量的提到过HandlerMethod这个类。java

经由我这么“忽悠”,你是否以为它仍是相对比较重要的一个类了呢?无论你信不信,反正我是这么认为的:HandlerMethod它是理解Spring MVC不可或缺的一个类,甚至能够说是你但愿参与到Spring MVC的定制化里面来不可忽略的一个关键API。react

HandlerMethod

HandlerMethod它不是一个接口,也不是个抽象类,且仍是public的。HandlerMethod封装了不少属性,在访问请求方法的时候能够方便的访问到方法、方法参数、方法上的注解、所属类等而且对方法参数封装处理,也能够方便的访问到方法参数的注解等信息。web

// @since 3.1
public class HandlerMethod {

    // Object类型,既能够是个Bean,也能够是个BeanName
    private final Object bean;
    // 若是是BeanName,拿就靠它拿出Bean实例了~
    @Nullable
    private final BeanFactory beanFactory;
    private final Class<?> beanType; // 该方法所属的类
    private final Method method; // 该方法自己
    private final Method bridgedMethod; // 被桥接的方法,若是method是原生的,它的值同method
    // 封装方法参数的类实例,**一个MethodParameter就是一个入参**
    // MethodParameter也是Spring抽象出来的一个很是重要的概念
    private final MethodParameter[] parameters;
    @Nullable
    private HttpStatus responseStatus; // http状态码(毕竟它要负责处理和返回)
    @Nullable
    private String responseStatusReason; // 若是状态码里还要复数缘由,就是这个字段  能够为null


    // 经过createWithResolvedBean()解析此handlerMethod实例的handlerMethod。
    @Nullable
    private HandlerMethod resolvedFromHandlerMethod;
    // 标注在**接口入参**上的注解们(此处数据结构复杂,List+二维数组)
    @Nullable
    private volatile List<Annotation[][]> interfaceParameterAnnotations;

    // 它的构造方法众多  此处我只写出关键的步骤
    public HandlerMethod(Object bean, Method method) {
        ...
        this.beanType = ClassUtils.getUserClass(bean);
        this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
        this.parameters = initMethodParameters();
        ...
        evaluateResponseStatus();
    }
    // 这个构造方法抛出了一个异常NoSuchMethodException 
    public HandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
        ...
        this.method = bean.getClass().getMethod(methodName, parameterTypes);
        this.parameters = initMethodParameters();
        ...
        evaluateResponseStatus();
    }
    // 此处传的是BeanName
    public HandlerMethod(String beanName, BeanFactory beanFactory, Method method) {
        ...
        // 这部判断:这个BeanName是必须存在的
        Class<?> beanType = beanFactory.getType(beanName);
        if (beanType == null) {
            throw new IllegalStateException("Cannot resolve bean type for bean with name '" + beanName + "'");
        }
        this.parameters = initMethodParameters();
        ...
        evaluateResponseStatus();
    }

    // 供给子类copy使用的
    protected HandlerMethod(HandlerMethod handlerMethod) { ... }
    
    // 全部构造都执行了两个方法:initMethodParameters和evaluateResponseStatus

    // 初始化该方法全部的入参,此处使用的是内部类HandlerMethodParameter
    // 注意:处理了泛型的~~~
    private MethodParameter[] initMethodParameters() {
        int count = this.bridgedMethod.getParameterCount();
        MethodParameter[] result = new MethodParameter[count];
        for (int i = 0; i < count; i++) {
            HandlerMethodParameter parameter = new HandlerMethodParameter(i);
            GenericTypeResolver.resolveParameterType(parameter, this.beanType);
            result[i] = parameter;
        }
        return result;
    }

    // 看看方法上是否有标注了@ResponseStatus注解(接口上或者父类 组合注解上都行)
    // 若方法上没有,还会去所在的类上去看看有没有标注此注解
    // 主要只解析这个注解,把它的两个属性code和reason拿过来,最后就是返回它俩了~~~
    // code状态码默认是HttpStatus.INTERNAL_SERVER_ERROR-->(500, "Internal Server Error")
    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();
        }
    }
    ... // 省略全部属性的get方法(无set方法)

    // 返回方法返回值的类型  此处也使用的MethodParameter 
    public MethodParameter getReturnType() {
        return new HandlerMethodParameter(-1);
    }
    // 注意和上面的区别。举个列子:好比方法返回的是Object,但实际return “fsx”字符串
    // 那么上面返回永远是Object.class,下面你实际的值是什么类型就是什么类型
    public MethodParameter getReturnValueType(@Nullable Object returnValue) {
        return new ReturnValueMethodParameter(returnValue);
    }

    // 该方法的返回值是不是void
    public boolean isVoid() {
        return Void.TYPE.equals(getReturnType().getParameterType());
    }
    // 返回标注在方法上的指定类型的注解   父方法也成
    // 子类ServletInvocableHandlerMethod对下面两个方法都有复写~~~
    @Nullable
    public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
        return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType);
    }
    public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) {
        return AnnotatedElementUtils.hasAnnotation(this.method, annotationType);
    }


    // resolvedFromHandlerMethod虽然它只能被构造进来,可是它实际是铜鼓调用下面方法赋值
    @Nullable
    public HandlerMethod getResolvedFromHandlerMethod() {
        return this.resolvedFromHandlerMethod;
    }
    // 根据string类型的BeanName把Bean拿出来,再new一个HandlerMethod出来~~~这才靠谱嘛
    public HandlerMethod createWithResolvedBean() {
        Object handler = this.bean;
        if (this.bean instanceof String) {
            Assert.state(this.beanFactory != null, "Cannot resolve bean name without BeanFactory");
            String beanName = (String) this.bean;
            handler = this.beanFactory.getBean(beanName);
        }
        return new HandlerMethod(this, handler);
    }

    public String getShortLogMessage() {
        return getBeanType().getName() + "#" + this.method.getName() + "[" + this.method.getParameterCount() + " args]";
    }


    // 这个方法是提供给内部类HandlerMethodParameter来使用的~~ 它使用的数据结构仍是蛮复杂的
    private List<Annotation[][]> getInterfaceParameterAnnotations() {
        List<Annotation[][]> parameterAnnotations = this.interfaceParameterAnnotations;
        if (parameterAnnotations == null) {
            parameterAnnotations = new ArrayList<>();

            // 遍历该方法所在的类全部的实现的接口们(能够实现N个接口嘛)
            for (Class<?> ifc : this.method.getDeclaringClass().getInterfaces()) {
            
                // getMethods:拿到全部的public的方法,包括父接口的  接口里的私有方法可不会获取来
                for (Method candidate : ifc.getMethods()) {
                    // 判断这个接口方法是否正好是当前method复写的这个~~~
                    // 恰好是复写的方法,那就添加进来,标记为接口上的注解们~~~
                    if (isOverrideFor(candidate)) {
                        // getParameterAnnotations返回的是个二维数组~~~~
                        // 由于参数有多个,且每一个参数前能够有多个注解
                        parameterAnnotations.add(candidate.getParameterAnnotations());
                    }
                }
            }
            this.interfaceParameterAnnotations = parameterAnnotations;
        }
        return parameterAnnotations;
    }

    
    // 看看内部类的关键步骤
    protected class HandlerMethodParameter extends SynthesizingMethodParameter {
        @Nullable
        private volatile Annotation[] combinedAnnotations;
        ...

        // 父类只会在本方法拿,这里支持到了接口级别~~~
        @Override
        public Annotation[] getParameterAnnotations() {
            Annotation[] anns = this.combinedAnnotations;
            if (anns == null) { // 都只须要解析一次
                anns = super.getParameterAnnotations();
                int index = getParameterIndex();
                if (index >= 0) { // 有入参才须要去分析嘛
                    for (Annotation[][] ifcAnns : getInterfaceParameterAnnotations()) {
                        if (index < ifcAnns.length) {
                            Annotation[] paramAnns = ifcAnns[index];
                            if (paramAnns.length > 0) {
                                List<Annotation> merged = new ArrayList<>(anns.length + paramAnns.length);
                                merged.addAll(Arrays.asList(anns));
                                for (Annotation paramAnn : paramAnns) {
                                    boolean existingType = false;
                                    for (Annotation ann : anns) {
                                        if (ann.annotationType() == paramAnn.annotationType()) {
                                            existingType = true;
                                            break;
                                        }
                                    }
                                    if (!existingType) {
                                        merged.add(adaptAnnotation(paramAnn));
                                    }
                                }
                                anns = merged.toArray(new Annotation[0]);
                            }
                        }
                    }
                }
                this.combinedAnnotations = anns;
            }
            return anns;
        }
    }

    // 返回值的真正类型~~~
    private class ReturnValueMethodParameter extends HandlerMethodParameter {
        @Nullable
        private final Object returnValue;
        public ReturnValueMethodParameter(@Nullable Object returnValue) {
            super(-1); // 此处传的-1哦~~~~ 比0小是颇有意义的
            this.returnValue = returnValue;
        }
        ...
        // 返回值类型使用returnValue就好了~~~
        @Override
        public Class<?> getParameterType() {
            return (this.returnValue != null ? this.returnValue.getClass() : super.getParameterType());
        }
    }
}

能够看到HandlerMethod它持有的属性是很是多的,提供的能力也是很强的。
可是不知道小伙伴有没有发现,虽然它持有了目标的Method,可是它并无提供invoke执行它的能力,若是你要执行它还得本身把Method拿去本身执行。spring

因此总的来讲它的职责仍是很单一的:HandlerMethod它只负责准备数据,封装数据,而而不提供具体使用的方式方法~数组

看看它的继承树:
在这里插入图片描述
它主要有两个子类:InvocableHandlerMethodServletInvocableHandlerMethod,从命名就知道他俩都是有invoke调用能力的~数据结构

InvocableHandlerMethod

它是对HandlerMethod的扩展,增长了调用能力。这个能力在Spring MVC但是很是很是重要的,它可以在调用的时候,把方法入参的参数都封装进来(从HTTP request里,固然借助的必然是HandlerMethodArgumentResolver架构

// @since 3.1
public class InvocableHandlerMethod extends HandlerMethod {
    private static final Object[] EMPTY_ARGS = new Object[0];

    // 它额外提供的几个属性,能够看到和数据绑定、数据校验就扯上关系了~~~

    // 用于产生数据绑定器、校验器
    @Nullable
    private WebDataBinderFactory dataBinderFactory;
    // HandlerMethodArgumentResolver用于入参的解析
    private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite();
    // 用于获取形参名
    private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
    
    ... // 省略构造函数 所有使用super的
    // 它本身的三大属性都使用set方法设置进来~~~而且没有提供get方法
    // 也就是说:它本身内部使用就好了~~~

    // 在给定请求的上下文中解析方法的参数值后调用该方法。 也就是说:方法入参里就可以自动使用请求域(包括path里的,requestParam里的、以及常规对象如HttpSession这种)
    // 解释下providedArgs做用:调用者能够传进来,而后直接doInvoke()的时候原封不动的使用它
    //(弥补了请求域没有全部对象的不足,毕竟有些对象是用户自定义的嘛~)
    @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        // 虽然它是最重要的方法,可是此处不讲,由于核心原来仍是`HandlerMethodArgumentResolver`
        // 它只是把解析好的放到对应位置里去~~~
        // 说明:这里传入了ParameterNameDiscoverer,它是可以获取到形参名的。
        // 这就是为什么注解里咱们不写value值,经过形参名字来匹配也是ok的核心缘由~
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        if (logger.isTraceEnabled()) { // trace信息,不然日志也特多了~
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        return doInvoke(args);
    }

    // doInvoke()方法就不说了,就是个普通的方法调用
    // ReflectionUtils.makeAccessible(getBridgedMethod());
    // return getBridgedMethod().invoke(getBean(), args); 
}

对于最后的invoke(),说明一点:这里但是执行的目标方法getBean()哦~~~app

这个子类主要提供的能力就是提供了invoke调用目标Bean目标方法的能力,在这个调用过程当中可大有文章可为,固然最为核心的逻辑但是各类各样的HandlerMethodArgumentResolver来完成的,详见下文有分晓。
InvocableHandlerMethod这个子类虽然它提供了调用了能力,可是它却依旧尚未和Servlet的API绑定起来,毕竟使用的是Spring本身通用的的NativeWebRequest,so很容易想到它还有一个子类就是干这事的~异步

ServletInvocableHandlerMethod

它是对InvocableHandlerMethod的扩展,它增长了返回值和响应状态码的处理,另外在ServletInvocableHandlerMethod有个内部类ConcurrentResultHandlerMethod继承于它,支持异常调用结果处理,Servlet容器下Controller在查找适配器时发起调用的最终就是ServletInvocableHandlerMethodide

public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
    private static final Method CALLABLE_METHOD = ClassUtils.getMethod(Callable.class, "call");

    // 处理方法返回值
    @Nullable
    private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

    // 构造函数略
    
    // 设置处理返回值的HandlerMethodReturnValueHandler
    public void setHandlerMethodReturnValueHandlers(HandlerMethodReturnValueHandlerComposite returnValueHandlers) {
        this.returnValueHandlers = returnValueHandlers;
    }


    // 它不是复写,可是是对invokeForRequest方法的进一步加强  由于调用目标方法仍是靠invokeForRequest
    // 本处是把方法的返回值拿来进一步处理~~~好比状态码之类的
    public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
        Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
        // 设置HttpServletResponse返回状态码 这里面仍是有点意思的  由于@ResponseStatus#code()在父类已经解析了  可是子类才用
        setResponseStatus(webRequest);


        // 重点是这一句话:mavContainer.setRequestHandled(true); 表示该请求已经被处理过了
        if (returnValue == null) {

            // Request的NotModified为true 有@ResponseStatus注解标注 RequestHandled=true 三个条件有一个成立,则设置请求处理完成并返回
            if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
                mavContainer.setRequestHandled(true);
                return;
            }
        // 返回值不为null,@ResponseStatus存在reason 一样设置请求处理完成并返回
        } else if (StringUtils.hasText(getResponseStatusReason())) {
            mavContainer.setRequestHandled(true);
            return;
        }

        // 前边都不成立,则设置RequestHandled=false即请求未完成
        // 继续交给HandlerMethodReturnValueHandlerComposite处理
        // 可见@ResponseStatus的优先级仍是蛮高的~~~~~
        mavContainer.setRequestHandled(false);
        Assert.state(this.returnValueHandlers != null, "No return value handlers");
        try {
        
            // 关于对方法返回值的处理,参见:https://blog.csdn.net/f641385712/article/details/90370542
            this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
        } catch (Exception ex) {
            if (logger.isTraceEnabled()) {
                logger.trace(formatErrorForReturnValue(returnValue), ex);
            }
            throw ex;
        }
    }

    // 设置返回的状态码到HttpServletResponse 里面去
    private void setResponseStatus(ServletWebRequest webRequest) throws IOException {
        HttpStatus status = getResponseStatus();
        if (status == null) { // 若是调用者没有标注ResponseStatus.code()此注解  此处就忽略它
            return;
        }

        HttpServletResponse response = webRequest.getResponse();
        if (response != null) {
            String reason = getResponseStatusReason();

            // 此处务必注意:如有reason,那就是sendError  哪怕你是200哦~
            if (StringUtils.hasText(reason)) {
                response.sendError(status.value(), reason);
            } else {
                response.setStatus(status.value());
            }
        }

        // 设置到request的属性,把响应码给过去。为了在redirect中使用
        // To be picked up by RedirectView
        webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, status);
    }

    private boolean isRequestNotModified(ServletWebRequest webRequest) {
        return webRequest.isNotModified();
    }


    // 这个方法RequestMappingHandlerAdapter里有调用
    ServletInvocableHandlerMethod wrapConcurrentResult(Object result) {
        return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result));
    }

    // 内部类们
    private class ConcurrentResultMethodParameter extends HandlerMethodParameter {
        @Nullable
        private final Object returnValue;
        private final ResolvableType returnType;
        public ConcurrentResultMethodParameter(Object returnValue) {
            super(-1);
            this.returnValue = returnValue;
            // 主要是这个解析 兼容到了泛型类型 好比你的返回值是List<Person> 它也能把你的类型拿出来
            this.returnType = (returnValue instanceof ReactiveTypeHandler.CollectedValuesList ?
                    ((ReactiveTypeHandler.CollectedValuesList) returnValue).getReturnType() :
                    ResolvableType.forType(super.getGenericParameterType()).getGeneric());
        }

        // 若返回的是List  这里就是List的类型哦  下面才是返回泛型类型
        @Override
        public Class<?> getParameterType() {
            if (this.returnValue != null) {
                return this.returnValue.getClass();
            }
            if (!ResolvableType.NONE.equals(this.returnType)) {
                return this.returnType.toClass();
            }
            return super.getParameterType();
        }

        // 返回泛型类型
        @Override
        public Type getGenericParameterType() {
            return this.returnType.getType();
        }


        // 即便实际返回类型为ResponseEntity<Flux<T>>,也要确保对@ResponseBody-style处理从reactive 类型中收集值
        // 是对reactive 的一种兼容
        @Override
        public <T extends Annotation> boolean hasMethodAnnotation(Class<T> annotationType) {
            // Ensure @ResponseBody-style handling for values collected from a reactive type
            // even if actual return type is ResponseEntity<Flux<T>>
            return (super.hasMethodAnnotation(annotationType) ||
                    (annotationType == ResponseBody.class && this.returnValue instanceof ReactiveTypeHandler.CollectedValuesList));
        }
    }


    // 这个很是有意思   内部类继承了本身(外部类) 进行加强
    private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod {
        // 返回值
        private final MethodParameter returnType;

        // 此构造最终传入的handler是个Callable
        // result方法返回值 它支持支持异常调用结果处理
        public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) {
            super((Callable<Object>) () -> {
                if (result instanceof Exception) {
                    throw (Exception) result;
                } else if (result instanceof Throwable) {
                    throw new NestedServletException("Async processing failed", (Throwable) result);
                }
                return result;
            }, CALLABLE_METHOD);


            // 给外部类把值设置上  由于wrapConcurrentResult通常都先调用,是对本类的一个加强
            if (ServletInvocableHandlerMethod.this.returnValueHandlers != null) {
                setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
            }
            this.returnType = returnType;
        }
        ...
    }
}

HandlerMethod用于封装Handler和处理请求的MethodInvocableHandlerMethod增长了方法参数解析和调用方法的能力;ServletInvocableHandlerMethod在此基础上在增长了以下三个能力:

  1. @ResponseStatus注解的支持

    1.当一个方法注释了`@ResponseStatus`后,**响应码就是注解上的响应码**。 **而且,而且若是returnValue=null或者reason不为空**(不为null且不为“”),将中断处理直接返回(再也不渲染页面)
  2. 对返回值returnValue的处理

    1. 对返回值的处理是使用`HandlerMethodReturnValueHandlerComposite`完成的
  3. 异步处理结果的处理

使用示例

文首说了,HandlerMethod做为一个非公开API,若是你要直接使用起来,仍是稍微要费点劲的。
但本文仍是给出一个Demo,给出小伙伴们最为关心也是对大家最有用的一个需求:ModelFactory.getNameForParameter(parameter)这个静态方法是给入参生成默认名称的,固然默认处理方案最底层依赖的是它Conventions.getVariableNameForParameter(parameter),为了验证这块对象、Object、List等等经常使用数据结构的默认处理,此处我借助HandlerMethod一次性所有打印出这个结论:

@Getter
@Setter
@ToString
public class Person {

    @NotNull
    private String name;
    @NotNull
    @Positive
    private Integer age;

    public Object demoMethod(Person person, Object object,
                             List<Integer> intList, List<Person> personList,
                             Set<Integer> intSet, Set<Person> personSet,
                             Map<String, Object> myMap,
                             String name, Integer age,
                             int number, double money) {
        return "hello parameter";
    }
}

借助HandlerMethod完成此测试用例

public static void main(String[] args) {
        // 准备一个HandlerMethod
        HandlerMethod handlerMethod = new HandlerMethod(new Person(), getPersonSpecfyMethod());
        // 拿到该方法全部的参数
        MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
        for (MethodParameter parameter : methodParameters) {
            Class<?> parameterType = parameter.getParameterType();
            String nameForParameter = ModelFactory.getNameForParameter(parameter);
            System.out.println("类型" + parameterType.getName() + "--->缺省的modelKey是:" + nameForParameter);
        }
    }

    private static Method getPersonSpecfyMethod() {
        for (Method method : Person.class.getMethods())
            if (method.getName().equals("demoMethod"))
                return method;
        return null;
    }

运行,打印结果以下:

类型com.fsx.bean.Person--->缺省的modelKey是:person
类型java.lang.Object--->缺省的modelKey是:object
类型java.util.List--->缺省的modelKey是:integerList
类型java.util.List--->缺省的modelKey是:personList
类型java.util.Set--->缺省的modelKey是:integerList // 能够看到即便是set 名称也是同List的
类型java.util.Set--->缺省的modelKey是:personList
类型java.util.Map--->缺省的modelKey是:map
类型java.lang.String--->缺省的modelKey是:string
类型java.lang.Integer--->缺省的modelKey是:integer
类型int--->缺省的modelKey是:int
类型double--->缺省的modelKey是:double

这个结果是不一样类型对应的缺省的ModelKey,但愿小伙伴们可以记下来,这对理解和正确使用`
@SessionAttribute、@ModelAttribute`都是很重要的~

总结

HandlerMethod虽然接触少,但并不影响它的重要性。在理解Spring MVC的处理流程上它很重要,在与使用者关系较大的拦截器HandlerInterceptor定制化处理的时候,学会使用它同样是很是有必要的。

在最后还提示你们一个你可能没有关心到的小细节:

  1. HandlerMethod位于org.springframework.web.method包下,且是3.1后才有的
  2. MethodParameter位于org.springframework.core核心包中。2.0就存在了

相关阅读

【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler

知识交流

==The last:若是以为本文对你有帮助,不妨点个赞呗。固然分享到你的朋友圈让更多小伙伴看到也是被做者本人许可的~==

**若对技术内容感兴趣能够加入wx群交流:Java高工、架构师3群
若群二维码失效,请加wx号:fsx641385712(或者扫描下方wx二维码)。而且备注:"java入群" 字样,会手动邀请入群**

若文章格式混乱或者图片裂开,请点击`: 原文连接-原文连接-原文连接

==若对Spring、SpringBoot、MyBatis等源码分析感兴趣,可加我wx:fsx641385712,手动邀请你入群一块儿飞==

相关文章
相关标签/搜索