後期更新內容移步到我的網站:www.upheart.top/javascript
Spring Session 就是使用 Spring 中的代理過濾器,將全部的 Session 操做攔截下來,自動的將數據 同步到 Redis 中,或者自動的從 Redis 中讀取數據php
對於開發者來講,全部關於 Session 同步的操做都是透明的,開發者使用 Spring Session,一旦配置完成後,具體的用法就像使用一個普通的 Session 同樣html
首先咱們須要有一個 https 證書,咱們能夠從各個雲服務廠商處申請一個免費的,不過本身作實驗沒有必要這麼麻煩,咱們能夠直接藉助 Java 自帶的 JDK 管理工具 keytool 來生成一個免費的 https 證書前端
進入到 %JAVVA_HOME%\bin
目錄下,執行以下命令生成一個數字證書:java
keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore D:\javaboy.p12 -validity 365
複製代碼
命令含義以下:web
命令執行完成後 ,咱們在 D 盤目錄下會看到一個名爲 javaboy.p12 的文件ajax
接下來咱們須要在項目中引入 httpsredis
將上面生成的 javaboy.p12 拷貝到 Spring Boot 項目的 resources 目錄下。而後在 application.properties 中添加以下配置:算法
server.ssl.key-store=classpath:javaboy.p12
server.ssl.key-alias=tomcathttps
server.ssl.key-store-password=111111
複製代碼
其中:spring
配置完成後,就能夠啓動 Spring Boot 項目了,使用https訪問
請求轉發
考慮到 Spring Boot 不支持同時啓動 HTTP 和 HTTPS ,爲了解決這個問題,咱們這裏能夠配置一個請求轉發,當用戶發起 HTTP 調用時,自動轉發到 HTTPS 上
具體配置以下:
@Configuration
public class TomcatConfig {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory(){
@Override
protected void postProcessContext(Context context) {
SecurityConstraint constraint = new SecurityConstraint();
constraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
constraint.addCollection(collection);
context.addConstraint(constraint);
}
};
factory.addAdditionalTomcatConnectors(createTomcatConnector());
return factory;
}
private Connector createTomcatConnector() {
Connector connector = new
Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(8081);
connector.setSecure(false);
connector.setRedirectPort(8080);
return connector;
}
}
複製代碼
在這裏,咱們配置了 Http 的請求端口爲 8081,全部來自 8081 的請求,將被自動重定向到 8080 這個 https 的端口上
如此以後,咱們再去訪問 http 請求,就會自動重定向到 https
Spring Boot 中的熱部署相信你們都用過吧,只須要添加 spring-boot-devtools
依賴就能夠輕鬆實現熱部署Spring Boot 中熱部署最最關鍵的原理就是兩個不一樣的 classloader:
實現修改Thymleaf不須要重啓服務器
<!-- 熱部署 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
複製代碼
在application.propeties中添加spring.thymeleaf.cache = false
複製代碼
IDAE中的設置:
File –> Setting –> Build, Execution, Deployment –> Compiler –> check this Build project automatically
Help---->Find Action打開一個小窗口後輸入Registry Find and check this option compiler.automake.allow.when.app.running
從新編譯類文件:
當咱們修改了一個java類的時候,咱們只須要從新編譯一下,SpringBoot的就會重啓了。由於devtools會監聽classpath下的文件變更,因此當java類從新編譯的時候,devtools會監聽到這個變化,而後就會自動從新啓動SpringBoot。這個重啓是很是快的一個過程。由於在SpringBoot中有兩個類加載器,一個是加載工程外部資源的,如jar包,還有一個類加載器是用來加載本工程的class的。因此在重啓SpringBoot的時候只加載本工程的class文件
監聽文件夾的變化:
若是你不想從新編譯java類的話,還有一種方式用來讓SpringBoot重啓,那就是讓devtools監聽文件夾的變化:好比咱們想讓com.zkn.learnspringboot這個文件夾下的文件改變的時候,自動從新啓動SpringBoot,那麼咱們只要在application.properties中添加這樣一句話就好了:spring.devtools.restart.additional-paths=com\zkn\learnspringboot
頁面模板改變ctrl+F9能夠從新編譯當前頁面並生效
# 熱部署生效
spring.devtools.restart.enabled=true
# 關閉緩存,即時刷新,生產環境需改成true
#spring.freemarker.cache=false
spring.thymeleaf.cache=false
# 重啓的目錄,添加那個目錄下的文件須要重啓
spring.devtools.restart.additional-paths=boot-start/src/main/java
# classpath目錄下的WEB-INF文件夾內容修改後不重啓
#spring.devtools.restart.exclude=WEB-INF/**,static/**,public/**
複製代碼
修改靜態資源必定要重啓項目纔會生效嗎?未必!
其中 base classloader 用來加載那些不會變化的類,例如各類第三方依賴,而 restart classloader 則用來加載那些會發生變化的類,例如你本身寫的代碼。Spring Boot 中熱部署的原理就是當代碼發生變化時,base classloader 不變,而 restart classloader 則會被廢棄,被另外一個新的 restart classloader 代替。在整個過程當中,由於只從新加載了變化的類,因此啓動速度要被重啓快
可是有另一個問題,就是靜態資源文件!使用 devtools ,默認狀況下當靜態資源發生變化時,並不會觸發項目重啓。雖然咱們能夠經過配置解決這一問題,可是沒有必要!由於靜態資源文件發生變化後不須要編譯,按理說保存後刷新下就能夠訪問到了
那麼如何才能實現靜態資源變化後,不編譯就能自動刷新呢? LiveReload 能夠幫助咱們實現這一功能!
devtools 中默認嵌入了 LiveReload 服務器,利用 LiveReload 能夠實現靜態文件的熱部署,LiveReload 能夠在資源發生變化時自動觸發瀏覽器更新,LiveReload 支持 Chrome、Firefox 以及 Safari 。以 Chrome 爲例,在 Chrome 應用商店搜索 LiveReload
在瀏覽器中打開項目的頁面,而後點擊瀏覽器右上角的 LiveReload 按鈕,打開 LiveReload 鏈接
LiveReload 是和瀏覽器選項卡綁定在一塊兒的,在哪一個選項卡中打開了 LiveReload,就在哪一個選項卡中訪問頁面,這樣纔有效果
打開 LiveReload 以後,咱們啓動一個加了 devtools 依賴的 Spring Boot 項目:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
複製代碼
此時隨便在 resources/static 目錄下添加一個靜態 html 頁面,而後啓動 Spring Boot 項目,在打開了 LiveReload 的選項卡中訪問 html 頁面
訪問成功後,咱們再去手動修改 html 頁面代碼,修改爲功後,回到瀏覽器,不用作任何操做,就會發現瀏覽器自動刷新了,頁面已經更新了
整個過程當中,個人 Spring Boot 項目並無重啓
若是開發者安裝而且啓動了 LiveReload 插件,同時也添加了 devtools 依賴,可是卻並不想當靜態頁面發生變化時瀏覽器自動刷新,那麼能夠在 application.properties 中添加以下代碼進行配置:
spring.devtools.livereload.enabled=false
複製代碼
建議開發者使用 LiveReload 策略而不是項目重啓策略來實現靜態資源的動態加載,由於項目重啓所耗費時間通常來講要超過使用LiveReload 所耗費的時間
Firefox 也能夠安裝 LiveReload 插件,裝好以後和 Chrome 用法基本一致
在 Spring Boot 中,默認狀況下,一共有5個位置能夠放靜態資源,五個路徑分別是以下5個:
classpath:/META-INF/resources/
classpath:/resources/
classpath:/static/
classpath:/public/
/
複製代碼
這裏第5個 /
其實就是表示 webapp 目錄中的靜態資源也不被攔截。若是同一個文件分別出如今五個目錄下,那麼優先級也是按照上面列出的順序。
不過,雖然有5個存儲目錄,除了第5個用的比較少以外,其餘四個,系統默認建立了 classpath:/static/
, 正常狀況下,咱們只須要將咱們的靜態資源放到這個目錄下便可,也不須要額外去建立其餘靜態資源目錄,例如我在 classpath:/static/
目錄下放了一張名爲1.png 的圖片,那麼個人訪問路徑是:
http://localhost:8080/1.png
複製代碼
固然,這個是系統默認配置,若是咱們並不想將資源放在系統默認的這五個位置上,也能夠自定義靜態資源位置和映射,自定義的方式也有兩種,能夠經過 application.properties 來定義,也能夠在 Java 代碼中來定義,下面分別來看。
application.properties
在配置文件中定義的方式比較簡單,以下:
spring.resources.static-locations=classpath:/
spring.mvc.static-path-pattern=/**
複製代碼
第一行配置表示定義資源位置,第二行配置表示定義請求 URL 規則。以上文的配置爲例,若是咱們這樣定義了,表示能夠將靜態資源放在 resources目錄下的任意地方,咱們訪問的時候固然也須要寫完整的路徑,例如在resources/static目錄下有一張名爲1.png 的圖片,那麼訪問路徑就是 http://localhost:8080/static/1.png
,注意此時的static不能省略。
Java 代碼定義
固然,在Spring Boot中咱們也能夠經過 Java代碼來自定義,方式和 Java 配置的 SSM 比較相似,以下:
@Configuration
public class WebMVCConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**").addResourceLocations("classpath:/aaa/");
}
}
複製代碼
只須要在Spring Boot工程的/src/main/resources
目錄下建立一個banner.txt
文件,而後將ASCII字符畫複製進去,就能替換默認的banner了
一些屬性設置:
${AnsiColor.BRIGHT_RED}
:設置控制檯中輸出內容的顏色${application.version}
:用來獲取MANIFEST.MF
文件中的版本號${application.formatted-version}
:格式化後的${application.version}
版本信息${spring-boot.version}
:Spring Boot的版本號${spring-boot.formatted-version}
:格式化後的${spring-boot.version}
版本信息生成工具
咱們能夠藉助下面這些工具,輕鬆地根據文字或圖片來生成用於Banner輸出的字符畫
同源策略
同源策略是由Netscape提出的一個著名的安全策略,它是瀏覽器最核心也最基本的安全功能,如今全部支持JavaScript的瀏覽器都會使用這個策略。所謂同源是指協議、域名以及端口要相同
同源策略是基於安全方面的考慮提出來的,這個策略自己沒問題,可是咱們在實際開發中,因爲各類緣由又常常有跨域的需求,傳統的跨域方案是JSONP,JSONP雖然能解決跨域可是有一個很大的侷限性,那就是隻支持GET請求,不支持其餘類型的請求,而今天咱們說的CORS(跨域源資源共享)(CORS,Cross-origin resource sharing)是一個W3C標準,它是一份瀏覽器技術的規範,提供了Web服務從不一樣網域傳來沙盒腳本的方法,以避開瀏覽器的同源策略,這是JSONP模式的現代版
在Spring框架中,對於CORS也提供了相應的解決方案,咱們就來看看SpringBoot中如何實現CORS
首先建立兩個普通的SpringBoot項目,這個就不用我多說,第一個命名爲provider提供服務,第二個命名爲consumer消費服務,第一個配置端口爲8080,第二個配置配置爲8081,而後在provider上提供兩個hello接口,一個get,一個post,以下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@PostMapping("/hello")
public String hello2() {
return "post hello";
}
}
複製代碼
在consumer的resources/static目錄下建立一個html文件,發送一個簡單的ajax請求,以下:
<div id="app"></div>
<input type="button" onclick="btnClick()" value="get_button">
<input type="button" onclick="btnClick2()" value="post_button">
<script> function btnClick() { $.get('http://localhost:8080/hello', function (msg) { $("#app").html(msg); }); } function btnClick2() { $.post('http://localhost:8080/hello', function (msg) { $("#app").html(msg); }); } </script>
複製代碼
而後分別啓動兩個項目,發送請求按鈕,觀察瀏覽器控制檯以下:
Access to XMLHttpRequest at 'http://localhost:8080/hello' from origin 'http://localhost:8081' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
複製代碼
能夠看到,因爲同源策略的限制,請求沒法發送成功
使用CORS能夠在前端代碼不作任何修改的狀況下,實現跨域,那麼接下來看看在provider中如何配置。首先能夠經過@CrossOrigin註解配置某一個方法接受某一個域的請求,以下:
@RestController
public class HelloController {
@CrossOrigin(value = "http://localhost:8081")
@GetMapping("/hello")
public String hello() {
return "hello";
}
@CrossOrigin(value = "http://localhost:8081")
@PostMapping("/hello")
public String hello2() {
return "post hello";
}
}
複製代碼
此時觀察瀏覽器請求網絡控制檯,能夠看到響應頭中多了以下信息:
這個表示服務端願意接收來自http://localhost:8081的請求,拿到這個信息後,瀏覽器就不會再去限制本次請求的跨域了
provider上,每個方法上都去加註解未免太麻煩了,在Spring Boot中,還能夠經過全局配置一次性解決這個問題,全局配置只須要在配置類中重寫addCorsMappings方法便可,以下:
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:8081")
.allowedMethods("*")
.allowedHeaders("*");
}
}
複製代碼
/**
表示本應用的全部方法都會去處理跨域請求,allowedMethods表示容許經過的請求數,allowedHeaders則表示容許的請求頭。通過這樣的配置以後,就沒必要在每一個方法上單獨配置跨域了
存在的問題
瞭解了整個CORS的工做過程以後,咱們經過Ajax發送跨域請求,雖然用戶體驗提升了,可是也有潛在的威脅存在,常見的就是CSRF(Cross-site request forgery)跨站請求僞造。跨站請求僞造也被稱爲one-click attack 或者 session riding,一般縮寫爲CSRF或者XSRF,是一種挾制用戶在當前已登陸的Web應用程序上執行非本意的操做的攻擊方法,舉個例子:
假如一家銀行用以運行轉帳操做的URL地址以下:
http://icbc.com/aa?bb=cc
,那麼,一個惡意攻擊者能夠在另外一個網站上放置以下代碼:<img src="http://icbc.com/aa?bb=cc">
,若是用戶訪問了惡意站點,而她以前剛訪問過銀行不久,登陸信息還沒有過時,那麼她就會遭受損失。
基於此,瀏覽器在實際操做中,會對請求進行分類,分爲簡單請求,預先請求,帶憑證的請求等,預先請求會首先發送一個options探測請求,和瀏覽器進行協商是否接受請求。默認狀況下跨域請求是不須要憑證的,可是服務端能夠配置要求客戶端提供憑證,這樣就能夠有效避免csrf攻擊
假設從 aaa@qq.com 發送郵件到 111@163.com :
SMTP 協議全稱爲 Simple Mail Transfer Protocol,譯做簡單郵件傳輸協議,它定義了郵件客戶端軟件與 SMTP 服務器之間,以及 SMTP 服務器與 SMTP 服務器之間的通訊規則
也就是說 aaa@qq.com 用戶先將郵件投遞到騰訊的 SMTP 服務器這個過程就使用了 SMTP 協議,而後騰訊的 SMTP 服務器將郵件投遞到網易的 SMTP 服務器這個過程也依然使用了 SMTP 協議,SMTP 服務器就是用來收郵件
而 POP3 協議全稱爲 Post Office Protocol ,譯做郵局協議,它定義了郵件客戶端與 POP3 服務器之間的通訊規則,那麼該協議在什麼場景下會用到呢?當郵件到達網易的 SMTP 服務器以後, 111@163.com 用戶須要登陸服務器查看郵件,這個時候就該協議就用上了:郵件服務商都會爲每個用戶提供專門的郵件存儲空間,SMTP 服務器收到郵件以後,就將郵件保存到相應用戶的郵件存儲空間中,若是用戶要讀取郵件,就須要經過郵件服務商的 POP3 郵件服務器來完成
最後,可能也有小夥伴們據說過 IMAP 協議,這個協議是對 POP3 協議的擴展,功能更強,做用相似
目前國內大部分的郵件服務商都不容許直接使用用戶名/密碼的方式來在代碼中發送郵件,都是要先申請受權碼,這裏以 QQ 郵箱爲例,向你們演示受權碼的申請流程:首先咱們須要先登陸 QQ 郵箱網頁版,點擊上方的設置按鈕,而後點擊帳戶選項卡,在帳戶選項卡中找到開啓POP3/SMTP選項,點擊開啓,開啓相關功能,開啓過程須要手機號碼驗證,按照步驟操做便可,不贅述。開啓成功以後,便可獲取一個受權碼,將該號碼保存好,一會使用
接下來,咱們就能夠建立項目了,Spring Boot 中,對於郵件發送提供了自動配置類,開發者只須要加入相關依賴,而後配置一下郵箱的基本信息,就能夠發送郵件了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
複製代碼
項目建立成功後,接下來在 application.properties 中配置郵箱的基本信息:
spring.mail.host=smtp.qq.com
spring.mail.port=587
spring.mail.username=1510161612@qq.com
spring.mail.password=ubknfzhjkhrbbabe
spring.mail.default-encoding=UTF-8
spring.mail.properties.mail.smtp.socketFactoryClass=javax.net.ssl.SSLSocketFactory
spring.mail.properties.mail.debug=true
複製代碼
配置含義分別以下:
若是不知道 smtp 服務器的端口或者地址的的話,能夠參考 騰訊的郵箱文檔
作完這些以後,Spring Boot 就會自動幫咱們配置好郵件發送類,相關的配置在 org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration
類中,部分源碼以下:
@Configuration
@ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
@ConditionalOnMissingBean(MailSender.class)
@Conditional(MailSenderCondition.class)
@EnableConfigurationProperties(MailProperties.class)
@Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
public class MailSenderAutoConfiguration {
}
複製代碼
從這段代碼中,能夠看到,導入了另一個配置 MailSenderPropertiesConfiguration
類,這個類中,提供了郵件發送相關的工具類:
@Configuration
@ConditionalOnProperty(prefix = "spring.mail", name = "host")
class MailSenderPropertiesConfiguration {
private final MailProperties properties;
MailSenderPropertiesConfiguration(MailProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public JavaMailSenderImpl mailSender() {
JavaMailSenderImpl sender = new JavaMailSenderImpl();
applyProperties(sender);
return sender;
}
}
複製代碼
能夠看到,這裏建立了一個 JavaMailSenderImpl
的實例, JavaMailSenderImpl
是 JavaMailSender
的一個實現,咱們將使用 JavaMailSenderImpl
來完成郵件的發送工做
作完如上兩步,郵件發送的準備工做就算是完成了,接下來就能夠直接發送郵件了
具體的發送,有 5 種不一樣的方式,咱們一個一個來看
簡單郵件就是指郵件內容是一個普通的文本文檔:
@Autowired
JavaMailSender javaMailSender;
@Test
public void sendSimpleMail() {
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("這是一封測試郵件");
message.setFrom("1510161612@qq.com");
message.setTo("25xxxxx755@qq.com");
message.setCc("37xxxxx37@qq.com");
message.setBcc("14xxxxx098@qq.com");
message.setSentDate(new Date());
message.setText("這是測試郵件的正文");
javaMailSender.send(message);
}
複製代碼
從上往下,代碼含義分別以下:
郵件的附件能夠是圖片,也能夠是普通文件,都是支持的
@Test
public void sendAttachFileMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
helper.setSubject("這是一封測試郵件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
helper.setText("這是測試郵件的正文");
helper.addAttachment("javaboy.jpg",new File("C:\\Users\\sang\\Downloads\\javaboy.png"));
javaMailSender.send(mimeMessage);
}
複製代碼
注意這裏在構建郵件對象上和前文有所差別,這裏是經過 javaMailSender 來獲取一個複雜郵件對象,而後再利用 MimeMessageHelper 對郵件進行配置,MimeMessageHelper 是一個郵件配置的輔助工具類,建立時候的 true 表示構建一個 multipart message 類型的郵件,有了 MimeMessageHelper 以後,咱們針對郵件的配置都是由 MimeMessageHelper 來代勞
最後經過 addAttachment 方法來添加一個附件
圖片資源和附件有什麼區別呢?圖片資源是放在郵件正文中的,即一打開郵件,就能看到圖片。可是通常來講,不建議使用這種方式,一些公司會對郵件內容的大小有限制(由於這種方式是將圖片一塊兒發送的)
@Test
public void sendImgResMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("這是一封測試郵件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
helper.setText("<p>hello 你們好,這是一封測試郵件,這封郵件包含兩種圖片,分別以下</p><p>第一張圖片:</p><img src='cid:p01'/><p>第二張圖片:</p><img src='cid:p02'/>",true);
helper.addInline("p01",new FileSystemResource(new File("C:\\Users\\sang\\Downloads\\javaboy.png")));
helper.addInline("p02",new FileSystemResource(new File("C:\\Users\\sang\\Downloads\\javaboy2.png")));
javaMailSender.send(mimeMessage);
}
複製代碼
這裏的郵件 text 是一個 HTML 文本,裏邊涉及到的圖片資源先用一個佔位符佔着,setText 方法的第二個參數 true 表示第一個參數是一個 HTML 文本
setText 以後,再經過 addInline 方法來添加圖片資源
首先須要引入 Freemarker 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
複製代碼
而後在 resources/templates
目錄下建立一個 mail.ftl
做爲郵件發送模板:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>hello 歡迎加入 xxx 你們庭,您的入職信息以下:</p>
<table border="1">
<tr>
<td>姓名</td>
<td>${username}</td>
</tr>
<tr>
<td>工號</td>
<td>${num}</td>
</tr>
<tr>
<td>薪水</td>
<td>${salary}</td>
</tr>
</table>
<div style="color: #ff1a0e">一塊兒努力創造輝煌</div>
</body>
</html>
複製代碼
接下來,將郵件模板渲染成 HTML ,而後發送便可
@Test
public void sendFreemarkerMail() throws MessagingException, IOException, TemplateException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("這是一封測試郵件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
//構建 Freemarker 的基本配置
Configuration configuration = new Configuration(Configuration.VERSION_2_3_0);
// 配置模板位置
ClassLoader loader = MailApplication.class.getClassLoader();
configuration.setClassLoaderForTemplateLoading(loader, "templates");
//加載模板
Template template = configuration.getTemplate("mail.ftl");
User user = new User();
user.setUsername("javaboy");
user.setNum(1);
user.setSalary((double) 99999);
StringWriter out = new StringWriter();
//模板渲染,渲染的結果將被保存到 out 中 ,將out 中的 html 字符串發送便可
template.process(user, out);
helper.setText(out.toString(),true);
javaMailSender.send(mimeMessage);
}
複製代碼
須要注意的是,雖然引入了 Freemarker
的自動化配置,可是咱們在這裏是直接 new Configuration
來從新配置 Freemarker
的,因此 Freemarker 默認的配置這裏不生效,所以,在填寫模板位置時,值爲 templates
推薦在 Spring Boot 中使用 Thymeleaf 來構建郵件模板。由於 Thymeleaf 的自動化配置提供了一個 TemplateEngine,經過 TemplateEngine 能夠方便的將 Thymeleaf 模板渲染爲 HTML ,同時,Thymeleaf 的自動化配置在這裏是繼續有效的
首先,引入 Thymeleaf 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
複製代碼
而後,建立 Thymeleaf
郵件模板:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>hello 歡迎加入 xxx 你們庭,您的入職信息以下:</p>
<table border="1">
<tr>
<td>姓名</td>
<td th:text="${username}"></td>
</tr>
<tr>
<td>工號</td>
<td th:text="${num}"></td>
</tr>
<tr>
<td>薪水</td>
<td th:text="${salary}"></td>
</tr>
</table>
<div style="color: #ff1a0e">一塊兒努力創造輝煌</div>
</body>
</html>
複製代碼
接下來發送郵件:
@Autowired
TemplateEngine templateEngine;
@Test
public void sendThymeleafMail() throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("這是一封測試郵件");
helper.setFrom("1510161612@qq.com");
helper.setTo("25xxxxx755@qq.com");
helper.setCc("37xxxxx37@qq.com");
helper.setBcc("14xxxxx098@qq.com");
helper.setSentDate(new Date());
Context context = new Context();
context.setVariable("username", "javaboy");
context.setVariable("num","000001");
context.setVariable("salary", "99999");
String process = templateEngine.process("mail.html", context);
helper.setText(process,true);
javaMailSender.send(mimeMessage);
}
複製代碼
Spring Boot 打包成的可執行 jar ,爲何不能被其餘項目依賴?
Spring Boot 中默認打包成的 jar 叫作 可執行 jar,這種 jar 不一樣於普通的 jar,普通的 jar 不能夠經過 java -jar xxx.jar
命令執行,普通的 jar
主要是被其餘應用依賴,Spring Boot
打成的 jar
能夠執行,可是不能夠被其餘的應用所依賴,即便強制依賴,也沒法獲取裏邊的類。可是可執行 jar 並非 Spring Boot 獨有的,Java 工程自己就能夠打包成可執行 jar
有的小夥伴可能就有疑問了,既然一樣是執行 mvn package
命令進行項目打包,爲何 Spring Boot 項目就打成了可執行 jar ,而普通項目則打包成了不可執行 jar 呢?
這咱們就不得不提 Spring Boot 項目中一個默認的插件配置 spring-boot-maven-plugin
這個打包插件存在 5 個方面的功能
五個功能分別是:
mvn package
執行以後,這個命令再次打包生成可執行的 jar,同時將 mvn package
生成的 jar 重命名爲 *.origin
mvn integration-test
階段,進行 Spring Boot
應用生命週期的管理mvn integration-test
階段,進行 Spring Boot
應用生命週期的管理這裏功能,默認狀況下使用就是 repackage 功能,其餘功能要使用,則須要開發者顯式配置
repackage 功能的 做用,就是在打包的時候,多作一點額外的事情:
mvn package
命令 對項目進行打包,打成一個 jar
,這個 jar
就是一個普通的 jar
,能夠被其餘項目依賴,可是不能夠被執行repackage
命令,對第一步 打包成的 jar
進行再次打包,將之打成一個 可執行 jar
,經過將第一步打成的 jar
重命名爲 *.original
文件對任意一個 Spring Boot 項目進行打包,能夠執行 mvn package
命令,也能夠直接在 IDEA
中點擊 package
打包成功以後, target
中的文件
這裏有兩個文件,第一個 restful-0.0.1-SNAPSHOT.jar
表示打包成的可執行 jar
,第二個 restful-0.0.1-SNAPSHOT.jar.original
則是在打包過程當中 ,被重命名的 jar
,這是一個不可執行 jar
,可是能夠被其餘項目依賴的 jar
。經過對這兩個文件的解壓,咱們能夠看出這二者之間的差別
通常來講,Spring Boot 直接打包成可執行 jar
就能夠了,不建議將 Spring Boot 做爲普通的 jar
被其餘的項目所依賴。若是有這種需求,建議將被依賴的部分,單獨抽出來作一個普通的 Maven
項目,而後在 Spring Boot 中引用這個 Maven
項目。
若是非要將 Spring Boot 打包成一個普通 jar
被其餘項目依賴,技術上來講,也是能夠的,給 spring-boot-maven-plugin
插件添加以下配置:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<classifier>exec</classifier>
</configuration>
</plugin>
</plugins>
</build>
複製代碼
配置的 classifier
表示可執行 jar
的名字,配置了這個以後,在插件執行 repackage
命令時,就不會給 mvn package
所打成的 jar
重命名了
打包後的 jar 以下:restful-0.0.1-SNAPSHOT.jar和restful-0.0.1-SNAPSHOT-exec.jar
第一個 jar 表示能夠被其餘項目依賴的 jar ,第二個 jar 則表示一個可執行 jar
在Spring Boot中,咱們只須要經過使用@Async
註解就能簡單的將原來的同步函數變爲異步函數,Task類改在爲以下模式:
@Component
public class Task {
@Async
public void doTaskOne() throws Exception {
// 同上內容,省略
}
@Async
public void doTaskTwo() throws Exception {
// 同上內容,省略
}
@Async
public void doTaskThree() throws Exception {
// 同上內容,省略
}
}
複製代碼
爲了讓@Async註解可以生效,還須要在Spring Boot的主程序中配置@EnableAsync,以下所示:
@SpringBootApplication
@EnableAsync
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
注: @Async所修飾的函數不要定義爲static類型,這樣異步調用不會生效
爲了讓doTaskOne
、doTaskTwo
、doTaskThree
能正常結束,假設咱們須要統計一下三個任務併發執行共耗時多少,這就須要等到上述三個函數都完成調動以後記錄時間,並計算結果
那麼咱們如何判斷上述三個異步調用是否已經執行完成呢?咱們須要使用Future<T>
來返回異步調用的結果,就像以下方式改造doTaskOne
函數:
@Async
public Future<String> doTaskOne() throws Exception {
System.out.println("開始作任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
System.out.println("完成任務一,耗時:" + (end - start) + "毫秒");
return new AsyncResult<>("任務一完成");
}
複製代碼
按照如上方式改造一下其餘兩個異步函數以後,下面咱們改造一下測試用例,讓測試在等待完成三個異步調用以後來作一些其餘事情
@Test
public void test() throws Exception {
long start = System.currentTimeMillis();
Future<String> task1 = task.doTaskOne();
Future<String> task2 = task.doTaskTwo();
Future<String> task3 = task.doTaskThree();
while(true) {
if(task1.isDone() && task2.isDone() && task3.isDone()) {
// 三個任務都調用完成,退出循環等待
break;
}
Thread.sleep(1000);
}
long end = System.currentTimeMillis();
System.out.println("任務所有完成,總耗時:" + (end - start) + "毫秒");
}
複製代碼
看看咱們作了哪些改變:
Future<String>
類型的結果對象Future<String>
對象來判斷三個異步函數是否都結束了。若都結束,就結束循環;若沒有都結束,就等1秒後再判斷執行一下上述的單元測試,能夠看到以下結果:
開始作任務一
開始作任務二
開始作任務三
完成任務三,耗時:37毫秒
完成任務二,耗時:3661毫秒
完成任務一,耗時:7149毫秒
任務所有完成,總耗時:8025毫秒
複製代碼
能夠看到,經過異步調用,讓任務1、2、三併發執行,有效的減小了程序的總運行時間
定義線程池
第一步,先在Spring Boot主類中定義一個線程池,好比:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
}
複製代碼
上面咱們經過使用ThreadPoolTaskExecutor
建立了一個線程池,同時設置瞭如下這些參數:
CallerRunsPolicy
策略,當線程池沒有處理能力的時候,該策略會直接在 execute 方法的調用線程中運行被拒絕的任務;若是執行程序已關閉,則會丟棄該任務使用線程池
在定義了線程池以後,咱們如何讓異步調用的執行任務使用這個線程池中的資源來運行呢?方法很是簡單,咱們只須要在@Async
註解中指定線程池名便可,好比:
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("開始作任務一");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任務一,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("開始作任務二");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任務二,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("開始作任務三");
long start = System.currentTimeMillis();
Thread.sleep(random.nextInt(10000));
long end = System.currentTimeMillis();
log.info("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
複製代碼
單元測試
最後,咱們來寫個單元測試來驗證一下
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
Thread.currentThread().join();
}
}
複製代碼
執行上面的單元測試,咱們能夠在控制檯中看到全部輸出的線程名前都是以前咱們定義的線程池前綴名開始的,說明咱們使用線程池來執行異步任務的試驗成功了!
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-1] com.didispace.async.Task : 開始作任務一
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-2] com.didispace.async.Task : 開始作任務二
2018-03-27 22:01:15.620 INFO 73703 --- [ taskExecutor-3] com.didispace.async.Task : 開始作任務三
2018-03-27 22:01:18.165 INFO 73703 --- [ taskExecutor-2] com.didispace.async.Task : 完成任務二,耗時:2545毫秒
2018-03-27 22:01:22.149 INFO 73703 --- [ taskExecutor-3] com.didispace.async.Task : 完成任務三,耗時:6529毫秒
2018-03-27 22:01:23.912 INFO 73703 --- [ taskExecutor-1] com.didispace.async.Task
複製代碼
第一步:如前文同樣,咱們定義一個ThreadPoolTaskScheduler
線程池:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EnableAsync
@Configuration
class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
return executor;
}
}
}
複製代碼
第二步:改造以前的異步任務,讓它依賴一個外部資源,好比:Redis
@Slf4j
@Component
public class Task {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Async("taskExecutor")
public void doTaskOne() throws Exception {
log.info("開始作任務一");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務一,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskTwo() throws Exception {
log.info("開始作任務二");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務二,耗時:" + (end - start) + "毫秒");
}
@Async("taskExecutor")
public void doTaskThree() throws Exception {
log.info("開始作任務三");
long start = System.currentTimeMillis();
log.info(stringRedisTemplate.randomKey());
long end = System.currentTimeMillis();
log.info("完成任務三,耗時:" + (end - start) + "毫秒");
}
}
複製代碼
第三步:修改單元測試,模擬高併發狀況下ShutDown的狀況:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
@SneakyThrows
public void test() {
for (int i = 0; i < 10000; i++) {
task.doTaskOne();
task.doTaskTwo();
task.doTaskThree();
if (i == 9999) {
System.exit(0);
}
}
}
}
複製代碼
說明:經過for循環往上面定義的線程池中提交任務,因爲是異步執行,在執行過程當中,利用System.exit(0)
來關閉程序,此時因爲有任務在執行,就能夠觀察這些異步任務的銷燬與Spring容器中其餘資源的順序是否安全
第四步:運行上面的單元測試,咱們將碰到下面的異常內容
org.springframework.data.redis.RedisConnectionFailureException: Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:204) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.getConnection(JedisConnectionFactory.java:348) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.doGetConnection(RedisConnectionUtils.java:129) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:92) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisConnectionUtils.getConnection(RedisConnectionUtils.java:79) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.execute(RedisTemplate.java:169) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at org.springframework.data.redis.core.RedisTemplate.randomKey(RedisTemplate.java:781) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
at com.didispace.async.Task.doTaskOne(Task.java:26) ~[classes/:na]
at com.didispace.async.Task$$FastClassBySpringCGLIB$$ca3ff9d6.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204) ~[spring-core-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:738) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor$1.call(AsyncExecutionInterceptor.java:115) ~[spring-aop-4.3.14.RELEASE.jar:4.3.14.RELEASE]
at java.util.concurrent.FutureTask.run(FutureTask.java:266) [na:1.8.0_151]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) [na:1.8.0_151]
at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) [na:1.8.0_151]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_151]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_151]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_151]
Caused by: redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool
at redis.clients.util.Pool.getResource(Pool.java:53) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:226) ~[jedis-2.9.0.jar:na]
at redis.clients.jedis.JedisPool.getResource(JedisPool.java:16) ~[jedis-2.9.0.jar:na]
at org.springframework.data.redis.connection.jedis.JedisConnectionFactory.fetchJedisConnector(JedisConnectionFactory.java:194) ~[spring-data-redis-1.8.10.RELEASE.jar:na]
... 19 common frames omitted
Caused by: java.lang.InterruptedException: null
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(AbstractQueuedSynchronizer.java:2014) ~[na:1.8.0_151]
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:2088) ~[na:1.8.0_151]
at org.apache.commons.pool2.impl.LinkedBlockingDeque.pollFirst(LinkedBlockingDeque.java:635) ~[commons-pool2-2.4.3.jar:2.4.3]
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:442) ~[commons-pool2-2.4.3.jar:2.4.3]
at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:361) ~[commons-pool2-2.4.3.jar:2.4.3]
at redis.clients.util.Pool.getResource(Pool.java:49) ~[jedis-2.9.0.jar:na]
... 22 common frames omitted
複製代碼
緣由分析
從異常信息JedisConnectionException: Could not get a resource from the pool
來看,咱們很容易的能夠想到,在應用關閉的時候異步任務還在執行,因爲Redis鏈接池先銷燬了,致使異步任務中要訪問Redis的操做就報了上面的錯。因此,咱們得出結論,上面的實現方式在應用關閉的時候是不優雅的,那麼咱們要怎麼作呢?
解決方法
要解決上面的問題很簡單,Spring的ThreadPoolTaskScheduler
爲咱們提供了相關的配置,只須要加入以下設置便可:
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("taskExecutor-");
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
return executor;
}
複製代碼
說明:setWaitForTasksToCompleteOnShutdown(true)
該方法就是這裏的關鍵,用來設置線程池關閉的時候等待全部任務都完成再繼續銷燬其餘的Bean,這樣這些異步任務的銷燬就會先於Redis線程池的銷燬。同時,這裏還設置了setAwaitTerminationSeconds(60)
,該方法用來設置線程池中任務的等待時間,若是超過這個時候尚未銷燬就強制銷燬,以確保應用最後可以被關閉,而不是阻塞住
定義異步任務
首先,咱們先使用@Async
註解來定義一個異步任務,這個方法返回Future
類型,具體以下:
@Slf4j
@Component
public class Task {
public static Random random = new Random();
@Async("taskExecutor")
public Future<String> run() throws Exception {
long sleep = random.nextInt(10000);
log.info("開始任務,需耗時:" + sleep + "毫秒");
Thread.sleep(sleep);
log.info("完成任務");
return new AsyncResult<>("test");
}
}
複製代碼
Tips:什麼是Future類型?
Future
是對於具體的Runnable
或者Callable
任務的執行結果進行取消、查詢是否完成、獲取結果的接口。必要時能夠經過get方法獲取執行結果,該方法會阻塞直到任務返回結果
它的接口定義以下:
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
複製代碼
它聲明這樣的五個方法:
也就是說Future提供了三種功能:
測試執行與定義超時
在完成了返回Future
的異步任務定義以後,咱們來嘗試實現一個單元測試來使用這個Future完成任務的執行,好比:
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class ApplicationTests {
@Autowired
private Task task;
@Test
public void test() throws Exception {
Future<String> futureResult = task.run();
String result = futureResult.get(5, TimeUnit.SECONDS);
log.info(result);
}
}
複製代碼
上面的代碼中,咱們在get方法中還定義了該線程執行的超時時間,經過執行這個測試咱們能夠觀察到執行時間超過5秒的時候,這裏會拋出超時異常,該執行線程就可以因執行超時而釋放回線程池,不至於一直阻塞而佔用資源
消息轉換器(Message Converter)
在Spring MVC中定義了HttpMessageConverter
接口,抽象了消息轉換器對類型的判斷、對讀寫的判斷與操做,具體可見以下定義:
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException;
}
複製代碼
衆所周知,HTTP請求的Content-Type有各類不一樣格式定義,若是要支持Xml格式的消息轉換,就必需要使用對應的轉換器。Spring MVC中默認已經有一套採用Jackson實現的轉換器MappingJackson2XmlHttpMessageConverter
第一步:引入Xml消息轉換器
在傳統Spring應用中,咱們能夠經過以下配置加入對Xml格式數據的消息轉換實現:
@Configuration
public class MessageConverterConfig1 extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
builder.indentOutput(true);
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
}
複製代碼
在Spring Boot應用不用像上面這麼麻煩,只須要加入jackson-dataformat-xml
依賴,Spring Boot就會自動引入MappingJackson2XmlHttpMessageConverter
的實現:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
複製代碼
同時,爲了配置Xml數據與維護對象屬性的關係所要使用的註解也在上述依賴中,因此這個依賴也是必須的
第二步:定義對象與Xml的關係
作好了基礎擴展以後,下面就能夠定義Xml內容對應的Java對象了,好比:
@Data
@NoArgsConstructor
@AllArgsConstructor
@JacksonXmlRootElement(localName = "User")
public class User {
@JacksonXmlProperty(localName = "name")
private String name;
@JacksonXmlProperty(localName = "age")
private Integer age;
}
複製代碼
其中:@Data
、@NoArgsConstructor
、@AllArgsConstructor
是lombok簡化代碼的註解,主要用於生成get、set以及構造函數。@JacksonXmlRootElement
、@JacksonXmlProperty
註解是用來維護對象屬性在xml中的對應關係
上述配置的User對象,其能夠映射的Xml樣例以下(後續可使用上述xml來請求接口):
<User>
<name>aaaa</name>
<age>10</age>
</User>
複製代碼
第三步:建立接收xml請求的接口
完成了要轉換的對象以後,能夠編寫一個接口來接收xml並返回xml,好比:
@Controller
public class UserController {
@PostMapping(value = "/user",
consumes = MediaType.APPLICATION_XML_VALUE,
produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public User create(@RequestBody User user) {
user.setName("didispace.com : " + user.getName());
user.setAge(user.getAge() + 100);
return user;
}
}
複製代碼
Spring Boot提供了一個默認的映射:/error
,當處理中拋出異常以後,會轉到該請求中處理,而且該請求有一個全局的錯誤頁面用來展現異常內容
進行統一異常處理的改造
建立全局異常處理類:經過使用@ControllerAdvice
定義統一的異常處理類,而不是在每一個Controller中逐個定義。@ExceptionHandler
用來定義函數針對的異常類型,最後將Exception對象和請求URL映射到error.html
中
@ControllerAdvice
class GlobalExceptionHandler {
public static final String DEFAULT_ERROR_VIEW = "error";
@ExceptionHandler(value = Exception.class)
public ModelAndView defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
ModelAndView mav = new ModelAndView();
mav.addObject("exception", e);
mav.addObject("url", req.getRequestURL());
mav.setViewName(DEFAULT_ERROR_VIEW);
return mav;
}
}
複製代碼
實現error.html
頁面展現:在templates
目錄下建立error.html
,將請求的URL和Exception對象的message輸出
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>統一異常處理</title>
</head>
<body>
<h1>Error Handler</h1>
<div th:text="${url}"></div>
<div th:text="${exception.message}"></div>
</body>
</html>
複製代碼
咱們只須要在Controller中拋出Exception,固然咱們可能會有多種不一樣的Exception。而後在@ControllerAdvice類中,根據拋出的具體Exception類型匹配@ExceptionHandler中配置的異常類型來匹配錯誤映射和處理
在上述例子中,經過@ControllerAdvice
統必定義不一樣Exception映射到不一樣錯誤處理頁面。而當咱們要實現RESTful API時,返回的錯誤是JSON格式的數據,而不是HTML頁面,這時候咱們也能輕鬆支持
本質上,只需在@ExceptionHandler
以後加入@ResponseBody
,就能讓處理函數return的內容轉換爲JSON格式
下面以一個具體示例來實現返回JSON格式的異常處理
public class ErrorInfo<T> {
public static final Integer OK = 0;
public static final Integer ERROR = 100;
private Integer code;
private String message;
private String url;
private T data;
// 省略getter和setter
}
複製代碼
public class MyException extends Exception {
public MyException(String message) {
super(message);
}
}
複製代碼
Controller
中增長json映射,拋出MyException
異常@Controller
public class HelloController {
@RequestMapping("/json")
public String json() throws MyException {
throw new MyException("發生錯誤2");
}
}
複製代碼
MyException
異常建立對應的處理@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = MyException.class)
@ResponseBody
public ErrorInfo<String> jsonErrorHandler(HttpServletRequest req, MyException e) throws Exception {
ErrorInfo<String> r = new ErrorInfo<>();
r.setMessage(e.getMessage());
r.setCode(ErrorInfo.ERROR);
r.setData("Some Data");
r.setUrl(req.getRequestURL().toString());
return r;
}
}
複製代碼
{
code: 100,
data: "Some Data",
message: "發生錯誤2",
url: "http://localhost:8080/json"
}
複製代碼
@EnableScheduling
註解,啓用定時任務的配置@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製代碼
@Component
public class ScheduledTasks {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
@Scheduled(fixedRate = 5000)
public void reportCurrentTime() {
System.out.println("如今時間:" + dateFormat.format(new Date()));
}
}
複製代碼
@Scheduled詳解
在上面的入門例子中,使用了@Scheduled(fixedRate = 5000)
註解來定義每過5秒執行的任務,對於@Scheduled
的使用能夠總結以下幾種方式:
@Scheduled(fixedRate = 5000)
:上一次開始執行時間點以後5秒再執行@Scheduled(fixedDelay = 5000)
:上一次執行完畢時間點以後5秒再執行@Scheduled(initialDelay=1000, fixedRate=5000)
:第一次延遲1秒後執行,以後按fixedRate的規則每5秒執行一次@Scheduled(cron="*/5 * * * * *")
:經過cron表達式定義規則將LocalDateTime字段以時間戳的方式返回給前端,添加日期轉化類
public class LocalDateTimeConverter extends JsonSerializer<LocalDateTime> {
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeNumber(value.toInstant(ZoneOffset.of("+8")).toEpochMilli());
}
}
複製代碼
並在LocalDateTime字段上添加註解,以下:
@JsonSerialize(using = LocalDateTimeConverter.class)
protected LocalDateTime gmtModified;
複製代碼
將LocalDateTime字段以指定格式化日期的方式返回給前端,在字段上添加註解便可,以下:
@JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
複製代碼
對前端傳入的日期進行格式化註解便可,以下:
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
protected LocalDateTime gmtModified;
複製代碼
Spring Boot 在啓動的時候會幹這幾件事情:
總結一下,其實就是 Spring Boot 在啓動的時候,按照約定去讀取 Spring Boot Starter 的配置信息,再根據配置信息對資源進行初始化,並注入到 Spring 容器中。這樣 Spring Boot 啓動完畢後,就已經準備好了一切資源,使用過程當中直接注入對應 Bean 資源便可