SpringMVC底层(二)

笔记参考B站黑马程序员视频:spring原理

1.对象绑定与类型转换

类型转换是将一种数据类型转换为另一种数据类型的过程。在Spring中,要确保外部数据正确转换为Java的属性类型。 底层第一套转换接口与实现 Spring提供

  • Printer 把其它类型转为 String
  • Parser 把 String 转为其它类型
  • Formatter 综合 Printer 与 Parser 功能
  • Converter 把类型 S 转为类型 T
  • Printer、Parser、Converter 经过适配转换成 GenericConverter 放入 Converters 集合
  • FormattingConversionService 利用它们实现转换

底层第二套转换接口 JDK提供

  • PropertyEditor 把 String 与其它类型相互转换
  • PropertyEditorRegistry 可以注册多个 PropertyEditor 对象
  • 与第一套接口直接可以通过 FormatterPropertyEditorAdapter 来进行适配

 

高层接口与实现

  • 它们都实现了 TypeConverter 这个高层转换接口,在转换时,会用到 TypeConverter Delegate 委派ConversionService 与 PropertyEditorRegistry 真正执行转换(Facade 门面模式),首先看是否有自定义转换器, @InitBinder 添加的即属于这种 (用了适配器模式把 Formatter 转为需要的 PropertyEditor),再看有没有 ConversionService 转换,再利用默认的 PropertyEditor 转换,最后有一些特殊处理
  • SimpleTypeConverter 仅做类型转换
  • BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property(Get、Set方法)
  • DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field
  • ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能

演示 SimpleTypeConverter 仅做类型转换

package com.dreams.demo23;

import org.springframework.beans.SimpleTypeConverter;
import java.util.Date;

public class TestSimpleConverter {
    public static void main(String[] args) {
        // 仅有类型转换的功能
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();
        Integer number = typeConverter.convertIfNecessary("13", int.class);
        Date date = typeConverter.convertIfNecessary("1999/03/04", Date.class);
        System.out.println(number);
        System.out.println(date);
    }
}

 

BeanWrapperImpl 为 bean 的属性赋值,当需要时做类型转换,走 Property(Get、Set方法) 也就是需要提供Getting Setting方法。

package com.dreams.demo23;

import org.springframework.beans.BeanWrapperImpl;
import java.util.Date;

public class TestBeanWrapper {
    public static void main(String[] args) {
        // 利用反射原理, 为 bean 的属性赋值
        MyBean target = new MyBean();
        BeanWrapperImpl wrapper = new BeanWrapperImpl(target);
        wrapper.setPropertyValue("a", "10");
        wrapper.setPropertyValue("b", "hello");
        wrapper.setPropertyValue("c", "1999/03/04");
        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;

        public int getA() {
            return a;
        }

        public void setA(int a) {
            this.a = a;
        }

        public String getB() {
            return b;
        }

        public void setB(String b) {
            this.b = b;
        }

        public Date getC() {
            return c;
        }

        public void setC(Date c) {
            this.c = c;
        }

        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

 

DirectFieldAccessor 为 bean 的属性赋值,当需要时做类型转换,走 Field 这里不需要提供Getting,Setting方法

package com.dreams.demo23;

import org.springframework.beans.DirectFieldAccessor;
import java.util.Date;

public class TestFieldAccessor {
    public static void main(String[] args) {
        // 利用反射原理, 为 bean 的属性赋值
        MyBean target = new MyBean();
        DirectFieldAccessor accessor = new DirectFieldAccessor(target);
        accessor.setPropertyValue("a", "10");
        accessor.setPropertyValue("b", "hello");
        accessor.setPropertyValue("c", "1999/03/04");
        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;
        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

 

ServletRequestDataBinder 为 bean 的属性执行绑定,当需要时做类型转换,根据 directFieldAccess 选择走 Property 还是 Field,具备校验与获取校验结果功能 下面不提供Getting,Setting方法,且不配置 directFieldAccess 非web环境使用DataBinder

package com.dreams.demo23;

import org.springframework.beans.MutablePropertyValues;
import org.springframework.validation.DataBinder;
import java.util.Date;

public class TestDataBinder {

    public static void main(String[] args) {
        // 执行数据绑定
        MyBean target = new MyBean();
        DataBinder dataBinder = new DataBinder(target);
        //dataBinder.initDirectFieldAccess();
        MutablePropertyValues pvs = new MutablePropertyValues();
        pvs.add("a", "10");
        pvs.add("b", "hello");
        pvs.add("c", "1999/03/04");
        dataBinder.bind(pvs);
        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;

        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

无法赋值

 

将注释删除

dataBinder.initDirectFieldAccess();

  如果是web环境的数据绑定,不使用DataBinder,而使用ServletRequestDataBinder

package com.dreams.demo23;

import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import java.util.Date;

public class TestServletDataBinder {

    public static void main(String[] args) {
        // web 环境下数据绑定
        MyBean target = new MyBean();
        ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(target);
        dataBinder.initDirectFieldAccess();
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("a", "10");
        request.setParameter("b", "hello");
        request.setParameter("c", "1999/03/04");
        dataBinder.bind(new ServletRequestParameterPropertyValues(request));

        System.out.println(target);
    }

    static class MyBean {
        private int a;
        private String b;
        private Date c;

        @Override
        public String toString() {
            return "MyBean{" +
                   "a=" + a +
                   ", b='" + b + '\'' +
                   ", c=" + c +
                   '}';
        }
    }
}

 

2.绑定器工厂自定义类型转换

对于request.setParameter(“birthday”, “1999|01|02”),无法识别1999|01|02的类型 但是request.setParameter(“address.name”, “西安”),可以识别address.name

package com.dreams.demo23;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;

import java.util.Date;

public class ServletDataBinderFactoryDemo {
    public static void main(String[] args) throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("birthday", "1999|01|02");
        request.setParameter("address.name", "西安");

        User target = new User();
        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
        dataBinder.bind(new ServletRequestParameterPropertyValues(request));
        System.out.println(target);
    }

    public static class User {
        //@DateTimeFormat(pattern = "yyyy|MM|dd")
        private Date birthday;
        private Address address;

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public Date getBirthday() {
            return birthday;
        }

        public void setBirthday(Date birthday) {
            this.birthday = birthday;
        }

        @Override
        public String toString() {
            return "User{" +
                   "birthday=" + birthday +
                   ", address=" + address +
                   '}';
        }
    }

    public static class Address {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Address{" +
                   "name='" + name + '\'' +
                   '}';
        }
    }
}

我们可以将创建DataBinder的工作交给工厂,这样便于扩展。 ServletRequestDataBinderFactory 是 Spring Framework 中的一个接口,用于创建 ServletRequestDataBinder 对象的工厂。 createBinder(NativeWebRequest webRequest, Object target, String objectName) 方法是接口中唯一的方法。它接收三个参数:

  • webRequest:表示当前的 Web 请求。
  • target:要绑定数据的目标对象。可以为 null,表示要创建一个新的绑定器,或者传入一个现有的对象。
  • objectName:目标对象的名称,用于绑定过程中的日志记录和异常处理。

修改一下main方法:

public static void main(String[] args) throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("birthday", "1999|01|02");
    request.setParameter("address.name", "西安");

    User target = new User();
    ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

    WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
    dataBinder.bind(new ServletRequestParameterPropertyValues(request));
    System.out.println(target);
}

虽然birthday还是null,但是是能够正常替代直接new这个dataBinder

 

@InitBinder使用

@InitBinder 是 Spring MVC 中的一个注解,用于自定义数据绑定规则和初始化数据绑定器。

使用它需要一个类,注解标注在方法上

initBinder 方法使用了 @InitBinder 注解,并且接收一个 WebDataBinder 参数。
在 test 方法中,然后通过 dataBinder.addCustomFormatter() 方法添加一个自定义的日期格式化器 MyDateFormatter。这个格式化器被用来将特定格式的日期字符串转换为 Date 类型

static class MyController {
    @InitBinder
    public void test(WebDataBinder dataBinder) {
        // 扩展 dataBinder 的转换器
        dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
    }
}

 

MyDateFormatter类如下,继承上面所说的Formatter接口

print 方法: 实现了 Formatter 接口的 print 方法,它接收一个 Date 对象和 Locale 对象,将 Date 对象格式化为指定格式的字符串。在这里,使用了 SimpleDateFormat 将日期格式化为 “yyyy|MM|dd” 的字符串格式。

parse 方法: 同样实现了 Formatter 接口的 parse 方法,它接收一个字符串 text 和 Locale 对象,并尝试将字符串解析为 Date 对象。在解析过程中,通过 SimpleDateFormat 将指定格式的字符串解析为 Date 对象。同时,在方法开始时记录了调试信息,展示了传入的描述信息 desc。

package com.dreams.demo23;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.Formatter;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class MyDateFormatter implements Formatter<Date> {
    private static final Logger log = LoggerFactory.getLogger(MyDateFormatter.class);
    private final String desc;

    public MyDateFormatter(String desc) {
        this.desc = desc;
    }

    @Override
    public String print(Date date, Locale locale) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
        return sdf.format(date);
    }

    @Override
    public Date parse(String text, Locale locale) throws ParseException {
        log.debug(">>>>>> 进入了: {}", desc);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy|MM|dd");
        return sdf.parse(text);
    }


}

 

修改mian方法

我们需要创建一个InvocableHandlerMethod类,它是 Spring MVC 中用于处理请求的处理方法的封装类。在这里,通过指定 上面定义的MyController 的实例和其 test 方法(该方法接受一个 WebDataBinder 参数)来创建一个 InvocableHandlerMethod 实例

传入给ServletRequestDataBinderFactory类即可

ServletRequestDataBinderFactory 的构造函数需要两个参数:

  • List<InvocableHandlerMethod> methods:
    这是一个 List 类型的参数,其中包含了 InvocableHandlerMethod 对象的实例。
  • WebBindingInitializer initializer:
    这是一个可选的参数,用于配置数据绑定的初始化设置。通常情况下,可以传入一个实现了 WebBindingInitializer 接口的对象,用于配置例如类型转换器、格式化器等。这里没有用到,传入了 null。
public static void main(String[] args) throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("birthday", "1999|01|02");
    request.setParameter("address.name", "西安");

    User target = new User();
    // 用 @InitBinder 转换            PropertyEditorRegistry PropertyEditor
    InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("test", WebDataBinder.class));
    ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), null);

    WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
    dataBinder.bind(new ServletRequestParameterPropertyValues(request));
    System.out.println(target);
}

 

完整代码如下:

package com.dreams.demo23;

import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.ServletRequestParameterPropertyValues;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;

import java.util.Collections;
import java.util.Date;

public class ServletDataBinderFactoryDemo {
    public static void main(String[] args) throws Exception {
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("birthday", "1999|01|02");
        request.setParameter("address.name", "西安");

        User target = new User();

        // 用 @InitBinder 转换            PropertyEditorRegistry PropertyEditor
        InvocableHandlerMethod method = new InvocableHandlerMethod(new MyController(), MyController.class.getMethod("test", WebDataBinder.class));
        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(Collections.singletonList(method), null);

        WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
        dataBinder.bind(new ServletRequestParameterPropertyValues(request));
        System.out.println(target);
    }


    static class MyController {
        @InitBinder
        public void test(WebDataBinder dataBinder) {
            // 扩展 dataBinder 的转换器
            dataBinder.addCustomFormatter(new MyDateFormatter("用 @InitBinder 方式扩展的"));
        }
    }

    public static class User {
        @DateTimeFormat(pattern = "yyyy|MM|dd")
        private Date birthday;
        private Address address;

        public Address getAddress() {
            return address;
        }

        public void setAddress(Address address) {
            this.address = address;
        }

        public Date getBirthday() {
            return birthday;
        }

        public void setBirthday(Date birthday) {
            this.birthday = birthday;
        }

        @Override
        public String toString() {
            return "User{" +
                   "birthday=" + birthday +
                   ", address=" + address +
                   '}';
        }
    }

    public static class Address {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Address{" +
                   "name='" + name + '\'' +
                   '}';
        }
    }
}

可以看到成功转换了

 

这里的底层是使用JDK自带的PropertyEditorRegistry

 

ConversionService 转换

FormattingConversionService 是 Spring Framework 提供的一个用于格式化和转换数据的服务类。通过 addFormatter 方法将自定义的格式化器注册到 service 中,以便在后续的数据绑定过程中使用。

ConfigurableWebBindingInitializer 是一个可以配置的数据绑定初始化器。在这里,将之前创建的 FormattingConversionService 实例 service 设置到 initializer 中。这样做的目的是为了在数据绑定过程中使用 service 中的格式化和转换规则。

ServletRequestDataBinderFactory 这里传入了上面的 initializer 参数,binderMethods不配置为null即可。

public static void main(String[] args) throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("birthday", "1999|01|02");
    request.setParameter("address.name", "西安");

    User target = new User();
    // 用 ConversionService 转换   ConversionService Formatter
    FormattingConversionService service = new FormattingConversionService();
    service.addFormatter(new MyDateFormatter("用 ConversionService 方式扩展转换功能"));
    ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
    initializer.setConversionService(service);
    ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

    WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
    dataBinder.bind(new ServletRequestParameterPropertyValues(request));
    System.out.println(target);
}

ServletRequestDataBinderFactory如果构造两个都传入,@InitBinder优先级更高一点。

 

 

使用默认 ConversionService 转换

只需要在实体类加上注解

@DateTimeFormat(pattern = "yyyy|MM|dd")
private Date birthday;

main方法使用DefaultFormattingConversionService,用于提供默认的格式化和转换服务

public static void main(String[] args) throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("birthday", "1999|01|02");
    request.setParameter("address.name", "西安");

    User target = new User();

    // 使用默认 ConversionService 转换
    DefaultFormattingConversionService service = new  DefaultFormattingConversionService();
    ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
    initializer.setConversionService(service);
    ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

    WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
    dataBinder.bind(new ServletRequestParameterPropertyValues(request));
    System.out.println(target);
}

 

如果是springboot项目中,可以使用ApplicationConversionService

public static void main(String[] args) throws Exception {
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setParameter("birthday", "1999|01|02");
    request.setParameter("address.name", "西安");

    User target = new User();

    // 使用默认 ConversionService 转换
    ApplicationConversionService service = new ApplicationConversionService();
    ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
    initializer.setConversionService(service);
    ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, initializer);

    WebDataBinder dataBinder = factory.createBinder(new ServletWebRequest(request), target, "user");
    dataBinder.bind(new ServletRequestParameterPropertyValues(request));
    System.out.println(target);
}

 

 

3.@ControllerAdvice

@ControllerAdvice 是 Spring Framework 中一个注解,它可以用来集中处理全局控制器的异常和数据绑定问题。主要用途包括:

  • 全局异常处理:
    使用 @ExceptionHandler 注解,可以在 @ControllerAdvice 注解的类中定义异常处理方法,用于处理应用程序中所有控制器抛出的特定异常。这种方法可以避免在每个控制器中重复编写相同的异常处理逻辑,提高代码的复用性和可维护性。
  • 全局数据绑定:
    使用 @InitBinder 注解,可以在 @ControllerAdvice 注解的类中定义全局的数据绑定规则。通过自定义绑定器初始化方法,可以注册全局的数据格式化器、校验器等,以确保每个控制器在数据绑定时都遵循统一的规则。
  • 全局模型属性:
    使用 @ModelAttribute 注解的方法,可以在 @ControllerAdvice 注解的类中定义全局的模型属性,这些属性可以在所有控制器中共享使用。这对于在多个控制器中需要共享的模型属性或者视图渲染前需要预处理的模型数据非常有用。
  • 上面的三个注解如果加在@Controller上就是只针对当前控制器了

注意:@ControllerAdvice虽然加了Advice,但是底层没有使用AOP切面或代理。

 

@InitBinder 的来源有两个:

  • @ControllerAdvice 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  • @Controller 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 会在控制器方法首次执行时解析并记录

RequestMappingHandlerAdapter 使用 @InitBinder 方法来初始化 WebDataBinder,以确保在执行控制器方法之前,所有请求参数都能按照预期的方式进行数据绑定和转换。注意执行顺序。

在RequestMappingHandlerAdapter中,这两个声明表示在Spring MVC中常用的用于存储控制器通知类中@InitBinder方法的缓存机制:

initBinderCache:这个缓存存储与控制器类直接关联的@InitBinder方法。Class<?>作为键表示控制器类,Set<Method>作为值包含该控制器类中声明的所有@InitBinder方法。也就是每一个控制器类中的方法所有InitBinder方法集合。通过为每个控制器类缓存@InitBinder方法,Spring MVC避免在每个请求中重复扫描和分析控制器类以寻找@InitBinder方法。

initBinderAdviceCache:这个缓存存储与@ControllerAdvice注解类关联的@InitBinder方法,控制器通知类可以全局定义适用于多个控制器类的@InitBinder方法。也就是存储@ControllerAdvice配置的全局生效的@InitBinder方法,这个缓存通过每个@ControllerAdvice bean (ControllerAdviceBean)来组织@InitBinder方法,确保高效地检索和应用这些方法。使用LinkedHashMap,保留插入顺序。

 

下面测试一下,上面两个缓存的初始化时机。

定义一个配置类,@ControllerAdvice注解和@Controller的情况

package com.dreams.demo24;

import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

@Configuration
public class WebConfig {

    @ControllerAdvice
    static class MyControllerAdvice {
        @InitBinder
        public void binder3(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder3 转换器"));
        }
    }

    @Controller
    static class Controller1 {
        @InitBinder
        public void binder1(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder1 转换器"));
        }

        public void foo() {

        }
    }

    @Controller
    static class Controller2 {
        @InitBinder
        public void binder21(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder21 转换器"));
        }

        @InitBinder
        public void binder22(WebDataBinder webDataBinder) {
            webDataBinder.addCustomFormatter(new MyDateFormatter("binder22 转换器"));
        }

        public void bar() {

        }
    }

}

 

showBindMethods只是简单的反射获取initBinderCache和initBinderAdviceCache

package com.dreams.demo24;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;


public class Demo {

    private static final Logger log = LoggerFactory.getLogger(Demo.class);

    public static void main(String[] args) throws Exception {

        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

        RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
        // 不把handlerAdapter交给容器,所以需要手动设置回调
        handlerAdapter.setApplicationContext(context);
        handlerAdapter.afterPropertiesSet();

        log.debug("1. 刚开始...");
        showBindMethods(handlerAdapter);

        context.close();
    }

    @SuppressWarnings("all")
    private static void showBindMethods(RequestMappingHandlerAdapter handlerAdapter) throws NoSuchFieldException, IllegalAccessException {
        Field initBinderAdviceCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderAdviceCache");
        initBinderAdviceCache.setAccessible(true);
        Map<ControllerAdviceBean, Set<Method>> globalMap = (Map<ControllerAdviceBean, Set<Method>>) initBinderAdviceCache.get(handlerAdapter);
        log.debug("全局的 @InitBinder 方法 {}",
                globalMap.values().stream()
                        .flatMap(ms -> ms.stream().map(m -> m.getName()))
                        .collect(Collectors.toList())
        );

        Field initBinderCache = RequestMappingHandlerAdapter.class.getDeclaredField("initBinderCache");
        initBinderCache.setAccessible(true);
        Map<Class<?>, Set<Method>> controllerMap = (Map<Class<?>, Set<Method>>) initBinderCache.get(handlerAdapter);
        log.debug("控制器的 @InitBinder 方法 {}",
                controllerMap.entrySet().stream()
                        .flatMap(e -> e.getValue().stream().map(v -> e.getKey().getSimpleName() + "." + v.getName()))
                        .collect(Collectors.toList())
        );
    }
}

可以看到initBinderAdviceCache成功赋值了,而initBinderCache还没有

 

initBinderAdviceCache的初始化时机在handlerAdapter.afterPropertiesSet();

 

下面给模拟调用Controller1的 foo方法后再反射获取一下initBinderCache和initBinderAdviceCache

public static void main(String[] args) throws Exception {

    AnnotationConfigApplicationContext context =
            new AnnotationConfigApplicationContext(WebConfig.class);

    RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
    // 不把handlerAdapter交给容器,所以需要手动设置回调
    handlerAdapter.setApplicationContext(context);
    handlerAdapter.afterPropertiesSet();

    log.debug("1. 刚开始...");
    showBindMethods(handlerAdapter);


    Method getDataBinderFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getDataBinderFactory", HandlerMethod.class);
    getDataBinderFactory.setAccessible(true);

    log.debug("2. 模拟调用 Controller1 的 foo 方法时 ...");
    // 这里是模拟调用Controller1的foo方法
    getDataBinderFactory.invoke(handlerAdapter, new HandlerMethod(new WebConfig.Controller1(), WebConfig.Controller1.class.getMethod("foo")));
    showBindMethods(handlerAdapter);

    context.close();
}

可以看到controller1的方法加入了initBinderCache缓存,也就是说每一个控制器类中的方法所有InitBinder方法集合只在普通控制器方法被调用是才加载。

 

@InitBinder 的初始化时机:

  • @ControllerAdvice 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 在初始化时解析并记录
  • @Controller 中 @InitBinder 标注的方法,由 RequestMappingHandlerAdapter 会在控制器方法首次执行时解析并记录
  • 对@InitBinder方法缓存,避免在每个请求中重复扫描和分析控制器类以寻找@InitBinder方法,确保高效地应用这些方法。

 

4.控制器方法执行流程

HandlerMethod 需要

  • bean 即是哪个 Controller
  • method 即是 Controller 中的哪个方法

ServletInvocableHandlerMethod 需要

  • WebDataBinderFactory 负责对象绑定、类型转换
  • ParameterNameDiscoverer 负责参数名解析
  • HandlerMethodArgumentResolverComposite 负责解析参数
  • HandlerMethodReturnValueHandlerComposite 负责处理返回值

 

图解:

第一步

第二步

下面使用代码演示流程

配置类,这里加入@ResponseStatus(HttpStatus.OK)配置,可以先暂时不用管返回值处理器。

package com.dreams.demo26;

import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;

@Configuration
public class WebConfig {
    
    @Controller
    static class Controller1 {
        @ResponseStatus(HttpStatus.OK)
        public ModelAndView foo(@ModelAttribute("u") User user) {
            System.out.println("foo");
            return null;
        }
    }

    static class User {
        private String name;

        public void setName(String name) {
            this.name = name;
        }

        public String getName() {

            return name;
        }

        @Override
        public String toString() {
            return "User{" +
                   "name='" + name + '\'' +
                   '}';
        }
    }
}

下面来测试

package com.dreams.demo26;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.ModelFactory;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.*;

import java.lang.reflect.Method;
import java.util.Collections;

import static com.dreams.demo26.WebConfig.Controller1;
import static com.dreams.demo26.WebConfig.User;

public class Demo {

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

        RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
        adapter.setApplicationContext(context);
        adapter.afterPropertiesSet();

        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setParameter("name", "张三");
        /*
            现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
         */
        ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
                new Controller1(), Controller1.class.getMethod("foo", User.class));

        ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);

        handlerMethod.setDataBinderFactory(factory);
        handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
        handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));

        ModelAndViewContainer container = new ModelAndViewContainer();

        // 获取模型工厂方法
        Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
        getModelFactory.setAccessible(true);
        ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

        // 初始化模型数据
        modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

        handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);

        System.out.println(container.getModel());

        context.close();
    }

    public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
        composite.addResolvers(
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
                new PathVariableMethodArgumentResolver(),
                new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletRequestMethodArgumentResolver(),
                new ServletModelAttributeMethodProcessor(false),
                new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
                new ServletModelAttributeMethodProcessor(true),
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
        );
        return composite;
    }



}

 

代码解释:

这执行器

RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

 

现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起,并完成控制器方法的调用,如下

ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
        new Controller1(), Controller1.class.getMethod("foo", User.class));

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));

这里的getArgumentResolvers方法就是参数解析器

public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
    HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
    composite.addResolvers(
            new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
            new PathVariableMethodArgumentResolver(),
            new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
            new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
            new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
            new ServletRequestMethodArgumentResolver(),
            new ServletModelAttributeMethodProcessor(false),
            new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
            new ServletModelAttributeMethodProcessor(true),
            new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
    );
    return composite;
}

 

这里可以直接执行一下

ModelAndViewContainer container = new ModelAndViewContainer();
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());

任何模型对象都会放在ModelAndViewContainer中。

ModelAndViewContainer 是Spring MVC中用于存储控制器方法处理结果的容器。它包含了模型数据(即控制器方法处理后产生的数据)以及视图名称等信息。

invokeAndHandle 方法用于执行并处理控制器方法,并将处理结果存储在 container 中。

container.getModel() 返回一个 Map<String, Object>,其中包含了控制器方法处理后的模型数据。

对照着这张图,流程就是ServletRequestDataBinderFactory调用HandlerMethodArgumentResolverComposite参数解析器,去匹配当前方法的参数是哪个注解该匹配哪个解析器,这里是@ModelAttribute对应的ServletModelAttributeMethodProcessor参数解析器,然后绑定参数依靠WebDataBinderFactory,这里是@ModelAttribute模型数据,所以最后会加入ModelAndViewContainer ,默认名字是第一个字母小写其余正常。

 

@ModelAttribute还可以可以加在方法上,这样就不是被ServletModelAttributeMethodProcessor参数解析器解析了,而是直接由RequestMappingHandlerAdapter解析(由ModelFactory向ModelAndViewContainer补充数据),把他的返回值作为@ModelAttribute模型数据,最后会加入ModelAndViewContainer

WebConfig.java加入加在方法上@ModelAttribute示例

@ControllerAdvice
static class MyControllerAdvice {
    @ModelAttribute("a")
    public String aa() {
        return "aa";
    }
}

示例代码如下:

AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(WebConfig.class);

MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("name", "张三");
/*
    现在可以通过 ServletInvocableHandlerMethod 把这些整合在一起, 并完成控制器方法的调用, 如下
 */
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
        new Controller1(), Controller1.class.getMethod("foo", User.class));

ServletRequestDataBinderFactory factory = new ServletRequestDataBinderFactory(null, null);
handlerMethod.setDataBinderFactory(factory);
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));


ModelAndViewContainer container = new ModelAndViewContainer();
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setApplicationContext(context);
adapter.afterPropertiesSet();

// 获取模型工厂方法,方法为私有,所以通过反射获取
Method getModelFactory = RequestMappingHandlerAdapter.class.getDeclaredMethod("getModelFactory", HandlerMethod.class, WebDataBinderFactory.class);
getModelFactory.setAccessible(true);
ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

// 初始化模型数据
modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);
handlerMethod.invokeAndHandle(new ServletWebRequest(request), container);
System.out.println(container.getModel());

context.close();

利用反射调用 getModelFactory 方法,

ModelFactory modelFactory = (ModelFactory) getModelFactory.invoke(adapter, handlerMethod, factory);

传入了三个参数:

  • adapter:RequestMappingHandlerAdapter 的实例对象,用于执行方法调用。
  • handlerMethod:之前准备好的 ServletInvocableHandlerMethod 对象,表示要调用的控制器方法。
  • factory:之前创建的 ServletRequestDataBinderFactory 实例,用于数据绑定。

调用结果被强制转换为 ModelFactory 类型,并赋给 modelFactory 变量。

 

然后使用获取到的 modelFactory 调用其 initModel() 方法,初始化模型数据。

modelFactory.initModel(new ServletWebRequest(request), container, handlerMethod);

传入的参数包括:

  • new ServletWebRequest(request):使用 MockHttpServletRequest 对象 request 创建一个 ServletWebRequest 实例,表示当前的Web请求。
  • container:之前创建的 ModelAndViewContainer 对象,用于存储模型数据和视图。
  • handlerMethod:之前准备好的 ServletInvocableHandlerMethod 对象,表示要执行的控制器方法。

可以看到这样就加入了模型数据

 

5.返回值处理器

控制器方法可以返回多种类型的结果,例如视图名称、ModelAndView 对象、String、void、ResponseBody 注解标注的对象等。返回值处理器根据返回值的类型,决定如何处理这些结果。

因为需要用到视图,所以加入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

webConfig.java

package com.dreams.demo27;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.view.AbstractUrlBasedView;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerView;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

@Configuration
public class WebConfig {

    @Bean
    public FreeMarkerConfigurer freeMarkerConfigurer() {
        FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
        configurer.setDefaultEncoding("utf-8");
        configurer.setTemplateLoaderPath("classpath:templates");
        return configurer;
    }

    @Bean // FreeMarkerView 在借助 Spring 初始化时,会要求 web 环境才会走 setConfiguration, 这里想办法去掉了 web 环境的约束
    public FreeMarkerViewResolver viewResolver(FreeMarkerConfigurer configurer) {
        FreeMarkerViewResolver resolver = new FreeMarkerViewResolver() {
            @Override
            protected AbstractUrlBasedView instantiateView() {
                FreeMarkerView view = new FreeMarkerView() {
                    @Override
                    protected boolean isContextRequired() {
                        return false;
                    }
                };
                view.setConfiguration(configurer.getConfiguration());
                return view;
            }
        };
        resolver.setContentType("text/html;charset=utf-8");
        resolver.setPrefix("/");
        resolver.setSuffix(".ftl");
        resolver.setExposeSpringMacroHelpers(false);
        return resolver;
    }
}

辅助代码如下:

package com.dreams.demo27;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.mvc.method.annotation.*;
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Locale;

public class Demo {
    private static final Logger log = LoggerFactory.getLogger(Demo.class);

    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

        test(context);
    }

    public static HandlerMethodReturnValueHandlerComposite getReturnValueHandler() {
        HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
        composite.addHandler(new ModelAndViewMethodReturnValueHandler());
        composite.addHandler(new ViewNameMethodReturnValueHandler());
        composite.addHandler(new ServletModelAttributeMethodProcessor(false));
        composite.addHandler(new HttpEntityMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
        composite.addHandler(new HttpHeadersReturnValueHandler());
        composite.addHandler(new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
        composite.addHandler(new ServletModelAttributeMethodProcessor(true));
        return composite;
    }

    @SuppressWarnings("all")
    private static void renderView(AnnotationConfigApplicationContext context, ModelAndViewContainer container,
                                   ServletWebRequest webRequest) throws Exception {
        log.debug(">>>>>> 渲染视图");
        FreeMarkerViewResolver resolver = context.getBean(FreeMarkerViewResolver.class);
        String viewName = container.getViewName() != null ? container.getViewName() : new DefaultRequestToViewNameTranslator().getViewName(webRequest.getRequest());
        log.debug("没有获取到视图名, 采用默认视图名: {}", viewName);
        // 每次渲染时, 会产生新的视图对象, 它并非被 Spring 所管理, 但确实借助了 Spring 容器来执行初始化
        View view = resolver.resolveViewName(viewName, Locale.getDefault());
        view.render(container.getModel(), webRequest.getRequest(), webRequest.getResponse());
        System.out.println(new String(((MockHttpServletResponse) webRequest.getResponse()).getContentAsByteArray(), StandardCharsets.UTF_8));
    }

    static class Controller {
        private static final Logger log = LoggerFactory.getLogger(Controller.class);

        public ModelAndView test1() {
            log.debug("test1()");
            ModelAndView mav = new ModelAndView("view1");
            mav.addObject("name", "张三");
            return mav;
        }

        public String test2() {
            log.debug("test2()");
            return "view2";
        }

        @ModelAttribute
        public User test3() {
            log.debug("test3()");
            return new User("李四", 20);
        }

        public User test4() {
            log.debug("test4()");
            return new User("王五", 30);
        }

        public HttpEntity<User> test5() {
            log.debug("test5()");
            return new HttpEntity<>(new User("赵六", 40));
        }

        public HttpHeaders test6() {
            log.debug("test6()");
            HttpHeaders headers = new HttpHeaders();
            headers.add("Content-Type", "text/html");
            return headers;
        }

        @ResponseBody
        public User test7() {
            log.debug("test7()");
            return new User("钱七", 50);
        }
    }

    // 必须用 public 修饰, 否则 freemarker 渲染其 name, age 属性时失败
    public static class User {
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                   "name='" + name + '\'' +
                   ", age=" + age +
                   '}';
        }
    }
}

上面的代码逻辑在test方法中。

 

对于读取以下返回值ModelAndView

public ModelAndView test1() {
    log.debug("test1()");
    ModelAndView mav = new ModelAndView("view1");
    mav.addObject("name", "张三");
    return mav;
}

使用到的返回值处理器是ModelAndViewMethodReturnValueHandler

test方法逻辑如下:

  1. 先通过反射获取了 Controller 类中名为 test1 的方法,并调用它获取返回值。
  2. HandlerMethod 是 Spring MVC 中用来表示控制器方法的类,包括控制器实例和方法信息。
  3. ModelAndViewContainer 用于承载视图渲染所需的模型数据和视图名称。
  4. HandlerMethodReturnValueHandlerComposite 是返回值处理器的组合对象,用于根据返回值类型选择合适的处理器进行处理。类似前面的参数解析器集合。
  5. composite.supportsReturnType 检查是否有合适的返回值处理器支持当前方法的返回类型。
  6. composite.handleReturnValue 根据支持的处理器处理返回值,并将结果设置到 ModelAndViewContainer 中。
  7. 最后,调用 renderView 方法来渲染视图,将视图名称和模型数据输出。
private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test1");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

提供一个视图view1.ftl如下:

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view1</title>
</head>
<body>
    <h1>Hello! ${name}</h1>
</body>
</html>

运行得到如下,可以看到模型成功渲染了:

 

第二种方法就是处理返回字符串

public String test2() {
    log.debug("test2()");
    return "view2";
}

返回字符串的话,交给ViewNameMethodReturnValueHandler返回值处理器处理。

代码修改为调用test2,其余与上面一致。

private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test2");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    ServletWebRequest webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

提供视图view2.ftl

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>view2</title>
</head>
<body>
    <h1>Hello!</h1>
</body>
</html>

成功输出

 

第三种是处理返回 @ModelAttribute标注的对象

@ModelAttribute
public User test3() {
    log.debug("test3()");
    return new User("李四", 20);
}

对应的返回值处理器是ServletModelAttributeMethodProcessor

没有指定视图。默认走如@RequestMapping(“/test3”)标注的路径,也就是找test3视图,这里我们没有使用@RequestMapping,所以下面代码手动request.setRequestURI(“/test3”),然后UrlPathHelper.defaultInstance 使用默认的 UrlPathHelper 实例来解析和缓存请求的查找路径。resolveAndCacheLookupPath(request) 方法的作用是根据请求对象(这里是 request)解析出请求的路径,并将解析后的路径进行缓存

private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test3");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    request.setRequestURI("/test3");
    UrlPathHelper.defaultInstance.resolveAndCacheLookupPath(request);
    ServletWebRequest webRequest = new ServletWebRequest(request, new MockHttpServletResponse());
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        renderView(context, container, webRequest); // 渲染视图
    }
}

提供一个视图test3.ftl

<!doctype html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>test3</title>
</head>
<body>
    <h1>Hello! ${user.name} ${user.age}</h1>
</body>
</html>

运行如下:

 

对于返回这种对象的,默认为@ModelAttribute标注的对象,使用的处理器还是ServletModelAttributeMethodProcessor,只不过设置为true。其余一致。

public User test4() {
    log.debug("test4()");
    return new User("王五", 30);
}

 

下面是不需要视图的,不需要走视图渲染流程,也就是直接返回响应。

第四种

public HttpEntity<User> test5() {
    log.debug("test5()");
    return new HttpEntity<>(new User("赵六", 40));
}

对应的返回值处理器是HttpEntityMethodProcessor

在该处理器中,handleReturnValue方法中

mavContainer.setRequestHandled(true); 这行代码的作用是告知 Spring MVC 框架当前请求已经被处理完毕,不需要进一步的处理,也就是不会走后面的视图解析和视图渲染了。

container.isRequestHandled()用来用于检查请求是否已经被处理,也就是mavContainer.setRequestHandled(true)是否已设置。

private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test5");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        if (!container.isRequestHandled()) {
            renderView(context, container, webRequest); // 渲染视图
        } else {
            System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        }
    }
}

 

第五种

public HttpHeaders test6() {
    log.debug("test6()");
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/html");
    return headers;
}

对应的返回值处理器是HttpHeadersReturnValueHandler,同样调用了mavContainer.setRequestHandled(true)

private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test6");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        if (!container.isRequestHandled()) {
            renderView(context, container, webRequest); // 渲染视图
        } else {
            for (String name : response.getHeaderNames()) {
                System.out.println(name + "=" + response.getHeader(name));
            }
            System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        }
    }
}

 

第六种

@ResponseBody
public User test7() {
    log.debug("test7()");
    return new User("钱七", 50);
}

对应的返回值处理器是RequestResponseBodyMethodProcessor,同样调用了mavContainer.setRequestHandled(true)

代码改为调用test7其余不变

private static void test(AnnotationConfigApplicationContext context) throws Exception {
    Method method = Controller.class.getMethod("test7");
    Controller controller = new Controller();
    Object returnValue = method.invoke(controller); // 获取返回值

    HandlerMethod methodHandle = new HandlerMethod(controller, method);

    ModelAndViewContainer container = new ModelAndViewContainer();
    HandlerMethodReturnValueHandlerComposite composite = getReturnValueHandler();
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    if (composite.supportsReturnType(methodHandle.getReturnType())) { // 检查是否支持此类型的返回值
        composite.handleReturnValue(returnValue, methodHandle.getReturnType(), container, webRequest);
        System.out.println(container.getModel());
        System.out.println(container.getViewName());
        if (!container.isRequestHandled()) {
            renderView(context, container, webRequest); // 渲染视图
        } else {
            for (String name : response.getHeaderNames()) {
                System.out.println(name + "=" + response.getHeader(name));
            }
            System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
        }
    }
}

 

6.MessageConverter

MessageConverter 是用来处理请求和响应中的消息转换的接口。它的主要作用是在处理器方法(控制器方法)的参数绑定和返回值处理过程中,负责将 HTTP 请求中的内容(例如 JSON、XML 等格式的数据)转换成 Java 对象,或者将 Java 对象转换成 HTTP 响应的内容。

比如上面的@ResponsetBody的返回值处理器。

MappingJackson2HttpMessageConverter 是 Spring MVC 提供的一个 HttpMessageConverter 的实现,用于处理 JSON 格式的数据转换。它可以将 JSON 数据转换成 Java 对象,或者将 Java 对象转换成 JSON 格式的数据。

返回值处理器

RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter()))

参数解析器

RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter()))

RequestResponseBodyMethodProcessor特殊一点,即可以作为返回值处理器,也可以作为参数解析器。

 

辅助代码

package com.dreams.demo28;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.http.MockHttpOutputMessage;
import org.springframework.web.HttpMediaTypeNotAcceptableException;

import java.io.IOException;

public class Demo {
    public static void main(String[] args) throws IOException, NoSuchMethodException, HttpMediaTypeNotAcceptableException {
        test();
    }

    public static class User {
        private String name;
        private int age;

        @JsonCreator
        public User(@JsonProperty("name") String name, @JsonProperty("age") int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "User{" +
                   "name='" + name + '\'' +
                   ", age=" + age +
                   '}';
        }
    }
}

 

比如对象转JSON

  • MappingJackson2HttpMessageConverter 是 Spring MVC 中用于处理 JSON 格式的消息转换器。它能够将 Java 对象序列化为 JSON 字符串,并且能够处理反向的 JSON 反序列化操作。
  • MockHttpOutputMessage 是 Spring 测试框架中用于模拟 HTTP 输出消息的类。可以将转换后的 JSON 数据输出到这个消息中,然后进行断言或者输出检查。
  • converter.canWrite(User.class, MediaType.APPLICATION_JSON) 方法用于检查是否能够将 User 类型的对象转换为指定的 MediaType(这里是 APPLICATION_JSON)的格式
  • converter.write(new User(“张三”, 18), MediaType.APPLICATION_JSON, message) 方法将一个新建的 User 对象序列化为 JSON 格式,并写入到 message 中。
public static void test() throws IOException {
    MockHttpOutputMessage message = new MockHttpOutputMessage();
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    if (converter.canWrite(User.class, MediaType.APPLICATION_JSON)) {
        converter.write(new User("张三", 18), MediaType.APPLICATION_JSON, message);
        System.out.println(message.getBodyAsString());
    }
}

 

对象转XML

不过需要用到转换为xml,所以需要加入依赖

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

代码如下:

MappingJackson2XmlHttpMessageConverter 是 Spring MVC 中用于处理 XML 格式的消息转换器。它能够将 Java 对象序列化为 XML 字符串,并支持 XML 到 Java 对象的反序列化操作。

private static void test() throws IOException {
    MockHttpOutputMessage message = new MockHttpOutputMessage();
    MappingJackson2XmlHttpMessageConverter converter = new MappingJackson2XmlHttpMessageConverter();
    if (converter.canWrite(User.class, MediaType.APPLICATION_XML)) {
        converter.write(new User("李四", 20), MediaType.APPLICATION_XML, message);
        System.out.println(message.getBodyAsString());
    }
}

 

如果将JSON转换成对象,同样是MappingJackson2HttpMessageConverter

  • converter.canRead(User.class, MediaType.APPLICATION_JSON) 方法用于检查是否能够将指定的 JSON 格式数据转换为 User 类型的 Java 对象
  • converter.read(User.class, message) 方法将 MockHttpInputMessage 中的 JSON 数据反序列化为 User 对象,并将其返回给变量 read。
private static void test() throws IOException {
    String jsonString = "{\n" +
            "    \"name\": \"李四\",\n" +
            "    \"age\": 20\n" +
            "}";
    MockHttpInputMessage message = new MockHttpInputMessage(jsonString.getBytes(StandardCharsets.UTF_8));
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    if (converter.canRead(User.class, MediaType.APPLICATION_JSON)) {
        Object read = converter.read(User.class, message);
        System.out.println(read);
    }
}

 

而当如果存在多个消息转换器

这里以返回值处理

private static void test() throws IOException, HttpMediaTypeNotAcceptableException, NoSuchMethodException {
    MockHttpServletRequest request = new MockHttpServletRequest();
    MockHttpServletResponse response = new MockHttpServletResponse();
    ServletWebRequest webRequest = new ServletWebRequest(request, response);

    request.addHeader("Accept", "application/xml");
    response.setContentType("application/json");

    RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
            Arrays.asList(
                    new MappingJackson2HttpMessageConverter(), new MappingJackson2XmlHttpMessageConverter()
            ));
    processor.handleReturnValue(
            new User("张三", 18),
            new MethodParameter(Demo.class.getMethod("user"), -1),
            new ModelAndViewContainer(),
            webRequest
    );
    System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
}

@ResponseBody
@RequestMapping(produces = "application/json")
public User user() {
    return null;
}

多个消息转换器默认按照顺序优先级

如果request设置请求头优先级会高点。

request.addHeader("Accept", "application/xml");

response设置优先级最高。

response.setContentType("application/json");
@RequestMapping(produces = "application/json")

 

MessageConverter 的作用: @ResponseBody 是返回值处理器解析的,但具体转换工作是 MessageConverter 做的

 

7.ResponseBodyAdvice

ResponseBodyAdvice 接口允许开发者在控制器方法返回响应体之前或之后,对响应体进行全局处理。这使得可以在不修改控制器方法本身的情况下,对响应体进行统一的处理或增强操作。

如果我们需要全局返回一个规定的格式给前端

package com.dreams.demo29;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonInclude(JsonInclude.Include.NON_NULL)
public class Result {
    private int code;
    private String msg;
    private Object data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    @JsonCreator
    private Result(@JsonProperty("code") int code, @JsonProperty("data") Object data) {
        this.code = code;
        this.data = data;
    }

    private Result(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public static Result ok() {
        return new Result(200, null);
    }

    public static Result ok(Object data) {
        return new Result(200, data);
    }

    public static Result error(String msg) {
        return new Result(500, "服务器内部错误:" + msg);
    }
}

 

重点就在这里:

  • 使用 @ControllerAdvice 注解标记,实现 ResponseBodyAdvice<Object> 接口。
    该类的主要作用是在响应体写入之前对其进行统一处理,将特定类型的响应体转换为统一的 Result 类型。
  • supports 方法用于确定是否应用该处理逻辑。它检查返回类型和其包含的类是否标记有 @ResponseBody 注解,如果是,则返回 true。
  • AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) 这段代码的作用是检查包含目标方法的类是否被 @ResponseBody 注解标记,这段代码能够往上找,@RestController注解里使用了@ResponseBody ,所以也会被检测出来。
  • beforeBodyWrite 方法在响应体写入之前被调用。如果响应体已经是 Result 类型,则直接返回;否则,通过 Result.ok(body) 方法将原始响应体封装为 Result 对象并返回。

代码:

package com.dreams.demo29;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@Configuration
public class WebConfig {

    @ControllerAdvice
    static class MyControllerAdvice implements ResponseBodyAdvice<Object> {
        // 满足条件才转换
        public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
            if (returnType.getMethodAnnotation(ResponseBody.class) != null ||
                AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null) {
                return true;
            }
            return false;
        }

        // 将 User 或其它类型统一为 Result 类型
        public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
            if (body instanceof Result) {
                return body;
            }
            return Result.ok(body);
        }
    }

    // @Controller
    // @ResponseBody
    @RestController
    public static class MyController {
        public User user() {
            return new User("王五", 18);
        }
    }

    public static class User {
        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
}

调用代码如下,就是之前的逻辑,没什么不同:

package com.dreams.demo29;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestHeaderMethodArgumentResolver;
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.*;

import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

public class Demo {

    // {"name":"王五","age":18}
    // {"code":xx, "msg":xx, data: {"name":"王五","age":18} }
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(WebConfig.class);

        ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(
                context.getBean(WebConfig.MyController.class),
                WebConfig.MyController.class.getMethod("user")
        );
        handlerMethod.setDataBinderFactory(new ServletRequestDataBinderFactory(Collections.emptyList(), null));
        handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
        handlerMethod.setHandlerMethodArgumentResolvers(getArgumentResolvers(context));
        handlerMethod.setHandlerMethodReturnValueHandlers(getReturnValueHandlers(context));

        MockHttpServletRequest request = new MockHttpServletRequest();
        MockHttpServletResponse response = new MockHttpServletResponse();
        ModelAndViewContainer container = new ModelAndViewContainer();
        handlerMethod.invokeAndHandle(new ServletWebRequest(request, response), container);

        System.out.println(new String(response.getContentAsByteArray(), StandardCharsets.UTF_8));
    }

    public static HandlerMethodArgumentResolverComposite getArgumentResolvers(AnnotationConfigApplicationContext context) {
        HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
        composite.addResolvers(
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), false),
                new PathVariableMethodArgumentResolver(),
                new RequestHeaderMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletCookieValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ExpressionValueMethodArgumentResolver(context.getDefaultListableBeanFactory()),
                new ServletRequestMethodArgumentResolver(),
                new ServletModelAttributeMethodProcessor(false),
                new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())),
                new ServletModelAttributeMethodProcessor(true),
                new RequestParamMethodArgumentResolver(context.getDefaultListableBeanFactory(), true)
        );
        return composite;
    }

    public static HandlerMethodReturnValueHandlerComposite getReturnValueHandlers(AnnotationConfigApplicationContext context) {
        // 添加 advice
        List<ControllerAdviceBean> annotatedBeans = ControllerAdviceBean.findAnnotatedBeans(context);
        List<Object> collect = annotatedBeans.stream().filter(b -> ResponseBodyAdvice.class.isAssignableFrom(b.getBeanType()))
                .collect(Collectors.toList());

        HandlerMethodReturnValueHandlerComposite composite = new HandlerMethodReturnValueHandlerComposite();
        composite.addHandler(new ModelAndViewMethodReturnValueHandler());
        composite.addHandler(new ViewNameMethodReturnValueHandler());
        composite.addHandler(new ServletModelAttributeMethodProcessor(false));
        composite.addHandler(new HttpEntityMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter())));
        composite.addHandler(new HttpHeadersReturnValueHandler());
        composite.addHandler(new RequestResponseBodyMethodProcessor(Collections.singletonList(new MappingJackson2HttpMessageConverter()), collect));
        composite.addHandler(new ServletModelAttributeMethodProcessor(true));
        return composite;
    }
}

 

 

8.参考

黑马程序员:spring

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇