1. 请求参数处理
1.1 请求映射
@xxxMapping
:- Rest 风格支持(使用 Http 请求方式动词来表示对资源的操作)
- 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 删除用户 /saveUser 保存用户
- 现在:/user GET-获取用户 DELTE-删除用户 PUT-修改用户 POST-保存用户
- 核心Filter:HiddenHttpMethodFilter
- 用法:标签method=post,隐藏域_method=put
- 如果想要使用 Rest 风格,那么在 SpringBoot 中还需要配置对应的属性:
spring: mvc: hiddenmethod: filter: enabled: true
1.1.1 开启 SpringBoot 的 Rest 风格支持
在源码中可看,属性默认为 false,因此 Rest 风格需要自己进行配置
@Bean @ConditionalOnMissingBean({HiddenHttpMethodFilter.class}) @ConditionalOnProperty( prefix = "spring.mvc.hiddenmethod.filter", name = {"enabled"} // 属性默认 false,表示未开启 ) public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() { return new OrderedHiddenHttpMethodFilter(); }
- Yaml文件开启代码如下:
# 选择性开启,页面提交可以用,客户端直接发送 PUT 请求不需要配置 filter spring: mvc: hiddenmethod: filter: enabled: true
1.1.2 演示 PUT 和 DELETE 方式提交
正常情况下,SpringBoot 已经为这两个请求配置了 HiddenHttpMethod,但并不是修改底层
//################ HiddenHttpMethodFilter 类的内部 对请求的定义 ########## private static final List<String> ALLOWED_METHODS; public static final String DEFAULT_METHOD_PARAM = "_method"; private String methodParam = "_method"; protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { HttpServletRequest requestToUse = request; if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) { String paramValue = request.getParameter(this.methodParam); if (StringUtils.hasLength(paramValue)) { // PUT 和 DELETE 经过 toUpperCase 方法转换,大小写都可以 String method = paramValue.toUpperCase(Locale.ENGLISH); if (ALLOWED_METHODS.contains(method)) { requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method); } } } filterChain.doFilter((ServletRequest)requestToUse, response); }
如果我们需要使用 PUT 或 DELTE 请求方式,那么我们需要将 method 参数改为 POST,在表单内部添加带有 name=“_method” 属性,在这个标签中的 value 处定义 PUT 或 DELETE 请求,做法如下:
// ############## 错误写法 ########### \\ <form action="/user" method="delete"> <input value="REST-DELETE 提交" type="submit"> </form> // ############## 正确写法 ########### \\ <form action="/user" method="post"> <input type="hidden" name="_method" value="PUT"> <input value="REST-PUT 提交" type="submit"> </form>
1.1.3 Rest 原理(表单提交)
- 表单提交会带上 _method=PUT 参数
- 请求被 HiddenHttpMethod 过滤类获取
- 请求是否正常,并且是POST
- 获取到 _method 的值,转发对应请求
- 兼容以下请求:PUT、DELETE、PATCH
- 原生 reqeust(post),
包装 requestWrapper 重写了 getMethod 方法,返回的是传入的值 - 过滤器链放行的时候用 wrapper,以后方法调用 getMethod 是调用经过 requstWrapper 重写后的 getMethod 方法
- 请求异常
- 默认转发到 GET 请求
- Rest 使用客户端工具
- 如 PostMan 直接发送 PUT、DELETE 等方式请求,无需 Filter
1.1.4 各请求方式的 映射地址注解
// 处理 Get 请求 @GetMapping("/user") public String getUser(){ return "GET-张三"; } // 处理 Post 请求 @PostMapping("/user") public String saveUser(){ return "POST-张三"; } // 处理 Put 请求 @PutMapping("/user") public String putUser(){ return "PUT-张三"; } // 处理 Delete 请求 @DeleteMapping("/user") public String deleteUser(){ return "DELETE-张三"; } // 处理 Patch 请求 @PatchMapping("/user") public String pacthUser(){ return "Pacth-张三"; }
1.1.5 更改默认 _method
在配置类中,重新声明 hiddenHttpMethodFilter 方法,并将该方法注入到容器中即可
/** * 该类配置 Web 各种属性 * - proxyBeanMethods = false - 表示该类没有任何依赖 */ @Configuration(proxyBeanMethods = false) public class WebConfig { /** * 覆盖原有的 hiddenHttpMethodFilter 类 * @return */ @Bean public HiddenHttpMethodFilter hiddenHttpMethodFilter(){ HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter(); // 设置默认携带参数 methodFilter.setMethodParam("_m"); return methodFilter; } }
1.2 请求映射原理
SpringMvc 功能分析,都从 org.springframework.web.servlet.DispatcherServlet -》 doService()
1.2.1 疑惑
1.2.1.1 找到处理对应请求的控制器方法
**在 DispatcherServlet 类中的 doDispatch() 里,如何找到处理请求的控制器的? **
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Object dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; // 下列一行代码获得到了该使用哪个 handler 处理这次请求的请求方式 mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null) { this.noHandlerFound(processedRequest, response); return; }
getHandler():
在获取控制器方法时,传入了 requst 参数,其中该参数中就携带了以下 Mapping 规则
RequestMappingHandlerMapping:保存了所有 @Requestmapping 和 handler 的映射规则
所有的请求映射都保存在 HandlerMapping 中
- SpringBoot 自动配置欢迎页的 HandlerMapping。访问
/
能访问到 index.html - SprigBoot 自动配置了默认的 RequestMappingHandlerMapping
- 请求一来,挨个尝试所有的HandlerMapping看是否有请求信息
- 如果有就找到这个请求对应的 handler
- 如果没有就是下一个 HandlerMapping
- 我们需要一些自定义的映射处理,我们也可以自己给容器中放 HandlerMapping。
- 自定义 HandlerMapping
2. 普通参数与基本注解
2.1 注解:
注解 | 说明 |
@PathVarivble | 路径变量; |
@RequestHeader | 获取请求头; |
@RequestAttribute | 获取 request 域属性; |
@Requestparam | 获取请求参数; |
@MatrixVariable | 矩阵变量; |
@CookieValue | 获取 cookie 值; |
@RequestBody | 获取请求体; |
2.1.1 @PathVarivble
获得路径中的变量参数,当变量如果是以两种变量组合的话,可以直接声明 Map 来获得 Map 对象,该对象的类型是<String,String>
@GetMapping("/car/{id}/owner/{name}") public Map<String, Object> getCar(@PathVariable("id") Integer id, @PathVariable("name") String name, @PathVariable Map<String, String> pv){ Map<String, Object> map = new HashMap<>(); map.put("id", id); map.put("name", name); map.put("pv", pv); return map; // 返回结果 {"pv":{"name":"zhangsan","id":"2"},"name":"zhangsan","id":2} }
2.1.2 @RequestHeader
获取请求头的中参数信息,当然如果以两种变量组合的话,一样可以用 Map 来接收参数
// 请求地址:http://localhost:8080/getHeader @GetMapping("/getHeader") public Map<String, Object> getRequsetHeader(@RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String, String> header){ Map<String, Object> map = new HashMap<>(); map.put("userAgent", userAgent); map.put("header", header); return map; // 返回结果:一大串内容,包括了全部的请求头数据 }
2.1.3 @Requestparam
获取请求体中的数据,可以用单个参数获取,也可以使用 List 列表来接收多个参数。
不同的参数也可以使用 Map 来接收,但是当出现相同的参数名,那么 Map 只会保留第一个存储的数据
// 发送请求:http://localhost:8080/getParam?age=18&inters=football&inters=basketball @GetMapping("/getParam") public Map<String, Object> getRequsetParam(@RequestParam("inters") List<String> inters, @RequestParam Map<String, String> params, @RequestParam("age") Integer age){ Map<String, Object> map = new HashMap<>(); map.put("inters", inters); map.put("params", params); map.put("age", age); return map; // 返回结果:{"inters":["football","basketball"],"params":{"age":"18","inters":"football"},"age":18} }
2.1.4 @CookieValue
获得 Cookie 的值,设置对应的 cookie 属性名,然后用字符串接收即可
如果想获得所有的 cookie 值,可以声明一个 Cookie 类型来接收数据
// 请求地址:http://localhost:8080/getCookie @GetMapping("/getCookie") public Map<String, Object> getCookieValue(@CookieValue("_ga") String _ga, @CookieValue Cookie cookie){ Map<String, Object> map = new HashMap<>(); map.put("_ga", _ga); map.put("cookie", cookie); return map; }
2.1.5 @RequestBody
获取请求体中的内容
// 请求地址:http://localhost:8080/save @PostMapping("/save") public Map<String, Object> getRequestBody(@RequestBody String content){ Map<String, Object> map = new HashMap<>(); map.put("content", content); return map; }
2.1.6 @RequestAttribute
获得 request 请求域中的内容信息
// 发送请求:http://localhost:8080/goto @GetMapping("/goto") public String goToPage(HttpServletRequest request){ request.setAttribute("msg","成功"); request.setAttribute("code",200); return "forward:/success";// 转发至 /success 请求 } @ResponseBody @GetMapping("/success") public Map<String, Object> success(@RequestAttribute("msg") String msg, @RequestAttribute("code") Integer code, // 因为传过来的是原生的 request, // 所以也可以使用原生 request 来接收参数 HttpServletRequest request){ String msg1 = (String) request.getAttribute("msg"); Map<String, Object> map = new HashMap<>(); map.put("code",code); map.put("msg",msg1); map.put("data",msg); return map; // 请求结果:{"msg":"成功","code":200,"data":"成功"} }
2.1.7 @MatrixVariable
获取 矩阵变量 内容。
SpringBoot 默认禁用矩阵变量的功能!
2.1.7.1 什么是矩阵变量?
根据 URI 规范 RFC 3986 中 URL 的定义,路径片段中可以包含键值对。规范中没有对应的术语…在 SpringMVC 它被称为矩阵变量.
2.1.7.2 如何声明矩阵变量?
矩阵变量可以出现在任何路径片段中,每一个矩阵变量都用分号(;)隔开
示例:/patch/{path;low=34;brand=byd,audi,yd}
2.1.7.3 开启 SpringBoot 的矩阵变量使用
SpringBoot 对于路径的处理,都要经过 UrlPathHelper 类来处理。
在该类中,将 removeSemicolonContent(移除分号内容)属性设置为 false 即可(默认 true)
在配置类中,编写以下代码即可
@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); // 设置 removeSemicolonContent 属性为 false urlPathHelper.setRemoveSemicolonContent(false); // 重新设置 configurer 对象 configurer.setUrlPathHelper(urlPathHelper); } }; }
2.1.7.4 地址映射编写
// 请求地址: http://localhost:8080/cars/sell;low=34;brand=byd,audi,yd @GetMapping("/cars/{path}") public Map<String, Object> carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand){ HashMap<String, Object> map = new HashMap<>(); map.put("low", low); map.put("brand", brand); return map; // 请求结果:{"low":34,"brand":["byd","audi","yd"]} }
在访问请求时,/{path} 是随意的,这里指的是一个通配符。
2.1.7.5 参数相同情况获取值
当参数相同,在 @MatrixVariable 注解里设置对应参数即可
- **value:**参数名称
- **pathVar:**路径名称
@MatrixVariable(value = "age",pathVar = "bossId")
:在 /{bossId} 路径中,获取参数名是 age 的值
// 请求地址: http://localhost:8080/boss/1;age=20/2;age=30 @GetMapping("/boss/{bossId}/{empId}") public Map<String, Object> boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ HashMap<String, Object> map = new HashMap<>(); map.put("bossId", bossAge); map.put("empId", empAge); return map; // 请求结果:{"empId":30,"bossId":20} }
2.2 Servlet API:
2.3 复杂参数:
2.4 自定义对象参数:
3. 参数处理原理
- HadlerMapping 中找到能处理请求的 Handler (Contoller.method())
- 为当前 Handler 找一个适配器 HandlerAdapter
3.1 HandlerAdapter
- 支持方法上标注 @RequestMapping
- 支持函数式编程
- …
3.2 执行目标方法
// DispatcherServler -- doDispatch mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// 调用映射后的 Handler 的方法 protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { this.checkRequest(request); ModelAndView mav; if (this.synchronizeOnSession) { HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized(mutex) { mav = this.invokeHandlerMethod(request, response, handlerMethod); } } else { mav = this.invokeHandlerMethod(request, response, handlerMethod); } } else { mav = this.invokeHandlerMethod(request, response, handlerMethod); } if (!response.containsHeader("Cache-Control")) { if (this.getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { this.applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); } else { this.prepareResponse(response); } } return mav; }
3.3 参数解析器
确定将要执行的目标方法的每一个参数的值是什么;
SpringMVC目标方法能写多少种参数类型。取决于参数解析器。
- 当前解析器是否支持解析这种参数
- 支持就调用 resolveArgument
3.4 返回值处理器
3.5 如何确定目标方法每一个参数的值
==========================InvocableHandlerMethod========================== protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } } return args; }
3.5.1 挨个判断所有参数解析器那个支持解析这个参数
@Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }
3.5.2 解析这个参数的值
调用各自 HandlerMethodArgumentResolver 的 resolveArgument 方法即可
3.5.3 自定义类型参数 封装POJO
ServletModelAttributeMethodProcessor 这个参数处理器支持
是否为简单类型。
public static boolean isSimpleValueType(Class<?> type) { return (Void.class != type && void.class != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type)); }
@Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Assert.state(mavContainer != null, "ModelAttributeMethodProcessor requires ModelAndViewContainer"); Assert.state(binderFactory != null, "ModelAttributeMethodProcessor requires WebDataBinderFactory"); String name = ModelFactory.getNameForParameter(parameter); ModelAttribute ann = parameter.getParameterAnnotation(ModelAttribute.class); if (ann != null) { mavContainer.setBinding(name, ann.binding()); } Object attribute = null; BindingResult bindingResult = null; if (mavContainer.containsAttribute(name)) { attribute = mavContainer.getModel().get(name); } else { // Create attribute instance try { attribute = createAttribute(name, parameter, binderFactory, webRequest); } catch (BindException ex) { if (isBindExceptionRequired(parameter)) { // No BindingResult parameter -> fail with BindException throw ex; } // Otherwise, expose null/empty value and associated BindingResult if (parameter.getParameterType() == Optional.class) { attribute = Optional.empty(); } bindingResult = ex.getBindingResult(); } } if (bindingResult == null) { // Bean property binding and validation; // skipped in case of binding failure on construction. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); if (binder.getTarget() != null) { if (!mavContainer.isBindingDisabled(name)) { bindRequestParameters(binder, webRequest); } validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new BindException(binder.getBindingResult()); } } // Value type adaptation, also covering java.util.Optional if (!parameter.getParameterType().isInstance(attribute)) { attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter); } bindingResult = binder.getBindingResult(); } // Add resolved attribute and BindingResult at the end of the model Map<String, Object> bindingResultModel = bindingResult.getModel(); mavContainer.removeAttributes(bindingResultModel); mavContainer.addAllAttributes(bindingResultModel); return attribute; }
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
byte – > file
@FunctionalInterfacepublic interface Converter<S, T>
3.6 目标方法执行完成
将所有的数据都放在 ModelAndViewContainer;包含要去的页面地址View。还包含Model数据
3.7 处理派发结果
- processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
- renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
InternalResourceView: @Override protected void renderMergedOutputModel( Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // Expose the model object as request attributes. exposeModelAsRequestAttributes(model, request); // Expose helpers as request attributes, if any. exposeHelpers(request); // Determine the path for the request dispatcher. String dispatcherPath = prepareForRendering(request, response); // Obtain a RequestDispatcher for the target resource (typically a JSP). RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath); if (rd == null) { throw new ServletException("Could not get RequestDispatcher for [" + getUrl() + "]: Check that the corresponding file exists within your web application archive!"); } // If already included or response already committed, perform include, else forward. if (useInclude(request, response)) { response.setContentType(getContentType()); if (logger.isDebugEnabled()) { logger.debug("Including [" + getUrl() + "]"); } rd.include(request, response); } else { // Note: The forwarded resource is supposed to determine the content type itself. if (logger.isDebugEnabled()) { logger.debug("Forwarding to [" + getUrl() + "]"); } rd.forward(request, response); } }
4. 响应数据与内容协商
4.1 响应页面
请求发到控制器,由控制器进行对应的请求跳转,这样就可以称为响应
4.2 响应数据
4.2.1 响应JSON
4.2.1.1 jackson.jar + @ResponseBody
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- web 场景自动引入了json场景--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-json</artifactId> <version>2.7.1</version> <scope>compile</scope> </dependency>
给前端自动返回json数据
4.2.1.1.1 RetruenValueHandler 返回值处理器原理
- 返回值处理器判断是否支持这种类型返回值 supportsRetruenType
- 返回值处理器调用 handleReturnValue 进行处理
- RequestResponseBodyMethodProcossor 可以处理返回值标注了 @ResponseBOdy注解。
- 利用MessageConverters 进行处理,将数据写为 json
- 内容协商(浏览器默认会以请求头的方式告诉服务器他能接收什么样的内容类型)
- 服务器最终根据自己自身的能力,决定服务器能生成出什么样的内容类型的数据
- SpringMvc 会遍历所有容器底层的 HttpMessageConverter,看谁能处理?
- 得到 MappingJackson2HttpMessageConverter 可以将对象写为json
- 利用 MappingJackson2HttpMessageConverter 将对象转为json再写回去
4.2.1.2 SpringMVC 到底支持哪些返回值
ModelAndView; Model; View; ResponseEntity; ResponseBodyEmitter; StreamingResponseBody; HttpEntity; HttpHeaders; Collable; DeferredResult; ListenableFutre; WebAsyncTask; @ModelAttribute; // 当标注 ResponseBody 注解,boot 底层就会使用 RequestResponseBodyMethodProcossor 来解析 @ResponseBody注解 --> RequestResponseBodyMethodProcossor
4.2.1.3 HttpMessageConverter 原理
4.2.1.3.1 MessageConverter 规范
HttpMessageConverter:看是否支持将此 Class 类型的对象转为MediaType类型的数据
例子: Person 对象转为 Json。Json 转为 Person 对象
4.2.1.3.2 默认的MessageConverter
- 只支持byte类型
- String
- Resource
- ResourceRegion
- DOM/SAX/StAX/Stream Source.class
- MultitValueMap
- true
- 支持注解方式 xml 处理
最终: MappingJackson2MessageConverter 把对象转为 JSON(利用底层的Jackson的objectmapper转换的)
4.2.2 内容协商
4.2.2.1 引入xml依赖
根据客户端接收能力不同,返回不同狗媒体类型的数据
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
4.2.2.2 postman 分别测试返回json和xml
只需要该变请求头中accept字段。http协议中规定的,告诉服务器本客户端可以接收的数据类型
4.2.2.3 开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能
# 开启参数模式-内容协商 spring: contentnegotiation: favor-parameter: true
发请求:
- localhost:8080/test/person?format=json
- localhost:8080/test/person?format=xml
确定客户端接收什么样的内容类型;
- Parameter策略优先确定实要返回json数据(获取请求头中的format的值)
- 通过遍历服务端可支持的媒体类型,来使用对应的接收类型,如果没有匹配到,那么默认都使用 */*
4.2.2.4 内容协商原理
- 判断当前响应头中是否已经由确定的媒体类型。MediaType
- 获取客户端(Postman、浏览器)支持接收的内容类型(请求)。(获取客户端accept请求头字段)
- contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
- HeaderContentNegotiationStrategy 确认客户端可以接收的内容类型
- 遍历所有当前系统的Message Converter ,看谁支持操作中国对象(Person)
- 找到支持操作Person的converter,把converter支持的媒体类型统计出来
- 客户端需要【application/xml】。服务端能力【10种、json、xml】
- 进行内容协商的最佳匹配媒体类型
- 用支持将对象转为最佳匹配媒体类型的converter,并调用它进行能转化
4.2.2.4.1 自定义MessageConverter
场景定义:
1、浏览器发送请求直接返回xml [application/xml] jacksonXmlConverter
2、如果是ajax请求 返回json [application/json] jacksonJsonConverter
3、如果另外app发送请求,返回自定义协商数据 [application/x-XXX] xxxConverter
执行步骤:
- 添加自定义的MessageConverter进行系统底层
- 系统底层就会统计出所有MessageConverter能操作哪些类型
- 客户端内容协商 [XXX —> XXXConverter]
执行操作:
- 创建自定义信息转换器。当继承HttpMessageConverter后,底层将自动检测到自定义的转换器
package com.renexdemo.converter; import com.renexdemo.bean.Person; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import java.io.IOException; import java.io.OutputStream; import java.util.List; public class RixMessageConverter implements HttpMessageConverter<Person> { /** * 能读 * @param clazz * @param mediaType * @return */ @Override public boolean canRead(Class<?> clazz, MediaType mediaType) { return false; } /** * 能写 * @param clazz * @param mediaType * @return */ @Override public boolean canWrite(Class<?> clazz, MediaType mediaType) { // 能写的类型设置为可通过类型,示例:Person.class return clazz.isAssignableFrom(Person.class); } /** * 获得所有媒体类型 * 服务器需要统计所有MessageConverter都能写出哪些内柔类型 * * applkicatoin/x-rix * @return */ @Override public List<MediaType> getSupportedMediaTypes() { // 设置转换的自定义类型 return MediaType.parseMediaTypes("application/x-rix"); } /** * 获得指定类型的媒体类型 * @param clazz * @return */ @Override public List<MediaType> getSupportedMediaTypes(Class<?> clazz) { return null; } /** * 读取 Person 类型的媒体类型 * @param clazz * @param inputMessage * @return * @throws IOException * @throws HttpMessageNotReadableException */ @Override public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } /** * 写 * @param person * @param contentType * @param outputMessage * @throws IOException * @throws HttpMessageNotWritableException */ @Override public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { // 自定义协议数据的写出 String data = person.getName()+";"+person.getAge()+";"+person.getBirth(); // 写出 OutputStream body = outputMessage.getBody(); body.write(data.getBytes()); } }
- 在自定义的 Web 配置类中,重写 configuraionContentNegotiation 方法,自定义内容协商策略
/** * 自定义内容协商策略 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 接收所有的媒体类型 Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("rix",MediaType.parseMediaType("application/x-rix")); // 指定支持解析哪些参数对应的哪些媒体类型 // 基于参数 ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes); parameterStrategy.setParameterName("ff");// 修改默认参数名(默认format) // 基于请求头 HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(parameterStrategy, headerStrategy)); }
- 再次重写 extendMessageConverters 方法,添加自定义信息转换器
@Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { // 添加自定义 信息转换器 converters.add(new RixMessageConverter()); }
5. 视图解析与模板引擎
视图解析:SpringBoot 默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染
5.1 视图解析
5.1.1 视图解析原理流程
- 目标方法处理的过程汇总,所有的数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
- 方法的参数是一个自定义类型对象(从请求参数中确定的),把它重写放在 ModelAndViewContainer 中
- 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
- processDispatchResult 处理派发结果(页面该如何响应)
- render(mv,request,reponse),该方法进行页面渲染逻辑
- 根据方法的String返回值,得到 view 对象【定义页面的渲染逻辑】
- 所有的视图解析器尝试是否能根据当前返回值得到 View 对象
- 得到了 redirect:/main.html --> Thymeleaf new RedirectView
- ContentNegotiationViewResolver 里面包含了所有的视图解析器,内部还是利用所有的视图解析器得到视图对象
- view.render(mv.getModeInternal(),request,response); 视图对象调用自定义的render机械能页面渲染工作
- redirectView 如何渲染【重定向到一个页面】
- 获取目标url地址
- response.sendRedirect(encodeURL)
视图解析:
- 返回值以 forward:开始 new InternalResourceView(forwardUrl); --> 转发 request.getRequestDispatcher(path).forward(request,response);
- 返回值以 redirect:开始 new RedirectView() --> render 就是重定向
5.2 默认引擎-Thymeleaf
概述:
Thymeleaf 是一个服务器端 Java 模板引擎,能够处理 HTML、XML、CSS、JAVASCRIPT 等模板文件。Thymeleaf 模板可以直接当作静态原型来使用,它主要目标是为开发者的开发工作流程带来优雅的自然模板,也是 Java 服务器端 HTML5 开发的理想选择。
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Index Page</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> </head> <body> <p th:text="${message}">Welcome to BeiJing!</p> </body> </html>
5.3 thymeleaf使用
5.3.1 引入场景
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
5.3.2 SpringBoot 自动配置 thymeleaf
@AutoConfiguration( after = {WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class} ) @EnableConfigurationProperties({ThymeleafProperties.class}) @ConditionalOnClass({TemplateMode.class, SpringTemplateEngine.class}) @Import({ReactiveTemplateEngineConfiguration.class, DefaultTemplateEngineConfiguration.class})
自动配好的策略:
- 所有 thymeleaf 的配置值都在 ThymeleafProperties
- 配置好了 SpringTemplateEngine
- 配置好了 ThymeleafViewResolver
- 只需要开发页面即可,无需配置信息
默认前缀后缀
public static final String DEFAULT_PREFIX = "classpath:/templates/"; public static final String DEFAULT_SUFFIX = ".html";
5.3.3 页面开发
<!DOCTYPE html> <!--xmlns:th="http://www.thymeleaf.org" 页面前言,可以出现 thymeleaf 提示--> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>thymeleaf_test</title> </head> <body> <h1 th:text="${msg}">hhhhhh</h1> <a th:href="${link}" href="www.baidu.com" >去百度</a> <a th:href="@{link}" href="www.baidu.com" >去百度2</a> </body> </html>
6. 拦截器
6.1 使用步骤
- 创建拦截器,继承 HandlerInterceptor 接口
- 实现接口的方法
/** * 目标方法执行完成前 * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return false; } /** * 目标方法执行完成后 * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } /** * 页面渲染后 * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { }
- 将拦截器添加进容器中(实现 WebMvcConfigurer 的 addInterceptors)
@Configuration public class AdminConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { } }
- 指定拦截器
registry.addInterceptor(new LoginInterceptor()) /* 所有请求都会被拦截,包括静态资源 */ .addPathPatterns("/**") .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");
6.2 拦截器原理
- 根据当前请求,找到 HandlerExectionChain 【可以处理请求的handler以及handler的所有 拦截器】
- 先来顺序执行所有拦截器的 perHandler 方法
- 如果当前拦截器 preHandler 返回为true。则执行下一个拦截器的 preHandler
- 如果当前拦截器返回为false。直接触发 triggerAfterCompletion 方法,然后倒序执行所有以及执行了的拦截器的 afterCompletion
- 如果任何一个拦截器执行失败(返回 false ),直接跳出不执行目标方法
- 所有拦截器都返回 true,都执行目标方法
- 倒序执行所有拦截器的 postHandler 方法
- 前面的步骤有任何异常,都会直接触发 afterCompletion 方法
- 页面成功渲染完成以后,也会倒序触发 afterCompletion
7. ❤️👌SpringBoot 专栏前文回顾
- 【SpringBoot(一)】Spring的认知、容器功能讲解与自动装配原理的入门,带你熟悉Springboot中基本的注解使用
- 【SpringBoot(二)】带你认识Yaml配置文件类型、SpringMVC的资源访问路径 和 静态资源配置的原理!
8. 💕👉 其他好文推荐
- 还不了解Git分布式版本控制器?本文将带你全面了解并掌握
- 带你认识Maven的依赖、继承和聚合都是什么!有什么用?
- 2-3树思想与红黑树的实现与基本原理
- 全网最全!ElasticSearch8.7 搭配 SpringDataElasticSearch5.1 的使用
- 全面深入Java GC!!带你完全了解 GC垃圾回收机制!!
- 全面了解Java的内存模型(JMM)!详细清晰!
- 在JVM中,类是如何被加载的呢?本篇文章就带你认识类加载的一套流程!
- 带你了解Go语言的判断语句、切片和映射表!!
- (Java并发编程—JUC)带你重新认识进程与线程!!让你深层次了解线程运行的睡眠与打断!!
- JUC:从JMM内存模型的角度来分析CAS并发性问题
- 常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!
- JUC:共享问题解决与synchronized对象锁分析!