java每日精进 3.21 【SpringBoot规范2.0】

1.任务管理

1.1@Scheduled实现定时任务

java 复制代码
@SpringBootApplication
@EnableScheduling
public class Application {

	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}

}
java 复制代码
@Component
public class ScheduledTasks {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

    @Scheduled(fixedRate = 5000)
    public void reportCurrentTime() {
        log.info("现在时间:" + dateFormat.format(new Date()));
    }

}

2021-07-13 14:56:56.413 INFO 34836 --- [ main] c.d.chapter71.Chapter71Application : Started Chapter71Application in 1.457 seconds (JVM running for 1.835)

2021-07-13 14:57:01.411 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:01

2021-07-13 14:57:06.412 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:06

2021-07-13 14:57:11.413 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:11

2021-07-13 14:57:16.413 INFO 34836 --- [ scheduling-1] com.didispace.chapter71.ScheduledTasks : 现在时间:14:57:16

源码中有如下配置

java 复制代码
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

	String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;

	String cron() default "";

	String zone() default "";

	long fixedDelay() default -1;

	String fixedDelayString() default "";

	long fixedRate() default -1;

	String fixedRateString() default "";

	long initialDelay() default -1;

	String initialDelayString() default "";

}
  • cron:通过cron表达式来配置执行规则
  • zone:cron表达式解析时使用的时区
  • fixedDelay:上一次执行结束到下一次执行开始的间隔时间(单位:ms)
  • fixedDelayString:上一次任务执行结束到下一次执行开始的间隔时间,使用java.time.Duration#parse解析
  • fixedRate:以固定间隔执行任务,即上一次任务执行开始到下一次执行开始的间隔时间(单位:ms),若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后立即执行下一次任务
  • fixedRateString:与fixedRate逻辑一致,只是使用java.time.Duration#parse解析
  • initialDelay:首次任务执行的延迟时间
  • initialDelayString:首次任务执行的延迟时间,使用java.time.Duration#parse解析

当在集群环境下的时候,如果任务的执行或操作依赖一些共享资源的话,就会存在竞争关系。如果不引入分布式锁等机制来做调度的话,就可能出现预料之外的执行结果。所以,@Scheduled注解更偏向于使用在单实例自身维护相关的一些定时任务上会更为合理一些,比如:定时清理服务实例某个目录下的文件、定时上传本实例的一些统计数据等。

1.2 Elastic Job

java 复制代码
<dependencies>
    <dependency>
        <groupId>org.apache.shardingsphere.elasticjob</groupId>
        <artifactId>elasticjob-lite-spring-boot-starter</artifactId>
        <version>3.0.0</version>
    </dependency>

    // ...
</dependencies>
java 复制代码
@Slf4j
@Service
public class MySimpleJob implements SimpleJob {

    @Override
    public void execute(ShardingContext context) {
        log.info("MySimpleJob start : didispace.com {}", System.currentTimeMillis());
    }

}
java 复制代码
#elasticjob
#elastic job的注册中心和namespace
elasticjob.reg-center.server-lists=localhost:2181
elasticjob.reg-center.namespace=didispace

#配置elastic-job-class是任务的实现类
elasticjob.jobs.my-simple-job.elastic-job-class=com.didispace.chapter72.MySimpleJob
elasticjob.jobs.my-simple-job.cron=0/5 * * * * ?
#分片总数
elasticjob.jobs.my-simple-job.sharding-total-count=1

多分片

java 复制代码
@Slf4j
@Service
public class MyShardingJob implements SimpleJob {

    @Override
    public void execute(ShardingContext context) {
        switch (context.getShardingItem()) {
            case 0:
                log.info("分片1:执行任务");
                break;
            case 1:
                log.info("分片2:执行任务");
                break;
            case 2:
                log.info("分片3:执行任务");
                break;
        }
    }

}
java 复制代码
elasticjob.jobs.my-sharding-job.elastic-job-class=com.didispace.chapter73.MyShardingJob
elasticjob.jobs.my-sharding-job.cron=0/5 * * * * ?
elasticjob.jobs.my-sharding-job.sharding-total-count=3

每间隔5秒,这个实例会打印这样的日志:

复制代码
05.254  INFO 63478 --- [-sharding-job-1] com.didispace.chapter73.MyShardingJob    : 分片1:执行任务
2021-07-21 17:42:05.254  INFO 63478 --- [-sharding-job-3] com.didispace.chapter73.MyShardingJob    : 分片3:执行任务
2021-07-21 17:42:05.254  INFO 63478 --- [-sharding-job-2] com.didispace.chapter73.MyShardingJob    : 分片2:执行任务
2021-07-21 17:42:10.011  INFO 63478 --- [-sharding-job-4] com.didispace.chapter73.MyShardingJob    : 分片1:执行任务
2021-07-21 17:42:10.011  INFO 63478 --- [-sharding-job-5] com.didispace.chapter73.MyShardingJob    : 分片2:执行任务
2021-07-21 17:42:10.011  INFO 63478 --- [-sharding-job-6] com.didispace.chapter73.MyShardingJob    : 分片3:执行任务

我们再启动一个实例

实例1的日志:

复制代码
2021-07-21 17:44:50.190  INFO 63478 --- [ng-job_Worker-1] com.didispace.chapter73.MyShardingJob    : 分片2:执行任务
2021-07-21 17:44:55.007  INFO 63478 --- [ng-job_Worker-1] com.didispace.chapter73.MyShardingJob    : 分片2:执行任务
2021-07-21 17:45:00.010  INFO 63478 --- [ng-job_Worker-1] com.didispace.chapter73.MyShardingJob    : 分片2:执行任务

实例2的日志:

复制代码
2021-07-21 17:44:50.272  INFO 63484 --- [-sharding-job-1] com.didispace.chapter73.MyShardingJob    : 分片1:执行任务
2021-07-21 17:44:50.273  INFO 63484 --- [-sharding-job-2] com.didispace.chapter73.MyShardingJob    : 分片3:执行任务
2021-07-21 17:44:55.009  INFO 63484 --- [-sharding-job-3] com.didispace.chapter73.MyShardingJob    : 分片1:执行任务
2021-07-21 17:44:55.009  INFO 63484 --- [-sharding-job-4] com.didispace.chapter73.MyShardingJob    : 分片3:执行任务

1.3 Async异步调度

java 复制代码
@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    public void doTaskOne() throws Exception {
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskTwo() throws Exception {
        log.info("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务二,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskThree() throws Exception {
        log.info("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务三,耗时:" + (end - start) + "毫秒");
    }

}
java 复制代码
@Slf4j
@SpringBootTest
public class Chapter75ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        asyncTasks.doTaskOne();
        asyncTasks.doTaskTwo();
        asyncTasks.doTaskThree();
    }

}

执行后是顺序调度

在Spring Boot的主程序中配置@EnableAsync

使用@Async注解就能简单的将原来的同步函数变为异步函数

异步调度

java 复制代码
package com.home.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.Random;
import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    @Async
    public CompletableFuture<String> doTaskOne() throws Exception {
        log.info("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务一,耗时:" + (end - start) + "毫秒");
        return CompletableFuture.completedFuture("任务一完成");
    }


    @Async
    public CompletableFuture<String> doTaskTwo() throws Exception {
        log.info("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务二,耗时:" + (end - start) + "毫秒");
        return CompletableFuture.completedFuture("任务二完成");
    }

    @Async
    public CompletableFuture<String> doTaskThree() throws Exception {
        log.info("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务三,耗时:" + (end - start) + "毫秒");
        return CompletableFuture.completedFuture("任务三完成");
    }
}
java 复制代码
package com.home.test;

import com.home.task.AsyncTasks;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CompletableFuture;

@Slf4j
@SpringBootTest
public class Chapter75ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();

        CompletableFuture<String> task1 = asyncTasks.doTaskOne();
        CompletableFuture<String> task2 = asyncTasks.doTaskTwo();
        CompletableFuture<String> task3 = asyncTasks.doTaskThree();
        CompletableFuture.allOf(task1, task2, task3).join();
        long end = System.currentTimeMillis();

        log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
    }


}

由于默认线程池的核心线程数是8,所以3个任务会同时开始执行,日志输出如上👆

当接口被客户端频繁调用的时候,异步任务的数量就会大量增长:3 x n(n为请求数量)

如果任务处理不够快,就很可能会出现内存溢出的情况

默认情况下,一般任务队列就可能把内存给堆满了

所以,我们真正使用的时候,还需要对异步任务的执行线程池做一些基础配置,以防止出现内存溢出导致服务不可用的问题

java 复制代码
spring.task.execution.pool.core-size=2
spring.task.execution.pool.max-size=5
spring.task.execution.pool.queue-capacity=10
spring.task.execution.pool.keep-alive=60s
spring.task.execution.pool.allow-core-thread-timeout=true
spring.task.execution.thread-name-prefix=task-

日志输出的顺序会变成如下的顺序

缓冲队列满了之后才会申请超过核心线程数的线程来进行处理

这里只有缓冲队列中10个任务满了,再来第11个任务的时候

才会在线程池中创建第三个线程来处理

默认情况下,所有用@Async创建的异步任务都是共用的一个线程池

所以当有一些异步任务碰到性能问题的时候,是会直接影响其他异步任务的

不同线程池异步调度

为了解决这个问题,我们就需要对异步任务做一定的线程池隔离,让不同的异步任务互不影响

初始化两个线程池

java 复制代码
@EnableAsync
@Configuration
public class TaskPoolConfig {

    @Bean
    public Executor taskExecutor1() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-1-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }

    @Bean
    public Executor taskExecutor2() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(2);
        executor.setQueueCapacity(10);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("executor-2-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        return executor;
    }
}
java 复制代码
@Slf4j
@Component
public class AsyncTasks {

    public static Random random = new Random();

    @Async("taskExecutor1")
    public CompletableFuture<String> doTaskOne(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }

    @Async("taskExecutor2")
    public CompletableFuture<String> doTaskTwo(String taskNo) throws Exception {
        log.info("开始任务:{}", taskNo);
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        log.info("完成任务:{},耗时:{} 毫秒", taskNo, end - start);
        return CompletableFuture.completedFuture("任务完成");
    }
}
java 复制代码
@Slf4j
@SpringBootTest
public class Chapter77ApplicationTests {

    @Autowired
    private AsyncTasks asyncTasks;

    @Test
    public void test() throws Exception {
        long start = System.currentTimeMillis();

        // 线程池1
        CompletableFuture<String> task1 = asyncTasks.doTaskOne("1");
        CompletableFuture<String> task2 = asyncTasks.doTaskOne("2");
        CompletableFuture<String> task3 = asyncTasks.doTaskOne("3");

        // 线程池2
        CompletableFuture<String> task4 = asyncTasks.doTaskTwo("4");
        CompletableFuture<String> task5 = asyncTasks.doTaskTwo("5");
        CompletableFuture<String> task6 = asyncTasks.doTaskTwo("6");

        // 一起执行
        CompletableFuture.allOf(task1, task2, task3, task4, task5, task6).join();

        long end = System.currentTimeMillis();

        log.info("任务全部完成,总耗时:" + (end - start) + "毫秒");
    }

}

可以看出两个线程池的异步任务互不影响;

再次优化

默认情况下,线程池的拒绝策略是:当线程池队列满了,会丢弃这个任务,并抛出异常。

实际开发过程中,有些业务场景,直接拒绝的策略往往并不适用,

有时候我们可能会选择舍弃最早开始执行而未完成的任务

也可能会选择舍弃刚开始执行而未完成的任务等更贴近业务需要的策略

所以,为线程池配置其他拒绝策略或自定义拒绝策略是很常见的需求,那么这个要怎么实现呢?

java 复制代码
// AbortPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());

// DiscardPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());

// DiscardOldestPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy());

// CallerRunsPolicy策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
  • AbortPolicy策略:默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
  • DiscardPolicy策略:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
  • DiscardOldestPolicy策略:如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
  • CallerRunsPolicy策略:如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。

自定义拒绝策略

java 复制代码
executor.setRejectedExecutionHandler(new RejectedExecutionHandler() {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 拒绝策略的逻辑
    }
});

2 任务管理

默认日志管理与Logback配置详解

日志的输出内容中一共有7种元素,具体如下:

  1. 时间日期:精确到毫秒
  2. 日志级别:ERROR, WARN, INFO, DEBUG or TRACE
  3. 进程ID
  4. 分隔符:--- 标识实际日志的开始
  5. 线程名:方括号括起来(可能会截断控制台输出)
  6. Logger名:通常使用源代码的类名
  7. 日志内容

开启DEBUG日志

我们可以通过两种方式切换至DEBUG级别:

第一种 :在运行命令后加入--debug标志,如:$ java -jar myapp.jar --debug

第二种 :在配置文件application.properties中配置debug=true

这里开启的DEBUG日志,仅影响核心Logger,包含嵌入式容器、hibernate、spring等这些框架层面的会输出更多内容,但是你自己应用的日志并不会输出为DEBUG级别,从下面的截图中我们就可以看到,我们自己编写的debug级别的Hello World并没有输出。

日志配置

彩色输出
java 复制代码
#日志配置
spring.output.ansi.enabled=ALWAYS
文件输出
java 复制代码
logging.file.name=run.log
logging.file.path=./
文件滚动

一直把日志输出在一个文件里显然是不合适的,任何一个日志框架都会为此准备日志文件的滚动配置。由于本篇将默认配置,所以就是Logback的配置,具体有这几个:

  • logging.logback.rollingpolicy.file-name-pattern:用于创建日志档案的文件名模式。
  • logging.logback.rollingpolicy.clean-history-on-start:应用程序启动时是否对进行日志归档清理,默认为false,不清理
  • logging.logback.rollingpolicy.max-history:要保留的最大归档日志文件数量,默认为7个
  • logging.logback.rollingpolicy.max-file-size:归档前日志文件的最大尺寸,默认为10MB
  • logging.logback.rollingpolicy.total-size-cap:日志档案在被删除前的最大容量,默认为0B
级别控制

如果要对各个Logger做一些简单的输出级别控制,那么只需要在application.properties中进行配置就能完成。

配置格式:logging.level.*=LEVEL

  • logging.level:日志级别控制前缀,*为包名或Logger名
  • LEVEL:选项TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF

举例:

  • logging.level.com.didispace=DEBUGcom.didispace包下所有class以DEBUG级别输出
  • logging.level.root=WARN:root日志以WARN级别输出

3. 邮件功能

依赖

java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-mail</artifactId>
</dependency>
java 复制代码
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTests {

    @Autowired
    private JavaMailSender mailSender;

    @Test
    public void sendSimpleMail() throws Exception {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setFrom("[email protected]");
        message.setTo("[email protected]");
        message.setSubject("主题:简单邮件");
        message.setText("测试邮件内容");

        mailSender.send(message);
    }

}
复制代码
spring.mail.password为授权码

发送附件邮件

java 复制代码
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTests {

    @Autowired
    private JavaMailSender mailSender;

    @Test
    public void sendAttachmentsMail() throws Exception {

        MimeMessage mimeMessage = mailSender.createMimeMessage();

        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        helper.setFrom("XXXXXXXXXXX");
        helper.setTo("XXXXXXXXXXX");
        helper.setSubject("主题:有附件");
        helper.setText("有附件的邮件");

        FileSystemResource file = new FileSystemResource(new File("D:\\Apic\\a\\123.jpeg"));
        helper.addAttachment("附件-1.jpg", file);
        helper.addAttachment("附件-2.jpg", file);

        mailSender.send(mimeMessage);
    }
}

引入图片等静态资源

java 复制代码
helper.setText("<html><body><img src=\"cid:123\" ></body></html>", true);

邮件格式

java 复制代码
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Email Template</title>
</head>
<body>
<h1>Hello, <span th:text="${name}">User</span>!</h1>
<p>This is a test email.</p>
<p>Your verification code is: <strong th:text="${code}">123456</strong></p>
</body>
</html>
java 复制代码
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Application.class)
public class ApplicationTests {

    @Autowired
    private JavaMailSender mailSender;
    @Autowired
    private TemplateEngine templateEngine;

    @Test
    public void sendAttachmentsMail() throws Exception {

        MimeMessage mimeMessage = mailSender.createMimeMessage();

        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
        helper.setFrom("[email protected]");
        helper.setTo("[email protected]");
        helper.setSubject("主题:有附件");

        helper.setText("有附件的邮件");
        Context context = new Context();
        context.setVariable("name", "John Doe"); // 设置模板中的变量
        context.setVariable("code", "654321");  // 设置模板中的变量
        String emailContent = templateEngine.process("email-template.html", context); // 渲染模板

        // 设置邮件正文为渲染后的 HTML 内容
        helper.setText(emailContent, true); // true 表示支持 HTML 内容

        FileSystemResource file = new FileSystemResource(new File("D:\\Apic\\a\\123.jpeg"));
        helper.addAttachment("附件-1.jpg", file);
        helper.addAttachment("附件-2.jpg", file);

        mailSender.send(mimeMessage);
    }
}
相关推荐
Answer_ism1 小时前
【SpringMVC】SpringMVC拦截器,统一异常处理,文件上传与下载
java·开发语言·后端·spring·tomcat
ta叫我小白1 小时前
Spring Boot集成Redis并设置密码后报错: NOAUTH Authentication required
spring boot·redis
-一杯为品-2 小时前
【小项目】四连杆机构的Python运动学求解和MATLAB图形仿真
开发语言·python·matlab
丁总学Java2 小时前
深入解析 Java Stream API:筛选根节点的优雅实现!!!
java·stream·collectors·lambda 表达式
神仙别闹2 小时前
基于Python的垃圾短信分类
java·python·分类
qq_13948428823 小时前
springboot444-基于Vue的网络小说交流平台(源码+数据库+纯前后端分离+部署讲解等)
java·vue.js·spring boot·mysql·spring·maven·intellij-idea
好好学习 6663 小时前
gitlab-ci.yml文件详解
java·ci/cd·gitlab
蝉叫醒了夏天3 小时前
【深度揭秘Maven】
java·maven
脑子慢且灵3 小时前
JavaIO流的使用和修饰器模式(直击心灵版)
java·开发语言·windows·eclipse·intellij-idea·nio
欣然~3 小时前
基于蒙特卡洛方法的网格世界求解
开发语言·python·信息可视化