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);
}
測試
註冊一個測試賬號
登陸