SpringBoot参数请求处理

一、请求映射

  1. 请求映射原理

  2. DispatcherServlet 继承了 FrameworkServlet(抽象类,继承了 HttpServletBean,实现了 ApplicationContextAware 接口),重写了 doService() 方法
    SpringBoot参数请求处理_第1张图片

  3. 在 doService() 方法里定义了 doDispatch() 方法;doDispatch() 方法最主要做了如下 2 件事

    2.1 通过 getHandler() 方法找出请求对应的 handler,该方法会遍历 handlerMappings(List) 从 5 个 HandlerMapping 中依次匹配请求对应的 handler
    SpringBoot参数请求处理_第2张图片
    从第一个 RequestMapping 中可以看到我们的路由和对应的处理方法信息
    SpringBoot参数请求处理_第3张图片
    2.2 通过找到的 mappedHandler 找出 HandlerAdapter 并执行 handle() 方法完成对应业务逻辑调用

  4. Rest 风格支持
    使用 Http 请求方式动词表示对资源的操作
    SpringBoot参数请求处理_第4张图片

  5. Rest 核心 Filter
    HiddenHttpMethodFilter;用法:表单的 method = post,隐藏域 _method = put;

  6. Rest 基本原理
    前端在提交表单的时候,另外多提交了一个参数 _method = DELETE/PUT/PATCH,在 HiddenHttpMethodFilter 中可以看到 _method 是默认的参数值

public class HiddenHttpMethodFilter extends OncePerRequestFilter {
    private static final List<String> ALLOWED_METHODS;
    // 默认参数值
    public static final String DEFAULT_METHOD_PARAM = "_method";
    private String methodParam = "_method";
    ... ...
}

在 WebMvcAutoConfiguration.class(web 所有自动配置类)下可以看到已默认配置了一个 HiddenHttpMethodFilter


(HiddenHttpMethodFilter.class)
(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new OrderedHiddenHttpMethodFilter();
}

从 @ConditionalOnProperty 注解中发现要添加如下配置才能打开 Rest 风格支持

# 打开 Rest 风格支持, 默认值 false
spring.mvc.hiddenmethod.filter.enabled = true
  1. Rest 流程分析

  2. 表单提交的时候带参数 _method = PUT/DELETE/PATCH

  3. 请求被 HiddenHttpMethodFilter 拦截,只拦截 POST 类型和 _method = PUT/DELETE/PATCH 的请求

  4. 原生的 request(post) 被包装模式 requestWrapper 重写了 getMethod() 方法并返回

  5. 过滤器链放行的时候用 wrapper,以后的方法调用 getMethod() 方法变为调用 requestWrapper 的方法

  6. Rest 修改默认参数 _method
    在配置类中定义 HiddenHttpMethodFilter Bean 对象,并对默认参数值覆盖即可

(proxyBeanMethods = false)
public class WebMvcConfig {
    
    public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
        HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
        // 修改默认参数
        methodFilter.setMethodParam("_myMethod");
        return methodFilter;
    }
}

二、普通参数和注解

在参数传递过程中,和参数绑定使用常见注解有:

@PathVariable:路径变量;适用于 rest 风格传参;如果写成 @PathVariable Map params 可获取前端传递的所有参数

/**
 * 根据用户主键查询用户
 */
("/user/{id}")
public String getUserById(("id") String id) {
	... ...
}

@RequestParam:获取请求参数;required 属性默认 true,如果前端没有传递 name 参数,就会报错;如果写成 @RequestParam Map params 可获取前端传递的所有参数

@GetMapping("/user/users")
public String getUserById(@RequestParam(value = "name", required = false) String userName) {
	return userName;
}

@RequestHeader:获取请求头信息,如果设置了 value 值(不区分大小写)就只取指定的请求头;如果写成 @RequestHeader Map headers 可获取所有的请求头;注意:取出的请求头名称全小写

@GetMapping("/user/users")
public void getUserById(@RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String, String> requestHeaders) {
	System.out.println("请求头 User-Agent 为:" + userAgent);
	requestHeaders.forEach((headerName, headerValue) -> System.out.println("header name: " 
	    + headerName + ", header value: " + headerValue));
}

@MatrixVariable:矩阵变量;例如:我们要查询用户名为 dufu,年龄为 18,31 岁的用户,一般的参数传递是:http://localhost:8080/user?name=dufu&age1=18&age2=31;对于年龄 age 这个参数,传值就不大方便;如果使用矩阵变量的方式传值,则可以写为:

  1. http://localhost:8080/user/xxx;name=dufu;age=18,31

  2. http://localhost:8080/user/xxx;name=dufu;age=18;age=31

@GetMapping("/user/{path}")
public void getUsers(@MatrixVariable("name") String name, @MatrixVariable("age") List<String> ages) {
	System.out.println(name);
	ages.forEach(age -> System.out.println(age));
}

注意

  1. 后端设置的请求路径包含动态路径才行,否则访问会报 404 异常

  2. 矩阵变量在 SpringBoot 中默认被禁用了;需要我们手动打开配置

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        UrlPathHelper urlPathHelper = new UrlPathHelper();
        // 不要移除请求路径中分号后面的内容;开启矩阵变量的功能
        urlPathHelper.setRemoveSemicolonContent(false);
        configurer.setUrlPathHelper(urlPathHelper);
    }
}

@CookieValue:获取请求的 cookie,可以使用 String 类型或 Cookie 对象接收

@GetMapping("/user/users")
public void getUserById(@CookieValue("_ga") String cookieValue, @CookieValue("_ga") Cookie cookie) {
    ... ...
}

@RequestBody:获取请求体,主要是 POST 类型的请求 JSON 格式参数获取

@PostMapping("/user/add")
public void getUserById(@RequestBody User user) {
    ... ...
}

@RequestAttribute:获取 request.setAttribute() 方法传递的值

@Controller
public class RequestController {
    @GetMapping("/test")
    public String test(HttpServletRequest request) {
        request.setAttribute("msg", "测试一下");
        request.setAttribute("code", 1);
        return "forward:/success";
    }

    @GetMapping("/success")
    @ResponseBody
    public void success(@RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, 
        HttpServletRequest request) {
        System.out.println("msg: " + msg);
        System.out.println("code: " + code);
        System.out.println(request.getAttribute("msg"));
        System.out.println(request.getAttribute("code"));
    }
}

三、Servlet API

我们可以在方法中直接添加 Servlet API 参数并直接使用;常用的 API 有:WebRequst、ServletRequest、MultipartRequest、HttpSession、javax.servlethttp.pushBuilder、Principal、InputStream、HttpMethod、Locale、TimeZone、ZoneId

四、复杂参数

在请求方法中同样可以直接添加这些复杂参数并使用:Map、Errors/BindingResult、Model、RedirectAttributes、ServletResponse、SessionStatus、UrlComponentsBuilder、ServletComponentsBuilder

Map、Model 里面的数据会被放在 request 的请求域中(相当于 request.setAttribute(xxx, xxx))

RedirectAttributes 是重定向携带数据的时候使用

测试代码如下

@Controller
public class RequestController {
    @GetMapping("/test")
    public String test(Map<String, Object> map, Model model, HttpServletRequest request, 
        HttpServletResponse response) {
        map.put("msg1", "a message from map");
        model.addAttribute("msg2", "a message from model");
        request.setAttribute("msg3", "a message from request");
        // 向客户端添加 cookie
        Cookie cookie = new Cookie("cookie", "cookie");
        response.addCookie(cookie);
        return "forward:/success";
    }

    @GetMapping("/success")
    @ResponseBody
    public Map<String, Object> success(HttpServletRequest request) {
        Map<String, Object> result = new HashMap<>();
        result.put("msg1", request.getAttribute("msg1"));
        result.put("msg2", request.getAttribute("msg2"));
        result.put("msg3", request.getAttribute("msg3"));
        return result;
    }
}

最终效果如下
SpringBoot参数请求处理_第5张图片

五、参数类型转换器(Convert)

在 SpringBoot 中已经存在很多的参数类型转换器,例如:NumberToCharacterConverter(数字子类型到 Character 转换);当然我们可以给 WebDataBinder(数据绑定器)里面放自己的 Converter(转换器),实现参数类型转换;

需求:User 对象(String name, Integer age),将前端参数传递的 “name,age” 类型的字符串转化为 User 对象

  1. 在配置类中注册自定义类型转换器
@Configuration
public class AppConfig implements WebMvcConfigurer {
    /**
     * 自定义类型转换器
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        // 把 "dufu,30" 这种格式字符串转化为 User 对象(name, age)
        Converter<String, User> userConverter = new Converter<String, User>() {
            @Override
            public User convert(String source) {
                if(!StringUtils.isEmpty(source)) {
                    String[] sourceArr = source.split(",");
                    User user = new User();
                    user.setName(sourceArr[0]);
                    user.setAge(Integer.parseInt(sourceArr[1]));
                    return user;
                }
                return null;
            }
        };
        registry.addConverter(userConverter);
    }
}
  1. 在 UserController.class 下的 saveUser() 方法中接收字符串参数并转化为 User 对象
@RestController
public class UserController {
    @GetMapping("/user/save")
    public User saveUser(User user) {
        return user;
    }
}

这里是 GET 请求,前端传递的实际上是字符串,通过类型转换器转化为对象后,返回给前端

  1. 测试一下,在浏览器访问:http://localhost:8080/user/save?user=dufu,30,得的返回值为 User 对象
    在这里插入图片描述

六、响应处理

  1. HttpMessageConverter 返回值类型转换器
    主要是将返回值转化为浏览器能接受处理的类型(MediaType);例如:将返回值 User 对象转化为 JSON 格式;默认的转换器有 10 个,当处理完业务逻辑返回处理结果给前端页面的时候,后端会从下面这 10 个转换器中遍历找出合适的转换器处理返回值

SpringBoot参数请求处理_第6张图片
1.1 返回值为 XML 格式
在默认情况下,SpringBoot 会对所有添加了 @ResponseBody(或 @RestController) 注解的方法的返回值转化为 JSON 格式返回(转换器为上图的 MappingJackson2HttpMessageConverter);如果要把返回值设置为 XML 格式可按照如下步骤实现

1)在配置文件中引入支持 jackson-dataformat-xml 依赖

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

2)在配置类中覆盖 MappingJackson2XmlHttpMessageConverter 转换器,因为 xml 格式返回值需要添加 xml 版本和字符集的声明

@Bean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter() {
	XmlMapper xmlMapper = new XmlMapper();
	XmlMapper.Builder builder = new XmlMapper.Builder(xmlMapper);
	// WRITE_XML_DECLARATION:<?xml version='1.0' encoding='UTF-8'?>
	// WRITE_XML_1_1:<?xml version='1.1' encoding='UTF-8'?>
	builder.enable(ToXmlGenerator.Feature.WRITE_XML_DECLARATION)
			.defaultUseWrapper(false);
	return new MappingJackson2XmlHttpMessageConverter(builder.build());
}

3) 在 UserController.class 添加如下代码即可

@GetMapping(value = "/user", produces = MediaType.APPLICATION_XML_VALUE)
public User getUser() {
	User user = new User();
	user.setName("dufu");
	user.setAge(30);
	return user;
}

4) 测试最终效果如下
SpringBoot参数请求处理_第7张图片
在浏览器中访问的时候,可以从请求头中看到 Accept 的值为下图所示
在这里插入图片描述
对于 application/xml 这种类型优先级更高,所以去掉 @GetMapping 注解的 produces = MediaType.APPLICATION_XML_VALUE 选项,浏览器端依然能拿到 XML 格式的返回值

如果实体类属性值和要求返回值属性不一致,可以通过在实体类属性上添加 @JacksonXmlProperty 注解指定返回值属性;例如:User 的属性 name,指定返回 XML 中属性为 userName;集合类型的返回值外层再嵌套一层父节点可使用 @JacksonXmlElementWrapper 注解

@Data
@JacksonXmlRootElement(localName = "response")
public class User {
    @JacksonXmlProperty(localName = "userName")
    private String name;
    private Integer age;
    @JacksonXmlElementWrapper(localName = "list")
    private List<String> appIds;
}

返回值如下
SpringBoot参数请求处理_第8张图片

扩展

  1. 如何在返回 XML 头中追加 DTD 信息?

1)新建 DtdXMLSerializerProvider.class 继承 XmlSerializerProvider.class 并重写如下方法

public class DtdXMLSerializerProvider extends XmlSerializerProvider {
    private String dtd;

    public DtdXMLSerializerProvider(XmlSerializerProvider src, SerializationConfig config, SerializerFactory f, 
        String dtd) {
        super(src, config, f);
        this.dtd = dtd;
    }

    @Override
    protected void _initWithRootName(ToXmlGenerator xgen, QName rootName) throws IOException {
        super._initWithRootName(xgen, rootName);
        try {
            xgen.getStaxWriter().writeDTD(dtd);
        } catch (XMLStreamException e) {
            e.printStackTrace();
        }
    }

    @Override
    public DefaultSerializerProvider createInstance(SerializationConfig config, SerializerFactory jsf) {
        return new DtdXMLSerializerProvider(this, config, jsf, dtd);
    }
}

2)在配置类中设置 XML 序列化提供器支持

@Bean
public MappingJackson2XmlHttpMessageConverter mappingJackson2XmlHttpMessageConverter() {
	XmlMapper xmlMapper = new XmlMapper();
	// 追加 DTD 信息
	String dtd = "\"-//Google//DTD GSA Feeds//EN\" \"\">";
	DtdXMLSerializerProvider provider = new DtdXMLSerializerProvider(
			(XmlSerializerProvider) xmlMapper.getSerializerProvider(),
			xmlMapper.getSerializationConfig(),
			xmlMapper.getSerializerFactory(),
			dtd);
	xmlMapper.setSerializerProvider(provider);
	XmlMapper.Builder builder = new XmlMapper.Builder(xmlMapper);
	// WRITE_XML_DECLARATION:<?xml version='1.0' encoding='UTF-8'?>
	// WRITE_XML_1_1:<?xml version='1.1' encoding='UTF-8'?>
	builder.enable(ToXmlGenerator.Feature.WRITE_XML_1_1)
			.defaultUseWrapper(false);
	return new MappingJackson2XmlHttpMessageConverter(builder.build());
}

3)最终效果如下
SpringBoot参数请求处理_第9张图片
2. 如何实现动态修改请求头返回不同格式数据?

如果不是通过 ajax 请求的方式(设置 content-type),直接在浏览器访问的 GET 请求无法设置请求头类型,那么可通过携带 format 参数的形式实现动态请求头功能;这个 format 参数是 ParameterContentNegotiationStrategy.class 中的属性

1)在 application.properties 配置文件中打开根据请求参数选择请求头的属性配置

# 开启请求参数内容协商机制(请求后带 format 参数的形式)
spring.mvc.contentnegotiation.favor-parameter = true

2)请求地址后添加参数 ?format=json 返回 json 格式返回值
SpringBoot参数请求处理_第10张图片

3)请求地址后添加参数 ?format=xml 返回 XML 格式返回值
SpringBoot参数请求处理_第11张图片
3. 自定义 MessageConverter
1)添加自定义返回值转换器 MyMessageConverter.class ,实现对 User 类型返回值的输出格式转换

public class MyMessageConverter implements HttpMessageConverter<Object> {
    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return true;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return MediaType.parseMediaTypes("application/dufu");
    }

    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, 
        HttpMessageNotReadableException {
        // 此处可定义怎么读取返回值
        return null;
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, 
        HttpMessageNotWritableException {
        OutputStream body = outputMessage.getBody();
        if(o instanceof User) {
            User user = (User)o;
            StringBuilder strUser = new StringBuilder();
            strUser.append("name=")
                    .append(user.getName())
                    .append(";")
                    .append("age=")
                    .append(user.getAge())
                    .append(";")
                    .append("appIds=")
                    .append(StringUtils.join(user.getAppIds(), '/'));
            body.write(strUser.toString().getBytes());
        } else {
            body.write(o.toString().getBytes());
        }
    }
}

2)配置类 AppConfig.class 中注册转换器和指定内容协商策略

@Configuration
public class AppConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new MyMessageConverter());
    }

    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        Map<String, MediaType> mediaTypes = new Hashtable<>();
        mediaTypes.put("json", MediaType.APPLICATION_JSON);
        mediaTypes.put("xml", MediaType.APPLICATION_XML);
        // 自定义的
        mediaTypes.put("dufu", MediaType.parseMediaType("application/dufu"));
        // 基于参数的内容协商策略
        ParameterContentNegotiationStrategy parameterContentNegotiationStrategy = 
            new ParameterContentNegotiationStrategy(mediaTypes);
        // 基于请求头的内容协商策略
        HeaderContentNegotiationStrategy headerContentNegotiationStrategy = 
            new HeaderContentNegotiationStrategy();
        // 把这两种策略都添加进去
        configurer.strategies(Arrays.asList(parameterContentNegotiationStrategy, headerContentNegotiationStrategy));
        WebMvcConfigurer.super.configureContentNegotiation(configurer);
    }
}

3)我们可以通过在 UserController.class 中指定请求头让此方法的返回值通过我们自定义的转换器转换返回

@RestController
public class UserController {
    @GetMapping(value = "/user", produces = "application/dufu")
    public User getUser() {
        User user = new User();
        user.setName("dufu");
        user.setAge(30);
        user.setAppIds(Arrays.asList("A", "B", "C"));
        return user;
    }
}

如果在 application.properties 配置文件中开启了请求参数内容协商机制,可以在请求地址后通过参数 ?format=dufu 的形式访问(但 @GetMapping 注解的 produces 属性要去掉)

你可能感兴趣的:(#,springBoot,spring,boot,spring,java)