Spring Boot 20天入门(day11)
Springboot定时与异步任务
Spring Schedule 实现定时任务
1、定制一个scheduled task
我们使用@Scheduled
注解能很方便的创建一个定时任务,下面代码涵盖了@Scheduled
的常见用法。包括:固定速率执行、固定延迟执行、初始延迟执行、使用 Cron 表达式执行定时任务。
Cron 表达式: 主要用于定时作业(定时任务)系统定义执行时间或执行频率的表达式,非常厉害,你可以通过 Cron 表达式进行设置定时任务每天或者每个月什么时候执行等等操作。
推荐一个在线Cron表达式生成器:http://cron.qqe2.com/
@Component
public class ScheduledTask {
private static final Logger log = LoggerFactory.getLogger(ScheduledTask.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* @Method:
* @DATE: 2020/5/31
* @Description: TODO 按固定速率执行,每5秒执行一次
* @Author Weleness
* @param
* @Return
*/
@Scheduled(fixedRate = 5000)
public void reportCurrentTimeWithFixedRate(){
log.info("Current Thread : {}", Thread.currentThread().getName());
log.info("Fixed Rate Task : The time is now {}", dateFormat.format(new Date()));
}
/**
* @Method:
* @DATE: 2020/5/31
* @Description: TODO 固定延迟执行,距离上次执行成功后2秒执行
* @Author Weleness
* @param
* @Return
*/
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay(){
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* @Method:
* @DATE: 2020/5/31
* @Description: TODO initialDelay:初始延迟。任务的第一次执行将延迟5秒,然后将以5秒的固定间隔执行。
* @Author Weleness
* @param
* @Return
*/
@Scheduled(initialDelay = 5000,fixedRate = 5000)
public void reportCurrentTimeWithInitialDelay(){
log.info("Fixed Rate Task with Initial Delay : The time is now {}",dateFormat.format(new Date()));
}
/**
* @Method:
* @DATE: 2020/5/31
* @Description: TODO cron:使用Cron表达式。 每分钟的1,2秒运行
* @Author Weleness
* @param
* @Return
*/
@Scheduled(cron = "1-2 * * * * ? ")
public void reportCurrentTimeWithCronExpression(){
log.info("Cron Expression:The time is now {}",dateFormat.format(new Date()));
}
}
2、加上@EnableScheduling注解
@EnableScheduling // 开启springboot对定时任务的支持
@SpringBootApplication
public class Anhtom2000Application {
public static void main(String[] args) {
SpringApplication.run(Anhtom2000Application.class, args);
}
}
3、自定义线程池创建Scheduled task
需要写一个配置类,重写方法将我们自定义的线程池配置进去
/**
* @Description : TODO 自定义异步任务线程池
* @Author : Weleness
* @Date : 2020/05/31
*/
@SpringBootConfiguration
public class SchedulerConfig implements SchedulingConfigurer {
private final int POOL_SIZE=10;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 需要注意的是,配置的线程池必须是指定的线程池
ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
threadPoolTaskScheduler.setPoolSize(POOL_SIZE);
threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
threadPoolTaskScheduler.initialize();
taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
}
}
4、@EnableAsync和@Async使定时任务并行执行
/**
* @Description : TODO
* @Author : Weleness
* @Date : 2020/05/31
*/
@Component
@EnableAsync
public class AsyncScheduledTasks {
private static final Logger log = LoggerFactory.getLogger(AsyncScheduledTasks.class);
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
/**
* @param
* @Method:
* @DATE: 2020/5/31
* @Description: TODO fixedDelay:固定延迟执行。距离上一次调用成功后2秒才执。
* @Author Weleness
* @Return
*/
@Async
@Scheduled(fixedDelay = 2000)
public void reportCurrentTimeWithFixedDelay() {
try {
TimeUnit.SECONDS.sleep(3);
log.info("Fixed Delay Task : The time is now {}", dateFormat.format(new Date()));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Spirngboot 异步任务
Future模式
异步编程再处理耗时操作以及多线程处理的场景下非常有用,我们可以更好的让我们的系统利用好机器的CPU和内存,提高他们的利用率。多线程设计模式有很多种,Future模式是多线程开发中非常常见的一种设计模式。
Future模式的核心思想
Future模式的核心思想是异步调用。当我们执行一个方式时,假设这个方法中有多个耗时的任务需要同时去做,而且又不着急等待这个结果返回,那么我们可以立即返回给客户端,再让后台慢慢的去计算任务,当然也可以等这些任务都执行完了,再返回给客户端。
Springboot使用异步编程
两个核心注解
1、@EnableAsync
:通过再配置类或启动类上加**@EnableAsync`**注解开启对异步方法的支持。
2、@Async
:标注在类上或者方法上。标注在类上表示这个类的所有方法都是异步方法。
自定义TaskExecutor
他是线程的执行者,用来启动线程的执行者接口。Spring 提供了TaskExecutor
接口作为任务执行者的抽象,它和java.util.concurrent
包下的Executor
接口很像。稍微不同的 TaskExecutor
接口用到了 Java 8 的语法@FunctionalInterface
声明这个接口是一个函数式接口。
@FunctionalInterface
public interface TaskExecutor extends Executor {
/**
* Execute the given {@code task}.
* <p>The call might return immediately if the implementation uses
* an asynchronous execution strategy, or might block in the case
* of synchronous execution.
* @param task the {@code Runnable} to execute (never {@code null})
* @throws TaskRejectedException if the given task was not accepted
*/
@Override
void execute(Runnable task);
}
如果没有自定义Executor,Spring将创建一个SimpleAsyncTaskExecutor
并使用它
@EnableAsync
@SpringBootConfiguration
public class AsyncConfig implements AsyncConfigurer {
private static final int CORE_POOL_SIZE = 6;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
@Bean
public Executor taskExecutor(){
// Spring 默认配置是核心线程数大小为1,最大线程容量大小不受限制,队列容量也不受限制。
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(CORE_POOL_SIZE);
// 最大线程数
executor.setMaxPoolSize(MAX_POOL_SIZE);
// 队列大小
executor.setQueueCapacity(QUEUE_CAPACITY);
// 当最大池已满时,此策略保证不会丢失任务请求,但是可能会影响应用程序整体性能。
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setThreadNamePrefix("My ThreadPoolTaskExecutor-");
executor.initialize();
return executor;
}
}
ThreadPollTaskExecutor
常见概念:
- **Core Pool Size:**核心线程数,定义了最小可以同时运行的线程数量。
- **Queue Capacity:**当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,是就添加信任并存放在队列中。
- Maximum Pool Size : 可同时运行线程的最大数量。
思考:如果队列以慢并且当前同时运行线程数达到最大线程数时,如果再有新任务过来会发生什么?
Spring默认使用的是ThreadPoolExecutor.AbortPolicy
策略。在默认情况下,ThreadPoolExecutor
将抛出 RejectedExecutionException
来拒绝新来的任务 ,这意味着你将丢失这个任务的处理。对于可伸缩的应用程序,建议使用ThreadPoolExecutor.CallerRunsPolicy
,当线程数达到最大时,为我们提供一个可伸缩的队列。
ThreadPoolTaskExecutor
饱和策略:
如果当前同时运行的线程数量达到最大线程数量时,ThreadPoolTaskExecutor
定义一些策略:
- ThreadPoolExecutor.AbortPolicy:抛出
RejectedExecutionException
来拒绝新任务的处理。 - ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
- ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
- ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
编写一个异步方法
@Service
@Slf4j // lombok的一个注解,标注上就可以直接使用log日志,里面封装了slf4j的实现
public class AsyncService {
private List<String> movies =
new ArrayList<>(
Arrays.asList(
"Forrest Gump",
"Titanic",
"Spirited Away",
"The Shawshank Redemption",
"Zootopia",
"Farewell ",
"Joker",
"Crawl"));
/** 示范使用:找到特定字符/字符串开头的电影 */
@Async
public CompletableFuture<List<String>> completableFutureTask(String start) {
// 打印日志
log.warn(Thread.currentThread().getName() + "start this task!");
// 找到特定字符/字符串开头的电影
List<String> results =
movies.stream().filter(movie -> movie.startsWith(start)).collect(Collectors.toList());
// 模拟这是一个耗时的任务
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
//返回一个已经用给定值完成的新的CompletableFuture。
return CompletableFuture.completedFuture(results);
}
}
测试
启动项目,访问接口
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-4] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-4start this task!
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-5] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-5start this task!
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-1] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-1start this task!
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-6] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-6start this task!
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-2] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-2start this task!
2020-06-04 12:14:11.966 WARN 4216 --- [lTaskExecutor-3] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-3start this task!
Elapsed time: 1043
首先我们可以看到处理所有任务花费的时间大概是 1 s。这与我们自定义的 ThreadPoolTaskExecutor
有关,我们配置的核心线程数是 6 ,然后通过通过下面的代码模拟分配了 6 个任务给系统执行。这样每个线程都会被分配到一个任务,每个任务执行花费时间是 1 s ,所以处理 6 个任务的总花费时间是 1 s。
从上面的运行结果可以看出,当所有任务执行完成之后才返回结果。这种情况对应于我们需要返回结果给客户端请求的情况下,假如我们不需要返回任务执行结果给客户端的话呢? 就比如我们上传一个大文件到系统,上传之后只要大文件格式符合要求我们就上传成功。普通情况下我们需要等待文件上传完毕再返回给用户消息,但是这样会很慢。采用异步的话,当用户上传之后就立马返回给用户消息,然后系统再默默去处理上传任务。这样也会增加一点麻烦,因为文件可能会上传失败,所以系统也需要一点机制来补偿这个问题,比如当上传遇到问题的时候,发消息通知用户。
下面演示一下立即返回的情况
将异步任务的返回值改为void
@Async
public void completableFutureTask2(String start) {
// 打印日志
log.warn(Thread.currentThread().getName() + "start this task!");
// 找到特定字符/字符串开头的电影
List<String> results =
movies.stream().filter(movie -> movie.startsWith(start)).collect(Collectors.toList());
// 模拟这是一个耗时的任务
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
修改controller的方法
@GetMapping("/movies2")
public String completableFutureTask2() throws ExecutionException, InterruptedException {
// Start the clock
long start = System.currentTimeMillis();
// Kick of multiple, asynchronous lookups
List<String> words = Arrays.asList("F", "T", "S", "Z", "J", "C");
words.stream()
.forEach(word -> asyncService.completableFutureTask2(word));
// Wait until they are all done
// Print results, including elapsed time
System.out.println("Elapsed time: " + (System.currentTimeMillis() - start));
return "Done";
}
访问接口
我们看到系统理解返回结果,然后再启动线程执行任务
Elapsed time: 5
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-3] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-3start this task!
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-2] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-2start this task!
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-6] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-6start this task!
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-1] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-1start this task!
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-4] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-4start this task!
2020-06-04 13:40:52.698 WARN 3368 --- [lTaskExecutor-5] c.g.anhtom2000.service.AsyncService : My ThreadPoolTaskExecutor-5start this task!
Springboot与安全
Springsecurity
什么是Springsecurity
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Springboot整合Springsecurity
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
简单的整合
要整合Springsecurity,我们需要重写一个Springsecurity的配置类
// 开启SpringSecurity功能。这个注解也包含了@configuration注解
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
// 配置路径
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll() // 首页,所有人都可以访问
.antMatchers("/user/**").hasRole("USER") // user下的所有接口,需要有USER权限
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/user") // 登陆页面的接口,登陆成功跳转到/user接口
.and()
.logout(); // 用户注销后跳转的配置,默认是/loginout
}
/**
* 在内存中创建一个名为 "weleness" 的用户,密码为 "weleness",拥有 "USER" 权限
*/
@Bean
@Override
protected UserDetailsService userDetailsService() {
User.UserBuilder user= User.withDefaultPasswordEncoder();
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
// 这里的password在springsecurity5中需要有一个密码加密器来进行加密(不允许存储明文密码)
manager.createUser(user.username("weleness").password("weleness").roles("USER").build());
return manager;
}
}
controller
@Controller
public class HomeController {
@GetMapping({"/","/home","/index"})
public String root(){return "index";}
@GetMapping("/login")
public String login(){
return "login";
}
}
@Controller
public class UserController {
@GetMapping("/user")
public String user(@AuthenticationPrincipal Principal principal, Model model){
model.addAttribute("username", principal.getName());
return "user/user";
}
}
页面效果
首页
页面参考的是 http://www.spring4all.com/article/428/,侵删。
登陆
输入账号和密码后,点击登陆
来到user页面
与数据库的整合
这次引入了jpa,配置文件如下
server:
port: 8798
spring:
thymeleaf:
cache: false
datasource:
url: jdbc:mysql://localhost:3306/security00?serverTimezone=UTC
username: root
password: 8761797
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
配置类
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
private DbUserDetailsService dbUserDetailsService;
@Autowired
public void setAnyUserDetailsService(DbUserDetailsService dbUserDetailsService){
this.dbUserDetailsService = dbUserDetailsService;
}
/**
* 匹配 "/" 路径,不需要权限即可访问
* 匹配 "/user" 及其以下所有路径,都需要 "USER" 权限
* 登录地址为 "/login",登录成功默认跳转到页面 "/user"
* 退出登录的地址为 "/logout",退出成功后跳转到页面 "/login"
* 默认启用 CSRF
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/user/**").hasAuthority("USER")
.and()
.formLogin().loginPage("/login").defaultSuccessUrl("/user")
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login");
}
/**
* 添加 UserDetailsService, 实现自定义登录校验
*/
@Override
protected void configure(AuthenticationManagerBuilder builder) throws Exception{
builder.userDetailsService(dbUserDetailsService);
}
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}
自定义登陆服务
// 当我们点击登陆后,springsecurity会获得表单提交的username和password字段,先从loadUserByUsername方法获得一个 UserDetails,然后再进行校验
@Service
public class DbUserDetailsService implements UserDetailsService {
private final UserService userService;
DbUserDetailsService(UserService userService){
this.userService = userService;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println(username);
UserEntity userDO = userService.getByUserName(username);
if (userDO == null){
throw new UsernameNotFoundException("用户不存在!");
}
List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
simpleGrantedAuthorities.add(new SimpleGrantedAuthority("USER"));
return new org.springframework.security.core.userdetails.User(userDO.getUsername(), userDO.getPassword(), simpleGrantedAuthorities);
}
}
用户服务
public interface UserService {
/**
* 添加新用户
*
* username 唯一, 默认 USER 权限
* @param userEntity
*/
void insert(UserEntity userEntity);
/**
* 查询用户信息
* @param username
* @return UserEntity
*/
UserEntity getByUserName(String username);
}
测试
注册一个测试账号
登陆