扩展你的Spring与Spring MVC:自定义扩展点详解

   本文将介绍一些常用的自定义能力接口和扩展点,并提供简单示例来帮助开发人员理解如何使用它们。了解这些接口和扩展点将帮助你更好地利用Spring与Spring MVC框架的能力,实现更灵活的应用开发,提升你的知识面和开发能力。

目录

Bean定义相关

BeanDefinitionRegistryPostProcessor

BeanFactoryPostProcessor 

Bean初始化

BeanPostProcessor

常见BeanPostProcessor的实现类

ApplicationContextAwareProcessor

InitDestroyAnnotationBeanPostProcessor

CommonAnnotationBeanPostProcessor

FactoryBean

应用程序上下文和事件相关

ApplicationContextInitializer

ApplicationListener

Web MVC相关

HandlerInterceptor

Filter

HandlerMethodArgumentResolver

HandlerMethodReturnValueHandler

类型转换和验证相关

Converter

ConverterFactory

GenericConverter

ConditionalGenericConverter

GenericConversionService

Validator

常用字段校验注解

如何自定义我们的字段校验注解

其他扩展点和接口

SmartLifecycle

ResourceLoader 


Bean定义相关

BeanDefinitionRegistryPostProcessor

继承自 BeanFactoryPostProcessor ,在Spring容器加载并解析完所有的Bean定义后,允许开发人员对Bean定义的注册进行额外的修改和扩展

@Component
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    /**
    * @param registry 提供我们对bean的定义信息增删改查的能力
    */
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 创建RootBeanDefinition实例,该实例对应的BeanClass是MyBean
        RootBeanDefinition beanDefinition1 = new RootBeanDefinition(MyBean.class);
        BeanDefinition beanDefinition2 = new GenericBeanDefinition();
        beanDefinition2.setBeanClassName("learning.MyBean");
        // 向BeanDefinitionRegistry注册MyBean的BeanDefinition
        registry.registerBeanDefinition("myBean1", beanDefinition1);
        registry.registerBeanDefinition("myBean2",beanDefinition2);
		// 查询/修改
		BeanDefinition myBean1 = registry.getBeanDefinition("myBean1");
		// 删除
		registry.removeBeanDefinition("myBean");
    }
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 空实现
    }
}
class MyBean {
   pubilc String name = "菜鸡";
   public MyBean(){}
}

BeanFactoryPostProcessor 

Bean定义注册之后,Bean实例化之前对已存在的BeanFactory内Bean的定义进行修改和扩展

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        // 修改已注册的Bean定义
        BeanDefinition beanDefinition = beanFactory.getBeanDefinition("myBean1");
        beanDefinition.getPropertyValues().add("property", "modified value");
    }
}

容器初始化阶段,BeanDefinitionRegistryPostProcessor和BeanFactoryPostProcessor 这两个处理器的所有子类会被自动调用并执行相应的处理逻辑,从而实现对Bean定义和配置的修改和扩展

调用时序

c642bc2439284b328573e9cc60edf4e5.png

生成beanDefinition的方式:

  1. 硬编码直接定义BeanDefinition,然后注册到bean工厂中
  2. 通过扫描包路径获取beanDefinition 
  3. 通过@Bean注解获取beanDefinition
  4. 解析spring.xml文件的,生成beanDefinition

Bean初始化

BeanPostProcessor

BeanPostProcessor后置处理器是在Bean实例化完及依赖注入完成(属性赋值完成)后触发的,该接口有两个方法定义

postProcessBeforeInitialization方法会在每一个bean对象的初始化方法调用之前回调postProcessAfterInitialization方法会在每个bean对象的初始化方法调用之后被回调

初始化和实例化是Spring容器中Bean生命周期中的两个不同阶段。

  1. 实例化:这是Bean生命周期的第一个阶段,在这个阶段,Spring容器会使用Bean的构造函数或工厂方法创建Bean实例。在这个阶段,Bean还没有完成所有的依赖注入和初始化操作。如果Bean是通过构造函数创建的,那么此时只是完成了对象的实例化,但还没有执行其他的操作。

  2. 初始化:这是Bean生命周期的第二个阶段,在这个阶段,Spring容器会对已实例化的Bean进行依赖注入、初始化和定制化的处理。在这个阶段,Spring容器会完成所有与Bean相关的初始化操作,包括依赖注入、调用自定义的初始化方法(如果有)、应用容器级别的AOP切面等。

容器会获取BeanPostProcessor的所有实现类,在对象初始化的前后将它们应用于所有bean

cb7904da67354a8caf1bcc80c0b7c066.png

简单使用:对初始化后的用户对象进行脏话校验

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);  
        User user = context.getBean(User.class);  
        System.out.println("User Name: " + user.getName());
    }
    @Bean
    public User user1(){
        return new User("sb");
    }
    // 在bean初始化前调用
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof User) {
            User user = (User) bean;
            System.out.println("Before Initialization: Creating User with name " + user.getName());
            if(user.equals("sb") throw new IllegalArgumentException("username illegal");
        }
        return bean;
    }

    // 在bean初始化后调用
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof User) {
            User user = (User) bean;
            System.out.println("After Initialization: User " + user.getName() + " created");
          
        }
        return bean;
    }
}
class User {
    private String name;
    public User(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
}

源码的话追org.springframework.beans.factory.support的initializeBean方法即可

源码理解配图

2e50073ab89f4ef1a5dd1e8fa505dd93.png

常见BeanPostProcessor的实现类

ApplicationContextAwareProcessor

该后置处理器的作用是,为所有ApplicationContextAware接口的实现类以回调的方式提供ApplicationContext对象,让实现类拥有访问spring的上下文的能力,如下

@Component  
public class MyApplicationContextAwareBean implements ApplicationContextAware {  
    private static ApplicationContext applicationContext;  
    @Override  
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {  
        MyApplicationContextAwareBean.applicationContext = applicationContext;  
    }  
    public static ApplicationContext getApplicationContext() {  
        return applicationContext;  
    }  
}
InitDestroyAnnotationBeanPostProcessor

该后置处理器是一个特殊的BeanPostProcessor实现类,用于专门处理带有初始化和销毁方法注解的Bean,即@PostConstruct、@PreDestroy

  1. 在Bean的初始化方法执行前,如果Bean类中标注了@PostConstruct注解的方法,会调用该方法进行一些预处理操作。
  2. 在Bean的销毁方法执行前,如果Bean类中标注了@PreDestroy注解的方法,会调用该方法进行一些清理操作。
CommonAnnotationBeanPostProcessor

继承自InitDestroyAnnotationBeanPostProcessor,除了能处理@PostConstruct、@PreDestroy注解,还扩展了@Resource等注解的处理实现

这里顺带提一下Spring中提供了3种自定义初始化和销毁方法

  1. 通过@Bean指定init-method和destroy-method属性;
  2. Bean实现InitializingBean(定义初始化逻辑),DisposableBean(定义销毁逻辑);
  3. @PostConstruct:在bean实例化及属性赋值完成;来执行初始化方法,@PreDestroy:在容器销毁bean之前通知我们进行清理工作

InitDestroyAnnotationBeanPostProcessor类就是为了使方法3的作用生效

1. 使用Bean的init-method和destroy-method属性

public class MyBean {
    public void init() {
        // 初始化操作
    }
    public void destroy() {
        // 销毁操作
    }
}
<bean id="myBean" class="com.example.MyBean" init-method="init" destroy-method="destroy"/>

2. 实现InitializingBean和DisposableBean接口

@Component
public class MyBean implements InitializingBean, DisposableBean {
    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化操作
    }
    @Override
    public void destroy() throws Exception {
        // 销毁操作
    }
}

3. 使用@PostConstruct和@PreDestroy注解

@Component
public class MyBean {
    @PostConstruct
    public void init() {
        // 初始化操作
    }
    @PreDestroy
    public void destroy() {
        // 销毁操作
    }
}

FactoryBean

FactoryBean是一个接口,用于需要创建复杂对象时可以自定义Bean的创建逻辑

422f4395364742f094a796ae94e3ceaa.png

来个使用栗子解释一下

public class XXX implements FactoryBean {
    // 核心方法创建复杂对象的逻辑
    @Override
    public Object getObject() throws Exception {
        return new YYY;
    }       
    // 返回值为Ioc容器getBean方法按类型查找时的所匹配的类型
    @Override
    public Class<?> getObjectType() {  
        return AAA.class;
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = 
                new AnnotationConfigApplicationContext(Appconfig.class);
        YYY y = annotationConfigApplicationContext.getBean(AAA.class);
    }
}

XXXFactoryBean类实现了FactoryBean接口,那么XXXFactoryBean就变成了一个工厂,根据AAA的名称获取到的实际上是工厂调用getObject()返回的对象,而不是XXXFactoryBean本身,如果想要获得该XXXFactoryBean对象本身怎么办,根据AAA的名称加上前缀&获取即可

应用场景

AOP的ProxyFactoryBean

Mybatis中的SqlSessionFactoryBean

Hibernate中的SessionFactoryBean

那BeanFactory又是什么:BeanFactory 对象管理工厂接口,负责生产和管理Bean、依赖注入和配置信息解析Bean等。该接口不是IOC容器的具体实现,所以Spring容器给出了很多种实现,如 DefaultListableBeanFactoryXmlBeanFactoryApplicationContext

理解透彻!Spring中BeanFactory与FactoryBean分析 - 知乎

应用程序上下文和事件相关

ApplicationContextInitializer

在Spring容器初始化之前对ApplicationContext进行自定义配置,它提供了一个spring容器刷新之前执行的一个回调函数initialize()(一堆日志开始打出来或者重新要打出来前的回调)

自定义初始化类

public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("====my initializer start===");
        // 自定义properties
        System.setProperty("mykey","myvalue");
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        environment.getSystemProperties().forEach((key, value) -> {
            System.out.println(key + "===" + value);
        });
        // 或者设置开发环境dev
        environment.setActiveProfiles("dev");
        System.out.println("====my initializer end===");
    }
}

若要将我们自定义的ApplicationContextInitializer实现类添加到应用程序,有3种方式

方法1

resources资源目录新建META-INF/spring.factories,\是换行转义符,在第二行填写我们自定义ApplicationContextInitializer实现类的类路径(如com.example.MyApplicationContextInitializer)

588f5abb06d6434580a43d22bb8df565.png

2d3251c8ff4c48808710742e0919a082.png

这种方式利用了SpringApplication加载过程中自动装配所有spring.factories的机制,源码的话可以看@SpringBootApplication注解可追溯到getSpringFactoriesInstances()方法中直接加载并实例后执行对应的initialize方法

方法2

application.properties中添加配置

context.initializer.classes=类路径

这种方式是通过Spring内置的DelegatingApplicationContextInitializer的initialize实现的

方法3

springboot启动类中直接调用add方法

@SpringBootApplication
public class InitializerDemoApplication { 
	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(InitializerDemoApplication.class);
		springApplication.addInitializers(new Demo01ApplicationContextInitializer());
		springApplication.run(args);
	}
}

ApplicationListener

主要功能是监听Spring框架中的事件发生后并触发相关的处理逻辑,很好降低我们代码耦合性

监听是一种机制,类似于js监听事件

监听应用程序内置的事件

public class SystemListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        // 容器上下文初始化和刷新时触发执行
         System.out.println("do something");
    }
}

其它一些Spring事件

ContextClosedEvent:容器关闭的时候,我们可以监听这个事件在容器关闭的时候去清理一些缓存(比如redis)的数据

ApplicationFailedEvent:该事件为spring boot启动失败时的操作

ApplicationPreparedEvent:上下文context准备时触发

ApplicationReadyEvent:上下文已经准备完毕的时候触发,做权限认证的时候。在这个时候就可以去初始化一些权限数据。或者预备其他数据

ApplicationEnvironmentPreparedEvent:环境事先准备    ...等

自定义事件和监听器

 定义订单创建事件,需要继承ApplicationEvent 并在构造方法中调用父类构造方法,其它自定义

/**
 * 订单创建事件
 */
public class OrderCreateEvent extends ApplicationEvent {
    //订单id
    private Long orderId;
    /**
     * @param source  事件源
     * @param orderId 订单id
     */
    public OrderCreateEvent(Object source, Long orderId) {
        super(source);
        this.orderId = orderId;
    }
    public Long getOrderId() {
        return orderId;
    }
    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }
}

定义监听事件,有以下两种方式

方法1

继承ApplicationListener并指定泛型为事件类型

/**
 * 订单创建成功给用户发送邮件
 */
@Component
public class SendEmailOnOrderCreateListener implements ApplicationListener<OrderCreateEvent> {
    @Override
    public void onApplicationEvent(OrderCreateEvent event) {
       try {
           System.out.println(String.format("订单【%d】创建成功,给下单人发送邮件通知!", event.getOrderId()));
           Thread.sleep(5000);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
    }
}

方法2

使用注解@EventListener,并在参数classes指定监听的事件类型

@Component
public class SendEmailOnOrderCreateListener {
    @EventListener(condition = "#event.id != null",classes={MyApplicationEvent.class})
    public void handleEvent(OrderCreateEvent event){
        try {
            System.out.println("do something");
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

事件发布的两种方式

方法1

在我们的组件中继承ApplicationEventPublisherAware后置处理回调注入对象ApplicationEventPublisher以获取发布事件的能力

/**
 * 订单服务
 */
@Service
public class OrderService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher applicationEventPublisher;
    /**
     * 负责用户注册及发布事件的功能
     *
     * @param orderId 订单id
     */
    public void createOrder(Long orderId) {
        System.out.println(String.format("订单【%s】下单成功", orderId));
        //发布注册成功事件
        this.applicationEventPublisher.publishEvent(new OrderCreateEvent(this, orderId));
    }
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {  
        this.applicationEventPublisher = applicationEventPublisher;
    }
}

方法2

或者直接注入@Autowire ApplicationEventPublisher,调用对象的发布方法即可

8e507de370214d6ba6e1744d5ae297f1.png

源码相关

容器上下文的refresh方法中initApplicationEventMulticaster方法初始化事件多播器,后续的事件发布都是由多播器来发布的;registerListeners是注册早期实例化好的监听器;

我们自定义的监听器组件在finishBeanFactoryInitialization实例对象初始化完成后才被初始化

62d612458a5845c58d86d8178eded472.png

通常情况下,我们会使用以ApplicationContext结尾的类作为spring的容器来启动应用,下面2个是比较常见的

  1. AnnotationConfigApplicationContext

  2. ClassPathXmlApplicationContext

1cf47c7b759c498fb1be6f2199ba7e6c.png

AbstractApplicationContext内部有个@NotNull的ApplicationEventMulticaster类型的成员,说明该类中将事件的功能委托给了内部的ApplicationEventMulticaster来实现

ApplicationEventMulticaster源码定义了增加移除监听器和广播事件功能

public interface ApplicationEventMulticaster {
    void addApplicationListener(ApplicationListener<?> listener);
    void addApplicationListenerBean(String listenerBeanName);
    void removeApplicationListener(ApplicationListener<?> listener);
    void removeApplicationListenerBean(String listenerBeanName);
    void removeApplicationListeners(Predicate<ApplicationListener<?>> predicate);
    void removeApplicationListenerBeans(Predicate<String> predicate);
    void removeAllListeners();
    void multicastEvent(ApplicationEvent event);
    void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType);
}

上面类图中多了一个新的接口ApplicationEventPublisher,来看一下源码

这个接口用来发布事件的,内部定义2个方法都是用来发布事件的。

spring中不是有个ApplicationEventMulticaster接口么,此处怎么又来了一个发布事件的接口?

这个接口的实现类中,比如AnnotationConfigApplicationContext内部将这2个方法委托给ApplicationEventMulticaster#multicastEvent进行处理了。

所以调用AbstractApplicationContext中的publishEvent方法,也实现广播事件的效果,不过使用AbstractApplicationContext也只能通过调用publishEvent方法来广播事件。

事务监听器
     @EnableTransactionManagement开启事务支持,@TransactionalEventListener标识事务监听器。
      发布事件的操作必须在事务(@Transactional)内进行,否则监听器不会生效,除非将fallbackExecution标志设置为true(@TransactionalEventListener(fallbackExecution = true))
      可以配置在事务的哪个阶段来监听事务(默认在事务提交后监听),@TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)。

异步支持
     @EnableAsync开启异步支持,@Async标识监听器异步处理。
     开启异步执行后,方法的异常不会抛出,只能在方法内部处理。

监听器顺序
     @Order控制多个监听器的执行顺序,值越小,监听器越先执行。

Web MVC相关

Spring Mvc处理用户请求的流程图

012810f69fc44f64a661ab20033aeb80.png

 SpringMVC 的执行流程如下

  1. 用户通过浏览器发起一个 HTTP 请求,该请求会被 DispatcherServlet(前端控制器)拦截;
  2. DispatcherServlet 调用 HandlerMapping(处理器映射器)找到具体的处理器(Handler)及拦截器,最后以 HandlerExecutionChain 执行链的形式返回给 DispatcherServlet。
  3. DispatcherServlet 将执行链返回的 Handler 信息发送给 HandlerAdapter(处理器适配器);
  4. HandlerAdapter 根据 Handler 信息找到并执行相应的 Handler(即 Controller 控制器)对请求进行处理;
  5. Handler 执行完毕后会返回给 HandlerAdapter 一个 ModelAndView 对象(Spring MVC 的底层对象,包括 Model 数据模型和 View 视图信息);
  6. HandlerAdapter 接收到 ModelAndView 对象后,将其返回给 DispatcherServlet ;
  7. DispatcherServlet 接收到 ModelAndView 对象后,会请求 ViewResolver(视图解析器)对视图进行解析;
  8. ViewResolver 解析完成后,会将 View 视图并返回给 DispatcherServlet;
  9. DispatcherServlet 接收到具体的 View 视图后,进行视图渲染,将 Model 中的模型数据填充到 View 视图中的 request 域,生成最终的 View(视图);
  10. 视图负责将结果显示到浏览器(客户端)。

HandlerInterceptor

拦截器用于拦截和处理HTTP请求,在请求处理之前、之后或视图渲染之前进行自定义的操作

定义一个Interceptor拦截器的两种方式

1. 实现Spring 的HandlerInterceptor 接口

public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        System.out.println("=================preHandle=================");
        // 返回为false 时,表示请求结束后续过滤器链不会再执行
        // 返回true,下一个过滤器preHandle调用,最终由Controller开始处理
        return true;
    }
    /**
     * 后处理回调方法,实现处理器的后处理(但在渲染视图之前),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
            ModelAndView modelAndView) throws Exception {
        System.out.println("=================postHandle=================");
    }
    /**
     * 整个请求处理完毕回调方法,即在视图渲染完毕时回调,如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex)
            throws Exception {
        System.out.println("================afterCompletion==================");
    }
}

2. 继承HandlerInterceptor下层实现类如HandlerInterceptorAdapter

public class MyInterceptor extends HandlerInterceptorAdapter {
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
        System.out.println("request请求地址path="+request.getServletPath()+",uri="+request.getRequestURI());
        // 返回为false 时,表示请求结束后续过滤器链不会再执行
        // 返回true,下一个过滤器preHandle调用,最终由Controller开始处理
        return true;
    }
    @Override
	public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
						   ModelAndView modelAndView) throws Exception {}

	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
        throws Exception {}
}

 配置到webmvc中

@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 可添加多个, pathPatterns为会拦截的路径
        registry.addInterceptor(new MytFilter()).addPathPatterns("/**");
    }
}

Filter

主要用于对请求和响应进行处理和过滤,触发时机是在Servlet容器接收到请求后,但在目标Servlet处理请求之前,即早于拦截器

自定义Filter的两种方式

方法1:创建 Filter过滤器类,并实现 javax.servlet.Filter 接口,在web.xml 文件中配置 该Filter

public class MyFilter implements Filter {
    private String name;
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("##############TestFilter init##############");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        //在DispatcherServlet之前执行
        System.out.println("##############doFilter before##############");
        filterChain.doFilter(servletRequest, servletResponse);
        // 在视图页面返回给客户端之前执行,但是执行顺序在Interceptor之后
        System.out.println("##############doFilter after##############");
    }
    @Override
    public void destroy() {
        System.out.println("##############TestFilter destroy##############");
    }
}
<filter>  
    <filter-name>MyFilter</filter-name>  
    <filter-class>com.example.MyFilter</filter-class>  
    <init-param>  
        <param-name>name</param-name>  
        <param-value>zhangsan</param-value>  
    </init-param>  
</filter>  
<filter-mapping>  
    <filter-name>MyFilter</filter-name>  
    <url-pattern>/*</url-pattern>  
</filter-mapping>

方法2: 使用 @WebFilter 注解,将过滤器配置到webmvc中

@WebFilter(urlPatterns = "/*")
public class MyFilter implements Filter {
    private String name;
    .....
}

将过滤器配置到webmvc中,有注解包扫描/配置类的两种方式,如下

//------ 启动类加上@ServletComponentScan扫描过滤器所在包
@SpringBootApplication 
@ServletComponentScan 
public class TestbootApplication { 
  public static void main(String[] args) { 
    SpringApplication.run(TestbootApplication.class, args); 
  } 
}
@Configuration
public class FilterConfig {
    @Bean
    public FilterRegistrationBean myFilterRegistrationBean() {
        FilterRegistrationBean registration = new FilterRegistrationBean(new MyFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }
}

过滤器和拦截器的区别

过滤器在Servlet容器中的请求和响应之间进行处理,拦截器在Spring MVC框架中的控制器(Controller)的执行前后进行处理,因此过滤先于拦截执行

配置方式:过滤器的配置是通过在web.xml文件中进行配置、使用注解@WebFilter或注入一个Bean来实现。而拦截器的配置是通过在配置类中实现WebMvcConfigurer接口或继承WebMvcConfigurationSupport类,并重写其中的方法来实现

作用对象:过滤器可以作用于Web应用程序中的任何资源,包括控制器、静态资源、Servlet等。而拦截器只能作用于Spring MVC框架中的控制器(Controller)

过滤器的使用示例:

  1. URL请求过滤器:根据请求URL进行过滤,例如限制某些URL只允许特定角色或权限的用户访问
  2. 编码过滤器:对请求和响应进行编码转换,确保字符编码的一致性
  3. 日志记录过滤器:记录请求的详细信息,例如请求URL、请求参数、响应时间等

拦截器的使用示例:

  1. 用户认证拦截器:在进入控制器之前对用户进行身份验证,确保只有经过认证的用户可以访问特定的控制器
  2. 权限验证拦截器:检查用户是否具有访问特定控制器或执行特定操作的权限
  3. 日志记录拦截器:在控制器执行前后记录请求的详细信息,如请求URL、请求参数、处理时间等
  4. 跨域拦截器:在进入控制器之前设置响应头,以允许跨域请求

HandlerMethodArgumentResolver

用于解析处理方法参数并提供对应的参数值将其映射到Controller方法的参数上。它可以用于自定义参数解析逻辑,为处理方法提供所需的参数

接口的定义

public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    /**
        @param parameter: 要解析的方法参数。
        @param mavContainer: 用于处理模型和视图的容器。
        @param webRequest: 当前请求的本地化Web请求。
        @param binderFactory: 用于创建和配置WebDataBinder的工厂类。
    */
    @Nullable
    Object resolveArgument(MethodParameter parameter,
         @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest,
         @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

很好理解,总体看就是对supportsParameter返回true的参数进行resolveArgument自定义参数映射处理

下面实现一个例子:当Controller层的处理器的接收参数为User时,自动解析请求/user?userId=1&username=John 中的userId和username参数映射为一个所需的User

public class User{
    private String userId;
    private String username;
    public User(String userId ,String username){
        this.userId = userId;
        this.username = username;
    }
}

 自定义参数解析器

public class CustomArgumentResolver implements HandlerMethodArgumentResolver {

  @Override
  public boolean supportsParameter(MethodParameter parameter) {
    // 判断是否支持解析该参数
    return parameter.getParameterType().equals(User.class);
  }

  @Override
  public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    // 解析参数并返回对应的参数值
    HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
    String userId = request.getParameter("userId");
    String username = request.getParameter("username");
    
    // 创建User对象并返回
    User user = new User(userId, username);
    return user;
  }
}

 配置参数解析器添加到Spring中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
  @Override
  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    // 添加自定义的参数解析器
    argumentResolvers.add(new CustomArgumentResolver());
  }
}

 控制器层

@RestController
public class UserController {
  @GetMapping("/user")
  public String getUserInfo(User user) {
    // 处理方法逻辑,使用解析得到的User对象
    return "User ID: " + user.getUserId() + ", Username: " + user.getUsername();
  }
}

HandlerMethodReturnValueHandler

一个接口,用于处理Controller方法的返回值,将其转换为视图或其他形式的响应

Spring MVC需要把Controller处理结果ModelAndView对象传递给合适的View,由View来负责把数据渲染到客户端,其中这个传递过程是通过HandlerMethodReturnValueHandler来完成的

接口定义

public interface HandlerMethodReturnValueHandler {
    boolean supportsReturnType(MethodParameter returnType);

    void handleReturnValue(@Nullable Object returnValue,
        MethodParameter returnType, 
        ModelAndViewContainer mavContainer, 
        NativeWebRequest webRequest) throws Exception;
}

和HandlerMethodArgumentResolver接口相似,先判断是否是支持返回值再进行处理

通过 HandlerMethodReturnValueHandler 我们可以对返回的数据进行进一步的封装,减少在业务代码中进行重复的返回值处理。例如例子实现对标注了@Encrypted的方法返回数据进行统一加密

// 自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypted {
    boolean value() default true;
}
@Data
public class ResultInfo<T> implements Serializable {
    public  int code;
    public String message;
    private T body;
    private boolean encrypt;
}
public class MyHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    protected final HandlerMethodReturnValueHandler handlerMethodReturnValueHandler;

    public MyHandlerMethodReturnValueHandler(HandlerMethodReturnValueHandler handlerMethodReturnValueHandler){
        this.handlerMethodReturnValueHandler = handlerMethodReturnValueHandler;
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        //如果方法或者所属类被@ResponseBody注解修饰,并且被@Encrypted自定义注解修饰 返回true
        return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class))
                && returnType.hasMethodAnnotation(Encrypted.class);
    }

    @Override
    public void handleReturnValue(Object returnValue,
                                  MethodParameter returnType,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest) throws Exception {
        if(returnValue instanceof ResultInfo){
            ResultInfo<?> resultInfo = (ResultInfo<?>)returnValue;
            ResultInfo<String> newResultInfo = new ResultInfo<>();
            newResultInfo.setCode(resultInfo.getCode());
            newResultInfo.setMessage(resultInfo.getMessage());
            newResultInfo.setEncrypt(true);
            newResultInfo.setBody(Base64Utils.encodeToString(JSON.toJSONString(resultInfo.getBody()).getBytes(StandardCharsets.UTF_8)));
            //ResponseBody注解执行器
            handlerMethodReturnValueHandler.handleReturnValue(newResultInfo,
                    returnType, mavContainer, webRequest);
        }else{
            handlerMethodReturnValueHandler.handleReturnValue(returnValue,
                    returnType, mavContainer,  webRequest);
        }
    }
}

配置解析器到容器中

spring-mvc.xml的方式

<mvc:annotation-driven >
    <mvc:return-value-handlers>
        <bean class="com.example.mvc.CustomerHandlerMethodReturnValueHandler"/>
    </mvc:return-value-handlers>
</mvc:annotation-driven>

springboot配置类的方式

@Configuration  
public class WebConfig implements WebMvcConfigurer {  
    @Override  
    public void extendHandlerMethodReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {  
        // 在这里注册您的自定义HandlerMethodReturnValueHandler实现类  
        handlers.add(new MyHandlerMethodReturnValueHandler());  
    }  
}

  Controller测试

@RestController
public class TestController {
  @Encrypted
  @RequestMapping("/test")
  public ResultInfo<Map<String, Object>> reqBody(){
    ResultInfo<Map<String, Object>> resultInfo = new ResultInfo<>();
    resultInfo.setCode(200);
    resultInfo.setMessage("success");
    Map<String, Object> map = new HashMap<>();
    map.put("userId", 1);
    map.put("tenantId", 1001);
    map.put("userName", "菠萝追雪");
    resultInfo.setBody(map);
    return resultInfo;
  }
}

在如今的前后端分离的开发方式中,@ResponseBody和@RequestBody注解我们应该十分常见,它们功能的实现也正是实现了HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler接口的RequestResponseBodyMethodProcessor类对请求参数、返回值进行序列化处理,具体看它的源码

类型转换和验证相关

Converter

Spring Mvc的一个功能点,用于实现UI端传递数据的类型转化

对于一般的数据类型,Spring Mvc内置了很多转换器,比如StringToBooleanCoverter较为规范地定义了什么字符串会转为true,什么字符串转为false,这就是为什么我们前端传来的字段值为1使用Boolean接收会是true

6a7a112813394335b40f19524ffb00c1.png

其它的看源码,需要注意的是SerializingConverter类的作用是把任意一个对象,转换成byte[]数组,它是唯一用public修饰的类可以供我们使用,其它所有的Converter对外不公开(因为没有public修饰的类只能在包内部使用)0d9df638697a43a2a36fcb7d2d9de117.png

f9314cd96b6b4aa79ee71c9fc210f63a.png

自定义我们的数据类型转换器

商品参数接收类(Target)

public class GoodsModel{
    //商品名
    private String name;
    // 商品单价
    private Double price;
    //数量
    private Integer num;
    // 商品总价格
    private Double totalPrice;
    //省略getter、setter方法
}

新建一个商品对象转换器,String转为商品类

public class GoodsConverter implements Converter<String, GoodsModel> {
    public GoodsModel convert(String source) {
        // 创建一个Goods实例
        GoodsModel goods = new GoodsModel();
        // 以“,”分隔
        String stringvalues[] = source.split(",");
        if (stringvalues != null && stringvalues.length == 3) {
            // 为Goods实例赋值
            goods.setName(stringvalues[0]);
            Double price = Double.parseDouble(stringvalues[1]);
            Integer num = Integer.parseInt(stringvalues[2]);
            goods.setPrice(price);
            goods.setNumber(num);
            goods.setTotalPrice(price*num);
            return goods;
        } else {
            throw new IllegalArgumentException(String.format(
                    "类型转换失败, 需要格式'apple,10.58,200',但格式是[% s ] ", source));
        }
    }
}

配置到webmvc中,有两种方式

1. spring-mvc.xml配置方式(ConversionService管理着所有的转换器)

 <!--注册类型转换器GoodsConverter-->
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <list>
                <bean class="converter.GoodsConverter"/>
            </list>
        </property>
    </bean>

 2. springboot配置类

@Configuration  
public class WebConfig implements WebMvcConfigurer {  
    @Bean  
    public GoodsConverter goodsConverter() {  
        return new GoodsConverter();  
    }  
    @Override  
    public void addFormatters(FormatterRegistry registry) {  
        registry.addConverter(goodsConverter());  
    }  
}

Controller的处理器参数(Target)为GoodsModel的会应用我们自定义的商品类型转换器

@RestController
public class TestController {
  @Encrypted
  @RequestMapping("/test")
  public String test(@RequestParam("goods") GoodsModel goods){
    return goods.getName() + " " + goods.getPrice() + " " + goods.getName() +" "+ goods.getTotal();
  }
}

访问测试url:/test?goods=apple,10.58,200

Converter只能处理单个源类型和目标类型之间的转换,即1:1,即如果有一系列的类型需要转化我们需要为这一系列的类都要生成Converter

ConverterFactory

我们注意到了上面的多个Converter图中有以ConverterFactory结尾的类,证明该类实现的接口是ConverterFactory而不是Converter

如果我们希望将一种类型的对象转换为另一种类型及其子类对象,例如将 String 转换为 Number 以及 Number 的子类 Integer、Double 等类型的对象,那么就需要一系列的 Converter,如 StringToInteger、StringToDouble 等。ConverterFactory<S,R> 接口的作用就是将这些相同系列的多个 Converter 封装在一起

ConverterFactory接口定义

public interface ConverterFactory<S, R> {
    //Get the converter to convert from S to target type T, where T is also an instance of R
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

三个泛型,S类型转为R类型,方法getConverter返回的是<S类型, R的子类T>的类型转换器

一些实现类如下,需要注意的是:这些类都是没有public修饰的不可对外访问

48fc6b530db24bceb59349fc46911532.png

 简单看一个StringToEnumConverterFactory 字符串类型转为Enum子类对象的Spring源码的实现

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
    StringToEnumConverterFactory() {
    }
    // ConversionUtils.getEnumType获取枚举的class类型
    public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEnum(ConversionUtils.getEnumType(targetType));
    }
    // 单个内部类实现了Converter,对String到T枚举子类型进行了转换实现
    private static class StringToEnum<T extends Enum> implements Converter<String, T> {
        private final Class<T> enumType;
        StringToEnum(Class<T> enumType) {
            this.enumType = enumType;
        }
        @Nullable
        public T convert(String source) {
            // valueof根据值找enum
            return source.isEmpty() ? null : Enum.valueOf(this.enumType, source.trim());
        }
    }
}

其它的实现类大差不差 

该工厂就是用来创建一个converter,把目标类型转换成子类型,因此能够实现1:N的映射

假如Controller的某个处理器参数为一个对象,该对象的有两个自定义枚举属性OrderStatus,GoodsStatus,前端传来orderStatus, goodsStatus的属性值为字符串都能被解析为对应的枚举类型就是这个StringToEnumConverterFactory 在起作用

GenericConverter

该接口会根据源类对象及目标类对象的上下文信息进行类型转换,支持多个source和目标类型的转化,即N:N,最灵活的转换器SPI接口,也是最复杂的

public interface GenericConverter {
    /**
        getConvertibleTypes()方法返回一个source->target的键值对,此处定义好
    支持source到target类型的转换,如string->list, string->map
    */
    @Nullable
    Set<ConvertiblePair> getConvertibleTypes();

    /**
	 * @param source     被转换的东西
	 * @param sourceType 被转换的东西的上下文,可以用于设置条件,具体咋玩看typeDescriptor,比如可以用来判断转换源是否拥有某些注解
	 *                   @see TypeDescriptor
	 * @param targetType 转换目标类型的上下文
    */
    @Nullable
    Object convert(@Nullable Object source, 
        TypeDescriptor sourceType, 
        TypeDescriptor targetType);

    public static final class ConvertiblePair {
        private final Class<?> sourceType;
        private final Class<?> targetType;

        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;
        }
        // 省略get set equals hashcode方法
    }
}

用法

例如实现一个案例:如果方法被resource注解修饰,前端传来的源类型为string并且目标类型为list或map,将string类型转为list或者map

@Component
public class MyGenericConverter implements GenericConverter {
	@Override
	public Set<ConvertiblePair> getConvertibleTypes() {
		Set<ConvertiblePair> convertiblePairs = Collections.newSetFromMap(new ConcurrentHashMap<>());
		ConvertiblePair stringToArray = new ConvertiblePair(String.class, Array.class);
		ConvertiblePair stringToMap = new ConvertiblePair(String.class, Map.class);
		convertiblePairs.add(stringToArray);
		convertiblePairs.add(stringToMap);
		return convertiblePairs;
	}
	@Override
	public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		Resource annotation = sourceType.getAnnotation(Resource.class);
		if (annotation!= null){
			if (targetType.getType() == List.class){
				return Arrays.asList(source.toString().split(":"));
			}
			if (targetType.getType() == Map.class){
				Map<String, String> map = new HashMap<>();
				map.put("a",source.toString().split(":")[0]);
				map.put("b",source.toString().split(":")[1]);
				return map;
			}
		}
		return null;
	}
}

Controller测试

@RestController
public class TestController {
  @RequestMapping("/test1")
  public String test1(@RequestParam("p") List<String> list){
    return list.get(0);
  }
  @RequestMapping("/test2")
  public String test2(@RequestParam("p") Map<String,String> map){
    return map.get("a");
  }
}

访问测试  /test1?p=3:6 

访问测试  /test2?p=3:6

ConditionalGenericConverter

该接口比GenericConverter多了一个有条件判断的方法定义,它继承了两个接口

c8744e5a073d4d37ab3dbb9e83ecefe9.png

刚刚我们知道了GenericConverter,那ConditionalConverter是什么 ,看它接口定义

public interface ConditionalConverter {
boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

从Conditional我们就可以看出来这个接口是用于定义有条件的类型转换器的,也就是说不是简单的满足类型匹配就可以使用该类型转换器进行类型转换了,必须要满足某种条件才能使用该类型转换器,用法看StringToArrayConverter的源码实现 

GenericConversionService

这个接口是一个转换器服务中心,它的属性converters包含所有的转换器服务,它是间接实现了ConversionService,实现判断souce类是否能转为target类,转换的逻辑,就是利用成员变量converters的key,value实现的

ConversionService接口定义

public interface ConversionService {
    boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
    boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
    @Nullable
    <T> T convert(@Nullable Object source, Class<T> targetType);
    @Nullable
    Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

因此,GenericConversionService类还有对converters对外的增删的方法,但是绝大多数情况下,我们不会直接使用GenericConversionService,而是使用它的子类DefaultConversionService。

DefaultConversionService子类默认将很多转换器都提前注册到converters中了,我们自定义的也会随后被add进来

fce98e68a4da4b95a418aab09994d3fd.png

more参考:https://www.cnblogs.com/xfeiyun/p/16921524.html

Formatter,将输入值格式化为字符串,并将格式化的字符串解析为特定类型的值

类似于 Converter<S,T> ,但是Formatter源数据可以是任意类型,但一定会被格式化为 String 类型,可以实现将Java对象转换为字符串和将字符串转换为Java对象

Formatter接口定义

public interface Formatter<T>{
    public T parse(String s,java.util.Locale locale);
    public String print(T object,java.util.Locale locale);
}

 自定义Formatter

public class LocalDateFormatter implements Formatter<LocalDate>{
    private DateTimeFormatter formatter;
    private String datePattern;
 
    public LocalDateFormatter(String datePattern) {
        this.datePattern = datePattern;
        formatter = DateTimeFormatter.ofPattern(datePattern);
    }
 
    @Override
    public LocalDate parse(String s, Locale locale) throws ParseException {
        try {
            return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern));
        }catch (Exception e){
            e.printStackTrace();
            throw e;
        }
    }
 
    @Override
    public String print(LocalDate localDate, Locale locale) {
        return localDate.format(formatter);
    }
}

将这个自定义的Formatter注册到Spring MVC的ConversionService中,两种方式

springboot配置类

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        LocalDateFormatter dateFormatter = new LocalDateFormatter("yyyy-MM-dd");
        registry.addFormatter(dateFormatter);
    }
}

spring-mvc.xml配置文件

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <set>
            <bean class="example.LocalDateFormatter">
                <constructor-arg type="java.lang.String" value="yyyy-MM-dd"/>
            </bean>
        </set>
    </property>
</bean>
 
<mvc:annotation-driven conversion-service="conversionService"/>

 Controller测试

@RequestMapping("/test")
public String test(@RequestParam(required = false) LocalDate date){
    System.out.println("date = " + date);
    return null;
}

Validator

Spring Mvc 提供的用于验证用户输入的工具,它允许您在处理用户提交的表单数据之前对其进行校验

接口定义

常用字段校验注解

@Null 验证对象是否为 null
@NotNull 验证对象是否不为 null, 无法查检长度为 0 的字符串
@NotBlank 检查约束字符串是不是 Null 还有被 Trim 的长度是否大于 0,只对字符串,且会去掉前后空格
@NotEmpty 检查约束元素是否为 NULL 或者是EMPTY

@AssertTrue 验证Boolean对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
@Size(min=, max=) 验证对象(Array, Collection , Map, String)长度是否在给定的范围之内
@Length(min=, max=) 验证字符串长度介于 min 和 max 之间
@Past 验证 Date 和 Calendar 对象是否在当前时间之前,验证成立的话被注释的元素一定是一个过去的日期
@Future 验证Date和 Calendar 对象是否在当前时间之后 ,验证成立的话被注释的元素一定是一个将来的日期
@Pattern 验证 String 对象是否符合正则表达式的规则,被注释的元素符合制定的正则表达式

@Min 验证 Number 和 String 对象是否大等于指定的值
@Max 验证 Number 和 String 对象是否小等于指定的值
@DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过 BigDecimal 定义的最大值的字符串表示 .小数 存在精度
@DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过 BigDecimal 定义的最小值的字符串表示 .小数 存在精度
@Digits 验证 Number 和 String 的构成是否合法
@Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,integer 指定整数精度,fraction 指定小数精度
@Range(min=, max=) 被指定的元素必须在合适的范围内
@Range(min=10000,max=50000,message=”range.bean.wage”)
@CreditCardNumber 信用卡验证
@Email 验证是否是邮件地址,如果为 null,不进行验证,算通过验证
@ScriptAssert(lang= ,script=, alias=)
@URL(protocol=,host=, port=,regexp=, flags=)

使用例子

@Data
class User {
    @NotNull(message = "用户名id不能为空")
    private Long id;

    @NotNull(message = "用户账号不能为空")
    @Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
    private String account;

    @NotNull(message = "用户密码不能为空")
    @Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
    private String password;

    //@Email使用的是org.hibernate包下的注解
    // javax下的包会报错,坑
    @NotNull(message = "用户邮箱不能为空")
    @Email(message = "邮箱格式不正确")
    private String email;

    @Valid
    private Address address;
}

如果对象需要嵌套校验,成员对象需要加上@Valid注释如上的address

Controller层对参数使用

@RestController
public class UserController {
    @PostMapping("/test1")
    public Res test1(@Valid User user){
        System.out.println(user);
        return Res.ok(user);
    }
    @GetMapping("/test2")
    public Res test2(@Validated User user){
        System.out.println(user);
        return Res.ok(user);
    }
    @GetMapping("/test3")
    public Res test3(@Valid @NotNull(message = "id不能为空") Long id){
        System.out.println(id);
        return Res.ok(id);
    }
} 

需要注意的是如/test3接口中字段校验注解如果直接出现在参数中,需要搭配@Valid或者@Validatetd注释下才会生效,@Validatetd可以放在类上

@Validated和@Valid的区别,@Validated可以放在类上,还可以分组校验分组(分组很简单就是对对象的字段校验注解分组,@Validated通过参数指定生效的分组),分组例子如下

定义分组

/**
 *分组校验 (配合spring的@Validated功能分组使用)
 */
public class ValidGroup {
    // 新增使用
    public interface Insert{}
    // 更新使用
    public interface Update{}
    // 删除使用
    public interface Delete{}
    // 属性必须有这两个分组的才验证
    @GroupSequence({Insert.class, Update.class,Delete.class})
    public interface All{}
}

 填写分组参数groups标识是那一组,不写属于默认组

@Data
public class User{
    //只在Delete和Update的时候才会校验
    @Min(value = 1,message = "ID不能小于1",groups = {ValidGroup.Delete.class,ValidGroup.Update.class})
    private int id;

    // 只有Insert插时才会校验
    @NotBlank(message = "用户名不能为空",groups = {ValidGroup.Insert.class})
    private String username;

    // 只有Insert插时才会校验
    @NotBlank(message = "密码不能为空",groups = {ValidGroup.Insert.class})
    @Length(min = 6,max = 20,message = "密码长度在6-20之间")
    private String password;

    @NotBlank(message = "邮箱不能为空")
    @Email(message = "邮箱格式不合理")
    private String email;
}

在Controller中接口@Validated的参数中动态指定对哪组进行校验,如上的email如果不指定分组会归属为默认分组,@Validated的不指定分组则激活的正是默认分组

@RestController
public class UserController{
    @RequestMapping("/saveUserInfo")
    public UserInfo saveUserInfo(@Validated({ValidGroup.Insert.class}) UserInfo userInfo){
        userInfoService.save(userInfo);
        return userInfo;
    }

    @RequestMapping("/updateUserInfo")
    public UserInfo updateUserInfo(@Validated({ValidGroup.Update.class}) UserInfo userInfo){
        userInfoService.update(userInfo);
        return userInfo;
    }

    @RequestMapping("/deleteUserInfo")
    public UserInfo deleteUserInfo(@Validated({ValidGroup.Delete.class}) UserInfo userInfo){
        userInfoService.delete(userInfo);
        return userInfo;
    }
}

如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里并抛出MethodArgumentNotValidException参数未校验通过的异常,我们可以自定义全局异常类捕获并处理

@Slf4j
@ControllerAdvice
public class ValidatedExceptionHandler {
    /**
     * 处理@Validated参数校验失败异常
     * @param exception 异常类
     * @return 响应
     */
    @ResponseBody
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Res exceptionHandler(MethodArgumentNotValidException exception){
        BindingResult result = exception.getBindingResult();
        StringBuilder stringBuilder = new StringBuilder();
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            if (errors != null) {
                errors.forEach(p -> {
                    FieldError fieldError = (FieldError) p;
                    log.warn("Bad Request Parameters: dto entity [{}],field [{}],message [{}]",fieldError.getObjectName(), fieldError.getField(), fieldError.getDefaultMessage());
                    stringBuilder.append(fieldError.getDefaultMessage());
                });
            }
        }
        return Res.fail(stringBuilder.toString());
    }
}

如何自定义我们的字段校验注解

一个简单的例子:  自定义我们的注解用来校验前端传来的Integer参数是否是动态指定的vals数组中其中的一个

定义注解

@Retention(RetentionPolicy.RUNTIME)
// 需要在哪里对参数校验
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
// 指定自己的校验器实现类,可以指定多个不同类型的数据校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class}) 
public @interface ListValue{
    String message() default "Invalid input";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    // 校验时指定的值列表
    int[] vals() default {};
}

定义校验器,实现校验逻辑的地方,需要实现ConstraintValidator<注解类型,要校验值的类型> 

public class ListValueConstraintValidator  implements ConstraintValidator<ListValue,Integer> {
    private final Set<Integer> set=new HashSet<>();
    // 初始化方法
    @Override
    public void initialize(ListValue constraintAnnotation) {
        ConstraintValidator.super.initialize(constraintAnnotation);
        int[] vals=constraintAnnotation.vals();
        for(int val : vals) set.add(val);
    }
    /** 具体校验流程
     * @参数1 需要校验的值
     * @参数2 在校验过程中,可以使用ConstraintValidatorContext对象来记录和传递校验错误信息。
     * @return 是否校验通过 
     */
    @Override
    public boolean isValid(Integer integer, ConstraintValidatorContext constraintValidatorContext) {
        return set.contains(integer);
    }
}

在Controller中使用我们的注解

@RestController
@Validated
public class UserController{
    @RequestMapping("/test")
    public Res saveUserInfo(@ListValue(vals={6,9},message="sb") Integer id){
        return Res.ok(id);
    }
}

另外提一下,注解中default默认值可以从自定义的properties文件获取

7e3c21c835d54a9b978424ccef946682.png

e703ba6c08334afd8d4d4abe92858da6.png

118c25aa25604281a37a30ab2263284c.png

其他扩展点和接口

SmartLifecycle

SmartLifecycle 用于控制 bean 的生命周期。它是 Lifecycle 接口的扩展,提供了更灵活的生命周期管理方式。如当Spring容器加载所有bean并完成初始化之后,会接着回调实现该接口的类中对应的方法(start()方法)

接口定义

  1. isAutoStartup():返回一个布尔值,表示当前 bean 是否自动启动。
  2. getPhase():返回一个整数值,表示当前 bean 的启动阶段。
  3. start():在 bean 启动时调用的方法。
  4. stop():在 bean 停止时调用的方法。
  5. isRunning():返回一个布尔值,表示当前 bean 是否正在运行。
@Component
public class TestSmartLifecycle implements SmartLifecycle {

    private boolean isRunning = false;

    /**
     * 1. 我们主要在该方法中启动任务或者其他异步服务,比如开启MQ接收消息<br/>
     * 2. 当上下文被刷新(所有对象已被实例化和初始化之后)时,将调用该方法,默认生命周期处理器将检查每个SmartLifecycle对象的isAutoStartup()方法返回的布尔值。
     * 如果为“true”,则该方法会被调用,而不是等待显式调用自己的start()方法。
     */
    @Override
    public void start() {
        System.out.println("start");

        // 执行完其他业务后,可以修改 isRunning = true
        isRunning = true;
    }

    /**
     * 如果工程中有多个实现接口SmartLifecycle的类,则这些类的start的执行顺序按getPhase方法返回值从小到大执行。<br/>
     * 例如:1比2先执行,-1比0先执行。 stop方法的执行顺序则相反,getPhase返回值较大类的stop方法先被调用,小的后被调用。
     */
    @Override
    public int getPhase() {
        // 默认为0
        return 0;
    }

    /**
     * 根据该方法的返回值决定是否执行start方法。<br/> 
     * 返回true时start方法会被自动执行,返回false则不会。
     */
    @Override
    public boolean isAutoStartup() {
        // 默认为false
        return true;
    }

    /**
     * 1. 只有该方法返回false时,start方法才会被执行。<br/>
     * 2. 只有该方法返回true时,stop(Runnable callback)或stop()方法才会被执行。
     */
    @Override
    public boolean isRunning() {
        // 默认返回false
        return isRunning;
    }

    /**
     * SmartLifecycle子类的才有的方法,当isRunning方法返回true时,该方法才会被调用。
     */
    @Override
    public void stop(Runnable callback) {
        System.out.println("stop(Runnable)");

        // 如果你让isRunning返回true,需要执行stop这个方法,那么就不要忘记调用callback.run()。
        // 否则在你程序退出时,Spring的DefaultLifecycleProcessor会认为你这个TestSmartLifecycle没有stop完成,程序会一直卡着结束不了,等待一定时间(默认超时时间30秒)后才会自动结束。
        // PS:如果你想修改这个默认超时时间,可以按下面思路做,当然下面代码是springmvc配置文件形式的参考,在SpringBoot中自然不是配置xml来完成,这里只是提供一种思路。
        // <bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
        //      <!-- timeout value in milliseconds -->
        //      <property name="timeoutPerShutdownPhase" value="10000"/>
        // </bean>
        callback.run();

        isRunning = false;
    }

    /**
     * 接口Lifecycle的子类的方法,只有非SmartLifecycle的子类才会执行该方法。<br/>
     * 1. 该方法只对直接实现接口Lifecycle的类才起作用,对实现SmartLifecycle接口的类无效。<br/>
     * 2. 方法stop()和方法stop(Runnable callback)的区别只在于,后者是SmartLifecycle子类的专属。
     */
    @Override
    public void stop() {
        System.out.println("stop");

        isRunning = false;
    }

}

ResourceLoader 

该资源加载器用于加载资源文件,支持获取类路径下的资源、URL资源等。Spring框架为了更方便的获取资源,尽量弱化程序员对各个Resource接口实现类的感知与分辨,降低学习与使用成本

Resource接口定义

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";
    // 核心方法,返回的对象是Spring容器中Resource接口的实例
    Resource getResource(String location);
    @Nullable
    ClassLoader getClassLoader();
}

Spring内置的常见Resource实现类

UrlResource 、ClassPathResource、 FileSystemResource 、PathResource ServletContextResource 、InputStreamResource、 ByteArrayResource

使用

Aware结尾的接口是为了注入变量而存在的,我们可以实现ResourceLoaderAware以获取ApplicationContext上下文中的ResourceLoader,回调并提供给的子实现类,提供的ResourceLoader为DefaultResourceLoader为类型

DefaultResourceLoader是一个通用的资源加载器,可以根据资源的路径前缀来选择不同的Resource实现类。

@Component
public class MyResourceUtil implements ResourceLoaderAware {  
    private ResourceLoader resourceLoader;  
    public ResourceLoader getResourceLoader() {  
        return resourceLoader;  
    } 
    @Override  
    public void setResourceLoader(ResourceLoader resourceLoader) {  
        this.resourceLoader = resourceLoader;  
    }  
    public void canDo(){
        Resource unknownRes = resourceLoader.getResource("classpath:unknown.txt");
        System.out.println(unknownRes.exists()); // false
        //   路径前缀 file:表示访问文件系统的URL,http:表示通过http协议访问,
        // ftp:表示通过ftp协议访问等
        Resource r0 = resourceLoader.getResourcee("http://example.com/resource.txt");
        Resource r1 = resourceLoader.getResource("classpath:file/abc.txt");
        Resource r2 = resourceLoader.getResource("classpath:/file/abc.txt");
        Resource r3 = resourceLoader.getResource("file:file/abc.txt");
        Resource r4 = resourceLoader.getResource("file:/file/abc.txt");
        Resource r5 = resourceLoader.getResource("file:d:\\remark.txt");
        Resource r5_2 = resourceLoader.getResource("file:d:/remark.txt");
        Resource r5_3 = resourceLoader.getResource("file:/d:/remark.txt");
        Resource r6 = resourceLoader.getResource("file:D:\\test.txt");
        Resource r7 = resourceLoader.getResource("/file/abc.txt");
        Resource r8 = resourceLoader.getResource("file/abc.txt");
    }
}  

获取到我们Resource可以干什么

  1. 读取资源内容:使用Resource的方法可以读取资源的内容,如getInputStream()方法可以获取资源的输入流,可以用于读取资源的字节数据
  2. 判断资源信息:使用Resource的方法可以获取资源的元数据信息,比如getFilename()方法可以获取资源的文件名,getContentLength()方法可以获取资源的长度等
  3. 进行资源的加载和解析:根据不同的资源类型,可以使用相应的解析器进行资源的加载和解析操作。比如使用PropertiesLoaderUtils可以将Resource中的属性配置文件加载为Properties对象,使用XmlBeanDefinitionReader可以将Resource中的XML文件加载为Bean定义
  4. 进行资源的复制和传输:可以使用Resource的方法将资源复制到另一个位置或传输到另一台机器上。例如,使用FileCopyUtils可以将资源复制到指定的目录或使用RestTemplate可以将资源传输到远程服务器

Aware结尾的接口是为了回调提供变量而存在的,这里列出常用的 Aware 子接口

LoadTimeWeaverAware:加载Spring Bean时织入第三方模块,如AspectJ
BeanClassLoaderAware:加载Spring Bean的类加载器
BootstrapContextAware:资源适配器BootstrapContext,如JCA,CCI
ResourceLoaderAware:底层访问资源的加载器
BeanFactoryAware:声明BeanFactory
PortletConfigAware:PortletConfig
PortletContextAware:PortletContext
ServletConfigAware:ServletConfig
ServletContextAware:ServletContext
MessageSourceAware:国际化
ApplicationEventPublisherAware:应用事件
NotificationPublisherAware:JMX通知
BeanNameAware:声明Spring Bean的名字

待续....!!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

菠萝追雪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值