专业的编程技术博客社区

网站首页 > 博客文章 正文

Spring路径-03-Spring高级话题(请问spring)

baijin 2024-08-12 13:38:11 博客文章 14 ℃ 0 评论

1 Spring Aware

1.1 什么是Spring Aware

在Spring学习中,依赖注入是最大的亮点,用户所有的Bean对Spring容器的存在是没有意识的,这样就可以降低Bean之间的耦合 但是在开发中,不可避免需要使用到Spring容器本身的功能和资源。这时,Bean需要意识到Spring容器的存在,才能使用Spring提供的资源,这就是所谓的Spring Aware。

Spring Aware是Spring设计给框架内部使用的,如果使用了Spring Aware,这时Bean将与Spring框架耦合。

1.2 设计Spring Aware的目的

设计Spring Aware的目的是为了让Bean获得Spring容器内置的组件。 例如:自定义组件要想使用Spring容器底层的一些组件(比如:ApplicationContext、BeanFactory等),此时,只需要让自定义组件实现XxxAware接口即可。此时,Spring在创建对象的时候,会调用XxxAware接口定义的方法(setXxx()方法),注入相关的组件。

常用Aware:

类名

作用

ApplicationContextAware

获得当前应用上下文

BeanNameAware

获取容器中bean名称

BeanClassLoaderAware

获得类加载器

BeanFactoryAware

获得bean工厂

EnviromentAware

获得环境变量

EnvironmentValueResolverAware

获取spring容器加载的properties文件属性值

ResourceLoaderAware

获得资源加载器

ApplicationEventPublisherAware

获得应用程序发布器

MessageSourceAware

获得文本信息

1.2.1 ApplicationContextAware

1)简介 ApplicationContextAware 是一个回调接口,用于在 Spring 容器实例化 Bean 后,将容器的上下文(ApplicationContext)传递给实现了该接口的 Bean。通过这个接口,Bean 可以获得对 Spring 容器的引用,从而获取容器中的其他 Bean 和资源。

2)作用 ApplicationContextAware 主要用于

  • 获取 ApplicationContext 允许 Bean 在运行时获取对 Spring 容器的引用。
  • 与容器交互 Bean 可以通过 ApplicationContext 与容器进行交互,例如获取其他 Bean 的引用、获取环境变量等。

3)使用 要使用 ApplicationContextAware 接口,需要按以下步骤进行:

创建并实现接口-->配置 Bean 信息-->创建启动类-->启动

3.1 )创建并实现接口

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class DemoBean implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void displayBeanNames() {
        // 打印 Bean 的名称
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        System.out.println("【ApplicationContextAware】ApplicationContext 容器内存在的 Bean 的名字是:");
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}

3.2) 配置 Bean 信息

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd"
       >
    <bean id="demoBean" class="org.example.cheney.DemoBean"/>
</beans>

3.3) 创建启动类

import org.springframework.context.support.AbstractXmlApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    public static void main(String[] args) throws Exception {
        String location = "applicationContext.xml";
        try (AbstractXmlApplicationContext context = new ClassPathXmlApplicationContext(location)) {
            DemoBean demoBean = (DemoBean) context.getBean("demoBean");
            demoBean.displayBeanNames();
            System.out.println("End.");
        }
    }
}

3.4) 启动

4)应用场景

ApplicationContextAware 接口通常用于以下场景

  • 获取其他 Bean 的引用:当一个 Bean 需要与容器中的其他 Bean 进行交互时,可以使用 ApplicationContext 获取其他 Bean 的引用。
  • 获取环境变量:Bean 可以通过 ApplicationContext 获取容器的环境变量,例如配置文件中的属性值。

1.2.2 BeanNameAware

1)简介

在 Spring 中,BeanNameAware 接口是一个回调接口,它提供了一个用于设置 Bean 名称的方法。当一个 Bean 实现了 BeanNameAware 接口时,在该 Bean 实例被实例化后,Spring 容器会调用 setBeanName 方法,并将该 Bean 在 Spring 容器中的名称作为参数传递进去。

2)BeanNameAware 接口的作用

BeanNameAware 接口的作用主要有以下几点:

  • 获取 Bean 在 Spring 容器中的名称:通过实现 BeanNameAware 接口,Bean 可以获取自己在 Spring 容器中的名称,从而可以在程序中使用该名称进行一些操作,比如日志输出、动态注册 Bean 等。
  • 与其他 Bean 进行交互:通过获取 Bean 在容器中的名称,Bean 可以与其他 Bean 进行交互,比如获取其他 Bean 的实例、调用其他 Bean 的方法等。
  • 扩展 Spring 容器的功能:通过实现 BeanNameAware 接口,Bean 可以在初始化阶段获取自己的 Bean 名称,从而可以在初始化过程中进行一些特定的操作,比如根据 Bean 名称动态加载配置文件、动态注册 Bean 等。

3)如何实现 BeanNameAware 接口?

要实现 BeanNameAware 接口,只需要在 Bean 类中实现 setBeanName() 方法即可。下面是一个简单的示例:

import org.springframework.beans.factory.BeanNameAware;

public class MyBean implements BeanNameAware {

    private String beanName;

    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }

    public String getBeanName() {
        return beanName;
    }
}

在上面的示例中,MyBean 类实现了 BeanNameAware 接口,并重写了 setBeanName() 方法,在该方法中将传入的 Bean 名称赋值给 beanName 属性。通过调用 getBeanName() 方法,可以获取该 Bean 在容器中的名称。

4)BeanNameAware 接口的应用场景

BeanNameAware 接口通常用于以下几种场景:

  • 日志输出:Bean 可以在初始化阶段获取自己在容器中的名称,从而可以在日志输出中使用该名称,方便调试和排查问题。
  • 动态注册 Bean:Bean 可以根据自己在容器中的名称动态注册其他 Bean,实现一些动态配置的功能。
  • 与其他 Bean 进行交互:Bean 可以通过获取自己在容器中的名称,与其他 Bean 进行交互,比如获取其他 Bean 的实例、调用其他 Bean 的方法等。

1.2.3 BeanFactoryAware

1)简介 在 Spring 中,BeanFactoryAware 接口是一个回调接口,它提供了一个用于设置 Bean 工厂的方法。当一个 Bean 实现了 BeanFactoryAware 接口时,在该 Bean 实例被实例化后,Spring 容器会调用 setBeanFactory 方法,并将该 Bean 所在的工厂作为参数传递进去。

2)作用

BeanFactoryAware 主要用于获取加载当前Bean的工厂(BeanFactory)的引用,使得Bean能够在运行时获取到关于自身所在工厂的信息。

3)使用

import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;

public class DemoBean implements BeanFactoryAware {
    @Override
    public void setBeanFactory(BeanFactory beanFactory) {
        System.out.println("【BeanFactoryAware】Bean 的工厂是:" + beanFactory);
    }
}

4)应用场景

BeanFactoryAware 接口通常用于以下场景:

  • 获取Bean工厂引用:当一个Bean需要在运行时获取自身所在工厂的引用,以便进行一些与工厂相关的操作
  • 动态加载其他Bean:有时需要动态地加载其他Bean,而加载所需的工厂引用就是加载该Bean的工厂

1.2.4 ApplicationContextAware

1)简介 ApplicationContextAware 是一个回调接口,用于在 Spring 容器实例化 Bean 后,将容器的上下文(ApplicationContext)传递给实现了该接口的 Bean。通过这个接口,Bean 可以获得对 Spring 容器的引用,从而获取容器中的其他 Bean 和资源。

2) 作用 ApplicationContextAware 主要用于

  • 获取 ApplicationContext:允许 Bean 在运行时获取对 Spring 容器的引用。
  • 与容器交互:Bean 可以通过 ApplicationContext 与容器进行交互,例如获取其他 Bean 的引用、获取环境变量等。

3)使用

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class DemoBean implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    public void displayBeanNames() {
        // 打印 Bean 的名称
        String[] beanNames = applicationContext.getBeanDefinitionNames();
        System.out.println("【ApplicationContextAware】ApplicationContext 容器内存在的 Bean 的名字是:");
        for (String beanName : beanNames) {
            System.out.println(beanName);
        }
    }
}

4)应用场景

ApplicationContextAware 接口通常用于以下场景

  • 获取其他 Bean 的引用:当一个 Bean 需要与容器中的其他 Bean 进行交互时,可以使用 ApplicationContext 获取其他 Bean 的引用。
  • 获取环境变量:Bean 可以通过 ApplicationContext 获取容器的环境变量,例如配置文件中的属性值。

1.3.5 ResourceLoaderAware

1)简介 在 Spring 中,ResourceLoaderAware 接口是一个回调接口,它提供了一个用于设置 Bean 所在的ResourceLoader 的方法。当一个 Bean 实现了 ResourceLoaderAware 接口时,在该 Bean 实例被实例化后,Spring 容器会调用 setResourceLoader 方法,并将该 Bean 所在的 ResourceLoader 作为参数传递进去。

2)作用

ResourceLoaderAware 主要用于获取加载当前 Bean 的 ResourceLoader,使得 Bean 能够在运行时获取到关于资源加载的能力。

3)使用

import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;

import java.io.InputStream;

public class DemoBean implements ResourceLoaderAware {
    private ResourceLoader resourceLoader;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
        System.out.println("【ResourceLoaderAware】: 通过 ResourceLoader 创建 Bean");
    }

    public void loadResource(String resourceName) throws Exception {
        // 使用 ResourceLoader 加载资源
        System.out.println("加载的文件名是: " + resourceName);
        Resource resource = resourceLoader.getResource(resourceName);
        InputStream inputStream = resource.getInputStream();
        // 读取资源内容
        byte[] contentBytes = new byte[inputStream.available()];
        inputStream.read(contentBytes);
        String content = new String(contentBytes);
        System.out.println("文件内容:\n" + content);
    }
}

3)应用场景

ResourceLoaderAware 接口通常用于以下场景:

  • 加载资源:当一个 Bean 需要在运行时加载外部资源时,可以使用 ResourceLoaderAware 获取 ResourceLoader 并使用它加载资源
  • 处理资源相关逻辑:当一个 Bean 与资源相关的操作时,例如读取配置文件、加载模板文件等,可以使用 ResourceLoaderAware 获取 ResourceLoader

1.2.6 ApplicationEventPulisherAware

1)简介 在 Spring 中,ApplicationEventPublisherAware 接口是一个回调接口,它提供了一个用于设置 Bean 所在的ApplicationEventPublisher 的方法。当一个 Bean 实现了 ApplicationEventPublisherAware 接口时,在该 Bean 实例被实例化后,Spring 容器会调用 setApplicationEventPublisher 方法,并将该 Bean 所在的ApplicationEventPublisher 作为参数传递进去。

2)作用

ApplicationEventPublisherAware 主要用于获取发布事件的 ApplicationEventPublisher,使得 Bean 能够在运行时发布自定义的事件。

3)使用

import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.ApplicationListener;

public class DemoBean implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher eventPublisher;

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
        System.out.println("【ApplicationEventPublisherAware】: 通过 ApplicationEventPublisher 创建 Bean");
    }

    public void publishCustomEvent(String message) {
        // 创建自定义事件
        CustomEvent customEvent = new CustomEvent(this, message);
        // 发布事件
        eventPublisher.publishEvent(customEvent);
    }
}
// 自定义一个事件
class CustomEvent extends ApplicationEvent {
    private final String message;
    public CustomEvent(Object source, String message) {
        super(source);
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}
// 自定义一个监听器
class EventListener implements ApplicationListener<CustomEvent> {
    @Override
    public void onApplicationEvent(CustomEvent event) {
        System.out.println("监听到的信息是:" + event.getMessage());
    }
}

4)应用场景 ApplicationEventPublisherAware 接口通常用于以下场景:

  • 自定义事件发布:当一个 Bean 需要在运行时发布自定义的事件时,可以使用 ApplicationEventPublisher 发布事件
  • 事件驱动开发:在事件驱动的架构中,ApplicationEventPublisher 可以用于触发和响应事件,实现松耦合的组件之间的通信

1.2.7 MessageSourceAware

1)简介 MessageSourceAware 接口是 Spring 提供的一个用于访问消息资源(例如文本消息)的接口。国际化的主要目标是使应用程序的用户界面能够根据用户的首选语言或地区显示相应的消息。

2)功能

提前根据不同国家的语言创建出一组资源文件,它们都具有相同的 key 值,我们的程序就可以根据不同的地域从相对应的资源文件中获取到 value 啦

3)使用

package org.example.cheney;

import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;

import java.util.Locale;

public class DemoBean implements MessageSourceAware {

    private MessageSource messageSource;

    @Override
    public void setMessageSource(MessageSource messageSource) {
        ReloadableResourceBundleMessageSource bundleSource = new ReloadableResourceBundleMessageSource();
        bundleSource.setBasename("messages");
        this.messageSource = bundleSource;
    }

    public void printMessage() {
        String code = "msg";
        Object[] args = new Object[]{"cheney"};
        Locale locale = new Locale("en", "US");
        String msg = messageSource.getMessage(code, args, locale);
        System.out.println(msg);
    }
}

资源文件

messages_en_US.properties

msg = {0} say: Hello world

messages_zh_CN.properties

msg = {0} 说: 你好世界

4)应用场景

  • 国际化支持 可以在应用中实现对不同语言环境下的消息文本的解析和展示。这对于多语言环境下的应用非常重要,可以根据用户的语言偏好动态地展示相应的消息文本,提升用户体验。
  • 消息解析 可以实现对消息文本的解析和格式化。这在应用中展示动态消息内容或者格式化消息文本时非常有用,比如展示动态的错误消息、通知消息等

2 多线程

2.1 简介

Spring通过任务执行器(TaskExecutor)来实现多线程和并发编程。

使用ThreadPoolTaskExecutor可实现一个基于线程池的TaskExecutor。

实际开发中一般是非阻塞的,即异步的,所以我们要在配置类中通过@EnableAsync开启对异步任务的支持,并通过在实际执行的Bean的方法中使用@Async注解来声明其是一个异步任务。

2.2 线程池ThreadPoolExecutor

线程池ThreadPoolExecutor执行规则如下:



构造一个线程池:

@Configuration
@EnableAsync
public class ThreadPoolConfig implements AsyncConfigurer {
  /**
   * 核心线程池大小
   */
  private static final int CORE_POOL_SIZE = 3;

  /**
   * 最大可创建的线程数
   */
  private static final int MAX_POOL_SIZE = 10;

  /**
   * 队列最大长度
   */
  private static final int QUEUE_CAPACITY = 10;

  /**
   * 线程池维护线程所允许的空闲时间
   */
  private static final int KEEP_ALIVE_SECONDS = 300;

  /**
   * 异步执行方法线程池
   *
   * @return
   */
  @Override
  @Bean
  public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setMaxPoolSize(MAX_POOL_SIZE);
    executor.setCorePoolSize(CORE_POOL_SIZE);
    executor.setQueueCapacity(QUEUE_CAPACITY);
    executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
    executor.setThreadNamePrefix("LiMingTest");
    // 线程池对拒绝任务(无线程可用)的处理策略
    executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
    executor.initialize();
    return executor;
  }
}

ThreadPoolExecutor是JDK中的线程池实现,这个类实现了一个线程池需要的各个方法,它提供了任务提交、线程管理、监控等方法。

(1)corePoolSize:核心线程数

线程池维护的最小线程数量,默认情况下核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。

大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。

(2)maximumPoolSize:最大线程数

线程池允许创建的最大线程数量。

当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。

(3)keepAliveTime:空闲线程存活时间

当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。

被回收的线程:

设置allowCoreThreadTimeout=true的核心线程。 大于核心线程数的线程(非核心线程)。

(4)workQueue:工作队列

新任务被提交后,如果核心线程数已满则会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。

(5)handler:拒绝策略

当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。

JDK默认的拒绝策略有四种:

AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。 DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。 CallerRunsPolicy:由调用线程处理该任务。

我们在非测试文件中直接使用new Thread创建新线程时编译器会发出警告:

不要显式创建线程,请使用线程池。 说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题

示例:

public class TestServiceImpl implements TestService {
  private final static Logger logger = LoggerFactory.getLogger(TestServiceImpl.class);
  @Override
  public void task(int i) {
      logger.info("任务: "+i);
  }
}

测试:

@Autowired
TestService testService;
@Test
public void test() {
    for (int i = 0; i < 50; i++) {
        testService.task(i);
    }
}

3 计划任务

Spring框架分别通过TaskExecutor和TaskScheduler接口为任务的异步执行和调度提供了抽象。Spring还提供了支持应用程序服务器环境中的线程池或CommonJ委托的那些接口的实现。最终,在公共接口后面使用这些实现,消除了JavaSE5、JavaSE6和JakartaEE环境之间的差异。

Spring还具有集成类,以支持Timer(自1.3以来JDK的一部分)和Quartz Scheduler的调度。您可以分别使用FactoryBean和可选的Timer或Trigger实例引用来设置这两个调度器。此外,Quartz Scheduler和Timer都有一个方便类,它允许您调用现有目标对象的方法(类似于普通的MethodInvokingFactoryBean操作)。

3.1 Spring TaskExecutor 概念

执行器是线程池概念的JDK名称。“executor”命名是因为无法保证底层实现实际上是一个池。执行器可以是单线程的,甚至可以是同步的。Spring的抽象隐藏了JavaSE和JakartaEE环境之间的实现细节。

Spring的TaskExecutor接口与java.util.concurrent.Executor接口相同。事实上,最初,它存在的主要原因是在使用线程池时不需要Java5。该接口有一个方法(execute(Runnable task)),该方法根据线程池的语义和配置接受要执行的任务。

创建TaskExecutor最初是为了在需要时为其他Spring组件提供线程池抽象。ApplicationEventMulticaster、JMS的AbstractMessageListenerContainer和Quartz集成等组件都使用TaskExecutor抽象来池线程。然而,如果您的bean需要线程池行为,您也可以根据自己的需要使用此抽象。

3.1.1 TaskExecutor 类型

Spring包括许多预先构建的TaskExecutor实现。很可能,你永远不需要实现你自己的。Spring提供的变体如下:

  • SyncTaskExecutor:此实现不会异步运行调用。相反,每次调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中。
  • SimpleAsyncTaskExecutor:此实现不重用任何线程。相反,它为每个调用启动一个新线程。然而,它确实支持一个并发限制,即在释放槽之前阻止任何超过该限制的调用。如果您正在寻找真正的池,请参阅此列表后面的ThreadPoolTaskExecutor。
  • ConcurrentSkExecutor:此实现是java.util.concurrent.Executor实例的适配器。还有一种替代方法(ThreadPoolTaskExecutor)将Executtor配置参数公开为bean财产。很少需要直接使用ConcurrentTaskExecutor。但是,如果ThreadPoolTaskExecutor不够灵活,无法满足您的需要,则ConcurrentTaskExecutor是另一种选择。
  • ThreadPoolTaskExecutor:此实现最常用。它公开用于配置java.util.concurrent.ThreadPoolExecutor的bean财产,并将其包装在TaskExecuttor中。如果您需要适应不同类型的java.util.concurrent.Executor,我们建议您改用ConcurrentSkExecutor。
  • DefaultManagedTaskExecutor:此实现在JSR-236兼容的运行时环境(如Jakarta EE应用程序服务器)中使用JNDI获得的ManagedExecutorService,以取代CommonJ WorkManager。

3.1.2 TaskExecutor 使用

Spring的TaskExecutor实现用作简单的JavaBeans。在下面的示例中,我们定义了一个bean,它使用ThreadPoolTaskExecutor异步打印一组消息:

import org.springframework.core.task.TaskExecutor;

public class TaskExecutorExample {

    private class MessagePrinterTask implements Runnable {

        private String message;

        public MessagePrinterTask(String message) {
            this.message = message;
        }

        public void run() {
            System.out.println(message);
        }
    }

    private TaskExecutor taskExecutor;

    public TaskExecutorExample(TaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    public void printMessages() {
        for(int i = 0; i < 25; i++) {
            taskExecutor.execute(new MessagePrinterTask("Message" + i));
        }
    }
}

正如您所看到的,您不是从池中检索线程并自己执行它,而是将Runnable添加到队列中。然后,TaskExecutor使用其内部规则来决定任务何时运行。

为了配置TaskExecutor使用的规则,我们公开了简单的bean财产:

<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
    <property name="corePoolSize" value="5"/>
    <property name="maxPoolSize" value="10"/>
    <property name="queueCapacity" value="25"/>
</bean>

<bean id="taskExecutorExample" class="TaskExecutorExample">
    <constructor-arg ref="taskExecutor"/>
</bean>

3.2 Spring TaskScheduler概述

除了TaskExecutor抽象之外,Spring还有一个TaskScheduler SPI,它具有多种方法来调度将来某个时刻运行的任务。以下列表显示了TaskScheduler接口定义:

public interface TaskScheduler {

    Clock getClock();

    ScheduledFuture schedule(Runnable task, Trigger trigger);

    ScheduledFuture schedule(Runnable task, Instant startTime);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);

    ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);

    ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);

最简单的方法是一个名为schedule的方法,它只需要一个Runnable和一个Instant。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受触发器的方法要灵活得多。

3.2.1 Trigger 接口

Trigger接口本质上受到JSR-236的启发。触发器的基本思想是,可以根据过去的执行结果甚至任意条件来确定执行时间。如果这些确定考虑了先前执行的结果,则该信息在TriggerContext中可用。Trigger接口本身非常简单,如下表所示:

public interface Trigger {

    Instant nextExecution(TriggerContext triggerContext);
}

TriggerContext是最重要的部分。它封装了所有相关数据,如果需要,将来可以进行扩展。TriggerContext是一个接口(默认使用SimpleTriggerContext实现)。下面的列表显示了Trigger实现的可用方法。

public interface TriggerContext {

    Clock getClock();

    Instant lastScheduledExecution();

    Instant lastActualExecution();

    Instant lastCompletion();
}

3.2.2 Trigger 接口实现

Spring提供了Trigger接口的两种实现。最有趣的是CronTrigger。它支持基于cron表达式的任务调度。例如,以下任务计划在每小时15分钟后运行,但仅在工作日的朝九晚五“工作时间”内运行:

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

另一个实现是PeriodicTrigger,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值,以指示该周期应该被解释为固定速率还是固定延迟。由于TaskScheduler接口已经定义了以固定速率或固定延迟调度任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger实现的价值在于,您可以在依赖Trigger抽象的组件中使用它。例如,允许交替使用周期性触发器、基于cron的触发器,甚至自定义触发器实现可能很方便。这样的组件可以利用依赖注入,这样您就可以在外部配置这样的触发器,从而轻松地修改或扩展它们。

3.2.3 TaskScheduler 实现

与Spring的TaskExecutor抽象一样,TaskScheduler安排的主要好处是应用程序的调度需求与部署环境分离。当部署到应用程序服务器环境时,这个抽象级别尤其重要,因为应用程序本身不应该直接创建线程。对于这样的场景,Spring提供了一个TimerManagerTaskScheduler,它委托给WebLogic或WebSphere上的CommonJ TimerManager,以及一个更新的DefaultManagedTaskScheduler,在Jakarta EE环境中委托给JSR-236 ManagedScheduledExecutorService。两者通常都配置有JNDI查找。

每当不需要外部线程管理时,一个更简单的替代方案就是在应用程序中设置本地ScheduledExecutorService,它可以通过Spring的ConcurrentTaskScheduler进行调整。为了方便起见,Spring还提供了ThreadPoolTaskScheduler,它在内部委托给ScheduledExecutorService,以提供与ThreadPoolTaskExecutor类似的通用bean样式配置。这些变体对于宽松的应用程序服务器环境中的本地嵌入式线程池设置也非常适用?—?特别是在Tomcat和Jetty上。

3.3 调度和异步执行的注解支持

Spring 为任务调度和异步方法提供了注释支持 执行。

3.3.1. 启用调度注解

要启用对@Scheduled和@Async注释的支持,可以将@EnableScheduling和@EnableAsync添加到@Configuration类之一,如下例所示:

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

您可以选择应用程序的相关注释。例如,如果只需要对@Scheduled的支持,则可以省略@EnableAsync。对于更细粒度的控制,可以另外实现SchedulingConfigurer接口、AsyncConfigurer接口或两者。有关详细信息,请参阅SchedulingConfigurer和AsyncConfigurer javadoc。

如果您喜欢XML配置,可以使用<task:annotation-driven>元素,如下例所示:

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>

注意,对于前面的XML,提供了一个executor引用来处理与带有@Async注释的方法相对应的任务,而提供了调度器引用来管理带有@Scheduled注释的方法。

3.3.2. @Scheduled注解

您可以将@Scheduled注释与触发器元数据一起添加到方法中。例如,以下方法每五秒(5000毫秒)调用一次,具有固定的延迟,这意味着该时间段是从每次前一次调用的完成时间开始计算的。

@Scheduled(fixedDelay = 5000)
public void doSomething() {
    // something that should run periodically
}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。如果您想使用不同的时间单位,例如秒或分钟,可以通过@Scheduled中的timeUnit属性进行配置。 例如,前面的示例也可以编写如下。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}

如果需要固定速率执行,可以在注释中使用fixedRate属性。以下方法每五秒调用一次(在每次调用的连续开始时间之间测量)。

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
    // something that should run periodically
}

对于固定延迟和固定速率的任务,可以通过指示在第一次执行方法之前等待的时间量来指定初始延迟,如下面的fixedRate示例所示。

@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
    // something that should run periodically
}

如果简单的周期性调度不够表达,可以提供cron表达式。以下示例仅在工作日运行:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
    // something that should run on weekdays only
}

从Spring Framework 4.3开始,任何范围的bean都支持@Scheduled方法。 确保您在运行时没有初始化同一@Scheduled注释类的多个实例,除非您确实希望调度对每个此类实例的回调。与此相关的是,请确保不要在用@Scheduled注释并在容器中注册为常规Springbean的bean类上使用@Configurationable。否则,您将获得两次初始化(一次通过容器,一次通过@Configurationable方面),结果是每个@Scheduled方法被调用两次。

3.3.3 @Async 注解

您可以在方法上提供@Async注释,以便异步调用该方法。换句话说,调用方在调用时立即返回,而方法的实际执行发生在已提交给Spring TaskExecutor的任务中。在最简单的情况下,可以将注释应用于返回void的方法,如下例所示:

@Async
void doSomething() {
    // this will be run asynchronously
}

与用@Scheduled注释注释的方法不同,这些方法可能需要参数,因为它们是由调用者在运行时以“正常”方式调用的,而不是由容器管理的计划任务调用的。例如,以下代码是@Async注释的合法应用程序:

@Async
void doSomething(String s) {
    // this will be run asynchronously
}

即使返回值的方法也可以异步调用。但是,此类方法需要具有Future类型的返回值。这仍然提供了异步执行的好处,因此调用者可以在调用Future上的get()之前执行其他任务。以下示例显示如何在返回值的方法上使用@Async:

@Async
Future<String> returnSomething(int i) {
    // this will be run asynchronously
}

@异步方法不仅可以声明常规java.util.concurrent.Future返回类型,还可以声明Spring的org.springframework.util.concurrent.ListenableFuture,或者从Spring 4.2开始,JDK 8的java.util.coccurrent.CompletableFuture,以便与异步任务进行更丰富的交互,并与进一步的处理步骤立即组合。

不能将@Async与生命周期回调(如@PostConstruct)结合使用。要异步初始化Spring bean,当前必须使用单独的初始化Spring bean来调用目标上的@Async注释方法,如下例所示:

public class SampleBeanImpl implements SampleBean {

    @Async
    void doSomething() {
        // ...
    }

}

public class SampleBeanInitializer {

    private final SampleBean bean;

    public SampleBeanInitializer(SampleBean bean) {
        this.bean = bean;
    }

    @PostConstruct
    public void initialize() {
        bean.doSomething();
    }

}

3.3.4 Executor Qualification with @Async

默认情况下,在方法上指定@Async时,所使用的执行器是在启用异步支持时配置的执行器,即,如果使用XML或AsyncConfigurer实现(如果有),则为“注释驱动”元素。但是,当需要指示在执行给定方法时应使用默认值以外的执行器时,可以使用@Async注释的value属性。以下示例显示了如何执行此操作:

@Async("otherExecutor")
void doSomething(String s) {
    // this will be run asynchronously by "otherExecutor"
}

在这种情况下,“otherExecutor”可以是Spring容器中任何Executor bean的名称,也可以是与任何Executoor关联的限定符的名称(例如,使用<qualifier>元素或Spring的@qualifier注释指定)。

3.3.5 @Async异常管理

当@Async方法具有Future类型的返回值时,很容易管理在方法执行期间引发的异常,因为在Future结果上调用get时会引发此异常。然而,对于void返回类型,异常是未捕获的,无法传输。您可以提供AsyncUnaughtExceptionHandler来处理此类异常。以下示例显示了如何执行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        // handle exception
    }
}

3.3.6 代码实例:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-beans</artifactId>
    <version>5.0.2.RELEASE</version>
</dependency>
/**
 * 在spring boot的启动类上面添加 @EnableScheduling 注解
 */
@SpringBootApplication
@EnableScheduling
public class ScheduleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ScheduleApplication.class,args);
    }
}

@Scheduled注解的另外两个重要属性:fixedRate和fixedDelay

  • fixedDelay:上一个任务结束后多久执行下一个任务
  • fixedRate:上一个任务的开始到下一个任务开始时间的间隔
@Component
public class ScheduleDoker {
/**
 * 测试fixedRate,每2s执行一次
 * @throws Exception
 */
@Scheduled(fixedRate = 2000)
public void fixedRate() throws Exception {
    System.out.println("fixedRate开始执行时间:" + new Date(System.currentTimeMillis()));
    //休眠1秒
    Thread.sleep(1000);
    System.out.println("fixedRate执行结束时间:" + new Date(System.currentTimeMillis()));
}

fixedRate开始执行时间:Sun Feb 12 19:59:05 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:06 CST 2022
fixedRate开始执行时间:Sun Feb 12 19:59:07 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:08 CST 2022
fixedRate开始执行时间:Sun Feb 12 19:59:09 CST 2022
fixedRate执行结束时间:Sun Feb 12 19:59:10 CST 2022
    
/**
 * 等上一次执行完等待1s执行
 * @throws Exception
 */
@Scheduled(fixedDelay = 1000)
public void fixedDelay() throws Exception {
    System.out.println("fixedDelay开始执行时间:" + new Date(System.currentTimeMillis()));
    //休眠两秒
    Thread.sleep(1000 * 2);
    System.out.println("fixedDelay执行结束时间:" + new Date(System.currentTimeMillis()));
}

fixedDelay执行结束时间:Sun Feb 12 13:07:23 CST 2022
fixedDelay开始执行时间:Sun Feb 12 13:07:24 CST 2022
fixedDelay执行结束时间:Sun Feb 12 13:07:26 CST 2022
fixedDelay开始执行时间:Sun Feb 12 13:07:27 CST 2022
fixedDelay执行结束时间:Sun Feb 12 13:07:29 CST 2022
}

3.4、task命名空间

从版本3.0开始,Spring包含一个用于配置TaskExecutor和TaskScheduler实例的XML命名空间。它还提供了一种方便的方式来配置要使用触发器调度的任务

3.5、cron 表达式

所有 Spring cron 表达式都必须符合相同的格式,无论您是在@Scheduled注释、任务:计划任务元素、 或其他地方。 格式正确的 cron 表达式(例如 )由六个空格分隔的时间和日期组成 字段,每个字段都有自己的有效值范围:

* * * * * *
┌───────────── second (0-59)
│ ┌───────────── minute (0 - 59)
│ │ ┌───────────── hour (0 - 23)
│ │ │ ┌───────────── day of the month (1 - 31)
│ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC)
│ │ │ │ │ ┌───────────── day of the week (0 - 7)
│ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN)
│ │ │ │ │ │
* * * * * *

有一些规则适用:

  • 字段可以是星号(*),始终代表“first-last”。对于月日或星期日字段,可以使用问号(?)代替星号。
  • 逗号(,)用于分隔列表中的项目。
  • 用连字符(-)分隔的两个数字表示一系列数字。指定的范围包含在内。
  • 在带/的范围(或*)之后指定数字值在该范围内的间隔。
  • 英文名称也可以用于月份和星期几字段。使用特定日期或月份的前三个字母(大小写无关紧要)。
  • “月日”和“星期日”字段可以包含L字符,其含义不同。
  • 在月日字段中,L代表该月的最后一天。如果后面跟着一个负偏移量(即L-n),则表示该月的第n天到最后一天。
  • 在星期几字段中,L代表一周的最后一天。如果前缀为数字或三个字母的名称(dL或DDDL),则表示当月的最后一天(d或DDD)。
  • “月日”字段可以是nW,它代表一个月中最近的一个工作日。如果n落在星期六,这将产生前一个星期五。如果n在星期天,这将生成后一个星期一,如果n为1并且落在星期天(即:1W代表一个月中的第一个工作日),也会发生这种情况。
  • 如果月日字段为LW,则表示该月的最后一个工作日。
  • 星期几字段可以是d#n(或DDD#n),表示一个月中第n个星期d(或DDD)。

以下是一些示例:

Cron 表达式

意义

0 0 * * * *

每天每个小时之巅

*/10 * * * * *

每十秒

0 0 8-10 * * *

每天8点、9点及10点

0 0 6,19 * * *

每天上午 6:00 和晚上 7:00

0 0/30 8-10 * * *

每天 8:00、8:30、9:00、9:30、10:00 和 10:30

0 0 9-17 * * MON-FRI

工作日朝九晚五的整点

0 0 0 25 DEC ?

每个圣诞节午夜

0 0 0 L * *

每月最后一天午夜

0 0 0 L-3 * *

每月倒数第三天的午夜

0 0 0 * * 5L

每月最后一个星期五午夜

0 0 0 * * THUL

每月最后一个星期四午夜

0 0 0 1W * *

每月第一个工作日的午夜

0 0 0 LW * *

每月最后一个工作日的午夜

0 0 0 ? * 5#2

每月第二个星期五午夜

0 0 0 ? * MON#1

每月第一个星期一午夜

5.1.1. 宏

对于人类来说,诸如0 0***之类的表达式很难解析,因此在出现错误时很难修复。为了提高可读性,Spring支持以下宏,这些宏表示常用的序列。您可以使用这些宏而不是六位数的值,例如:@Scheduled(cron=“@hourly”)。

意义

@yearly(或@annually)

每年一次(0 0 0 1 1 *)

@monthly

每月一次(0 0 0 1 * *)

@weekly

每周一次(0 0 0 * * 0)

@daily(或@midnight)

每天一次 (),或0 0 0 * * *

@hourly

每小时一次,(0 0 * * * *)

4 条件注解、元注解、组合注解

4.1 概述

从Spring 2开始,为了相应JDK 1.5推出的注解功能,Spring开始加入注解来替代xml配置。Spring的注解主要用来配置和注入Bean,以及AOP相关配置。随着注解的大量使用,尤其相同的多个注解用到各个类或方法中,会相当繁琐。出现了所谓的样本代码,这是Spring设计要消除的代码。

元注解:可以注解到别的注解上去的注解。 组合注解:被注解的注解,组合注解具备其上的元注解的功能。 Spring的很多注解都可以作为元注解,而且Spring本身已经有很多组合注解,如@Configuration就是一个组合了@Component的注解,表明被注解的类其实也是一个Bean。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
  String value() default "";
}

4.2 自定义组合注解

自定义一个组合注解,它的元注解是@Configuration和@ConfigurationScan

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration //组合@Configuration元注解
@ComponentScan //组合@ComponentScan元注解
public @interface WiselyConfiguration {
  String[] value() default {}; //覆盖value参数
}
import org.springframework.stereotype.Service;
@Service
public class DemoService {
   public void outputResult(){
     System.out.println("从组合注解配置照样获得的bean");
   }
}
@WiselyConfiguration("com.wisely.highlight_spring4.ch3.annotation")
public class DemoConfig {
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Main {
   public static void main(String[] args) {
     AnnotationConfigApplicationContext context =
        new AnnotationConfigApplicationContext(DemoConfig.class);
     DemoService demoService = context.getBean(DemoService.class);
     demoService.outputResult();
     context.close();
   }
}

从组合注解配置照样获得的bean

4.3 条件注解

在Spring中,条件注解可根据特定的条件来决定是否创建或配置Bean,这些条件可以基于类、属性、环境等因素。通过使用条件注解,我们可以在Spring容器中更加灵活地管理和控制组件的创建和注入,帮助我们更加灵活地管理和控制Bean的创建和注入,提高代码的灵活性和可维护性。

  • 自动化配置:根据特定的条件来决定是否创建或配置Bean,例如根据类路径下是否包含特定的库、特定的环境变量是否设置等条件来控制Bean的创建。
  • 条件化装配:在Spring容器中,根据条件来决定是否加载某个Bean,例如根据当前所处的环境(如开发、测试、生产)来控制Bean的创建。
  • Bean依赖:在Spring容器中,根据条件来决定是否创建依赖的Bean,例如只有当另外某个特定Bean也声明了之后才创建某个Bean。
  • 自定义条件:通过实现Condition接口,自定义条件逻辑,根据特定条件来控制Bean的创建行为。

@Conditional:这是最基本的条件注解,允许你定义一个条件接口,然后通过实现这个接口来决定是否创建某个Bean。例如:

@Conditional(OnWebApplicationCondition.class)
public class WebConfig {
    // ...
}

@ConditionalOnBean:这个注解用来检查容器中是否存在某个Bean。如果存在,则创建当前Bean。 \

@ConditionalOnBean(name = "someBean")
public class SomeBeanConfig {
    // ...
}

@ConditionalOnMissingBean:与@ConditionalOnBean相反,这个注解检查容器中是否不存在某个Bean。如果不存在,则创建当前Bean。

@ConditionalOnMissingBean
public class DefaultBeanConfig {
    // ...
}

@ConditionalOnClass:这个注解检查类路径上是否存在某个类。如果存在,则创建当前Bean。

@ConditionalOnClass(Servlet.class)
public class ServletConfig {
    // ...
}

@ConditionalOnMissingClass:与@ConditionalOnClass相反,这个注解检查类路径上是否不存在某个类。如果不存在,则创建当前Bean。

@ConditionalOnMissingClass("javax.servlet.Servlet")
public class NonServletConfig {
    // ...
}

@ConditionalOnProperty:这个注解检查配置文件中是否存在某个属性,并且其值是否符合预期。如果符合,则创建当前Bean。

@ConditionalOnProperty(name = "some.property", havingValue = "true")
public class SomePropertyConfig {
    // ...
}

@ConditionalOnResource:这个注解检查类路径上是否存在某个资源文件。如果存在,则创建当前Bean。

@ConditionalOnResource(resources = "classpath:somefile.txt")
public class SomeResourceConfig {
    // ...
}

@ConditionalOnWebApplication:这个注解检查应用程序是否是一个Web应用程序。如果是,则创建当前Bean。

@ConditionalOnWebApplication
public class WebAppConfig {
    // ...
}

@ConditionalOnNotWebApplication:与@ConditionalOnWebApplication相反,这个注解检查应用程序是否不是一个Web应用程序。如果不是,则创建当前Bean。

@ConditionalOnNotWebApplication
public class NonWebAppConfig {
    // ...
}

@ConditionalOnExpression:这个注解允许你指定一个SpEL表达式,根据表达式的计算结果决定是否创建当前Bean。

@ConditionalOnExpression("#{systemProperties['some.property'] == 'someValue'}")
public class SomeExpressionConfig {
    // ...
}

@ConditionalOnJava:根据Java版本进行判断,不同版本有不同的处理方式。这用于根据Java版本进行条件化的配置。

4.4 元注解

就是定义注解的注解,或者说注解注解的注解

@Retention :用于提示注解被保留多长时间,有三种取值:

public enum RetentionPolicy {
  /**
   * Annotations are to be discarded by the compiler.
   */
  SOURCE,
  /**
   * Annotations are to be recorded in the class file by the compiler
   * but need not be retained by the VM at run time. This is the default
   * behavior.
   */
  CLASS,
  /**
   * Annotations are to be recorded in the class file by the compiler and
   * retained by the VM at run time, so they may be read reflectively.
   *
   * @see java.lang.reflect.AnnotatedElement
   */
  RUNTIME
}

RetentionPolicy.SOURCE 保留在源码级别,被编译器抛弃(@Override就是此类); RetentionPolicy.CLASS被编译器保留在编译后的类文件级别,但是被虚拟机丢弃; RetentionPolicy.RUNTIME保留至运行时,可以被反射读取。

@Target:_ :用于提示该注解使用的地方,取值有:

public enum ElementType {
  /** Class, interface (including annotation type), or enum declaration */
  TYPE,
  /** Field declaration (includes enum constants) */
  FIELD,
  /** Method declaration */
  METHOD,
  /** Formal parameter declaration */
  PARAMETER,
  /** Constructor declaration */
  CONSTRUCTOR,
  /** Local variable declaration */
  LOCAL_VARIABLE,
  /** Annotation type declaration */
  ANNOTATION_TYPE,
  /** Package declaration */
  PACKAGE,
  /**
   * Type parameter declaration
   * @since 1.8
   */
  TYPE_PARAMETER,
  /**
   * Use of a type
   * @since 1.8
   */
  TYPE_USE
}

分别表示该注解可以被使用的地方: 1.(类,接口,注解,enum); 2. 属性域;3.方法;4.参数;5.构造函数;6.局部变量;7.注解类型;8.包

@Documented :表示注解是否能被 javadoc 处理并保留在文档中。

@Override:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以被理解为: 只能使用在方法上,保留在源码级别,被编译器处理,然后抛弃掉。

5 @Enable注解

@Enable** 注解,一般用于开启某一类功能。类似于一种开关,只有加了这个注解,才能使用某些功能。

spring 中经常遇到这样的场景,老大让你写一个定时任务脚本、开启一个spring缓存,或者让你提供spring 异步支持。你的做法肯定是 @EnableScheduling+@Scheduled,@EnableCaching+@Cache,@EnableAsync+@Async 立马开始写逻辑了,但你是否真正了解其中的原理呢?之前有写过一个项目,是日志系统,其中要提供spring 注解支持,简化配置,当时就是参考以上源码的技巧实现的。

5.1 原理

先来看@EnableScheduling源码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

可以看到这个注解是一个混合注解,和其他注解的唯一区别就是多了一个@Import注解

通过这些@Enable注解的源码可以看出,所有@Enable注解里面都有一个@Import注解,而@Import是用来导入配置类的,所以@Enable自动开启的实现原理其实就是导入了一些自动配置的Bean。

5.2 @Import注解的用法

@Import只允许放到类上面,不能放到方法上。 @Import注解允许导入@Configuration类,ImportSelector和ImportBeanDefinitionRegistrar的实现类,普通的Bean。

通常有以下三种使用方式:

(1)直接导入配置类

@EnableEurekaServer使用了这种方式,注解源码如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerMarkerConfiguration.class})
public @interface EnableEurekaServer {
}

可以看到@EnableEurekaServer注解直接导入了配置类EurekaServerMarkerConfiguration,而这个配置类中向spring容器中注册了一个EurekaServerMarkerConfiguration的Bean。

EurekaServerMarkerConfiguration的源码如下:

@Configuration
public class EurekaServerMarkerConfiguration {
    public EurekaServerMarkerConfiguration() {
    }

    @Bean
    public EurekaServerMarkerConfiguration.Marker eurekaServerMarkerBean() {
        return new EurekaServerMarkerConfiguration.Marker();
    }

    class Marker {
        Marker() {
        }
    }
}

(2)依据条件选择配置类

@EnableAsync使用了这种方式,注解源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {

	Class<? extends Annotation> annotation() default Annotation.class;

	boolean proxyTargetClass() default false;

	AdviceMode mode() default AdviceMode.PROXY;

	int order() default Ordered.LOWEST_PRECEDENCE;
}

EnableAsync注解中导入了AsyncConfigurationSelector,AsyncConfigurationSelector通过条件来选择需要导入的配置类,继承AdviceModeImportSelector又实现了ImportSelector接口,接口重写selectImports方法进行事先条件判断PROXY或者ASPECTJ选择不同的配置类。

AsyncConfigurationSelector源码如下:

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

	private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME =
			"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";


	/**
	 * Returns {@link ProxyAsyncConfiguration} or {@code AspectJAsyncConfiguration}
	 * for {@code PROXY} and {@code ASPECTJ} values of {@link EnableAsync#mode()},
	 * respectively.
	 */
	@Override
	@Nullable
	public String[] selectImports(AdviceMode adviceMode) {
		switch (adviceMode) {
			case PROXY:
				return new String[] {ProxyAsyncConfiguration.class.getName()};
			case ASPECTJ:
				return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
			default:
				return null;
		}
	}

}

(3)动态注册Bean

@EnableAspectJAutoProxy使用了这种方式,注解源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

	boolean proxyTargetClass() default false;
	 
	boolean exposeProxy() default false;
}

EnableAspectJAutoProxy注解中导入了AspectJAutoProxyRegistrar,AspectJAutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口,在运行时把Bean注册到spring容器中。

AspectJAutoProxyRegistrar的源码如下:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

	/**
	 * Register, escalate, and configure the AspectJ auto proxy creator based on the value
	 * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
	 * {@code @Configuration} class.
	 */
	@Override
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

		AnnotationAttributes enableAspectJAutoProxy =
				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
		if (enableAspectJAutoProxy != null) {
			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
			}
			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
			}
		}
	}

}

6 Spring TestContext集成测试

集成测试是在单元测试之上,通常是将一个或多个已进行过单元测试的组件组合起来完成的,即集成测试中一般不会出现Mock对象,都是实实在在的真实实现。

对于单元测试,如前边在进行数据访问层单元测试时,通过Mock HibernateTemplate对象然后将其注入到相应的DAO实现,此时单元测试只测试某层的某个功能是否正确,对其他层如何提供服务采用Mock方式提供。

对于集成测试,如要进行数据访问层集成测试时,需要实实在在的HibernateTemplate对象然后将其注入到相应的DAO实现,此时集成测试将不仅测试该层功能是否正确,还将测试服务提供者提供的服务是否正确执行。

使用Spring的一个好处是能非常简单的进行集成测试,无需依赖web服务器或应用服务器即可完成测试。Spring通过提供一套TestContext框架来简化集成测试,使用TestContext测试框架能获得许多好处,如Spring IoC容器缓存、事务管理、依赖注入、Spring测试支持类等等。

6.1 概要

能够执行一些集成测试而无需部署到应用程序服务器或连接到其他企业基础结构,这一点很重要。这样可以测试以下内容:

  • 正确连接Spring IoC容器上下文。
  • 使用JDBC或ORM工具进行数据访问。这可以包括诸如SQL语句的正确性、Hibernate查询、JPA实体映射之类的东西。

Spring框架为Spring测试模块中的集成测试提供了一流的支持。实际的JAR文件的名称可能包括发行版,也可能采用长org.springframework.test格式,具体取决于你从何处获取。该库包含org.springframework.test包,其中包含用于与Spring容器进行集成测试的有价值的类。此测试不依赖于应用程序服务器或其他部署环境。此类测试的运行速度比单元测试慢,但比依赖于部署到应用程序服务器的等效Selenium测试或远程测试快。

单元和集成测试支持以注解驱动的Spring TestContext 框架]的形式提供。TestContext框架与实际使用的测试框架无关,该框架允许各种环境(包括JUnit,TestNG和其他环境)中对测试进行检测。

6.2 集成测试目标

Spring的集成测试支持的主要目标如下:

  • 在测试之间管理Spring IoC容器缓存。
  • 提供测试fixture实例的依赖注入。
  • 提供适合集成测试的事务管理。
  • 提供特定于Spring的基类,以帮助开发人员编写集成测试

接下来的几节描述了每个目标,并提供了有关实现和配制详细信息的链接。

Fixture意思:JUnit提供了编写测试前准备、测试后清理的固定代码,我们称之为Fixture

6.2.1 上下文管理和缓存

Spring TestContext 框架提供了Spring ApplicationContext实例和WebApplicationContext实例的一致加载以及这些上下文的缓存。支持加载上下文的缓存很重要,因为启动时间可能会成为一个问题-不是因为Spring本身的开销,而是因为Spring容器实例化的对象需要时间才能实例化。例如,具有50到100个Hibernate映射文件的项目可能需要10到20秒来加载映射文件,并且在每个测试fixture中运行每个测试之前要承担该消耗,这会导致整体测试运行速度变慢,从而降低了开发人员的工作效率。

测试类通常声明XML或Groovy配置元数据的资源位置数组(通常是在类路径中)或用于配置应用程序的组件类的数组。这些位置或类与web.xml或其他用于生产部署的配置文件中指定的位置或类相同或相似。

默认情况下,加载后,已配置的ApplicationContext将重新用每个测试。因此,每个测试套件仅产生一次安装成本,并且随后的测试执行要快得多。在这种情况下,术语“测试套件”是指所有测试都在相同JVM中运行,例如,所有测试都从给定项目或模块的AntMavenGradle构建运行。在不太可能的情况下,测试破坏了应用程序上下文并需要重新加载(例如,通过修改bean定义或应用程序对象的状态),可以将TestContext框架配置为重新加载配置并重建应用程序上下文,然后再执行下一个测试。

请参见使用TestContext框架进行上下文管理和上下文缓存。

6.2.2 测试装置的依赖注入

TestContext框架加载你的应用程序上下文时,可以选择地用依赖注入来配置测试类的实例。这提供了一种方便的机制,可以通过在应用程序上下文中使用预配置的bean来设置测试fixture。这里一个强大的好处是,你可以跨各种测试场景重用应用程序上下文(例如,用于配置spring管理的对象图、事务代理、数据源实例等),从而避免为单个测试用例重复复杂的测试fixture设置。

例如,考虑一个场景,其中我们有一个类(HibernateTitleRepository),该类为Title域实体实现数据访问逻辑。我们要编写集成测试来测试以下方面:

  • Spring配置:基本上,与HibernateTitleRepository bean的配置有关的一切都正确并存在吗?
  • Hibernate映射文件配置:是否已正确映射所有内容,并且是否有正确的延迟加载配置?
  • HibernateTitleRepository的逻辑:此类的配置实例是否按预期执行?

请参见使用TestContext框架进行测试fixture的依赖注入。

6.2.3 事物管理

访问真实数据库的测试中的一个常见问题是它们对持久存储状态的影响。即使使用开发数据库,对状态的更改也可能会影响以后的测试。同样,许多操作(例如插入或修改持久数据)无法在事物之外执行(或验证)。

TestContext框架解决了这个问题。默认情况下,框架为每个测试创建并回滚事务。你可以编写可以假定存在事务的代码。如果在测试中调用事务代理对象,则对象将根据其配置事务语义正确运行。此外,如果测试方法在测试管理的事务中运行时删除了选定表的内容,则该事务将默认回滚,并且数据库将返回到执行测试之前的状态。通过使用在测试的应用程序上下文中定义的PlatformTransactionManager bean,可以为测试提供事务支持。

如果你想要提交一个事务(不常见,但是当你想要一个特定的测试填充或修改数据库时,偶尔会有用),你可以通过使用@Commit注解告诉TestContext框架使事务提交而不是回滚。

请参阅使用TestContext框架进行事务管理。

6.2.4 集成测试支持的类

Spring TestContext框架提供了几个抽象支持类,这些基础测试类为测试框架提供了定义明确的钩子,以方便的实例变量和方法,可用于访问以下内容:

  • ApplicationContext,用于执行显式的bean查找或测试整个上下文的状态。
  • 一个JdbcTemplate,用于执行SQL语句来查询数据库。你可以使用此类查询在执行与数据库相关的应用程序代码之前和之后确认数据库状态,并且Spring确保此类查询在与应用程序代码相同的事务范围内运行。与ORM工具一起使用时,请确保避免误报。

另外,你可能希望使用针对你的项目的实例变量和方法构建自己的自定义,应用程序范围的超类。

请参阅TestContext框架的支持类。

6.3 JDBC测试支持

org.springframework.test.jdbc包包含JdbcTestUtils,它是JDBC相关实用程序功能的集合,旨在简化标准数据库测试方案。具体来说,JdbcTestUtils提供以下静态实用程序方法。

  • countRowsInTable(..):计算给定表中的行数。
  • countRowsInTableWhere(..):使用提供的WHERE子句计算给定表中的行数。
  • deleteFromTables(..):删除指定表中的所有行。
  • deleteFromTableWhere(..): 使用提供的WHERE子句从给定表中删除行。

AbstractTransactionalJUnit4SpringContextTestsAbstractTransactionalTestNGSpringContextTests提供了便利的方法,这些方法委托给JdbcTestUtils中的上述方法。spring-jdbc模块提供了对配置和启动嵌入式数据库的支持你可以在与数据库交互的集成测试中使用它。有关详细信息,请参见嵌入式数据库支持和使用嵌入式数据库测试数据访问逻辑。

6.4 注解

本节介绍了在测试Spring应用程序时可以使用的注解。它包括以下主题:

  • Spring测试注解
  • 标准注解支持
  • Spring JUnit4测试注解
  • Spring JUnit Jupiter测试注解
  • 测试元注解支持

6.4.1 Spring测试注解

Spring框架提供了以下特定于Spring的注解集,你可以在单元测试和集成测试中将它们与TestContext框架结合使用。有关更多信息,请参见相应的javadoc,包括默认属性值、属性别名和其他详细信息。

Spring的测试注解包括以下内容:

  • @BootstrapWith
  • @ContextConfiguration
  • @WebAppConfiguration
  • @ContextHierarchy
  • @ActiveProfiles
  • @TestPropertySource
  • @DynamicPropertySource
  • @DirtiesContext
  • @TestExecutionListeners
  • @Commit
  • @Rollback
  • @BeforeTransaction
  • @AfterTransaction
  • @Sql
  • @SqlConfig
  • @SqlMergeMode
  • @SqlGroup

@BootstrapWith

@BootstrapWith是一个类级别的注解,可用于配置如何引导Spring TestContext 框架。具体来说,你可以使用@BootstrapWith指定自定义TestContextBootstrapper。有关更多详细信息,请参见有关引导TestContext框架的部分。

参考代码:org.liyong.test.annotation.test.spring.ConfigClassApplicationContextTests

@ContextConfiguration

@ContextConfiguration定义了用于确定如何为集成测试加载和配置ApplicationContext的类级元数据。具体来说,@ContextConfiguration声明应用程序上下文资源位置或用于加载上下文的组件类。

资源位置通常是位于类路径中的XML配置文件或Groovy脚本,而组件类通常是@Configuration类。但是,资源位置也可以引用文件系统中的文件和脚本,组件类可以是@Component类、@Service类等等。有关更多详细信息,请参见组件类。

以下示例显示了一个指向XML文件的@ContextConfiguration注解:

@ContextConfiguration("/test-config.xml") //
class XmlApplicationContextTests {
    // class body...
}

引用XML文件。

以下示例显示了一个@ContextConfiguration注解,该注解引用了一个类:

@ContextConfiguration(classes = TestConfig.class) //1
class ConfigClassApplicationContextTests {
    // class body...
}

引用类文件

参考代码:org.liyong.test.annotation.test.spring.ConfigClassApplicationContextTests

作为声明资源位置或组件类的替代方法或补充,可以使用@ContextConfiguration声明ApplicationContextInitializer类。以下示例显示了这种情况:

@ContextConfiguration(initializers = CustomContextIntializer.class) 
class ContextInitializerTests {
    // class body...
}

参考代码:org.liyong.test.annotation.test.spring.ContextInitializerTests

你可以选择使用@ContextConfiguration来声明ContextLoader策略。但是,你通常不需要显式配置加载器,因为默认加载器支持初始化程序以及资源位置或组件类。

以下示例同时使用配置位置和加载器:

@ContextConfiguration(locations = "/test-context.xml", loader = CustomContextLoader.class) //1
class CustomLoaderXmlApplicationContextTests {
    // class body...
}

配置位置和自定义加载器。

@ContextConfiguration为继承资源位置或配置类以及超类声明上下文初始化器提供支持。

有关更多详细信息,请参见上下文管理和@ContextConfiguration javadocs。

参考代码:org.liyong.test.annotation.test.spring.CustomLoaderXmlApplicationContextTests

@WebAppConfiguration

@WebAppConfiguration是一个类级别的注解,可用于声明为集成测试加载的ApplicationContext应该是WebApplicationContext@WebAppConfiguration仅存在于测试类上,可以确保为测试加@WebApplicationContext,并使用默认值file:src/main/webapp作为Web应用程序根目录(也就是即资源基本路径)。资源基础路径用于在后台创建MockServletContext,该MockServletContext用作测试的WebApplicationContextServletContext

以下示例显示了如何使用@WebAppConfiguration注解:

@ContextConfiguration
@WebAppConfiguration //
class WebAppTests {
    // class body...
}

要覆盖默认值,可以使用隐式值属性指定其他基础资源路径。classpath:file:资源前缀均受支持。如果未提供资源前缀,则假定该路径是文件系统资源。以下示例显示如何指定类路径资源:

@ContextConfiguration
@WebAppConfiguration("classpath:test-web-resources") //1
class WebAppTests {
    // class body...
}
  1. 指定类路径资源。

注意,@WebAppConfiguration必须与@ContextConfiguration一起使用,可以在单个测试类中使用,也可以在测试类层次结构中使用。

有关更多详细信息,请参见@WebAppConfiguration javadoc。

参考代码:org.liyong.test.annotation.test.spring.WebAppTests

@ContextHierarchy

@ContextHierarchy是一个类级注解,用于定义集成测试的ApplicationContext实例的层次结构。 @ContextHierarchy应该用一个或多个@ContextConfiguration实例的列表声明,每个实例定义上下文层次结构中的一个级别。以下示例演示了在单个测试类中使用@ContextHierarchy(也可以在测试类层次结构中使用@ContextHierarchy):

@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class ContextHierarchyTests {
    // class body...
}

参考代码:org.liyong.test.annotation.test.spring.ContextHierarchyTests

@WebAppConfiguration
@ContextHierarchy({
    @ContextConfiguration(classes = AppConfig.class),
    @ContextConfiguration(classes = WebConfig.class)
})
class WebIntegrationTests {
    // class body...
}

参考代码:org.liyong.test.annotation.test.spring.WebIntegrationTests

如果需要合并或覆盖测试类层次结构中上下文层次结构的给定级别的配置,则必须通过在类层次结构的每个对应级别上为@ContextConfiguration中的name属性提供相同的值来显式地命名该级别。有关更多示例,请参见上下文层次结构和@ContextHierarchy javadoc。

@ActiveProfiles

@ActiveProfiles是一个类级别的注解,用于声明在为集成测时加载ApplicationContext时应启用哪些bean定义配置文件。

以下示例表明dev配置文件应处于活动状态:

@ContextConfiguration
@ActiveProfiles("dev") //1
class DeveloperTests {
    // class body...
}
  1. 指示开发配置文件应处于活动状态。

以下示例表明devintegration配置文件均应处于活动状态:

@ContextConfiguration
@ActiveProfiles({"dev", "integration"}) //1
class DeveloperIntegrationTests {
    // class body...
}
  1. 指示devintegration配置文件应该处于活动状态。

@ActiveProfiles提供了对继承默认情况下超类声明的活动bean定义配置文件的支持。你还可以通过实现自定义ActiveProfilesResolver并使用@ActiveProfilesresolver属性对其进行注册,以编程方式解析活动bean定义配置文件。

参见环境配置文件和@ActiveProfiles javadoc的上下文配置以获得示例和更多细节。

参考代码:org.liyong.test.annotation.test.spring.DeveloperIntegrationTests

@TestPropertySource

@TestPropertySource是类级别的注解,可用于配置属性文件和内联属性的位置,这些属性和内联属性将被添加到环境中针对集成测试加载的ApplicationContextPropertySources集中。

下面的示例演示如何从类路径声明属性文件:

@ContextConfiguration
@TestPropertySource("/test.properties") //1
class MyIntegrationTests {
    // class body...
}
  1. 从类路径根目录中的test.properties获取属性。

下面的示例演示如何声明内联属性:

@ContextConfiguration
@TestPropertySource(properties = { "timezone = GMT", "port: 4242" }) //1
class MyIntegrationTests {
    // class body...
}

声明时区和端口属性。

有关示例和更多详细信息,请参见使用测试属性源进行上下文配置。

@DynamicPropertySource

@DynamicPropertySource是方法级别的注解,可用于注册动态属性,以将动态属性添加到环境中针对集成测试加载的ApplicationContextPropertySources集中。当你不预先知道属性的值时,例如,如果属性是由外部资源管理的,例如由Testcontainers项目管理的容器,则动态属性很有用。

下面的示例演示如何注册动态属性:

@ContextConfiguration
class MyIntegrationTests {

    static MyExternalServer server = // ...

    @DynamicPropertySource //1
    static void dynamicProperties(DynamicPropertyRegistry registry) { //2
        registry.add("server.port", server::getPort); //3
    }

    // tests ...
}
  1. 使用@DynamicPropertySource注解静态方法。
  2. 接受DynamicPropertyRegistry作为参数。
  3. 注册要从服务器延迟检索的动态server.port属性。

有关更多详细信息,请参见使用动态属性源进行上下文配置。

@DirtiesContext

@DirtiesContext表示底层的Spring ApplicationContext在执行测试期间已被清理(即,该测试以某种方式修改或破坏了它(例如,通过更改单例bean的状态)),应将其关闭。当应用程序上下文被标记为清理时,它将从测试框架的缓存中删除并关闭。因此,对于需要具有相同配置元数据的上下文的后续测试,将重新构建底层Spring容器。

你可以将@DirtiesContext用作同一类或类层次结构中的类级别和方法级别的注解。在这种情况下,取决于配置的methodModeclassMode,在任何此类带注解的方法之前或之后以及当前测试类之前或之后,ApplicationContext均标记为清理。

以下示例说明了在各种配置情况下何时清理上下文:

  • 在当前测试类之前,在类模式设置为BEFORE_CLASS的类上声明时。
@DirtiesContext(classMode = BEFORE_CLASS) //1
class FreshContextTests {
    // some tests that require a new Spring container
}

在当前测试类之前清理上下文。

  • 在当前测试类之后,当在类模式设置为AFTER_CLASS(即默认类模式)的类上声明时。
@DirtiesContext 
class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}

当前测试类之后清理上下文。

  • 在当前测试类中的每个测试方法之后,在类模式设置为AFTER_EACH_TEST_METHOD的类上声明时。
@DirtiesContext(classMode = AFTER_EACH_TEST_METHOD) 
class ContextDirtyingTests {
    // some tests that result in the Spring container being dirtied
}

每种测试方法后清理上下文。

  • 在当前测试之前,当在方法模式设置为BEFORE_METHOD的方法上声明时。
@DirtiesContext(methodMode = BEFORE_METHOD) //1
@Test
void testProcessWhichRequiresFreshAppCtx() {
    // some logic that requires a new Spring container
}

在当前测试方法之前清理上下文。

  • 当前测试之后,当在方法模式设置为AFTER_METHOD的方法上声明时(即默认方法模式)。
@DirtiesContext //1
@Test
void testProcessWhichDirtiesAppCtx() {
    // some logic that results in the Spring container being dirtied
}
  

当前测试方法后清理上下文。

如果在使用@ContextHierarchy将上下文配置为上下文层次结构的一部分的测试中使用@DirtiesContext,则可以使用hierarchyMode标志控制清除上下文缓存的方式。默认情况下,使用穷举算法清除上下文缓存,不仅包括当前级别,还包括共享当前测试共有的祖先上下文的所有其他上下文层次结构。驻留在公共祖先上下文的子层次结构中的所有ApplicationContext实例都将从上下文缓存中删除并关闭。如果穷举算法对于特定用例来说过于强大,那么你可以指定更简单的当前级别算法,如下面的示例所示。

@ContextHierarchy({
    @ContextConfiguration("/parent-config.xml"),
    @ContextConfiguration("/child-config.xml")
})
class BaseTests {
    // class body...
}

class ExtendedTests extends BaseTests {

    @Test
    @DirtiesContext(hierarchyMode = CURRENT_LEVEL) //1
    void test() {
        // some logic that results in the child context being dirtied
    }
}

使用当前级别的算法。

有关EXHAUSTIVECURRENT_LEVEL算法的更多详细信息,请参见DirtiesContext.HierarchyMode javadoc。

@TestExecutionListeners

@TestExecutionListeners定义了用于配置应在TestContextManager中注册的TestExecutionListener实现的类级元数据。通常,@TestExecutionListeners@ContextConfiguration结合使用。

下面的示例演示如何注册两个TestExecutionListener实现:

@ContextConfiguration
@TestExecutionListeners({CustomTestExecutionListener.class, AnotherTestExecutionListener.class}) //1
class CustomTestExecutionListenerTests {
    // class body...
}

注册两个TestExecutionListener实现。

默认情况下,@TestExecutionListeners支持继承的监听器。有关示例和更多详细信息,请参见javadoc。

@Commit

@Commit表示应在测试方法完成后提交用于事务性测试方法的事务。你可以将@Commit用作@Rollback(false)的直接替代品,以更明确地传达代码的意图。与@Rollback类似,@ Commit也可以声明为类级别或方法级别的注解。

以下示例显示了如何使用@Commit注解:

@Commit //1
@Test
void testProcessWithoutRollback() {
    // ...
}

将测试结果提交到数据库。

@Rollback

@Rollback表示在测试方法完成后是否应回退用于事务性测试方法的事务。如果为true,则回滚该事务。否则,将提交事务(另请参见@Commit)。即使未明确声明@Rollback,Spring TestContext框架中用于集成测试的回滚默认为true。

当声明为类级注解时,@Rollback定义测试类层次结构中所有测试方法的默认回滚语义。当声明为方法级别的注解时,@Rollback定义特定测试方法的回滚语义,从而可能覆盖类级别的@Rollback@Commit语义。

以下示例使测试方法的结果不回滚(即,结果已提交到数据库):

@Rollback(false) //1
@Test
void testProcessWithoutRollback() {
    // ...
}

不要回滚结果。

@BeforeTransaction

@BeforeTransaction表示,对于已配置为使用Spring的@Transactional注解在事务内运行的测试方法,带注解的void方法应在事务开始之前运行。@BeforeTransaction方法不需要public访问限定,可以在基于Java 8的接口默认方法。

以下示例显示了如何使用@BeforeTransaction注解:

@BeforeTransaction //1
void beforeTransaction() {
    // logic to be executed before a transaction is started
}

在事务之前运行此方法。

@AfterTransaction

@AfterTransaction表示,对于已配置为通过使用Spring的@Transactional注解在事务内运行的测试方法,带注解的void方法应在事务结束后运行。@AfterTransaction方法不需要public访问限定,可以在基于Java 8的接口默认方法中声明。

@AfterTransaction //1
void afterTransaction() {
    // logic to be executed after a transaction has ended
}

事务后运行此方法。

@Sql

@Sql用于注解测试类或测试方法,以配置在集成测试期间针对给定数据库运行的SQL脚本。以下示例显示了如何使用它:

@Test
@Sql({"/test-schema.sql", "/test-user-data.sql"})//1
void userTest() {
    // execute code that relies on the test schema and test data
}

运行此测试的两个脚本。

有关更多详细信息,请参见使用@Sql声明式执行SQL脚本。

@SqlConfig

@SqlConfig定义元数据,该元数据用于确定如何解析和运行使用@Sql注解配置的SQL脚本。以下示例显示了如何使用它:

@Test
@Sql(
    scripts = "/test-user-data.sql",
    config = @SqlConfig(commentPrefix = "`", separator = "@@") //
)
void userTest() {
    // execute code that relies on the test data
}

在SQL脚本中设置注释前缀和分隔符。

@SqlMergeMode

@SqlMergeMode用于注释测试类或测试方法,以配置是否将方法级@Sql声明与类级@Sql声明合并。如果未在测试类或测试方法上声明@SqlMergeMode,则默认情况下将使用OVERRIDE合并模式。在OVERRIDE模式下,方法级别的@Sql声明将有效地覆盖类级别的@Sql声明。

请注意,方法级别的@SqlMergeMode声明将覆盖类级别的声明。

下面的示例演示如何在类级别使用@SqlMergeMode

@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
@SqlMergeMode(MERGE) //1
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    void standardUserProfile() {
        // execute code that relies on test data set 001
    }
}

对于类中的所有测试方法,将@Sql合并模式设置为MERGE

下面的示例演示如何在方法级别使用@SqlMergeMode

@SpringJUnitConfig(TestConfig.class)
@Sql("/test-schema.sql")
class UserTests {

    @Test
    @Sql("/user-test-data-001.sql")
    @SqlMergeMode(MERGE) //1
    void standardUserProfile() {
        // execute code that relies on test data set 001
    }
}

对于特定的测试方法,将@Sql合并模式设置为MERGE

@SqlGroup

@SqlGroup是一个容器注解,它聚合了多个@Sql注解。你可以本地使用@SqlGroup声明多个嵌套的@Sql注解,也可以将其与Java 8对可重复注解的支持结合使用,其中@Sql可以在同一类或方法上多次声明,从而隐式生成此容器注解。下面的示例显示如何声明一个SQL组:

@Test
@SqlGroup({ //1
    @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
    @Sql("/test-user-data.sql")
)}
void userTest() {
    // execute code that uses the test schema and test data
}

声明一组SQL脚本。

6.4.2 标准注解支持

Spring TestContext 框架的所有配置的标准语义都支持以下注解。请注意,这些注解并非特定于测试,可以在Spring 框架中的任何地方使用。

  • @Autowired
  • @Qualifier
  • @Value
  • @Resource (javax.annotation) 如果支持JSR-250
  • @ManagedBean (javax.annotation) 如果支持 JSR-250
  • @Inject (javax.inject) 如果支持 JSR-330
  • @Named (javax.inject) 如果支持 JSR-330
  • @PersistenceContext (javax.persistence) 如果支持JPA
  • @PersistenceUnit (javax.persistence) 如果支持JPA
  • @Required
  • @Transactional (org.springframework.transaction.annotation) with limited attribute support

JSR-250生命周期注解

在Spring TestContext 框架中,可以在ApplicationContext中配置的任何应用程序组件上使用具有标准语义的@PostConstruct@PreDestroy。但是,这些生命周期注解在实际测试类中的使用受到限制。

如果测试类中的方法使用@PostConstruct进行注解,则该方法将在基础测试框架的before方法之前运行(例如,使用JUnit Jupiter的@BeforeEach注解的方法),并且该方法适用于测试类中的每个测试方法。另一方面,如果测试类中的方法使用@PreDestroy注解,则该方法将永远不会运行。因此,在测试类中,建议你使用来自基础测试框架的测试生命周期回调,而不是@PostConstruct@PreDestroy

3.4.3 Spring JUnit4测试注解

以下注解仅在与SpringRunner、Spring的JUnit 4规则或Spring的JUnit 4支持类一起使用时才受支持:

  • @IfProfileValue
  • @ProfileValueSourceConfiguration
  • @Timed
  • @Repeat

@IfProfileValue

@IfProfileValue表示已为特定测试环境启用带注解的测试。如果配置的ProfileValueSource返回提供的名称的匹配值,则使用测试。否则,测试将被禁用,并且实际上将被忽略。

你可以在类级别、方法级别或两者上应用@IfProfileValue。对于该类或其子类中的任何方法,@IfProfileValue的类级别用法优先于方法级别用法。具体来说,如果在类级别和方法级别都启用了测试,则启用该测试。缺少@IfProfileValue意味着隐式启用了测试。这类似于JUnit 4的@Ignore注解的语义,不同之处在于@Ignore的存在始终会禁用测试。

以下示例显示了具有@IfProfileValue注解的测试:

@IfProfileValue(name="java.vendor", value="Oracle Corporation") //1
@Test
public void testProcessWhichRunsOnlyOnOracleJvm() {
    // some logic that should run only on Java VMs from Oracle Corporation
}
  1. 仅当Java供应商是“ Oracle Corporation”时才运行此测试。

另外,你可以为@IfProfileValue配置值列表(具有OR语义)以在JUnit 4环境中实现类似于TestNG的测试组支持。考虑以下示例:

@IfProfileValue(name="test-groups", values={"unit-tests", "integration-tests"}) //1
@Test
public void testProcessWhichRunsForUnitOrIntegrationTestGroups() {
    // some logic that should run only for unit and integration test groups
}

对单元测试和集成测试运行此测试。

@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration是一个类级别的注解,它指定检索通过@IfProfileValue注解配置的配置文件值时要使用哪种ProfileValueSource类型。如果未为测试声明@ProfileValueSourceConfiguration,则默认使用SystemProfileValueSource。以下示例显示了如何使用@ProfileValueSourceConfiguration

@ProfileValueSourceConfiguration(CustomProfileValueSource.class) //1
public class CustomProfileValueSourceTests {
    // class body...
}

使用自定义配置文件值源。

参考代码:org.liyong.test.annotation.test.spring.ProfileValueTest

@Timed

@Timed表示带注解的测试方法必须在指定的时间段(以毫秒为单位)内完成执行。如果单元测试片段执行时间超过指定的时间段,则测试将失败。

该时间段包括运行测试方法本身,测试的任何重复(请参见@Repeat)以及测试套件的任何设置或拆除。以下示例显示了如何使用它:

@Timed(millis = 1000)//1
public void testProcessWithOneSecondTimeout() {
    // some logic that should not take longer than 1 second to execute
}

将测试时间设置为一秒。

Spring的@Timed注解与JUnit 4的@Test(timeout = ...)支持具有不同的语义。具体来说,由于JUnit 4处理测试执行超时的方式(即通过在单独的线程中执行测试方法),如果测试花费的时间太长,@Test(timeout = ...)会抢先通过测试。另一方面,Spring的@Timed不会抢先通过测试,而是在失败之前等待测试完成。

@Repeat

@Repeat表示必须重复运行带注解的测试方法。注解中指定了要执行测试方法的次数。重复执行的范围包括测试方法本身的执行以及测试套件中任何安装或拆除。以下示例显示了如何使用@Repeat注解:

@Repeat(10) //1
@Test
public void testProcessRepeatedly() {
    // ...
}

重复此测试十次。

6.4.4 Spring JUnit Jupiter测试注解

以下注解仅在与SpringExtensionJUnit Jupiter(即JUnit 5中的编程模型)结合使用时才受支持:

  • @SpringJUnitConfig
  • @SpringJUnitWebConfig
  • @TestConstructor
  • @EnabledIf
  • @DisabledIf

@SpringJUnitConfig

@SpringJUnitConfig是一个组合注解,它将JUnit Jupiter中的@ExtendWith(SpringExtension.class)与Spring TestContext 框架中的@ContextConfiguration组合在一起。它可以在类级别用作@ContextConfiguration的直接替代。关于配置选项,@ContextConfiguration@SpringJUnitConfig之间的唯一区别是可以使用@SpringJUnitConfig中的value属性声明组件类。

以下示例显示如何使用@SpringJUnitConfig注解指定配置类:

@SpringJUnitConfig(TestConfig.class) //1
class ConfigurationClassJUnitJupiterSpringTests {
    // class body...
}

指定配置类。

以下示例显示如何使用@SpringJUnitConfig注解指定配置文件的位置:

@SpringJUnitConfig(locations = "/test-config.xml") //1
class XmlJUnitJupiterSpringTests {
    // class body...
}

指定配置文件的位置。

有关更多详细信息,请参见上下文管理以及@SpringJUnitConfig@ContextConfiguration的javadoc。

@SpringJUnitWebConfig

@SpringJUnitWebConfig是一个组合的注解,它将来自JUnit Jupiter@ExtendWith(SpringExtension.class)与来自Spring TestContext 框架的@ContextConfiguration@WebAppConfiguration组合在一起。你可以在类级别使用它作为@ContextConfiguration@WebAppConfiguration的直接替代。关于配置选项,@ ContextConfiguration@SpringJUnitWebConfig之间的唯一区别是可以使用@SpringJUnitWebConfig中的value属性来声明组件类。另外,只能使用@SpringJUnitWebConfig中的resourcePath属性来覆盖@WebAppConfiguration中的value属性。

以下示例显示如何使用@SpringJUnitWebConfig注解指定配置类:

@SpringJUnitWebConfig(TestConfig.class) //1
class ConfigurationClassJUnitJupiterSpringWebTests {
    // class body...
}

指定配置类。

以下示例显示如何使用@SpringJUnitWebConfig注解指定配置文件的位置:

@SpringJUnitWebConfig(locations = "/test-config.xml") //1
class XmlJUnitJupiterSpringWebTests {
    // class body...
}
  1. 指定配置文件的位置。

有关更多详细信息,请参见上下文管理以及@SpringJUnitWebConfig,@ContextConfiguration和@WebAppConfiguration的javadoc。

参考代码:org.liyong.test.annotation.test.spring.ConfigurationClassJUnitJupiterSpringWebTests

@TestConstructor

@TestConstructor是类型级别的注解,用于配置如何从测试的ApplicationContext中的组件自动连接测试类构造函数的参数。

如果在测试类上不存在@TestConstructormeta-present,则将使用默认的测试构造函数自动装配模式。有关如何更改默认模式的详细信息,请参见下面的提示。但是请注意,构造函数上的@Autowired本地声明优先于@TestConstructor和默认模式。

更改默认的测试构造函数自动装配模式

可以通过将JVM系统属性spring.test.constructor.autowire.mode设置为all来更改默认的测试构造函数自动装配模式。或者,可以通过SpringProperties机制更改默认模式。

如果未设置spring.test.constructor.autowire.mode属性,则不会自动装配测试类构造函数。

从Spring框架5.2开始,仅将@TestConstructorSpringExtension结合使用以与JUnit Jupiter一起使用。请注意,SpringExtension通常会自动为你注册-例如,在使用@SpringJUnitConfig@SpringJUnitWebConfig之类的注解或Spring Boot Test中与测试相关的各种注解时。

@EnabledIf

@EnabledIf用于表示已注解的JUnit Jupiter测试类或测试方法启用,如果提供的表达式的值为true,则应运行@EnabledIf。具体来说,如果表达式的计算结果为Boolean.TRUE或等于true的字符串(忽略大小写),则启用测试。在类级别应用时,默认情况下也会自动启用该类中的所有测试方法。

表达式可以是以下任意一种:

  • Spring表达式语言。例如:@EnabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring Environment中可用属性的占位符。例如:@EnabledIf("${smoke.tests.enabled}")
  • 文本文字。例如:@EnabledIf("true")

但是请注意,不是属性占位符的动态解析结果的文本文字的实际值为零,因为@EnabledIf(“ false”)等效于@Disabled,而@EnabledIf(“ true”)在逻辑上是没有意义的。

你可以使用@EnabledIf作为元注解来创建自定义的组合注释。例如,你可以创建一个自定义@EnabledOnMac注解,如下所示:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@EnabledIf(
    expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
    reason = "Enabled on Mac OS"
)
public @interface EnabledOnMac {}

@DisabledIf

@DisabledIf用于表示已注解的JUnit Jupiter测试类或测试方法被禁用,并且如果提供的表达式的值为true,则不应执行该操作。具体来说,如果表达式的计算结果为Boolean.TRUE或等于true的字符串(忽略大小写),则测试将被禁用。当在类级别应用时,该类中的所有测试方法也会自动禁止。

表达式可以是以下任意一种:

  • Spring表达式语言。例如:@DisabledIf("#{systemProperties['os.name'].toLowerCase().contains('mac')}")
  • Spring Environment中可用属性的占位符。例如:@DisabledIf("${smoke.tests.disabled}")
  • 文本文字:例如:@DisabledIf("true")

但是请注意,不是属性占位符的动态解析结果的文本文字的实际值为零,因为@DisabledIf(“ true”)等效于@Disabled,而@DisabledIf(“ false”)在逻辑上是没有意义的。

你可以将@DisabledIf用作元注释,以创建自定义的组合注解。例如,你可以创建一个自定义@DisabledOnMac注解,如下所示:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@DisabledIf(
    expression = "#{systemProperties['os.name'].toLowerCase().contains('mac')}",
    reason = "Disabled on Mac OS"
)
public @interface DisabledOnMac {}

3.4.5 测试的元注解支持

你可以将大多数与测试相关的注解用作元注解,以创建自定义的组合注解,并减少整个测试套件中的重复配置。

你可以将以下各项用作与TestContext框架结合使用的元注解。

  • @BootstrapWith
  • @ContextConfiguration
  • @ContextHierarchy
  • @ActiveProfiles
  • @TestPropertySource
  • @DirtiesContext
  • @WebAppConfiguration
  • @TestExecutionListeners
  • @Transactional
  • @BeforeTransaction
  • @AfterTransaction
  • @Commit
  • @Rollback
  • @Sql
  • @SqlConfig
  • @SqlMergeMode
  • @SqlGroup
  • @Repeat (仅支持 JUnit 4)
  • @Timed (仅支持 JUnit 4)
  • @IfProfileValue (仅支持 JUnit 4)
  • @ProfileValueSourceConfiguration (仅支持 JUnit 4)
  • @SpringJUnitConfig (仅支持 JUnit Jupiter)
  • @SpringJUnitWebConfig (仅支持JUnit Jupiter)
  • @TestConstructor (仅支持 JUnit Jupiter)
  • @EnabledIf (仅支持 JUnit Jupiter)
  • @DisabledIf (仅支持 JUnit Jupiter)

考虑以下示例:

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public class UserRepositoryTests { }

如果发现我们要在基于JUnit 4的测试套件中重复上述配置,则可以通过引入一个自定义的组合注解来减少重复,该注解集中了Spring的通用测试配置,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

然后,我们可以使用我们的自定义@TransactionalDevTestConfig的注解来简化单个基于JUnit 4的测试类的配置,如下所示:

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class OrderRepositoryTests { }

@RunWith(SpringRunner.class)
@TransactionalDevTestConfig
public class UserRepositoryTests { }

如果我们编写使用JUnit Jupiter的测试,则可以进一步减少代码重复,因为JUnit 5中的注解也可以用作元注解。考虑以下示例:

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }

@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }

如果发现我们在基于JUnit Jupiter的测试套件中重复了前面的配置,则可以通过引入一个自定义组合注解来减少重复,该注解集中了Spring和JUnit Jupiter的通用测试配置,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }

然后,我们可以使用我们的自定义@TransactionalDevTestConfig的注解来简化基于单个JUnit Jupiter的测试类的配置,如下所示:

@TransactionalDevTestConfig
class OrderRepositoryTests { }

@TransactionalDevTestConfig
class UserRepositoryTests { }

由于JUnit Jupiter支持使用@Test@RepeatedTestParameterizedTest和其他作为元注解,因此你也可以在测试方法级别创建自定义的组合注解。例如,如果我们希望创建一个组合的注解,将JUnit Jupiter的@Test@Tag注解与Spring的@Transactional注解相结合,则可以创建一个@TransactionalIntegrationTest注解,如下所示:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Transactional
@Tag("integration-test") // org.junit.jupiter.api.Tag
@Test // org.junit.jupiter.api.Test
public @interface TransactionalIntegrationTest { }

然后,我们可以使用自定义的@TransactionalIntegrationTest注解来简化基于单个JUnit Jupiter的测试方法的配置,如下所示:

@TransactionalIntegrationTest
void saveOrder() { }

@TransactionalIntegrationTest
void deleteOrder() { }

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表