【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler (中)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring MVC容器的web九大组件之---HandlerAdapter源码详解---一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler (中)

ModelAndViewResolverMethodReturnValueHandler


这个就很厉害了,它是Spring MVC交给我们自定义返回值处理器的一个非常重要的渠道。从官方的javadoc里也能看出来:


 * This return value handler is intended to be ordered after all others as it
 * attempts to handle _any_ return value type (i.e. returns {@code true} for
 * all return types).


简单的说它是放在所有的其它的处理器最后一位的,所以它的supportsReturnType()是永远return true。 但它默认并没有给我们配置进来(而是我们根据需要自己选装~),装配的源码如下:


...
    // Catch-all
    if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
      handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
    }
    else {
      handlers.add(new ModelAttributeMethodProcessor(true));
    }
...


由此可见默认情况下它添加进来的是ModelAttributeMethodProcessor,但凡你RequestMappingHandlerAdapter#setModelAndViewResolvers()自己往里set了个ModelAndViewResolver,它就会被添加,进而让ModelAndViewResolver生效~。


ModelAndViewResolver它是一个接口,Spring并没有默认的实现类。Spring对它的定位很清楚:SPI for resolving custom return values from a specific handler method,它就是给我们自己来自定义处理返回值的一个处理器。通常用于检测特殊的返回类型,解析它们的已知结果值,下面我们自己玩一把试试~~~

public class MyModelAndViewResolver implements ModelAndViewResolver {
    @Override
    public ModelAndView resolveModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue, ExtendedModelMap implicitModel, NativeWebRequest webRequest) {
        System.out.println("...MyModelAndViewResolver...");
        if (returnValue instanceof Person) {
            ModelAndView modelAndView = new ModelAndView();
            Person person = (Person) returnValue;
            // 把属性值放进Model里
            implicitModel.addAttribute("name", person.name).addAttribute("age", person.age);
            modelAndView.setViewName("person");
            modelAndView.setStatus(HttpStatus.CREATED); //返回201的状态码
            return modelAndView;
        } else {
            return UNRESOLVED;
        }
    }
}


读源码发现我们重点就是要在RequestMappingHandlerAdapter这个Bean初始化,也就是执行afterPropertiesSet()方法的时候把ModelAndViewResolver给放进去,这样子就会生效了。


通读之后,我们发现WebMvcConfigurationSupport它的createRequestMappingHandlerAdapter()方法是受保护的。因此我们可以通过重新注册一个它来达到效果:


至于扩展Spring MVC采用WebMvcConfigurer接口还是继承WebMvcConfigurationSupport,建议参见:

WebMvcConfigurationSupport与WebMvcConfigurer的关系


因此我们只需要这么来定义即可:


@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
  // 通过继承WebMvcConfigurationSupport 的方式去覆盖,前提是你对原理比较熟悉~
    @Configuration
    public static class MyWebMvcConfigurationSupport extends WebMvcConfigurationSupport {
        @Override
        protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
            RequestMappingHandlerAdapter requestMappingHandlerAdapter = super.createRequestMappingHandlerAdapter();
            requestMappingHandlerAdapter.setModelAndViewResolvers(Arrays.asList(new MyModelAndViewResolver()));
            return requestMappingHandlerAdapter;
        }
    }
}


这样我们controller返回值类型如下


    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public Person helloGet() {
        Person person = new Person();
        person.name = "fsx";
        person.age = 18;
        return person;
    }


本来我们是不能够解析Person类型的,现在我们也能够正常解析了~~~~~ 这就是Spring MVC留给我们处理自定义类型的一个钩子,可以这么来用~~~


备注:好几个小伙伴问这个核心原理是什么,其实核心原理就是Bean定义的覆盖,希望可以举一反三,它是扩展Spring的一个较为常用的方式~


备注:ModelAndViewResolver从setModelAndViewResolvers()的javadoc里可以看出,它一般用于来做向下兼容。如果你要自定义,一般需要重写HandlerMethodReturnValueHandler和ModelAndViewResolver


ModelAndViewResolverMethodReturnValueHandler它的解释如下:


public class ModelAndViewResolverMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
  @Nullable
  private final List<ModelAndViewResolver> mavResolvers;
  // 持有modelAttributeProcessor 的引用,所以是对它的一个加强~~~~
  private final ModelAttributeMethodProcessor modelAttributeProcessor = new ModelAttributeMethodProcessor(true);
  public ModelAndViewResolverMethodReturnValueHandler(@Nullable List<ModelAndViewResolver> mavResolvers) {
    this.mavResolvers = mavResolvers;
  }
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return true;
  }
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    // 若我们配置了处理器,那就一个一个的处理吧~~~~~
    // 当然,最终真正处理的可能只有一个,这里也是责任链的形式~~~~一般会用if判断
    if (this.mavResolvers != null) {
      for (ModelAndViewResolver mavResolver : this.mavResolvers) {
        Class<?> handlerType = returnType.getContainingClass();
        Method method = returnType.getMethod();
        Assert.state(method != null, "No handler method");
        ExtendedModelMap model = (ExtendedModelMap) mavContainer.getModel();
        // 处理ModelAndView,若返回的不是ModelAndViewResolver.UNRESOLVED
        // 那就说明它处理了,那就return掉~~~~ 逻辑还是很简单的~~~
        ModelAndView mav = mavResolver.resolveModelAndView(method, handlerType, returnValue, model, webRequest);
        // 这一步相当于如果我们自定义了model,会把它的属性合并进来~~~
        // 大多数情况下,我们外部直接操作ExtendedModelMap model这个对象即可
        // 当然你也可以不指定view,自己写成同@ResponseBody一样的效果也是阔仪的
        if (mav != ModelAndViewResolver.UNRESOLVED) {
          mavContainer.addAllAttributes(mav.getModel());
          mavContainer.setViewName(mav.getViewName());
          if (!mav.isReference()) {
            mavContainer.setView(mav.getView());
          }
          return;
        }
      }
    }
    // No suitable ModelAndViewResolver...
    if (this.modelAttributeProcessor.supportsReturnType(returnType)) {
      this.modelAttributeProcessor.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
    }
    else {
      throw new UnsupportedOperationException("Unexpected return type: " +
          returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
    }
  }
}


ModelAttributeMethodProcessor


它是一个Processor,既处理入参封装,也处理返回值,本文只处理返回值部分。

@ModelAttribute能标注在入参处来处理入参,能标在方法处来处理方法返回值。源码部分也只展示处理返回值部分:

public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
  // 默认值是false
  // true:
  private final boolean annotationNotRequired;
  public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
    this.annotationNotRequired = annotationNotRequired;
  }
  // 方法上标注有@ModelAttribute这个注解
  // 或者annotationNotRequired为true并且是简单类型isSimpleProperty() = true
  // 简单类型释义:8大基本类型+包装类型+Enum+CharSequence+Number+Date+URI+Local+Class
  // 数组类型并且是简单的数组(上面那些类型的数组类型)类型  也算作简单类型
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
        (this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
  }
  // 做法相当简单,就是吧返回值放在model里面~~~~
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
    if (returnValue != null) {
      // 这个方法:@ModelAttribute指明了name,那就以它的为准
      // 否则就是一个复杂的获取过程:string...
      String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
      mavContainer.addAttribute(name, returnValue);
    }
  }
}


这个时候因为没有指定viewName,so~~~ 不解释


ServletModelAttributeMethodProcessor

它继承自ModelAttributeMethodProcessor。主要是对入参数据绑定方面做了一些方法的复写,支持到了Servlet等,它主要是对入参做了更多支持,因此本文先不做讨论。


Spring MVC内部实际应用中,ServletModelAttributeMethodProcessor仅仅被用于getDefaultArgumentResolvers()方法内。而ModelAttributeMethodProcessor都用于getDefaultReturnValueHandlers()内。当然它还用于ExceptionHandlerExceptionResolver~~~


AbstractMessageConverterMethodProcessor

一看它以Processor命名结尾,所以它既能处理入参,又能处理返回值。因此一样的,本文只关注返回值处理部分代码:


因为它都没有对supportsReturnType和handleReturnValue进行实现,此抽象类暂时飘过~~~


其实它有一个非常重要的方法:writeWithMessageConverters(),下面详述


RequestResponseBodyMethodProcessor


它继承自AbstractMessageConverterMethodProcessor。从名字或许就能看出来,这个处理器及其重要,因为它处理着我们最为重要的一个注解@ResponseBody(其实它还处理@RequestBody,只是我们这部分不讲请求参数~~~) 并且它在读、写的时候和HttpMessageConverter还有深度结合~~

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
  // 显然可以发现,方法上或者类上标注有@ResponseBody都是可以的~~~~
  // 这也就是为什么现在@RestController可以代替我们的的@Controller + @ResponseBody生效了
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
        returnType.hasMethodAnnotation(ResponseBody.class));
  }
  @Override
  public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
      throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    // 首先就标记:此请求已经被处理了~~~
    mavContainer.setRequestHandled(true);
    ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
    ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
    // Try even with null return value. ResponseBodyAdvice could get involved.
    // 这个方法是核心,也会处理null值~~~  这里面一些Advice会生效~~~~
    // 会选择到合适的HttpMessageConverter,然后进行消息转换~~~~(这里只指写~~~)  这个方法在父类上,是非常核心关键自然也是非常复杂的~~~
    writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
  }
}


HttpEntityMethodProcessor


显然它是处理返回值为HttpEntity类型的。


public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
  // 看这个判断。绝大多数情况下我们使用的返回值是ResponseEntity<T>
  // 当然你也可以直接使用HttpEntity<T> 作为返回值也是可以的
  @Override
  public boolean supportsReturnType(MethodParameter returnType) {
    return (HttpEntity.class.isAssignableFrom(returnType.getParameterType()) &&
        !RequestEntity.class.isAssignableFrom(returnType.getParameterType()));
  }
  // 它的handleReturnValue方法就不详细说了,在RequestResponseBodyMethodProcessor的基础上主要做了如下增强:
  // 1、对请求中有Vary的进行特殊处理
  // 2、Http状态码是200的话。如果是get请求或者Head请求,并且内容没有改变isResourceNotModified  那就直接outputMessage.flush()  然后return掉
  // 3、做Http状态码是3打头(returnStatus / 100 == 3),如果有location的key,就特殊处理
  // 最终,最终。正常情况下:依然同上调用父类writeWithMessageConverters()方法~~~
}

显然,若我们想自己设置管理Http状态码,可以使用ResponseEntity。但显然绝大多数情况下,我们使用@ResponseBody更加的便捷~~~~


因为这块特别重要,所以这里逃不开的要深入了解。毕竟它还和非常重要消息转换器也有非常重要的联系。所以要对父类方法writeWithMessageConverters()进行深入的解释:


你会发现其它的返回值处理器都是不会调用消息转换器的,而只有AbstractMessageConverterMethodProcessor它的两个子类才会这么做。而刚巧,这种方式(@ResponseBody方式)是我们当下最为流行的处理方式,因此非常有必要进行深入的了解~~~


AbstractMessageConverterMethodProcessor#writeWithMessageConverters详解


为了方便讲解,此处我们采用解析此处理器结合讲解:

    @ResponseBody
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public Person helloGet() {
        Person person = new Person();
        person.name = "fsx";
        person.age = 18;
        return person;
    }


很显然它标注了@ResponseBody,所以最终会调用ResponseBodyEmitterReturnValueHandler进行转换、解析~~~~


// @since 3.1  会发现它也处理请求,但是不是本文讨论的重点
//return values by writing to the response with {@link HttpMessageConverter HttpMessageConverters}
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
    implements HandlerMethodReturnValueHandler {
  ...
  // 此处我们只关注它处理返回值的和信访方法
  // Writes the given return type to the given output message
  // 从JavaDoc解释可以看出,它的作用很“单一“:就是把返回值写进output message~~~
  @SuppressWarnings({"rawtypes", "unchecked"})
  protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
      ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
      throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
    Object body;
    Class<?> valueType;
    Type targetType;
    // 注意此处的特殊处理,相当于把所有的CharSequence类型的,都最终当作String类型处理的~
    if (value instanceof CharSequence) {
      body = value.toString();
      valueType = String.class;
      targetType = String.class;
    }
    // 我们本例;body为返回值对象  Person@5229
    // valueType为:class com.fsx.bean.Person
    // targetType:class com.fsx.bean.Person
    else {
      body = value;
      valueType = getReturnValueType(body, returnType);
      // 此处相当于兼容了泛型类型的处理
      targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
    }
    // 若返回值是个org.springframework.core.io.Resource  就走这里  此处忽略~~
    if (isResourceType(value, returnType)) {
      outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
      if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
          outputMessage.getServletResponse().getStatus() == 200) {
        Resource resource = (Resource) value;
        try {
          List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
          outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
          body = HttpRange.toResourceRegions(httpRanges, resource);
          valueType = body.getClass();
          targetType = RESOURCE_REGION_LIST_TYPE;
        }
        catch (IllegalArgumentException ex) {
          outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
          outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
        }
      }
    }
    // selectedMediaType表示最终被选中的MediaType,毕竟请求放可能是接受N多种MediaType的~~~
    MediaType selectedMediaType = null;
    // 一般情况下 请求方很少会指定contentType的~~~
    // 如果请求方法指定了,就以它的为准,就相当于selectedMediaType 里面就被找打了
    // 否则就靠系统自己去寻找到一个最为合适的~~~
    MediaType contentType = outputMessage.getHeaders().getContentType();
    if (contentType != null && contentType.isConcrete()) {
      if (logger.isDebugEnabled()) {
        logger.debug("Found 'Content-Type:" + contentType + "' in response");
      }
      selectedMediaType = contentType;
    }
    else {
      HttpServletRequest request = inputMessage.getServletRequest();
      // 前面我们说了 若是谷歌浏览器  默认它的accept为:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/s
      // 所以此处数组解析出来有7对
      List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
      // 这个方法就是从所有已经注册的转换器里面去找,看看哪些转换器.canWrite,然后把他们所支持的MediaType都加入进来~~~
      // 比如此例只能匹配到MappingJackson2HttpMessageConverter,所以匹配上的有application/json、application/*+json两个
      List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
      // 这个异常应该我们经常碰到:有body体,但是并没有能够支持的转换器,就是这额原因~~~
      if (body != null && producibleTypes.isEmpty()) {
        throw new HttpMessageNotWritableException("No converter found for return value of type: " + valueType);
      }
      // 下面相当于从浏览器可议接受的MediaType里面,最终抉择出N个来
      // 原理也非常简单:你能接受的isCompatibleWith上了我能处理的,那咱们就好说,处理就完了
      List<MediaType> mediaTypesToUse = new ArrayList<>();
      for (MediaType requestedType : acceptableTypes) {
        for (MediaType producibleType : producibleTypes) {
          if (requestedType.isCompatibleWith(producibleType)) {
          // 从两个中选择一个最匹配的  主要是根据q值来比较  排序
          // 比如此例,最终匹配上的有两个:application/json;q=0.8和application/*+json;q=0.8
            mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
          }
        }
      }
      // 这个异常也不少见,比如此处如果没有导入Jackson相关依赖包
      // 就会抛出这个异常了:HttpMediaTypeNotAcceptableException:Could not find acceptable representation
      if (mediaTypesToUse.isEmpty()) {
        if (body != null) {
          throw new HttpMediaTypeNotAcceptableException(producibleTypes);
        }
        if (logger.isDebugEnabled()) {
          logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
        }
        return;
      }
      // 根据Q值进行排序:
      MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
      // 因为已经排过
      for (MediaType mediaType : mediaTypesToUse) {
        if (mediaType.isConcrete()) {
          selectedMediaType = mediaType;
          break;
        }
        else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
          selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
          break;
        }
      }
      if (logger.isDebugEnabled()) {
        logger.debug("Using '" + selectedMediaType + "', given " +
            acceptableTypes + " and supported " + producibleTypes);
      }
    }
    // 最终的最终 都会找到一个决定write的类型,必粗此处为:application/json;q=0.8
    //  因为最终会决策出来一个MediaType,所以此处就是要根据此MediaType找到一个合适的消息转换器,把body向outputstream写进去~~~
    // 注意此处:是RequestResponseBodyAdviceChain执行之处~~~~
    if (selectedMediaType != null) {
      selectedMediaType = selectedMediaType.removeQualityValue();
      for (HttpMessageConverter<?> converter : this.messageConverters) {
        // 从这个判断可以看出 ,处理body里面内容,GenericHttpMessageConverter类型的转换器是优先级更高,优先去处理的
        GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
        if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
            converter.canWrite(valueType, selectedMediaType)) {
          // 在写body之前执行~~~~  会调用我们注册的所有的合适的ResponseBodyAdvice#beforeBodyWrite方法
          // 相当于在写之前,我们可以介入对body体进行处理
          body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
              (Class<? extends HttpMessageConverter<?>>) converter.getClass(),
              inputMessage, outputMessage);
          if (body != null) {
            Object theBody = body;
            LogFormatUtils.traceDebug(logger, traceOn ->
                "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
            // 给响应Response设置一个Content-Disposition的请求头(若需要的话)  若之前已经设置过了,此处将什么都不做
            // 比如我们常见的:response.setHeader("Content-Disposition", "attachment; filename=" + java.net.URLEncoder.encode(fileName, "UTF-8"));
            //Content-disposition 是 MIME 协议的扩展,MIME 协议指示 MIME 用户代理如何显示附加的文件。
            // 当 Internet Explorer 接收到头时,它会激活文件下载对话框,它的文件名框自动填充了头中指定的文件名
            addContentDispositionHeader(inputMessage, outputMessage);
            if (genericConverter != null) {
              genericConverter.write(body, targetType, selectedMediaType, outputMessage);
            }
            else {
              ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
            }
          }
          // 如果return null,body里面是null 那就啥都不写,输出一个debug日志即可~~~~
          else {
            if (logger.isDebugEnabled()) {
              logger.debug("Nothing to write: null body");
            }
          }
          // 这一句表示:只要一个一个消息转换器处理了,就立马停止~~~~
          return;
        }
      }
    }
    if (body != null) {
      throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes);
    }
  }
  ...
}


从上分析可以看出,这里面也提供了ResponseBodyAdvice钩子,我们可以通过实现此接口,来对接口的返回值进行干预、修改。相关注解为:@ControllerAdvice、@RestControllerAdvice

比如我下面这个可以让所有的@ResponseBody的处理器都返回固定值"hello,world":

@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        return "hello,world";
    }
}


这样,访问任何这种rest请求,返回的都是:


image.png


这里面还是有个问题的:我们发现返回的hello world没错,,但是却都是带双引号的,显然这不是我想要的呀。怎么回事?怎么办呢?


原因,其实了解了上面的原理就能知道了。因为执行我们的MyResponseBodyAdvice#beforeBodyWrite此时候消息转换器已经选好了:MappingJackson2HttpMessageConverter


它最后调用writer方法其实底层其实就是调用objectMapper.writeValueAsString()进行写入,而为何会有双引号,看下面这个ObjectMapper的例子就一目了然了:


    public static void main(String[] args) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        System.out.println(objectMapper.writeValueAsString("hello world")); // "hello world"  两边有分号
        Person person = new Person();
        person.name = "fsx";
        person.age = 18;
        System.out.println(objectMapper.writeValueAsString(person)); // {"name":"fsx","age":18}   非常正规的json数据
    }


解决办法:参考StringHttpMessageConverter写字符串的方法,然后自己进一步替换默认操作~~(自定义消息转换器)

相关文章
|
1月前
|
SQL JavaScript Java
springboot+springm vc+mybatis实现增删改查案例!
springboot+springm vc+mybatis实现增删改查案例!
25 0
|
5天前
|
JSON Java fastjson
Spring Boot 底层级探索系列 04 - Web 开发(2)
Spring Boot 底层级探索系列 04 - Web 开发(2)
15 0
|
11天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
23 3
|
11天前
|
存储 前端开发 Java
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
14 1
|
11天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
29 3
|
20天前
|
前端开发 安全 Java
使用Java Web框架:Spring MVC的全面指南
【4月更文挑战第3天】Spring MVC是Spring框架的一部分,用于构建高效、模块化的Web应用。它基于MVC模式,支持多种视图技术。核心概念包括DispatcherServlet(前端控制器)、HandlerMapping(请求映射)、Controller(处理请求)、ViewResolver(视图解析)和ModelAndView(模型和视图容器)。开发流程涉及配置DispatcherServlet、定义Controller、创建View、处理数据、绑定模型和异常处理。
使用Java Web框架:Spring MVC的全面指南
|
27天前
|
敏捷开发 监控 前端开发
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
61 0
|
6月前
|
XML 前端开发 安全
Spring Mvc 拦截器详解
Spring Mvc 拦截器详解
67 0
|
5月前
|
前端开发 Java Spring
Spring MVC拦截器+注解方式实现防止表单重复提交
Spring MVC拦截器+注解方式实现防止表单重复提交
|
3月前
|
前端开发 Java 应用服务中间件
掌握Spring MVC拦截器整合技巧,实现灵活的请求处理与权限控制!
掌握Spring MVC拦截器整合技巧,实现灵活的请求处理与权限控制!