Spring定时任务与拦截器
定时任务
定时任务是一种在计算机中按计划自动执行的任务。它允许设置一个特定的时间,当系统时间达到这个设定时,系统会自动执行相关的程序或脚本。定时任务在很多场景下都非常有用,比如系统维护、数据备份、定期更新等。
在Spring Boot中实现定时任务,有两种常见方案。
使用Spring @Scheduled注解
使用@Scheduled
注解的基本步骤:
- 开启定时任务支持:在Spring Boot的主类或者任何配置类上添加
@EnableScheduling
注解。 - 创建定时任务:在需要定时执行的方法上添加
@Scheduled
注解,并配置相应的属性。
@Scheduled
注解支持的属性有:
fixedRate
:定义任务执行的频率,单位为毫秒。fixedDelay
:定义任务完成后下一次任务开始执行的延迟时间,单位为毫秒。cron
:使用cron表达式定义任务的执行时间。
@Configuration // 标记配置类
@EnableScheduling // 开启定时任务
public class SaticScheduleTask {
@Resource
UserService userService;
// 添加定时任务 每隔5秒调用一次
@Scheduled(cron = "0/5 * * * * ?")
public void configureTasks() {
userService.addUser();
System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
}
}
cron = [秒] [分] [时] [日] [月] [周] [年]
说明 | 必填 | 允许填写的值 | 允许的通配符 |
---|---|---|---|
秒 | √ | 0 - 59 | - * / |
分 | √ | 0 - 59 | - * / |
时 | √ | 0 - 23 | - * / |
日 | √ | 1 - 31 | - * ? / L W |
月 | √ | 1 - 12 or JAN - DEC | - * / |
周 | √ | 1 - 7 or SUN - SAT | - * ? / L # |
年 | × | 1970 - 2099 | - * / |
- ?:表示不指定值,即不关心某个字段的取值时使用。需要注意的是,月份中的日期和星期可能会起冲突,因此在配置时这两个得有一个是?
- *****:表示所有值,例如:在秒的字段上设置 *,表示每一秒都会触发
- ,:用来分开多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
- -:表示区间,例如在秒上设置 “10-12”,表示 10,11,12秒都会触发
- /:用于递增触发,如在秒上面设置"5/15" 表示从5秒开始,每增15秒触发(5,20,35,50)
- #:序号(表示每月的第几个周几),例如在周字段上设置"6#3"表示在每月的第三个周六,(用在母亲节和父亲节再合适不过了)周字段的设置,若使用英文字母是不区分大小写的 ,即 MON 与 mon 相同
- L:表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会自动判断是否是润年),在周字段上表示星期六,相当于"7"或"SAT"(注意周日算是第一天)。如果在"L"前加上数字,则表示该数据的最后一个。例如在周字段上设置"6L"这样的格式,则表示"本月最后一个星期五"
- W:表示离指定日期的最近工作日(周一至周五),例如在日字段上设置"15W",表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发,如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。
(注,“W"前只能设置具体的数字,不允许区间”-")
L 和 W 可以一组合使用。如果在日字段上设置"LW",则表示在本月的最后一个工作日触发
0 0 2 1 * ?
表示每月 1 日的凌晨 2 点执行。
0 15 10 ? * MON-FRI
表示周一到周五每天上午 10:15 执行。
0 15 10 ? 6L 2019-2020
表示 2019-2020 年每个月的最后一个星期五上午 10:15 执行。
0 0 10,14,16 ? * *
表示每天上午 10 点,下午 2 点,4 点执行。
0 0/30 9-17 ? * *
表示朝九晚五工作时间内每半小时执行。
0 0 12 ? * WED
表示每个星期三中午 12 点执行。
0 0 12 * * ?
表示每天中午 12 点执行。
0 15 10 * * ?
表示每天上午 10:15 执行。
0 15 10 * * 2019
表示 2019 年的每天上午 10:15 执行。
创建多线程定时任务
@Configuration // 标记配置类,兼备Component的效果。
@EnableScheduling // 开启定时任务
@EnableAsync // 开启多线程
public class SaticScheduleTask {
@Resource
UserService userService;
// 添加定时任务
@Scheduled(cron = "0/5 * * * * ?")
@Async // 异步方法 异步调用 默认为同步
@Transactional // 添加事务
public void configureTasks() {
userService.addUser();
System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
}
}
根据 Spring 的文档说明,默认采用的是单线程的模式的。所以在 Java 应用中,绝大多数情况下都是通过同步的方式来实现交互处理的。
当多个任务的执行势必会相互影响。例如,如果 A 任务执行时间比较长,那么 B 任务必须等到 A 任务执行完毕后才会启动执行。又如在处理与第三方系统交互的时候,容易造成响应迟缓的情况,之前大部分都是使用多线程来完成此类任务,其实,在 spring3.x 之后,已经内置了 @Async
来完美解决这个问题。
用定时任务编写一个定时发送邮箱的功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
@Configuration
@EnableScheduling //开启定时任务
@EnableAsync //开启异步任务
public class ScheduledConfig {
@Async
@Scheduled(cron = "0/5 * * * * ?")
public void sendEmail(){
try {
System.out.println(Thread.currentThread().getName());
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost("smtp.qq.com");
javaMailSender.setPort(465);
javaMailSender.setUsername("发送者邮箱");
javaMailSender.setPassword("发送者密码");
javaMailSender.setDefaultEncoding("UTF-8");
Properties properties = new Properties();
properties.setProperty("mail.smtp.timeout", "30000");
properties.setProperty("mail.smtp.auth", "true");
properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
javaMailSender.setJavaMailProperties(properties);
// 构建一个邮件对象
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
// true表示构建一个可以带附件的邮件对象
MimeMessageHelper mimeMessageHelper = null;
mimeMessageHelper = new MimeMessageHelper(mimeMessage, true);
// 设置邮件主题
mimeMessageHelper.setSubject("这是一封测试邮件");
// 设置邮件发送者
mimeMessageHelper.setFrom("发送者邮箱");
// 设置邮件接收者,可以有多个接收者
mimeMessageHelper.addTo("接收者邮箱");
// 设置邮件抄送人,可以有多个抄送人
// mimeMessageHelper.addCc("xxx@qq.com");
// 设置邮件隐秘抄送人,可以有多个隐秘抄送人
// mimeMessageHelper.addBcc("xxx@qq.com");
// 设置邮件发送日期
mimeMessageHelper.setSentDate(new Date());
// 设置邮件的正文
mimeMessageHelper.setText("<p>猜猜我是谁,嘿嘿</p>", true);
// 发送邮件
javaMailSender.send(mimeMessage);
}catch (Exception e){
throw new EmailException("邮件发送失败");
}
System.out.println("这是发送邮件功能...."+ LocalDateTime.now());
}
}
QQ邮箱授权码:SMTP/IMAP服务
在 QQ 邮箱安全设置中开启 POP3/IMAP/SMTP/Exchange/CardDAV 服务,生成的授权码即密码。
/**
* @Description: 全局异常
*/
@ControllerAdvice(basePackages = "com.hz")
@Slf4j
public class GlobalException {
/**
* 处理自定义异常 IdException
*/
@ExceptionHandler(IdException.class)
@ResponseBody
public ResultJSON handleException(IdException e){
log.error("异常信息:",e);
return ResultJSON.error("请求失败");
}
/**
* 处理邮件发送异常 EmailException
*/
@ExceptionHandler(EmailException.class)
@ResponseBody
public ResultJSON handleException(EmailException e){
log.error("异常信息:",e);
return ResultJSON.error("邮件发送失败");
}
/**
* 处理运行时异常 RuntimeException
*/
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public ResultJSON handleException(RuntimeException e){
log.error("异常信息:",e);
return ResultJSON.error("请求失败");
}
}
/**
* @Description: 自定义异常
*/
public class EmailException extends RuntimeException{
public EmailException(String msg){
super(msg);
}
}
然后对方每隔 5 秒就会收到一封邮件,终止服务,发送停止。
使用Quartz框架
Quartz 是一个功能丰富的任务调度库,它提供了比 Spring 的@Scheduled
更高级的特性,如作业存储、作业集群、触发器等。
添加依赖:在pom.xml
中添加 Quartz 的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
配置Quartz:在application.properties
或application.yml
中配置 Quartz。
# application.properties
spring.quartz.job-store-type = memory
定义Job和Trigger:创建一个继承自QuartzJobBean
的类,并定义触发器。
public class MyJob extends QuartzJobBean {
private final MyService myService;
@Autowired
public MyJob(MyService myService) {
this.myService = myService;
}
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
myService.performTask();
}
}
然后在配置类中定义 JobDetail 和 Trigger。
@Configuration
public class QuartzConfig {
@Bean
public JobDetail myJobDetail() {
return JobBuilder.newJob(MyJob.class)
.withIdentity("myJob")
.storeDurably()
.build();
}
@Bean
public Trigger myJobTrigger() {
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder
.simpleSchedule()
.withIntervalInSeconds(10)
.repeatForever();
return TriggerBuilder.newTrigger()
.forJob(myJobDetail())
.withIdentity("myTrigger")
.withSchedule(scheduleBuilder)
.build();
}
}
@Scheduled
注解简单易用,适用于简单的定时任务,Quartz 则更适合复杂的调度需求。
拦截器
Spring MVC 的处理拦截器类似于 Servlet 开发中的过滤器 Filter,用于对处理器进行预处理和后处理。
过滤器
Servlet 技术的一部分,属 于Java EE 规范,用于对请求进行预处理和后处理。过滤器可以过滤所有的请求,包括静态资源(如HTML、CSS、JavaScript文件)和动态请求。通常用于处理请求的通用任务,如日志记录、权限检查、事务管理等。多个过滤器可以组成一个过滤链,按照配置的顺序执行。
- 配置:在
web.xml
中配置,可以针对 URL 模式进行设置。
拦截器
Spring MVC 框架的一部分,仅限于 Spring MVC 环境下使用。拦截器仅拦截控制器(Controller)中的请求处理方法,不会拦截对静态资源的请求。通常用于处理与 Spring MVC 相关的任务,如用户登录验证、数据绑定、请求和视图的预处理和后处理等。多个拦截器同样可以组成拦截器链,按照添加的顺序执行。
- 配置:通常通过 Spring 的配置文件或注解进行配置。
在 SpringBoot 中使用拦截器
1、实现 HandlerInterceptor 接口
@Component
public class LoginInterceptor implements HandlerInterceptor {
// preHandle 是请求执行前执行的
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("进入拦截器");
return true;
}
// postHandler 是请求结束执行的 当 preHandle 返回 true 才会执行
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
System.out.println("postHandle......");
}
// afterCompletion 是视图渲染完成后才执行
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
System.out.println("afterCompletion......");
}
}
2、实现 WebMvcConfigurer 接口配置拦截路径
三种方式:
- 继承
WebMvcConfigurerAdapter spring5.0
(已弃用,不推荐) - 实现
WebMvcConfigurer
(推荐) - 继承
WebMvcConfigurationSupport
(会导致 springboot 自动配置失效)
@Configuration
public class WebJavaBeanConfiguration implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
// addInterceptor 需要一个实现 HandlerInterceptor 接口的拦截器实例
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 设置拦截器的过滤路径规则(对所有请求都拦截)
.excludePathPatterns("/user/login") // 设置不需要拦截的过滤规则
.excludePathPatterns("/user/logout");
}
}