请求映射原理
DispatcherServlet 继承了 FrameworkServlet(抽象类,继承了 HttpServletBean,实现了 ApplicationContextAware 接口),重写了 doService() 方法
在 doService() 方法里定义了 doDispatch() 方法;doDispatch() 方法最主要做了如下 2 件事
2.1 通过 getHandler() 方法找出请求对应的 handler,该方法会遍历 handlerMappings(List) 从 5 个 HandlerMapping 中依次匹配请求对应的 handler
从第一个 RequestMapping 中可以看到我们的路由和对应的处理方法信息
2.2 通过找到的 mappedHandler 找出 HandlerAdapter 并执行 handle() 方法完成对应业务逻辑调用
Rest 核心 Filter
HiddenHttpMethodFilter;用法:表单的 method = post,隐藏域 _method = put;
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
Rest 流程分析
表单提交的时候带参数 _method = PUT/DELETE/PATCH
请求被 HiddenHttpMethodFilter 拦截,只拦截 POST 类型和 _method = PUT/DELETE/PATCH 的请求
原生的 request(post) 被包装模式 requestWrapper 重写了 getMethod() 方法并返回
过滤器链放行的时候用 wrapper,以后的方法调用 getMethod() 方法变为调用 requestWrapper 的方法
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
/**
* 根据用户主键查询用户
*/
("/user/{id}")
public String getUserById(("id") String id) {
... ...
}
@RequestParam:获取请求参数;required 属性默认 true,如果前端没有传递 name 参数,就会报错;如果写成 @RequestParam Map
@GetMapping("/user/users")
public String getUserById(@RequestParam(value = "name", required = false) String userName) {
return userName;
}
@RequestHeader:获取请求头信息,如果设置了 value 值(不区分大小写)就只取指定的请求头;如果写成 @RequestHeader Map
@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 这个参数,传值就不大方便;如果使用矩阵变量的方式传值,则可以写为:
http://localhost:8080/user/xxx;name=dufu;age=18,31
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));
}
注意
后端设置的请求路径包含动态路径才行,否则访问会报 404 异常
矩阵变量在 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 参数并直接使用;常用的 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 中已经存在很多的参数类型转换器,例如:NumberToCharacterConverter(数字子类型到 Character 转换);当然我们可以给 WebDataBinder(数据绑定器)里面放自己的 Converter(转换器),实现参数类型转换;
需求:User 对象(String name, Integer age),将前端参数传递的 “name,age” 类型的字符串转化为 User 对象
@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);
}
}
@RestController
public class UserController {
@GetMapping("/user/save")
public User saveUser(User user) {
return user;
}
}
这里是 GET 请求,前端传递的实际上是字符串,通过类型转换器转化为对象后,返回给前端
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) 测试最终效果如下
在浏览器中访问的时候,可以从请求头中看到 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;
}
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)最终效果如下
2. 如何实现动态修改请求头返回不同格式数据?
如果不是通过 ajax 请求的方式(设置 content-type),直接在浏览器访问的 GET 请求无法设置请求头类型,那么可通过携带 format 参数的形式实现动态请求头功能;这个 format 参数是 ParameterContentNegotiationStrategy.class 中的属性
1)在 application.properties 配置文件中打开根据请求参数选择请求头的属性配置
# 开启请求参数内容协商机制(请求后带 format 参数的形式)
spring.mvc.contentnegotiation.favor-parameter = true
2)请求地址后添加参数 ?format=json 返回 json 格式返回值
3)请求地址后添加参数 ?format=xml 返回 XML 格式返回值
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 属性要去掉)