Spring MVC处理器的执行过程

在SpringMVC的流程中,它会把控制器的方法封装为处理器(Handler),为了更加灵活,SpringMVC还提供了处理器的拦截器,从而形成了一条包括处理器和拦截器的执行链,即HandlerExecutionChain,当请求到达服务器时,根据路径和我们配置的路由,就能找到对应的HandlerExecutionChain,源码如下

/**
 * 处理器执行链,用于管理处理器(Handler)及其拦截器(HandlerInterceptor)的执行序列。
 * 提供了添加拦截器、获取处理器和执行拦截器逻辑的方法。
 */
public class HandlerExecutionChain {
    // 日志对象,用于记录执行链中处理程序和拦截器的执行日志
    private static final Log logger = LogFactory.getLog(HandlerExecutionChain.class);

    // 处理程序对象,这个处理程序将负责处理请求
    private final Object handler;

    // 拦截器数组,这些拦截器将按照声明的顺序在处理程序执行前或执行后进行干预
    @Nullable
    private HandlerInterceptor[] interceptors;

    // 拦截器列表,作为拦截器数组的替代或补充,提供更灵活的拦截器管理
    @Nullable
    private List<HandlerInterceptor> interceptorList;

    // 当前拦截器索引,用于跟踪拦截器链的执行进度
    private int interceptorIndex;

    /**
     * 构造函数,初始化处理器执行链。
     * @param handler 处理器对象
     */
    public HandlerExecutionChain(Object handler) {
        this(handler, (HandlerInterceptor[])null);
    }

    /**
     * 构造函数,初始化处理器执行链并包含一组拦截器。
     * @param handler 处理器对象
     * @param interceptors 一个或多个拦截器对象
     */
    public HandlerExecutionChain(Object handler, @Nullable HandlerInterceptor... interceptors) {
        this.interceptorIndex = -1;
        if (handler instanceof HandlerExecutionChain) {
            HandlerExecutionChain originalChain = (HandlerExecutionChain)handler;
            this.handler = originalChain.getHandler();
            this.interceptorList = new ArrayList<>();
            CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList);
            CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList);
        } else {
            this.handler = handler;
            this.interceptors = interceptors;
        }
    }

    /**
     * 获取处理器对象。
     * @return 处理器对象
     */
    public Object getHandler() {
        return this.handler;
    }

    /**
     * 添加一个拦截器到执行链中。
     * @param interceptor 拦截器对象
     */
    public void addInterceptor(HandlerInterceptor interceptor) {
        this.initInterceptorList().add(interceptor);
    }

    /**
     * 在指定位置添加一个拦截器到执行链中。
     * @param index 拦截器的插入位置
     * @param interceptor 拦截器对象
     */
    public void addInterceptor(int index, HandlerInterceptor interceptor) {
        this.initInterceptorList().add(index, interceptor);
    }

    /**
     * 添加多个拦截器到执行链中。
     * @param interceptors 一个或多个拦截器对象
     */
    public void addInterceptors(HandlerInterceptor... interceptors) {
        if (!ObjectUtils.isEmpty(interceptors)) {
            CollectionUtils.mergeArrayIntoCollection(interceptors, this.initInterceptorList());
        }
    }

    /**
     * 初始化拦截器列表,如果尚未初始化。
     * @return 拦截器列表
     */
    private List<HandlerInterceptor> initInterceptorList() {
        if (this.interceptorList == null) {
            this.interceptorList = new ArrayList<>();
            if (this.interceptors != null) {
                CollectionUtils.mergeArrayIntoCollection(this.interceptors, this.interceptorList);
            }
        }
        this.interceptors = null;
        return this.interceptorList;
    }

    /**
     * 获取拦截器数组,如果拦截器列表为空,则转换列表为数组。
     * @return 拦截器数组
     */
    @Nullable
    public HandlerInterceptor[] getInterceptors() {
        if (this.interceptors == null && this.interceptorList != null) {
            this.interceptors = (HandlerInterceptor[])this.interceptorList.toArray(new HandlerInterceptor[0]);
        }
        return this.interceptors;
    }

    /**
     * 遍历并执行所有拦截器的preHandle方法,如果所有拦截器都通过,则继续执行处理器。
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @return 是否继续执行处理器
     * @throws Exception 可能抛出的异常
     */
    boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if (!interceptor.preHandle(request, response, this.handler)) {
                    this.triggerAfterCompletion(request, response, (Exception)null);
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * 遍历并执行所有拦截器的postHandle方法。
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @param mv 视图模型对象,可能为null
     * @throws Exception 可能抛出的异常
     */
    void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = interceptors.length - 1; i >= 0; --i) {
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }
    }

    /**
     * 触发所有拦截器的afterCompletion方法。
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     * @param ex 异常对象,可能为null
     * @throws Exception 可能抛出的异常
     */
    void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = this.interceptorIndex; i >= 0; --i) {
                HandlerInterceptor interceptor = interceptors[i];
                try {
                    interceptor.afterCompletion(request, response, this.handler, ex);
                } catch (Throwable var8) {
                    Throwable ex2 = var8;
                    logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
                }
            }
        }
    }

    /**
     * 对于支持异步处理的拦截器,触发afterConcurrentHandlingStarted方法。
     * @param request HTTP请求对象
     * @param response HTTP响应对象
     */
    void applyAfterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response) {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if (!ObjectUtils.isEmpty(interceptors)) {
            for(int i = interceptors.length - 1; i >= 0; --i) {
                if (interceptors[i] instanceof AsyncHandlerInterceptor) {
                    try {
                        AsyncHandlerInterceptor asyncInterceptor = (AsyncHandlerInterceptor)interceptors[i];
                        asyncInterceptor.afterConcurrentHandlingStarted(request, response, this.handler);
                    } catch (Throwable var6) {
                        Throwable ex = var6;
                        logger.error("Interceptor [" + interceptors[i] + "] failed in afterConcurrentHandlingStarted", ex);
                    }
                }
            }
        }
    }

    /**
     * 返回处理器执行链的字符串表示,包含处理器和拦截器的数量。
     * @return 字符串表示
     */
    public String toString() {
        Object handler = this.getHandler();
        StringBuilder sb = new StringBuilder();
        sb.append("HandlerExecutionChain with [").append(handler).append("] and ");
        if (this.interceptorList != null) {
            sb.append(this.interceptorList.size());
        } else if (this.interceptors != null) {
            sb.append(this.interceptors.length);
        } else {
            sb.append(0);
        }

        return sb.append(" interceptors").toString();
    }
}

源码中不难看出,HandlerExecutionChain的核心内容就是处理器和拦截器,并且拦截器可以是多个;处理器包含了控制器方法的逻辑,为了调用处理器的方法,必须先从请求中获取参数,接着才能调用控制器的方法,因此第一步是获取参数,第二步才是执行控制器的方法,第三步则是分析控制器方法的返回

获取参数是通过接口HandlerMethodArgumentResolver以及其实现类完成的,而分析控制器方法返回是通过接口HandlerMethodReturnValueHandler以及其实现类完成的,如下图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC拦截器

HandlerMethodArgumentResolver

该接口的主要作用使用请求中分析、获取和验证参数,它有大量的实现类,如图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_02


其中有一个抽象类AbstractNamedValueMethodArgumentResolver实现了该接口,又有几个类继承了该抽象类,如图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC通知_03


这些类是用来解析注解,诸如@PathVariable@RequestAttribute@RequestHeader@RequestParam@SessionAttribute等等注解的解析器

/**
 * 处理器方法参数解析器接口。
 * 该接口定义了如何支持和解析处理器方法参数的逻辑。
 * 具体实现类负责识别并解析特定类型的请求参数,将其转换为处理器方法调用所需的参数值。
 */
public interface HandlerMethodArgumentResolver {
    
    /**
     * 检查是否支持给定的参数。
     * 
     * @param parameter 方法参数信息,包含参数类型和参数修饰符等。
     * @return 如果支持该参数,则返回true;否则返回false。
     */
    boolean supportsParameter(MethodParameter parameter);

    /**
     * 解析请求参数并返回解析后的值。
     * 
     * @param parameter 方法参数信息,用于确定如何解析请求参数。
     * @param mavContainer ModelAndViewContainer,可用于存储解析过程中产生的ModelAndView信息,如果不需要则可为null。
     * @param webRequest 表示当前请求的NativeWebRequest,可用于获取请求特定的数据。
     * @param binderFactory WebDataBinderFactory,可用于创建用于数据绑定的WebDataBinder,如果不需要则可为null。
     * @return 解析后的参数值,如果参数为可空且确实为空,则可能返回null。
     * @throws Exception 如果解析过程中发生错误。
     */
    @Nullable
    Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

HandlerMethodArgumentResolver接口主要作用是处理控制器方法参数的解析和绑定。这个接口允许开发者自定义如何从HTTP请求中获取和转换数据,以供控制器方法使用。

  • boolean supportsParameter(MethodParameter parameter)方法: 这个方法用于判断当前的HandlerMethodArgumentResolver实现是否能够处理给定的MethodParameterMethodParameter包含了控制器方法的参数信息,包括参数类型、参数名称等。当Spring MVC框架需要解析控制器方法的参数时,它会遍历所有注册的HandlerMethodArgumentResolver实例,调用此方法来确定哪个实现可以处理特定的参数。如果返回true,那么Spring将调用resolveArgument方法进行实际的参数解析。
  • @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception方法: 这个方法是核心的参数解析方法。它接受四个参数:
  • MethodParameter parameter:表示需要解析的控制器方法参数。
  • ModelAndViewContainer mavContainer:用于存储视图模型和视图信息,如果在解析过程中需要设置ModelAndView,可以使用这个容器。
  • NativeWebRequest webRequest:封装了原始的HTTP请求,可以从中获取请求头、请求参数等信息。
  • WebDataBinderFactory binderFactory:用于创建WebDataBinder,在某些情况下,可以使用它来执行数据绑定。

这个方法的职责是根据MethodParameterwebRequest中提取和转换数据,然后返回解析后的参数值。如果参数为可空类型且实际为空,可以返回null。如果在解析过程中出现错误,应抛出异常

通过实现HandlerMethodArgumentResolver接口,可以扩展Spring MVC的功能,自定义参数解析逻辑,比如处理自定义的请求类型、自定义的请求头信息,或者进行复杂的类型转换等。

在Spring框架中,WebDataBinder主要用于数据绑定,即将请求参数绑定到控制器方法的参数对象上。类型转换是这一过程中的一部分,但它通常不是直接在WebDataBinder接口或其基础使用中显式声明的。类型转换主要通过以下几种方式在Spring中实现,它们与WebDataBinder间接相关联:

  • PropertyEditors: 在较早的Spring版本中,类型转换主要依赖于JavaBeans的PropertyEditor接口。开发者可以通过实现PropertyEditor来自定义类型转换逻辑,并通过CustomEditorConfigurer注册到Spring容器中,这些转换器会在数据绑定期间被WebDataBinder自动应用。
  • ConversionService: 随着Spring的发展,ConversionService接口成为更推荐的类型转换机制。它提供了更强大和灵活的转换服务。你可以通过实现ConverterGenericConverter接口来定义类型转换逻辑,并配置ConversionService实例(如DefaultFormattingConversionService),然后在WebDataBinder初始化时通过DataBinder.setConversionService()方法设置,这样在数据绑定过程中就会使用这些转换器。
  • @InitBinder: 在控制器类中,你可以使用@InitBinder注解的方法来初始化WebDataBinder,在这个方法里你可以注册自定义的转换器、格式化器等,从而影响类型转换的行为。例如,你可以通过binder.registerCustomEditor()binder.setConversionService()来配置类型转换服务。

因此,虽然可能不会直接在WebDataBinder的使用中看到类型转换器的声明或实现,但它们实际上是通过上述机制与数据绑定过程紧密结合在一起的。类型转换逻辑隐藏在这些配置和服务的背后,默默地工作着。如果你需要查看或修改类型转换行为,应该检查上述提到的配置点和自定义实现。

转换器

Spring MVC提供了一个服务类FormattingConversionService,通过这个类能够获取对应的转换器和格式化器,FormattingConversionService扩展了GenericConversionService类并实现了FormatterRegistryEmbeddedValueResolverAware接口,GenericConversionService类实现了ConfigurableConversionService接口,ConfigurableConversionService接口扩展了 ConversionServiceConverterRegistry接口,FormatterRegistry也扩展了ConverterRegistry接口,类型的转换和格式化是一个庞大的体系,各种接口和类的实现可以通过阅读源码全面的理解

从名字上不难看出FormatterRegistry接口和ConverterRegistry接口都是注册机,也就是说FormattingConversionService具备注册ConverterFormatter的功能,其中ConverterRegistry既可以注册Converter也可以注册GenericConverter,而FormatterRegistry则可以注册Formatter

在这个庞大的转换和格式化体系下,不需要任何人为的再另行注册,Spring框架已经做完了大部分工作,但是仍然可以自定义,例如后端和前端约定提交字符串格式为{id}-{role_name}-{note},这样就没有对应的转换器将这个实体转换为POJO,为了方便对Spring MVC的组件进行定制,可以使用WebMvcConfigurer接口,通过它可以很方便的配置转换器和格式化器,也可以通过XML的方式进行注册

一对一转换器

Converter接口是一种一对一转换器,源码如下

/**
 * 定义了一个从一种类型到另一种类型的转换器。
 * <p>
 * 这个接口是一个函数式接口,可以使用Lambda表达式进行实现。它为核心转换系统提供了基础,
 * 允许将任何对象从一种类型转换为另一种类型。转换器的具体实现决定了如何进行这种转换。
 *
 * @param <S> 源类型,即将被转换的类型的参数化类型。
 * @param <T> 目标类型,即转换结果的类型的参数化类型。
 */
package org.springframework.core.convert.converter;

import org.springframework.lang.Nullable;

@FunctionalInterface
public interface Converter<S, T> {
    /**
     * 将给定的源对象转换为目标类型。
     * <p>
     * 这个方法执行实际的转换逻辑。调用者期望根据实现的具体逻辑,将源对象转换为目标类型。
     * 如果转换无法执行或者源对象为null,应该返回null或者抛出异常,这取决于具体实现。
     *
     * @param var1 源对象,即将被转换的对象。它可以为null,具体是否支持null值转换取决于实现。
     * @return 转换后的目标类型对象。如果无法进行转换,可能返回null。
     */
    @Nullable
    T convert(S var1);
}

实现了该接口的类完成了非常多的类型转换

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC执行过程_04

Spring MVC就是通过这些转换器将字符或者字符流等内容转换为控制器不同类型的参数的,这也是能在控制器上获得各类参数的原因

定制化转换器

Spring MVC提供的功能能够满足绝大多数场景,如果需要自定义转换规则,只要实现接口Converter,然后注册给对应的转换服务类就可以了,例如有一个角色对象,是按照格式{id}-{role_name}-{note}传递的,需要定义一个关于字符串和角色的转换类,代码如下

package com.springrest.converter;

import com.springrest.pojo.Role;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;

/**
 * 实现将字符串转换为Role对象的Converter。
 * 该转换器用于处理字符串格式的Role信息,解析字符串并创建相应的Role对象。
 * 字符串格式应为"id-roleName-note",其中id为长整型,roleName和note为字符串。
 */
public class StringToRoleConverter implements Converter<String, Role> {

    /**
     * 将字符串转换为Role对象。
     * 首先检查字符串是否为空,然后检查是否包含"-"字符,再分割字符串并验证分割后的数组长度。
     * 如果字符串格式正确,创建一个新的Role对象,并设置其id、roleName和note属性。
     *
     * @param str 待转换的字符串,格式应为"id-roleName-note"。
     * @return 如果字符串格式正确,返回一个新的Role对象;否则返回null。
     */
    @Override
    public Role convert(String str) {
        // 检查字符串是否为空
        if (StringUtils.isEmpty(str)) {
            return null;
        }

        // 检查字符串是否包含"-"字符
        if (!str.contains("-")) {
            return null;
        }

        // 使用"-"字符分割字符串
        String[] arr = str.split("-");
        // 验证分割后的数组长度是否为3
        if (arr.length != 3) {
            return null;
        }

        // 创建并配置新的Role对象
        Role role = new Role();
        role.setId(Long.parseLong(arr[0]));
        role.setRoleName(arr[1]);
        role.setNote(arr[2]);
        return role;
    }
}

只有这个类还不能工作,Spring MVC并不会将所传递的字符串转换为角色对象,还需要进行注册;而如果在配置Spring MVC时,使用了注解@EnableWebMvc或者在XML配置文件中使用<mvc:annotation-driven/>,系统会自动初始化FormattingConversionService实例,而为了更好的可读性,一般不直接使用这个实例进行注册,而是使用接口WebMvcConfigurer接口,代码如下

package com.springrest.config;

import com.springrest.converter.StringToRoleConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Spring MVC的配置类,用于自定义Spring MVC的行为。
 */
@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {

    /**
     * 注册自定义的转换器,用于将字符串转换为角色对象。
     * 这允许在表单提交或URL参数中直接使用角色名称,而无需手动进行字符串到角色对象的转换。
     * @param registry 格式化注册表,用于向Spring MVC添加自定义转换器。
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToRoleConverter());
    }
}

WebMvcConfigurer接口所定义的addFormatters方法是SpringMVC为我们提供的自定义注册方法,通过它就能够注册自定义的Converter了,也可以使用XML配置完成同样的事

<?xml version="1.0" encoding="UTF-8"?>
<!-- 配置文件声明,指定XML文档的版本和编码 -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 开启Spring MVC的注解驱动,以便支持@ModelAttribute、@PathVariable、@RequestParam等注解 -->
    <mvc:annotation-driven conversion-service="conversionService"/>

    <!-- 配置转换服务,用于支持自定义的数据类型转换 -->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!-- 配置自定义的转换器 -->
        <property name="converters">
            <list>
                <!-- 注册将字符串转换为Role实体类的转换器 -->
                <bean class="com.springrest.converter.StringToRoleConverter"/>
            </list>
        </property>
    </bean>
</beans>

首先在<mvc:annotation-driven/>元素上指定转换服务类conversionService,Spring MVC会生成对应的默认组件,然后通过名称为conversionService的Bean配置自定义转换器,这就是XML的配置方法,这里使用了FormattingConversionServiceFactoryBean,通过它能够生成一个FormattingConversionService实例,然后编写一个新的控制器,代码如下

package com.springrest.controller;


import com.springrest.pojo.Role;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * ConversionController 类负责处理与角色转换相关的HTTP请求。
 * 它使用Spring MVC的@RestController和@RequestMapping注解来定义控制器和请求映射。
 */
@RestController
@RequestMapping("/converter")
public class ConversionController {

    /**
     * 处理GET请求,根据路径变量role返回相应的Role对象。
     * 此方法演示了如何使用Spring MVC的@PathVariable注解将URL路径中的变量绑定到方法参数上。
     *
     * @param role 通过@PathVariable注解从URL路径中获取的角色名称,用于构建或查找相应的Role对象。
     * @return 返回与给定角色名称相对应的Role对象。
     */
    @GetMapping("/{role}")
    public Role convert(@PathVariable("role") Role role){
        return role;
    }
}

启动服务,通过访问http://localhost:8080/springrest_war/mvc/converter/2-role_name_2-note_2查看页面如下

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC通知_05

Spring MVC已经能够通过自定义转换器转换参数了

数组和集合转换器

Converter是一对一的转换器,只能从一种类型转换为另一种类型,不能进行一对多转换,例如把String转换为List<String>或者String[]甚至List<Role>;为此Spring Core加入了另一个转换器结构GenericConverter,它能够满足数组和集合转换,其接口源码如下

package org.springframework.core.convert.converter;

import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * 通用转换器接口,定义了将一个对象转换为另一个对象的能力。
 * 这个接口的实现可以处理各种类型的转换。
 */
public interface GenericConverter {
    /**
     * 返回这个转换器能够转换的类型对的集合。
     * 类型对表示源类型到目标类型的转换关系。
     *
     * @return 转换器支持的类型对的集合,可能为空。
     */
    @Nullable
    Set<ConvertiblePair> getConvertibleTypes();

    /**
     * 将给定的对象转换为目标类型。
     *
     * @param source 要转换的对象,可能为null。
     * @param sourceType 源对象的类型描述。
     * @param targetType 目标类型的类型描述。
     * @return 转换后的对象,可能为null。
     */
    @Nullable
    Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

    /**
     * 表示一种类型到另一种类型的转换关系。
     */
    public static final class ConvertiblePair {
        private final Class<?> sourceType;
        private final Class<?> targetType;

        /**
         * 创建一个类型对实例。
         *
         * @param sourceType 源类型,不能为null。
         * @param targetType 目标类型,不能为null。
         */
        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
            Assert.notNull(sourceType, "Source type must not be null");
            Assert.notNull(targetType, "Target type must not be null");
            this.sourceType = sourceType;
            this.targetType = targetType;
        }

        /**
         * 获取源类型。
         *
         * @return 源类型的Class对象。
         */
        public Class<?> getSourceType() {
            return this.sourceType;
        }

        /**
         * 获取目标类型。
         *
         * @return 目标类型的Class对象。
         */
        public Class<?> getTargetType() {
            return this.targetType;
        }

        /**
         * 比较两个ConvertiblePair是否相等。
         * 两个ConvertiblePair相等的条件是它们的源类型和目标类型都相等。
         *
         * @param other 另一个ConvertiblePair对象。
         * @return 如果两个ConvertiblePair相等,则返回true,否则返回false。
         */
        public boolean equals(@Nullable Object other) {
            if (this == other) {
                return true;
            } else if (other != null && other.getClass() == ConvertiblePair.class) {
                ConvertiblePair otherPair = (ConvertiblePair)other;
                return this.sourceType == otherPair.sourceType && this.targetType == otherPair.targetType;
            } else {
                return false;
            }
        }

        /**
         * 计算ConvertiblePair的哈希码。
         *
         * @return ConvertiblePair的哈希码。
         */
        public int hashCode() {
            return this.sourceType.hashCode() * 31 + this.targetType.hashCode();
        }

        /**
         * 返回ConvertiblePair的字符串表示形式。
         *
         * @return 字符串表示形式,格式为"源类型 -> 目标类型"。
         */
        public String toString() {
            return this.sourceType.getName() + " -> " + this.targetType.getName();
        }
    }
}

为了进行类型匹配判断,还定义了另一个接口ConditionalConverter,源码如下

/**
 * 该接口表示一个条件转换器,用于在特定条件下判断是否支持类型转换。
 * 转换器实现此接口以提供一种方式来决定它们是否应该为给定的源和目标类型执行转换。
 * 这允许转换器仅在特定条件下注册,例如当源和目标类型满足某些约束时。
 * @see org.springframework.core.convert.converter.Converter
 * @see org.springframework.core.convert.support.GenericConversionService
 */
package org.springframework.core.convert.converter;

import org.springframework.core.convert.TypeDescriptor;

public interface ConditionalConverter {
    /**
     * 判断此转换器是否适用于给定的源和目标类型。
     * 此方法用于在尝试进行转换之前确定转换器是否适合处理特定的类型转换。
     * 如果转换器不支持给定的源和目标类型,则应返回false。
     * @param sourceType 源对象的类型描述
     * @param targetType 目标对象的类型描述
     * @return 如果转换器支持从源类型到目标类型的转换,则返回true;否则返回false。
     */
    boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

它是一个有条件的转换器,也就是只有当它所定义的方法matches返回true时,才说明源类型可转换为目标类型,但它仅仅是一个方法,为了整个原有的接口GenericConverter有了一个新的接口ConditionalGenericConverter,它是最常用的集合转换器接口,ConditionalGenericConverter,继承了两个接口的方法,技能判断也能转换

package org.springframework.core.convert.converter;

/**
 * ConditionalGenericConverter接口定义了一个条件通用转换器。
 * 这个接口扩展了GenericConverter接口,并实现了ConditionalConverter接口,
 * 为转换过程提供了条件判断的能力,使得只有在特定条件下,转换器才会被应用。
 *
 * 作为一个条件通用转换器,它的作用是根据特定的条件判断是否对给定的对象进行转换。
 * 这种能力在处理复杂类型转换,或者需要根据特定上下文进行转换的场景中非常有用。
 *
 * @see GenericConverter
 * @see ConditionalConverter
 */
public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

基于此接口,Spring Core开发了不少的实现类,这些实现类都会注册到FormattingConversionService对象里,通过ConditionalConverter的matches进行匹配,如果匹配则调用convert方法进行转换,它能够提供各种对数组和集合的转换

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC通知_06


看一下SpringToArrayConverter实现类的源码

package org.springframework.core.convert.support;

import java.lang.reflect.Array;
import java.util.Collections;
import java.util.Set;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * 将字符串转换为对象数组的转换器。
 * 该转换器旨在处理从字符串到对象数组的转换,字符串表示的是一组以特定分隔符(如逗号)分隔的值。
 * 使用ConversionService来转换每个字符串元素到目标数组元素类型。
 */
final class StringToArrayConverter implements ConditionalGenericConverter {
    private final ConversionService conversionService;

    /**
     * 构造函数初始化转换器,传入ConversionService用于元素类型的转换。
     * 
     * @param conversionService 转换服务,用于处理字符串到目标数组元素类型的转换。
     */
    public StringToArrayConverter(ConversionService conversionService) {
        this.conversionService = conversionService;
    }

    /**
     * 定义可转换的类型对,即从String到Object[]的转换。
     * 
     * @return 可转换的类型对的集合。
     */
    public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
        return Collections.singleton(new GenericConverter.ConvertiblePair(String.class, Object[].class));
    }

    /**
     * 判断当前转换器是否适用于给定的源类型到目标类型的转换。
     * 
     * @param sourceType 源类型描述。
     * @param targetType 目标类型描述。
     * @return 如果可以转换,则返回true;否则返回false。
     */
    public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
        return ConversionUtils.canConvertElements(sourceType, targetType.getElementTypeDescriptor(), this.conversionService);
    }

    /**
     * 执行从字符串到对象数组的实际转换。
     * 
     * @param source 源对象,应为字符串类型。
     * @param sourceType 源类型描述。
     * @param targetType 目标类型描述。
     * @return 转换后的对象数组。
     */
    @Nullable
    public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (source == null) {
            return null;
        } else {
            // 将源字符串分割成数组
            String string = (String)source;
            String[] fields = StringUtils.commaDelimitedListToStringArray(string);
            TypeDescriptor targetElementType = targetType.getElementTypeDescriptor();
            Assert.state(targetElementType != null, "No target element type");
            // 创建目标数组
            Object target = Array.newInstance(targetElementType.getType(), fields.length);

            // 遍历字符串数组,转换每个元素,并设置到目标数组中
            for(int i = 0; i < fields.length; ++i) {
                String sourceElement = fields[i];
                Object targetElement = this.conversionService.convert(sourceElement.trim(), sourceType, targetElementType);
                Array.set(target, i, targetElement);
            }

            return target;
        }
    }
}

可以参考这个源码,修改之前的转换器StringToRoleConverter , 让其转换为数组,通过如下方式进行验证

/**
     * 通过路径变量获取角色列表并转换为特定的角色类型。
     * 此方法解释了为什么需要将List<Role>转换为Role,即为了符合特定的业务逻辑或API设计需求。
     * @param roleList 从路径变量中获取的角色列表,类型为List<Role>。这里解释了为什么使用路径变量来传递角色列表,可能是为了支持批量操作或灵活的角色查询。
     * @return 返回转换后的Role对象。这里解释了为什么需要进行类型转换,可能是为了符合方法返回类型的要求或进行进一步的操作。
     */
    @GetMapping("/roles/{infoes}")
    public Role convert(@PathVariable("infoes") List<Role> roleList){
        return (Role) roleList;
    }

当请求这样的地址mvc/converter/roles/1-update_role_name_1-update_note_1,2-update_role_name_2-update_note_2,3-update_role_name_3-update_note_3,传递参数时,页面以数组的形式呈现

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_07


也可以自定义ConditionalGenericConverter实现类,然后通过WebMvcConfigurer注册使用

格式化器

通常涉及到金额、日期等数据需要进行数据的格式化,按字符串传递但最终需要格式化为明确的金额格式日期格式等等,Spring提供了相关的Formatter接口用于处理格式化,Formatter接口又扩展了Printer和Parser两个接口,接口源码如下

package org.springframework.format;

/**
 * 格式化接口定义了对数据进行格式化输出和解析输入的通用行为。
 * 该接口扩展了Printer和Parser接口,旨在提供一种通用的方法来处理特定类型T的数据格式化和解析。
 * 通过实现这个接口,一个类可以同时提供数据的格式化输出和解析输入的能力,适用于各种应用场景。
 * @param <T> 格式化和解析的数据类型。
 */
public interface Formatter<T> extends Printer<T>, Parser<T> {
}
package org.springframework.format;

import java.util.Locale;

/**
 * Printer接口定义了一个函数式接口,用于根据特定的Locale打印对象。
 * 它被设计为可以使用Lambda表达式进行实例化,专注于提供国际化打印能力。
 *
 * @param <T> 该接口的泛型参数,表示可以打印任意类型的对象。
 */
@FunctionalInterface
public interface Printer<T> {
    /**
     * 根据指定的Locale打印对象。
     * @param var1 要打印的对象,可以是任何类型。
     * @param var2 用于格式化输出的Locale对象,确保打印结果符合特定地区的语言习惯。
     * @return 返回格式化后的字符串,准备进行打印。
     */
    String print(T var1, Locale var2);
}
package org.springframework.format;

import java.text.ParseException;
import java.util.Locale;

/**
 * 解析器接口,定义了将字符串转换为特定类型对象的方法。
 * <p>该接口作为一个函数接口使用,旨在通过lambda表达式或方法引用来实现字符串到特定类型(T)的解析。
 * 它被设计用于Spring的格式化框架中,特别是在数据绑定和表单验证方面。
 * @param <T> 要解析的目标类型。
 */
@FunctionalInterface
public interface Parser<T> {
    /**
     * 将给定的字符串根据指定的地区信息解析为特定类型(T)的对象。
     * <p>该方法是解析器的核心功能,它负责将字符串表示转换为应用程序可以使用的对象形式。
     * 解析过程中可能需要考虑地区相关信息来正确解析字符串,比如日期和数字的格式。
     * @param text 待解析的字符串。
     * @param locale 解析过程中使用的地区信息。
     * @return 解析后的目标类型对象。
     * @throws ParseException 如果解析过程中遇到错误。
     */
    T parse(String var1, Locale var2) throws ParseException;
}

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC通知_08


通过print方法能将结果按照一定的格式输出字符串,通过parse方法能够将满足一定格式的字符串转换为对象,如此便满足了格式化数据的需求了,它内部实际是委托给Converter机制实现的,同样的需要定制化的场景并不多

日期格式化器在Spring MVC中是由系统在启动时完成初始化的,并不需要干预,同时它提供注解@DateTimeFormat定义日期格式,采用注解@NumberFormat进行数字的格式转换

首先新建一个表单/WEB-INF/jsp/formatter.jsp, 代码如下

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>date</title>
</head>
<body>
    <form id="form" action="./formatter" method="post">
        <table>
            <tr>
                <td>日期</td>
                <td><input id="date" name="date" type="text" value="2024-3-13"/></td>
            </tr>
            
            <tr>
                <td>金额</td>
                <td><input id="amount" name="amount" type="text" value="123,000.00"/></td>
            </tr>
            
            <tr>
                <td></td>
                <td align="right">
                    <input id="commit" type="submit" value="提交"/>
                </td>
            </tr>
        </table>
    </form>
</body>
</html>

然后编写对应的控制器,代码如下

/**
     * 处理GET请求的页面路由方法。
     * 该方法用于响应浏览器的GET请求,返回一个ModelAndView对象,其中包含要显示的视图名称。
     * @return ModelAndView 对象,指定要渲染的视图名称。
     */
    @GetMapping("/page")
    public ModelAndView page(){
        ModelAndView modelAndView = new ModelAndView("formatter");
        return modelAndView;
    }

    /**
     * 处理POST请求的数据格式化方法。
     * 该方法接收一个日期和一个金额作为参数,对这两个参数进行格式化,并将格式化后的结果返回。
     * 使用@DateTimeFormat和@NumberFormat注解分别对日期和金额参数进行格式化指定。
     * @param date 日期参数,使用ISO日期格式进行解析。
     * @param amount 金额参数,使用特定的数字格式进行解析。
     * @return 包含格式化后日期和金额的Map对象。
     */
    @PostMapping("/formatter")
    public Map<String, Object> format(@RequestParam("date") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) Date date,
                                      @RequestParam("amount") @NumberFormat(pattern = "#,###.##") Double amount){
        Map<String, Object> result = new HashMap<>();
        result.put("date", date);
        result.put("amount", amount);
        return result;
    }

代码中使用了注解@RequestParam来获取对应的参数,通过注解@DateTimeFormat@NumberFormat配上目标格式,处理器就能够将参数通过对应的格式化器进行转换,并传递给控制器了

参数可以是一个POJO,而不是简单的日期和数字,我们要给POJO加入对应的注解,比如日期POJO和数字POJO,首先创建这样的POJO,代码如下

package com.springrest.pojo;

import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.util.Date;

/**
 * FormatPojo 类用于展示日期和十进制字段的格式化。
 * 它使用了 Spring 的 @DateTimeFormat 注解来进行日期格式指定,
 * 并通过 BigDecimal 类型处理十进制数字以确保精度。
 */
public class FormatPojo {
    /**
     * 日期字段,使用 @DateTimeFormat 注解进行日期格式规范。
     * 注解的 iso 属性设置为 DATE,表示传入的日期应遵循 ISO 日期格式。
     */
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private Date date;

    /**
     * 十进制金额字段,使用 @DateTimeFormat 注解进行格式规范。
     * 此处 pattern 属性设置为 "#,###.00",用于将十进制数格式化为常见的货币格式。
     */
    @DateTimeFormat(pattern = "#,###.00")
    private BigDecimal amount;

    /**
     * 获取日期值。
     * @return 返回当前日期字段的值。
     */
    public Date getDate() {
        return date;
    }

    /**
     * 设置日期值。
     * @param date 要设置的新日期值。
     */
    public void setDate(Date date) {
        this.date = date;
    }

    /**
     * 获取十进制金额值。
     * @return 返回当前金额字段的值。
     */
    public BigDecimal getAmount() {
        return amount;
    }

    /**
     * 设置十进制金额值。
     * @param amount 要设置的新金额值。
     */
    public void setAmount(BigDecimal amount) {
        this.amount = amount;
    }
}

在需要格式化的属性上加了注解,Spring的处理器会根据注解使用对应的格式化器,按照配置转换,然后在控制器中添加对应的方法,代码如下

/**
     * 对传入的FormatPojo对象进行格式化处理。
     * 该方法接收一个FormatPojo对象作为参数,并直接返回该对象。
     * 主要用于演示如何通过RESTful API接收和返回POJO对象。
     * @param formatPojo 待格式化的POJO对象,包含了需要进行格式化处理的属性。
     * @return 经过格式化处理后的FormatPojo对象,保持原样返回。
     */
    @PostMapping("/formatter/pojo")
    public FormatPojo formatPojo(FormatPojo formatPojo){
        return formatPojo;
    }

然后修改formatter.jsp中表单的提交路径为./formatter/pojo,再请求页面便能看到结果

HttpMessageConverter消息转换器

@RequestBody这个注解能够解析媒体类型为JSON的请求体,@RestController@RequestBody的返回结果能够直接转换为JSON的媒体类型,其原因是在SpringMVC中通过HttpMessageConverter消息转换器实现了中间的转换,这个消息转换器的作用其一是接收并转换HTTP请求体的内容,其二是转换控制器方法返回值,其接口源码如下

package org.springframework.http.converter;

/**
 * 接口HttpMessageConverter用于将HTTP消息体与Java对象之间进行相互转换。
 * 它支持读取(解析)HTTP输入消息和写入(序列化)HTTP输出消息。
 * 
 * @param <T> 该接口支持泛型,允许转换特定类型的Java对象。
 */
public interface HttpMessageConverter<T> {

    /**
     * 判断是否支持读取指定的Java类型和媒体类型。
     * 
     * @param clazz 指定的Java类型。
     * @param contentType 指定的媒体类型,可能为null。
     * @return 如果支持读取给定的Java类型和媒体类型,则返回true;否则返回false。
     */
    boolean canRead(Class<?> clazz, @Nullable MediaType contentType);

    /**
     * 判断是否支持写入指定的Java类型和媒体类型。
     * 
     * @param clazz 指定的Java类型。
     * @param contentType 指定的媒体类型,可能为null。
     * @return 如果支持写入给定的Java类型和媒体类型,则返回true;否则返回false。
     */
    boolean canWrite(Class<?> clazz, @Nullable MediaType contentType);

    /**
     * 获取此转换器支持的所有媒体类型。
     * 
     * @return 支持的媒体类型的列表。
     */
    List<MediaType> getSupportedMediaTypes();

    /**
     * 从HTTP输入消息中读取数据,并将其转换为指定类型的Java对象。
     * 
     * @param clazz 指定要转换成的Java类型的Class对象。
     * @param inputMessage HTTP输入消息,从中读取数据。
     * @return 转换后的Java对象。
     * @throws IOException 如果读取输入消息时发生I/O错误。
     * @throws HttpMessageNotReadableException 如果无法解析输入消息。
     */
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;

    /**
     * 将给定的Java对象写入HTTP输出消息。
     * 
     * @param object 要写入的Java对象。
     * @param contentType 指定的媒体类型,可能为null。
     * @param outputMessage HTTP输出消息,向其中写入数据。
     * @throws IOException 如果写入输出消息时发生I/O错误。
     * @throws HttpMessageNotWritableException 如果无法序列化Java对象。
     */
    void write(T object, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}

Spring MVC中已经提供了许多实现类,大部分情况下无需定制化实现

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC执行过程_09


真正用的比较多的还是MappingJackson2HttpMessageConverter,这个JSON消息的转换类能够让控制器参数接收JSON数据,也可以将控制器返回的结果在处理器内转换为JSON数据,其源码如下

package org.springframework.http.converter.json;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;

/**
 * 基于Jackson 2的HTTP消息转换器,用于将Java对象转换为JSON格式,并且支持从JSON格式转换为Java对象。
 * 这个类继承自AbstractJackson2HttpMessageConverter,具体实现了对JSON数据的序列化和反序列化处理。
 */
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
    @Nullable
    private String jsonPrefix;

    /**
     * 默认构造函数,使用Jackson2ObjectMapperBuilder创建默认ObjectMapper实例。
     */
    public MappingJackson2HttpMessageConverter() {
        this(Jackson2ObjectMapperBuilder.json().build());
    }

    /**
     * 构造函数,使用提供的ObjectMapper实例初始化转换器。
     * 
     * @param objectMapper 用于序列化和反序列化JSON的ObjectMapper实例。
     */
    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
    }

    /**
     * 设置JSON前缀,用于在JSON数据前添加特定的字符串。
     * 
     * @param jsonPrefix 要设置的JSON前缀字符串。
     */
    public void setJsonPrefix(String jsonPrefix) {
        this.jsonPrefix = jsonPrefix;
    }

    /**
     * 设置是否在JSON数据前添加特定的前缀。
     * 
     * @param prefixJson 如果为true,则添加前缀 ")]}', ";否则不添加前缀。
     */
    public void setPrefixJson(boolean prefixJson) {
        this.jsonPrefix = prefixJson ? ")]}', " : null;
    }

    /**
     * 在写入JSON数据前,如果设置了JSON前缀,则将其写入。
     * 
     * @param generator 用于生成JSON数据的JsonGenerator。
     * @param object 要序列化为JSON的对象。
     * @throws IOException 如果写入过程中发生IO错误。
     */
    protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
        if (this.jsonPrefix != null) {
            generator.writeRaw(this.jsonPrefix);
        }
    }
}

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC拦截器_10


在Spring5及之后的版本中,当RequestMappingHandlerAdapter初始化时,Spring MVC会自动初始化MappingJackson2HttpMessageConverter无需注册,如果是之前的Spring版本则需要用Java代码自行注册:

  • 可以通过继承抽象类WebMvcConfigurerAdapter然后覆盖其configureMessageConverters方法或者extendMessageConverters方法,源码如下
  • 也可以实现WebMvcConfigurer接口,如果Java的版本在8之前,实现WebMvcConfigurer接口需要编写不少的代码,不是很方便
/**
 * 配置消息转换器。
 * 该方法用于自定义Spring MVC中使用的HttpMessageConverter列表。消息转换器负责将HTTP请求消息转换为Java对象,
 * 以及将Java对象转换为HTTP响应消息。通过重写此方法,我们可以添加、删除或配置默认的消息转换器,以满足特定的序列化和反序列化需求。
 * @param converters HttpMessageConverter的列表,用于处理HTTP请求和响应的转换。
 */
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 默认实现为空,子类可根据需要重写以自定义消息转换器配置
}

/**
 * 扩展消息转换器。
 * 该方法提供了一个机制,用于在Spring MVC的默认消息转换器基础上进行扩展。通过此方法,我们可以向系统中添加自定义的消息转换器,
 * 以支持额外的序列化和反序列化需求,例如处理特定的媒体类型或定制化的转换逻辑。
 * @param converters HttpMessageConverter的列表,用于处理HTTP请求和响应的转换,可以通过添加新的转换器来扩展系统的能力。
 */
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    // 默认实现为空,子类可根据需要重写以扩展消息转换器的功能
}

configureMessageConverters方法是覆盖掉默认的HttpMessageConverter,而extendMessageConverters是在原有的基础上添加自定义的HttpMessageConverter

package com.springrest.config;

import com.springrest.converter.StringToRoleConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * Spring MVC的配置类,用于自定义Spring MVC的行为。
 */
@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {

    /**
     * 注册自定义的转换器,用于将字符串转换为角色对象。
     * 这允许在表单提交或URL参数中直接使用角色名称,而无需手动进行字符串到角色对象的转换。
     * @param registry 格式化注册表,用于向Spring MVC添加自定义转换器。
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToRoleConverter());
    }

    /**
     * 扩展消息转换器列表。
     * <p>
     * 此方法的目的是向Spring MVC中处理HTTP请求和响应的消息转换器列表中添加一个新的转换器。
     * 具体来说,添加了一个MappingJackson2HttpMessageConverter,它用于序列化和反序列化JSON数据。
     * 这对于支持API返回JSON格式的数据或接受JSON格式的请求体是必需的。
     *
     * @param converters 消息转换器的列表,这个列表将被扩展以包含新的JSON转换器。
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加MappingJackson2HttpMessageConverter实例到转换器列表
        converters.add(new MappingJackson2HttpMessageConverter());
    }
}

有了这个HttpMessageConverter,当我们在控制器中加入了@ResponseBody或者@ResponseController时,Spring MVC便会将应答请求转变为JSON的类型;处理器会再控制器返回结果后,遍历其注册的多个HttpMessageConverter实例,根据每个实例的canWrite方法进行判断,如果为true,则采用该实例转换方法返回的结果

由于MappingJackson2HttpMessageConverter支持JSON数据的转换,它和注解@ResponseBody@ResponseController的媒体类型一致,因此Spring MVC采用MappingJackson2HttpMessageConverter处理控制器返回的结果,这样就能让处理器将控制器返回的结果转换为JSON数据集了

对于注解@RequestBody也是类似的处理,通过MappingJackson2HttpMessageConverter接收请求体,将其转换为控制器的参数,此时在Spring MVC的流程中返回的ModelAndView为null,所以也就没有后面的视图渲染的过程了,换句话说,SpringMVC处理这样的流程,在处理器阶段就已经完成了

也可以使用XML配置自定义的HttpMessageConverter,代码如下

<?xml version="1.0" encoding="UTF-8"?>
<!-- 配置文件声明,指定XML文档的版本和编码 -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!-- 开启Spring MVC的注解驱动,以便支持@ModelAttribute、@PathVariable、@RequestParam等注解 -->
    <mvc:annotation-driven conversion-service="conversionService">
        <!-- 配置消息转换器,用于处理HTTP请求和响应的转换 -->
        <mvc:message-converters>
            <!-- 配置JSON消息转换器,支持将Java对象转换为JSON格式的数据 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 配置转换服务,用于支持自定义的数据类型转换 -->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!-- 配置自定义的转换器 -->
        <property name="converters">
            <list>
                <!-- 注册将字符串转换为Role实体类的转换器 -->
                <bean class="com.springrest.converter.StringToRoleConverter"/>
            </list>
        </property>
    </bean>
</beans>

对于MappingJackson2HttpMessageConverter的应用很简单,只需要注解@RequestBody@ResponseBody@RestController就可以了,当遇到这3个注解时,SpringMVC会将接收或者响应类型转变为JSON媒体类型,然后通过媒体类型找到配置的MappingJackson2HttpMessageConverter进行转换;

这三个注解都是用了类RequestResponseBodyMethodProcessor作为处理的参数和结果解析器,该类分别实现了处理器参数解析接口HandlerMethodArgumentResolver和处理器返回结果解析接口HandlerMethodReturnValueHandler并且在RequestMappingHandlerAdapter中获得了初始化

HttpMessageConverter是SpringMVC用处较广的设计,主要作用在于请求体和响应体的处理,对于一些内部属性的转换,还用到了Converter和GenericConverter,以及Formatter机制

Spring MVC拦截器

拦截器是SpringMVC中强大的控件,它可以在进入处理器前后或者请求完成时,甚至在渲染师徒后加入自己的逻辑

拦截器定义

Spring要求处理器的拦截器都要实现org.springframework.web.servlet.HandlerInterceptor接口,这个接口定义了三个方法,代码如下

/**
 * 处理器拦截器接口,用于在请求处理链中插入自定义逻辑。
 * 拦截器可以在请求处理前、处理后和完成后执行自定义逻辑。
 * 例如,用于认证、日志记录、性能监控等场景。
 */
package org.springframework.web.servlet;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.lang.Nullable;

public interface HandlerInterceptor {
    
    /**
     * 在目标处理器执行前调用。
     * 可用于进行权限检查、初始化上下文等。
     * 如果返回false,将跳过目标处理器的执行。
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  将要处理请求的目标对象
     * @return true表示继续处理,false表示中断处理链
     * @throws Exception 如果需要,可以抛出异常
     */
    default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }

    /**
     * 在目标处理器执行后,但在视图渲染前调用。
     * 可用于修改模型数据、进行业务逻辑处理等。
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  处理请求的目标对象
     * @param modelAndView 视图模型对象,可能为null,表示不返回视图
     * @throws Exception 如果需要,可以抛出异常
     */
    default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }

    /**
     * 在整个请求处理完成后调用,包括视图渲染。
     * 可用于清理资源、日志记录等。
     * 如果在处理过程中发生了异常,也会调用此方法。
     *
     * @param request  HTTP请求对象
     * @param response HTTP响应对象
     * @param handler  处理请求的目标对象
     * @param ex 异常对象,如果处理过程中没有异常,则为null
     * @throws Exception 如果需要,可以抛出异常
     */
    default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
    }
}

这事Java8的版本,在8之前接口不存在默认方法,所以一般要通过继承抽象类HandlerInterceptorAdapter实现同样的效果

单个拦截器执行流程如图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_11

开发拦截器

首先了解Spring自身发开了很多拦截器,如下图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC通知_12


当XML配置文件中加入了元素<mvc:annotation-driven>或者使用Java配置并使用注解@EnableWebMvc时,系统就会初始化拦截器ConversionServiceExposingIntercepter,它是一个一开始就被SpringMVC系统默认加载的拦截器,其主要作用是根据配置在控制器上的注解完成对应的功能

SpringMVC提供公共拦截器适配器HandlerInterceptorAdapter,在JDK8之前继承这个适配器可以得到拦截器三个方法的空实现, 在Spring5之后,可以直接实现HandlerInterceptor接口,它给与了默认实现方法,编写一个自定义处理器拦截器,代码如下

package com.springrest.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RoleInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RoleInterceptor.preHandle()");
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("RoleInterceptor.postHandle()");
    }
    
    @Override
    
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("RoleInterceptor.afterCompletion()");
    }

使用Java配置该拦截器,使用之前的SpringMvcConfiguration,它实现了接口WebMvcConfigurer,这个接口中定义了添加拦截器的方法,如下所示

/**
 * 添加拦截器到拦截器注册表中。
 * 该方法的目的是为了扩展框架功能,允许开发者注册自定义的拦截器,以 intercept 某些特定的请求或操作。
 * 不过,当前的实现是空的,即没有实际添加任何拦截器。这可能是为了保留扩展点,或者在未来的版本中进行扩展。
 * 
 * @param registry 拦截器注册表,用于管理拦截器。通过这个注册表,可以添加新的拦截器或者管理已经注册的拦截器。
 */
default void addInterceptors(InterceptorRegistry registry) {
}

通过它可以加入自定义的RoleInterceptor,如下代码第三个方法所示

package com.springrest.config;

import com.springrest.converter.StringToRoleConverter;
import com.springrest.interceptor.RoleInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * Spring MVC的配置类,用于自定义Spring MVC的行为。
 */
@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {

    /**
     * 注册自定义的转换器,用于将字符串转换为角色对象。
     * 这允许在表单提交或URL参数中直接使用角色名称,而无需手动进行字符串到角色对象的转换。
     * @param registry 格式化注册表,用于向Spring MVC添加自定义转换器。
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToRoleConverter());
    }

    /**
     * 扩展消息转换器列表。
     * <p>
     * 此方法的目的是向Spring MVC中处理HTTP请求和响应的消息转换器列表中添加一个新的转换器。
     * 具体来说,添加了一个MappingJackson2HttpMessageConverter,它用于序列化和反序列化JSON数据。
     * 这对于支持API返回JSON格式的数据或接受JSON格式的请求体是必需的。
     *
     * @param converters 消息转换器的列表,这个列表将被扩展以包含新的JSON转换器。
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加MappingJackson2HttpMessageConverter实例到转换器列表
        converters.add(new MappingJackson2HttpMessageConverter());
    }


    /**
     * 添加拦截器到拦截器注册表中。
     * 本方法旨在注册一个自定义拦截器`RoleInterceptor`,并配置其拦截规则。
     * 拦截器应用于以`/role/`开头的所有URL路径,采用ANT风格路径匹配:
     *   - `**`表示任意层级的目录及其下的所有文件(在这里即为任意子路径)。
     * 这种配置允许拦截器对角色相关的一系列URL进行统一的预处理或后处理操作。
     * @param registry InterceptorRegistry实例,用于注册拦截器并配置拦截路径模式。
     */
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器并设置拦截路径模式
        registry.addInterceptor(new RoleInterceptor()).addPathPatterns("/role/**");
    }
}

此外也可以在dispatcher-servlet.xml文件中进行同样的配置,如下代码所示

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 开启Spring MVC的注解驱动,以便支持@ModelAttribute、@PathVariable、@RequestParam等注解 -->
    <mvc:annotation-driven conversion-service="conversionService">
        <!-- 配置消息转换器,用于处理HTTP请求和响应的转换 -->
        <mvc:message-converters>
            <!-- 配置JSON消息转换器,支持将Java对象转换为JSON格式的数据 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 配置拦截器,用于拦截以/role/开头的URL请求 -->
    <mvc:interceptors>
        <!-- 定义一个拦截器 -->
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptor"/>
        </mvc:interceptor>
    </mvc:interceptors>


    <!-- 配置转换服务,用于支持自定义的数据类型转换 -->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!-- 配置自定义的转换器 -->
        <property name="converters">
            <list>
                <!-- 注册将字符串转换为Role实体类的转换器 -->
                <bean class="com.springrest.converter.StringToRoleConverter"/>
            </list>
        </property>
    </bean>
</beans>

多个拦截器的情况下执行顺序是前置方法是按配置顺序顺序运行的,然后运行处理器方法,然后再运行后置方法,而后置方法是按照配置顺序逆序运行的,后置方法运行完后再运行完成方法,完成方法也是按照配置顺序逆序运行的,这和责任链模式的运行顺序是类似的,可以用如下代码进行测试

package com.springrest.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RoleInterceptorI implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RoleInterceptorI.preHandle()");
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("RoleInterceptorI.postHandle()");
    }


    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("RoleInterceptorI.afterCompletion()");
    }
}
package com.springrest.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RoleInterceptorII implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RoleInterceptorII.preHandle()");
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("RoleInterceptorII.postHandle()");
    }


    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("RoleInterceptorII.afterCompletion()");
    }
}
package com.springrest.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RoleInterceptorIII implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("RoleInterceptorIII.preHandle()");
        return true;
    }


    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("RoleInterceptorIII.postHandle()");
    }


    @Override

    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("RoleInterceptorIII.afterCompletion()");
    }
}
package com.springrest.config;

import com.springrest.converter.StringToRoleConverter;
import com.springrest.interceptor.RoleInterceptor;
import com.springrest.interceptor.RoleInterceptorI;
import com.springrest.interceptor.RoleInterceptorII;
import com.springrest.interceptor.RoleInterceptorIII;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * Spring MVC的配置类,用于自定义Spring MVC的行为。
 */
@Configuration
public class SpringMvcConfiguration implements WebMvcConfigurer {

    /**
     * 注册自定义的转换器,用于将字符串转换为角色对象。
     * 这允许在表单提交或URL参数中直接使用角色名称,而无需手动进行字符串到角色对象的转换。
     * @param registry 格式化注册表,用于向Spring MVC添加自定义转换器。
     */
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToRoleConverter());
    }

    /**
     * 扩展消息转换器列表。
     * <p>
     * 此方法的目的是向Spring MVC中处理HTTP请求和响应的消息转换器列表中添加一个新的转换器。
     * 具体来说,添加了一个MappingJackson2HttpMessageConverter,它用于序列化和反序列化JSON数据。
     * 这对于支持API返回JSON格式的数据或接受JSON格式的请求体是必需的。
     *
     * @param converters 消息转换器的列表,这个列表将被扩展以包含新的JSON转换器。
     */
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加MappingJackson2HttpMessageConverter实例到转换器列表
        converters.add(new MappingJackson2HttpMessageConverter());
    }


    /**
     * 添加拦截器到拦截器注册表中。
     * 本方法旨在注册一个自定义拦截器`RoleInterceptor`,并配置其拦截规则。
     * 拦截器应用于以`/role/`开头的所有URL路径,采用ANT风格路径匹配:
     *   - `**`表示任意层级的目录及其下的所有文件(在这里即为任意子路径)。
     * 这种配置允许拦截器对角色相关的一系列URL进行统一的预处理或后处理操作。
     * @param registry InterceptorRegistry实例,用于注册拦截器并配置拦截路径模式。
     */
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器并设置拦截路径模式
        registry.addInterceptor(new RoleInterceptorI()).addPathPatterns("/role/**");
        registry.addInterceptor(new RoleInterceptorII()).addPathPatterns("/role/**");
        registry.addInterceptor(new RoleInterceptorIII()).addPathPatterns("/role/**");

    }



}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <!-- 开启Spring MVC的注解驱动,以便支持@ModelAttribute、@PathVariable、@RequestParam等注解 -->
    <mvc:annotation-driven conversion-service="conversionService">
        <!-- 配置消息转换器,用于处理HTTP请求和响应的转换 -->
        <mvc:message-converters>
            <!-- 配置JSON消息转换器,支持将Java对象转换为JSON格式的数据 -->
            <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
        </mvc:message-converters>
    </mvc:annotation-driven>

    <!-- 配置拦截器,用于拦截以/role/开头的URL请求 -->
    <mvc:interceptors>
        <!-- 定义一个拦截器 -->
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorI"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorII"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorIII"/>
        </mvc:interceptor>
    </mvc:interceptors>


    <!-- 配置转换服务,用于支持自定义的数据类型转换 -->
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <!-- 配置自定义的转换器 -->
        <property name="converters">
            <list>
                <!-- 注册将字符串转换为Role实体类的转换器 -->
                <bean class="com.springrest.converter.StringToRoleConverter"/>
            </list>
        </property>
    </bean>
</beans>

当其中一个preHandle方法返回false后,按配置顺序,后面的方法就都不会执行了,只会执行执行过preHandle方法且该方法返回true的拦截器的完成方法(afterCompletion), 且还是逆序,例如三个拦截器,执行到第二个拦截器的preHandle方法返回了false,那么实际上这个时候执行了第一个拦截器的preHandle方法和第二个拦截器的preHandle方法,返回了false,其他的就都不在执行了,包括控制器本身的方法也不会执行了,这个时候只会继续执行第一个拦截器的afterCompletion方法

控制器通知

和SpringAOP一样,SpringMVC也能够给控制器添加通知,主要涉及到如下4个注解

  • @ControllerAdvice:可用于标识类,用以标识该类为控制器通知,它将给对应的控制器植入通知
  • @InitBinder:同于标注方法,可以在控制器方法前运行,用于请求参数属性编辑,一般可以定制属性编辑器或者做数据验证,比如可以定制日期格式等
  • @ExceptionHandler:用于标注方法,通过它可以注册一个控制器异常处理方法,使得控制器发生注册的异常时,都跳转到该方法
  • @ModelAttribute:是一种针对数据模型的注解,它先于控制器方法运行,当标注方法返回对象时,它会保存到数据模型中
package com.springrest.advice;

import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 全局控制器通知类,提供数据绑定、模型属性填充和异常处理的功能。
 * 通过@ControllerAdvice注解,指定此类为全局异常处理器,并且只对@RestController注解的控制器生效。
 */
@ControllerAdvice(basePackages = {"com.springrest.controller.advice"}, annotations = {RestController.class})
public class CommonControllerAdvice {

    /**
     * 初始化数据绑定器,注册自定义日期编辑器。
     * 通过@InitBinder注解,此方法应用于所有控制器方法的参数绑定。
     * 它使得日期类型的输入可以按照"yyyy-MM-dd"格式解析。
     * @param binder WebDataBinder实例,用于注册自定义编辑器。
     */
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));
    }

    /**
     * 在每个请求处理方法执行之前,为模型添加全局属性。
     * 通过@ModelAttribute注解,此方法应用于所有控制器方法。
     * 它使得"projectName"属性对所有控制器方法都可用。
     * @param model Spring MVC的模型对象,用于添加属性。
     */
    @ModelAttribute
    public void populateModel(Model model) {
        model.addAttribute("projectName", "springrest");
    }

    /**
     * 全局异常处理方法。
     * 通过@ExceptionHandler注解,此方法处理所有抛出的异常。
     * 它返回"exception"字符串,该字符串被解析为视图名称,用于显示异常页面。
     * @return 视图名称,用于显示异常信息。
     */
    @ExceptionHandler(Exception.class)
    public String exception(){
        return "exception";
    }
}

注解@ControllerAdvice标识这个类是控制器通知,而这个注解本身是被Component标识的,所以SpringMVC在扫描的时候会将其放置到SpringIoC容器中,而他的属性basePackages用于指定拦截的控制器,其次通过注解@InitBinder完成数据绑定器的初始化,注册了一个自定义日期编辑器,再次注解@ModelAttribute是关于数据模型的,它会在进入控制器方法前运行,加入了一个键值对属性(“projectName”, “springrest”),最后注解@ExceptionHandler的作用是在被拦截的控制器发生异常后,如果异常匹配,就是用该方法处理,返回字符串exception,它会找到对应的JSP响应,可以使用如下代码测试

package com.springrest.controller.advice;

import org.springframework.format.annotation.NumberFormat;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;


@RestController
@RequestMapping("/advice")
public class AdviceController {

    /**
     * 根据提供的日期和金额信息,返回一个包含项目名称、日期和金额的Map。
     * 此方法演示了如何在模型中绑定路径变量和格式化数字。
     * @param date 日期路径变量,格式为yyyy-MM-dd。
     * @param amount 金额路径变量,以特定格式显示的大额数字。
     * @param model Spring MVC模型,用于从视图中检索项目名称。
     * @return 包含项目信息、日期和金额的Map。
     */
    @RequestMapping("/model/attribute/{date}/{amount}")
    public Map<String, Object> testAdvice(@PathVariable("date") Date date, @PathVariable("amount")@NumberFormat(pattern = "##,###,00")BigDecimal amount, Model model){
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("project_name", model.asMap().get("project_name"));
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
        map.put("date", sdf.format(date));
        map.put("amount", amount);
        return map;
    }

    /**
     * 故意抛出运行时异常,以演示Spring MVC如何处理异常。
     * 这个方法没有返回值,因为它在抛出异常后不会到达方法的结尾。
     */
    @RequestMapping("/exception")
    public void exception(){
        throw new RuntimeException("测试异常跳转");
    }
}

这个控制器位于控制器通知类指定的扫描的包中即basePackages = {"com.springrest.controller.advice"},它将被通知拦截,在testAdvice方法中,日期并没有加入格式化,因为在通知那里已经被注解@InitBinder标注的通知方法加入了,此处无需重复处理,如此在访问地址/mvc/advice/model/attribute/2020-03-01/1,234,567.89的时候,被通知类处理过的数据,这便是控制器的通知功能的作用

实际上控制器自己也可以使用@InitBinder@ExceptionHandler、和@ModelAttribute,但它不是全局的,只在当前控制器有效

对于注解@ModelAttribute它是一个和数据模型有关的注解,可以给他变量名称,当它返回值的时候,就能保存到数据模型中,这样就可以通过它和变量名获取数据模型的数据,如下代码所示

/**
     * 初始化角色对象。
     * 此方法用于在处理请求时初始化角色对象,并根据请求路径中的{id}参数尝试从数据库中获取对应的角色信息。
     * 如果{id}参数不存在或无效(小于1),则不进行数据库查询,直接返回null。
     * 这种设计允许后续的处理逻辑根据返回值是否为null来决定是否需要进一步处理或展示错误信息。
     * @param id 角色的唯一标识符,可选参数。
     * @return 初始化后的角色对象,如果id无效或不存在对应的角色,则返回null。
     */
    @ModelAttribute("role")
    public Role initRole(@PathVariable(value = "id", required = false) Long id){
        if(id==null || id<1){
            return null;
        }
        Role role = roleService.getRole(id);
        return role;
    }

    /**
     * 获取角色信息。
     * 此方法用于处理GET请求,从请求模型中获取已经初始化的角色对象,并直接返回。
     * 这里的角色对象可以是由之前的initRole方法初始化的,或者是从其他地方设置的。
     * 方法的设计允许在不需要额外处理的情况下直接返回角色对象,简化了请求处理的流程。
     * @param role 已初始化的角色对象。
     * @return 获取到的角色对象。
     */
    @GetMapping("/info/{id}")
    public Role getRole(@ModelAttribute("role") Role role){
        return role;
    }

上边代码所在的控制器并不在控制器通知CommonControllerAdvice的注解@ControllerAdvice所指定的扫描包内,所以不会被公共通知拦截,因此这个内部的注解@ModelAttribute只是针对当前所在控制器起作用,它所注解的方法会在控制器之前运行,这里定义了变量名为role,这样在运行这个方法后,返回查询的角色对象,系统就会把返回的角色对象以键role保存到数据模型,后面的getRole方法的橘色参数也只需要注解@ModelAttribute通过变量名role取出即可,这样也完成了参数传递

控制器异常

控制器的通知注解@ExceptionHandler可以处理异常,此外,SpringMVC提供了其他的异常处理机制,通过这些机制可以更精准的获取异常信息,默认情况下Spring MVC会将自身产生的异常转换为合适的状态码,通过这些状态码可以进行近一步异常确认

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_13


表中的异常映射码只是一部分,实际上比较多,在枚举类org.springframework.http.HttpStatus中有更多的映射码定义,在实际工作中也可以自定义一些异常如下代码所示

/**
	 * 根据角色ID获取角色信息的处理器方法。
	 * 通过{@code @GetMapping}注解指定处理GET请求的URL路径为/found/{id},其中{id}是角色的ID。
	 * 方法参数{@code id}通过{@code @PathVariable}注解绑定到URL路径中的{id}部分。
	 * 如果角色不存在,抛出自定义的{@code RoleException}异常。
	 * @param id 角色的唯一标识ID。
	 * @return 查询到的角色对象。
	 * @throws RoleException 如果角色不存在,则抛出此异常。
	 */
	@GetMapping("/found/{id}")
	public Role notFound(@PathVariable("id") Long id) {
		Role role = roleService.getRole(id);
		if (role == null) {
			throw new RoleException();
		}
		return role;
	}

	/**
	 * 处理{@code RoleException}异常的方法。
	 * 通过{@code @ExceptionHandler}注解指定此方法处理所有抛出的{@code RoleException}异常。
	 * 使用{@code @ResponseStatus}注解指定异常处理后的HTTP状态码为404(NOT_FOUND),并提供异常信息。
	 * @param RoleException 抛出的异常对象。
	 */
	@ExceptionHandler(RoleException.class)
	@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "Role Exception message is not found")
	public void handlerRoleException() {
	}

notFound方法先从角色编号中查找角色信息,如果失败,则抛出RoleException,当抛出异常后,SpringMVC就会找到被标注@ExceptionHandler的方法,如果和配置的异常匹配,那么就进入该方法,由于只是在当前控制器上加入,所以这个规则只对当前控制器内部有效

Spring MVC国际化

在SpringMVC中对国际化也做了良好的支持,有时候简称国际化为i18n,因为internationalization,最前边i最后边n中间还剩下18个字母,就出现了这个称号;但凡业务涉及到跨国,多语言是必然的,除了语言国际化还要考虑时区、国家习俗等因素

SpringMVC中国际化原理

在Spring MVC中,DispatcherServlet会解析一个LocaleResolver接口对象,通过它来决定用户区域(User Locale),读出对应用户系统设定的内容或者用户选择,从而确定采用何种国际化策略,并且Dispatcher只能注册一个LocaleResolver接口对象,LocaleResolver接口在SpringMVC中存在多个实现类,如图所示

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_14

DispatcherServlet初始化的时候会解析LocaleResolver接口的一个实现类,而LocaleResolver的主要作用是实现解析HTTP请求的上下文,用某种策略确定国际化方案;在LocaleResolver的基础上,Spring还扩展了LocaleContextResolver,它加强了用户国际化方面的功能,包括语言和时区等等;CookieLocalResolver主要使用浏览器的Cookie实现国际化,而Cookie有时候需要通过服务器写入浏览器,所以它是继承一个产生Cookie的类即CookieGeneratorFixedLocaleResolverSessionLocaleResolver有公共的方法,所以提取出了公共的父类AbstractLocaleContextResolver,它是一个能够提供语言和时区的抽象类,其语言功能继承了AbstractLocaleResolver,而时区的实现扩展了LocaleContextResolver接口,这便是整个国际化的体系

其中的4个实现类的作用如下:

  • AcceptHeaderLocaleResolver:Spring默认的国际化解析器,通过检验HTTP请求的accept-language头来解析,这个头部由用户的Web浏览器根据底层操作系统的区域设置设定,并且区域解析器无法改变用户的区域,因为它无法修改用户操作系统和浏览器的区域设置,这些只能由用户自行修改
  • FixedLocaleResolver:使用固定Locale国际化,不可修改Locale,可以由开发者提供固定的规则,一般不用,且比较简单
  • CookieLocaleResolver:根据Cookie上下文获取国际化数据,用户可以删除或者不启用Cookie,在这种情况下,它会根据HTTP头参数accept-language确定国际化
  • SessionLocaleResolver:使用Session存储国际化内容,也就是根据用户Session的参数读取区域设置,所以它是可变的,如果没有设置Session参数,那么它会使用开发者设置的默认值

两个是固定的,而另外两个一个是Cookie相关一个是Session相关

SpringMVC还提供了一个国际化的拦截器即LocaleChangeInterceptor,通过它可以获取参数,既可以通过CookieLocaleResolver使用浏览器的Cookie实现国际化,也可以用SessionLocaleResolver通过服务器的Session实现国际化;

使用Cookie的问题是用户可以删除或者禁用Cookie,显得不是那么可靠,而使用Session虽然可靠但又存在过期的问题

MessageSource接口

MessageSource接口是Spring MVC为了加载消息而设置的,通过它可以加载对应的国际化属性文件,其UML关系如下

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_控制器异常_15

StaticMessageSource类是一种静态的消息源,DelegatingMessageSource实现的是一个代理功能,在实际应用开发中此两者并不常用,而用的较多的是ResourceBundleMessageSourceReloadableResourceBundleMessageSource

ResourceBundleMessageSourceReloadableResourceBundleMessageSource的区别主要是,前者使用JDK提供的ResourceBundle,它只能把文件放在对应的路径下,且不具备热加载的特性,需要重启系统才能重新加载消息源文件;而后者更为灵活,它可以把属性文件放在任意位置,可以在系统不重新启动的情况下重新加载属性文件,这样就可以在系统运行期间修改并更新国际化文件重新定制国际化消息

/**
     * 初始化一个消息源 bean,用于国际化支持。
     * 使用ResourceBundle作为数据源,配置默认编码为UTF-8,并指定消息文件的基础名称为"messages"。
     * 这个方法定义了一个名为"messageSource"的bean。
     * @return 返回一个配置好的ResourceBundleMessageSource实例。
     */
    @Bean(name = "messageSource")
    public MessageSource initMessageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setBasenames("messages");
        return messageSource;
    }

    /**
     * 配置一个可重载的资源包消息源 bean,用于支持动态更新的消息文件。
     * 设置默认编码为UTF-8,消息文件的基础名称为"classpath:messages",并设置缓存时间为3600秒。
     * 这个方法定义了一个同样名为"messageSource"的bean,替代了前一个定义。
     * @return 返回一个配置好的ReloadableResourceBundleMessageSource实例。
     */
    @Bean(name="messageSource")
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setBasename("classpath:messages");
        messageSource.setCacheSeconds(3600);
        return messageSource;
    }

Bean名称被定义为@Bean(name="messageSource"),这个名称是SpringIoC容器约定的名称,不能自定义,否则就找不到它了,当看到这个名称的时候就会找到对应的属性文件,设置编码为UTF-8,而setBasename方法可以传递一个文件名(带路径从classpath算起)的前缀,这里是messages,而后缀则是通过locale确定的

在第二个方法中使用的是ReloadableResourceBundleMessageSource,其实大同小异,只是它的setBasename方法指定了"classpath:messages"意思是在classpath下查找前缀为messages的属性文件,实际上也可以指定为非classpath下的文件路径,然后设置了3600s的探测时间,每隔3600s就会探测属性文件的最后修改时间,如果被修改过就重新加载,这个时间如果设置为-1,表示永远缓存,也就是系统运行期间不进行探测不重新加载,如果设置为0,则每次访问国际化文件时都会探测属性文的最后修改时间而非间隔性质

也可以在XML做同样的配置

<!-- 配置消息资源 bean,用于国际化处理 -->
    <bean id = "messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <!-- 指定消息资源文件的基础名称,通常为properties文件名 -->
        <property name="basename" value="messages"/>
        <!-- 指定消息资源文件的编码格式 -->
        <property name="defaultEncoding" value="UTF-8"/>
    </bean>

    <!-- 配置可重载的消息资源 bean,用于热更新国际化内容 -->
    <bean id = "messageSource2" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <!-- 指定消息资源文件的基础名称,使用classpath:前缀表示从类路径下加载 -->
        <property name="basename" value="classpath:messages"/>
        <!-- 指定消息资源文件的编码格式 -->
        <property name="defaultEncoding" value="UTF-8"/>
        <!-- 设置资源文件的缓存时间,单位为秒,3600秒即1小时 -->
        <property name="cacheSeconds" value="3600"/>
    </bean>

CookieLocaleResolve和SessionLocaleResolver

CookieLocaleResolverSessionLocaleResolver这两个LocaleResolver大同小异

/**
     * 初始化基于Cookie的Locale解析器。
     * 该方法创建并配置了一个CookieLocaleResolver实例,用于处理用户的语言和地区偏好设置,
     * 通过Cookie在客户端进行持久化。
     * @return CookieLocaleResolver 一个配置好的CookieLocaleResolver实例。
     */
    @Bean(name="localeResolver")
    public LocaleResolver initCookieLocaleResolver() {
        CookieLocaleResolver localeResolver = new CookieLocaleResolver();
        // 设置Cookie的名称,用于存储用户语言偏好
        localeResolver.setCookieName("clientlanguage");
        // 设置Cookie的最长有效时间,单位为秒
        localeResolver.setCookieMaxAge(3600);
        // 设置默认的语言和地区为简体中文
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        // 设置默认的时区为GMT+8
        localeResolver.setDefaultTimeZone(TimeZone.getTimeZone("GMT+8"));
        return localeResolver;
    }

    /**
     * 初始化基于Session的Locale解析器。
     * 该方法创建并配置了一个SessionLocaleResolver实例,用于处理用户的语言和地区偏好设置,
     * 通过Session在服务器端进行管理。
     * @return SessionLocaleResolver 一个配置好的SessionLocaleResolver实例。
     */
    @Bean(name="localeResolver")
    public LocaleResolver initSessionLocaleResolver() {
        SessionLocaleResolver localeResolver = new SessionLocaleResolver();
        // 设置默认的语言和地区为简体中文
        localeResolver.setDefaultLocale(Locale.SIMPLIFIED_CHINESE);
        // 设置默认的时区为GMT+8
        localeResolver.setDefaultTimeZone(TimeZone.getTimeZone("GMT+8"));
        return localeResolver;
    }

Bean名称为localeResolver这也是Spring 约定的名称不能改变,由于Cookie是用户可以删除或者禁用的,所以使用Cookie并不能保证读到对应的设置,这时候就要使用大量的默认值

SessionLocaleResolver定义了两个静态公共常亮LOCALE_SESSION_ATTRIBUTE_NAMETIME_ZONE_SESSION_ATTRIBUTE_NAME前者是Session的Locale的键,后者是时区,可以通过控制器去控制

同样也可以使用XML进行同样的配置

<!-- 配置会话级地区解析器,用于支持多语言显示 -->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver">
        <!-- 设置默认语言为中文简体 -->
        <property name="defaultLocale" value="zh_CN"/>
    </bean>

    <!-- 配置基于Cookie的地区解析器,用于支持多语言显示 -->
    <bean id="localeResolver2" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
        <!-- 设置默认语言为中文简体 -->
        <property name="defaultLocale" value="zh_CN"/>
        <!-- 设置用于存储语言信息的Cookie名称 -->
        <property name="cookieName" value="locale"/>
        <!-- 设置Cookie的有效期为3600秒,即1小时 -->
        <property name="cookieMaxAge" value="3600"/>
    </bean>

在cookie的设置中,maxAge属性可以被设置为特定的正整数、0,或者-1,每种情况代表不同的行为:

  • 当maxAge设置为正整数(例如3600):这表示cookie的有效期,以秒为单位。上面的例子中,cookie会在1小时后过期。
  • 当maxAge设置为0:这将命令浏览器立即删除该cookie。这对于需要即时移除某个cookie的情况非常有用,比如用户执行注销操作后,可能需要删除与会话相关的cookie。
  • 当maxAge设置为-1:这表示cookie将成为一个会话级别的cookie。也就是说,这个cookie不会被持久化到硬盘上,而是仅在浏览器的内存中保留,其有效期与浏览器会话相同。当用户关闭浏览器窗口或标签页时,这个cookie就会被自动删除。这是实现诸如购物车这类不需要长期保存用户信息功能的常用方式。

国际化拦截器LocaleChangeInterceptor

通过请求参数去改变国际化的值时,可以使用Spring提供的拦截器LocaleChangeInterceptor,它继承了HandlerInterceptorAdapter,可以覆盖它的preHandle方法,使用系统配置的LocaleResolver实现国际化,修改之前的Java配置文件中添加拦截器的方法,增加国际化拦截器相关配置,代码如下

/**
     * 添加拦截器到拦截器注册表中。
     * 本方法旨在注册一个自定义拦截器`RoleInterceptor`,并配置其拦截规则。
     * 拦截器应用于以`/role/`开头的所有URL路径,采用ANT风格路径匹配:
     *   - `**`表示任意层级的目录及其下的所有文件(在这里即为任意子路径)。
     * 这种配置允许拦截器对角色相关的一系列URL进行统一的预处理或后处理操作。
     * @param registry InterceptorRegistry实例,用于注册拦截器并配置拦截路径模式。
     */
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册拦截器并设置拦截路径模式
        registry.addInterceptor(new RoleInterceptorI()).addPathPatterns("/role/**");
        registry.addInterceptor(new RoleInterceptorII()).addPathPatterns("/role/**");
        registry.addInterceptor(new RoleInterceptorIII()).addPathPatterns("/role/**");

        // 创建LocaleChangeInterceptor实例,用于处理语言环境的改变
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();

        // 设置参数名,用于指定哪个请求参数用于改变语言环境
        localeChangeInterceptor.setParamName("language");

        // 将拦截器添加到拦截器注册表中,并指定拦截所有路径
        // 这样任何请求都可以触发语言环境的改变
        registry.addInterceptor(localeChangeInterceptor).addPathPatterns("/**");
    }

当请求过来时,拦截器首先会监控有没有language请求参数,如果有则获取它,然后通过使用系统配置的LocalResolver实现国际化,如果获取不到language参数,或者获取到的不在设置范围内,就会采用默认的国际化配置,也就是LocaleResolver调用的setDefaultLocale方法指定的配置

同样使用XML也可以做同样的配置

<mvc:interceptors>
        <!-- 定义一个拦截器 -->
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorI"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorII"/>
        </mvc:interceptor>
        <mvc:interceptor>
            <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/role/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="com.springrest.interceptor.RoleInterceptorIII"/>
        </mvc:interceptor>
        <mvc:interceptor>
        <!-- 拦截器的作用路径,匹配/role/开头的所有URL -->
            <mvc:mapping path="/**"/>
            <!-- 拦截器的实现类,这里使用RoleInterceptor类作为拦截器 -->
            <bean class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
                <property name="paramName" value="language"/>
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>

代码实例

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC执行过程_16

<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>
<%@taglib prefix="mvc" uri="http://www.springframework.org/tags/form"%>
<%@taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>国际化</title>
</head>
<body>
<!-- 使用spring:message标签来显示国际化消息,这里的"welcome"是在资源文件中定义的键 -->
<h2>
    <spring:message code="welcome" />
</h2>
<!-- 获取当前请求的Locale信息 -->
Locale:
<%
    /* 获取响应的Locale语言部分 */
    String language = response.getLocale().getLanguage();
    /* 获取响应的Locale国家部分 */
    String country = response.getLocale().getCountry();
    /* 以下两行代码用于调试,打印当前Locale的信息到控制台 */
    out.println(language + "-" + country);
%>
/>
</body>
</html>

再加上一个控制器

package com.springrest.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * MessageController类负责处理与消息相关的HTTP请求。
 * 它被标注为@Controller,表示它是一个Spring MVC控制器。
 * @Controller注解告诉Spring框架这个类将处理来自客户端的请求。
 * @RequestMapping注解用于映射URL到处理器方法,这里指定了所有以"/message"开头的URL都将由这个控制器处理。
 */
@Controller
@RequestMapping("/message")
public class MessageController {

    /**
     * 处理/i18n/page请求,返回一个视图名称。
     * 这个方法的目的是展示一个国际化页面。
     * @param model Spring MVC中的Model对象,用于向视图传递数据,在这个方法中,我们没有向视图传递任何数据,所以模型参数没有被使用。
     * @return 返回字符串"i18n",这个字符串是视图的名称,Spring MVC会根据这个字符串寻找对应的视图进行渲染。
     */
    @RequestMapping("/i18n/page")
    public String page(Model model){
        return "i18n";
    }
}

互联网应用主流框架整合之SpingMVC运转逻辑及高级应用_SpringMVC拦截器_17