【SpringBoot(三)】从请求到响应再到视图解析与模板引擎,本文带你领悟SpringBoot请求接收全流程!

本文涉及的产品
应用实时监控服务-可观测链路OpenTelemetry版,每月50GB免费额度
可观测可视化 Grafana 版,10个用户账号 1个月
应用实时监控服务-应用监控,每月50GB免费额度
简介: Springboot专栏第三章,从请求的接收到视图解析,再到thymeleaf模板引擎的使用!本文带你领悟SpringBoot请求接收到渲染的使用全流程!

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 返回值处理器原理
  1. 返回值处理器判断是否支持这种类型返回值 supportsRetruenType
  2. 返回值处理器调用 handleReturnValue 进行处理
  3. RequestResponseBodyMethodProcossor 可以处理返回值标注了 @ResponseBOdy注解。
  1. 利用MessageConverters 进行处理,将数据写为 json
  1. 内容协商(浏览器默认会以请求头的方式告诉服务器他能接收什么样的内容类型)
  2. 服务器最终根据自己自身的能力,决定服务器能生成出什么样的内容类型的数据
  3. SpringMvc 会遍历所有容器底层的 HttpMessageConverter,看谁能处理?
  1. 得到 MappingJackson2HttpMessageConverter 可以将对象写为json
  2. 利用 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

  1. 只支持byte类型
  2. String
  3. Resource
  4. ResourceRegion
  5. DOM/SAX/StAX/Stream Source.class
  6. MultitValueMap
  7. true
  8. 支持注解方式 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

确定客户端接收什么样的内容类型;

  1. Parameter策略优先确定实要返回json数据(获取请求头中的format的值)
  2. 通过遍历服务端可支持的媒体类型,来使用对应的接收类型,如果没有匹配到,那么默认都使用 */*

4.2.2.4 内容协商原理

  1. 判断当前响应头中是否已经由确定的媒体类型。MediaType
  2. 获取客户端(Postman、浏览器)支持接收的内容类型(请求)。(获取客户端accept请求头字段)
  1. contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
  2. HeaderContentNegotiationStrategy 确认客户端可以接收的内容类型

  1. 遍历所有当前系统的Message Converter ,看谁支持操作中国对象(Person)
  2. 找到支持操作Person的converter,把converter支持的媒体类型统计出来
  3. 客户端需要【application/xml】。服务端能力【10种、json、xml】

  1. 进行内容协商的最佳匹配媒体类型
  2. 用支持将对象转为最佳匹配媒体类型的converter,并调用它进行能转化
4.2.2.4.1 自定义MessageConverter

场景定义:

1、浏览器发送请求直接返回xml [application/xml] jacksonXmlConverter

2、如果是ajax请求 返回json [application/json] jacksonJsonConverter

3、如果另外app发送请求,返回自定义协商数据 [application/x-XXX] xxxConverter

执行步骤:

  1. 添加自定义的MessageConverter进行系统底层
  2. 系统底层就会统计出所有MessageConverter能操作哪些类型
  3. 客户端内容协商 [XXX —> XXXConverter]

执行操作:

  1. 创建自定义信息转换器。当继承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());
    }
}
  1. 在自定义的 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));
}
  1. 再次重写 extendMessageConverters 方法,添加自定义信息转换器
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 添加自定义 信息转换器
    converters.add(new RixMessageConverter());
}

5. 视图解析与模板引擎

视图解析:SpringBoot 默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染

5.1 视图解析

5.1.1 视图解析原理流程

  1. 目标方法处理的过程汇总,所有的数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把它重写放在 ModelAndViewContainer 中
  3. 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
  4. processDispatchResult 处理派发结果(页面该如何响应)
  1. render(mv,request,reponse),该方法进行页面渲染逻辑
  • 根据方法的String返回值,得到 view 对象【定义页面的渲染逻辑】
  1. 所有的视图解析器尝试是否能根据当前返回值得到 View 对象
  2. 得到了 redirect:/main.html --> Thymeleaf new RedirectView
  3. ContentNegotiationViewResolver 里面包含了所有的视图解析器,内部还是利用所有的视图解析器得到视图对象
  4. view.render(mv.getModeInternal(),request,response); 视图对象调用自定义的render机械能页面渲染工作
  1. redirectView 如何渲染【重定向到一个页面】
  1. 获取目标url地址
  2. 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})

自动配好的策略:

  1. 所有 thymeleaf 的配置值都在 ThymeleafProperties
  2. 配置好了 SpringTemplateEngine
  3. 配置好了 ThymeleafViewResolver
  4. 只需要开发页面即可,无需配置信息

默认前缀后缀

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 使用步骤

  1. 创建拦截器,继承 HandlerInterceptor 接口
  2. 实现接口的方法
/**
     * 目标方法执行完成前
     * @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 {
}
  1. 将拦截器添加进容器中(实现 WebMvcConfigurer 的 addInterceptors)
@Configuration
public class AdminConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        
    }
}
  1. 指定拦截器
registry.addInterceptor(new LoginInterceptor())
    /* 所有请求都会被拦截,包括静态资源 */
    .addPathPatterns("/**")     
    .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**","/js/**");

6.2 拦截器原理

  1. 根据当前请求,找到 HandlerExectionChain 【可以处理请求的handler以及handler的所有 拦截器】
  2. 先来顺序执行所有拦截器的 perHandler 方法
  1. 如果当前拦截器 preHandler 返回为true。则执行下一个拦截器的 preHandler
  2. 如果当前拦截器返回为false。直接触发 triggerAfterCompletion 方法,然后倒序执行所有以及执行了的拦截器的 afterCompletion
  1. 如果任何一个拦截器执行失败(返回 false ),直接跳出不执行目标方法
  2. 所有拦截器都返回 true,都执行目标方法
  3. 倒序执行所有拦截器的 postHandler 方法
  4. 前面的步骤有任何异常,都会直接触发 afterCompletion 方法
  5. 页面成功渲染完成以后,也会倒序触发 afterCompletion

7. ❤️👌SpringBoot 专栏前文回顾

8. 💕👉 其他好文推荐

目录
相关文章
|
11天前
|
存储 关系型数据库 分布式数据库
PostgreSQL 18 发布,快来 PolarDB 尝鲜!
PostgreSQL 18 发布,PolarDB for PostgreSQL 全面兼容。新版本支持异步I/O、UUIDv7、虚拟生成列、逻辑复制增强及OAuth认证,显著提升性能与安全。PolarDB-PG 18 支持存算分离架构,融合海量弹性存储与极致计算性能,搭配丰富插件生态,为企业提供高效、稳定、灵活的云数据库解决方案,助力企业数字化转型如虎添翼!
|
10天前
|
存储 人工智能 搜索推荐
终身学习型智能体
当前人工智能前沿研究的一个重要方向:构建能够自主学习、调用工具、积累经验的小型智能体(Agent)。 我们可以称这种系统为“终身学习型智能体”或“自适应认知代理”。它的设计理念就是: 不靠庞大的内置知识取胜,而是依靠高效的推理能力 + 动态获取知识的能力 + 经验积累机制。
356 131
|
10天前
|
存储 人工智能 Java
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
本文讲解 Prompt 基本概念与 10 个优化技巧,结合学术分析 AI 应用的需求分析、设计方案,介绍 Spring AI 中 ChatClient 及 Advisors 的使用。
443 131
AI 超级智能体全栈项目阶段二:Prompt 优化技巧与学术分析 AI 应用开发实现上下文联系多轮对话
|
4天前
|
存储 安全 前端开发
如何将加密和解密函数应用到实际项目中?
如何将加密和解密函数应用到实际项目中?
206 138
|
10天前
|
人工智能 Java API
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
本文介绍AI大模型的核心概念、分类及开发者学习路径,重点讲解如何选择与接入大模型。项目基于Spring Boot,使用阿里云灵积模型(Qwen-Plus),对比SDK、HTTP、Spring AI和LangChain4j四种接入方式,助力开发者高效构建AI应用。
405 122
AI 超级智能体全栈项目阶段一:AI大模型概述、选型、项目初始化以及基于阿里云灵积模型 Qwen-Plus实现模型接入四种方式(SDK/HTTP/SpringAI/langchain4j)
|
4天前
|
存储 JSON 安全
加密和解密函数的具体实现代码
加密和解密函数的具体实现代码
204 136
|
22天前
|
弹性计算 关系型数据库 微服务
基于 Docker 与 Kubernetes(K3s)的微服务:阿里云生产环境扩容实践
在微服务架构中,如何实现“稳定扩容”与“成本可控”是企业面临的核心挑战。本文结合 Python FastAPI 微服务实战,详解如何基于阿里云基础设施,利用 Docker 封装服务、K3s 实现容器编排,构建生产级微服务架构。内容涵盖容器构建、集群部署、自动扩缩容、可观测性等关键环节,适配阿里云资源特性与服务生态,助力企业打造低成本、高可靠、易扩展的微服务解决方案。
1363 8
|
9天前
|
监控 JavaScript Java
基于大模型技术的反欺诈知识问答系统
随着互联网与金融科技发展,网络欺诈频发,构建高效反欺诈平台成为迫切需求。本文基于Java、Vue.js、Spring Boot与MySQL技术,设计实现集欺诈识别、宣传教育、用户互动于一体的反欺诈系统,提升公众防范意识,助力企业合规与用户权益保护。

热门文章

最新文章