Spring boot框架
1、什么是Spring Boot?
? Spring Boot是Spring開源組織下的子項目,是Spring組件一站式解決方案,主要是簡化了使用Spring的難度,簡省了繁重的配置,提供了各種啟動器,開發者能快速上手。
Spring Boot官方文檔:https://spring.io/projects/spring-boot/
2、Spring Boot的優點與特點
優點:
- 獨立運行
- Spring Boot內嵌了各種servlet容器,Tomcat、Jetty等,不再需要打成war包部署到容器中,Spring Boot只要打成一個可執行的jar包就能獨立運行,所有的依賴包都在一個jar包內
- 簡化配置
- spring-boot-starter-web啟動器自動依賴其他組件,簡少了maven的配置。
- 自動配置
- Spring Boot能根據當前類路徑下的類、jar包來自動配置bean,如添加一個spring-boot-starter-web啟動器就能擁有web的功能,無需其他配置
- 無代碼生成和XML配置
- Spring Boot配置過程中無代碼生成,也無需XML配置文件就能完成所有配置工作,這一切都是借助于條件注解完成的,這也是Spring4.x的核心功能之一。
- 應用監控
- Spring Boot提供一系列端點可以監控服務及應用,做健康檢測。
- 上手容易
特點:
- 為 Spring 開發提供一個更快、更廣泛的入門體驗。
- 開箱即用,遠離繁瑣的配置。
- 提供了一系列大型項目通用的非業務性功能,例如:內嵌服務器、安全管理、運行數據監控、運行狀況檢查和外部化配置等。
- 絕對沒有代碼生成,也不需要XML配置。
3、Spring Boot的配置文件以及之間的區別
.properties 和 .yml,它們的區別主要是書寫格式不同。
1).properties
app.user.name = javastack
2).yml
app:user:name: javastack
注:.yml 格式不支持 @PropertySource
注解導入配置。
4、Spring Boot的常用注解
@SpringBootApplication
:
申明讓spring boot自動給程序進行必要的配置,這個配置等同于:@Configuration @EnableAutoConfiguration 和 @ComponentScan 三個配置。
@ResponseBody
:
表示該方法的返回結果直接寫入HTTP response body中,一般在異步獲取數據時使用,用于構建RESTful的api。在使用@RequestMapping后,返回值通常解析為跳轉路徑,加上@esponsebody后返回結果不會被解析為跳轉路徑,而是直接寫入HTTP response body中。比如異步獲取json數據,加上@Responsebody后,會直接返回json數據。該注解一般會配合@RequestMapping一起使用。
@Controller
:
用于定義控制器類,在spring項目中由控制器負責將用戶發來的URL請求轉發到對應的服務接口(service層),一般這個注解在類中,通常方法需要配合注解@RequestMapping。
@RestController
:
用于標注控制層組件(如struts中的action),@ResponseBody和@Controller的合集。
@RequestMapping
:
提供路由信息,負責URL到Controller中的具體函數的映射。
@EnableAutoConfiguration
:
SpringBoot自動配置(auto-configuration):嘗試根據你添加的jar依賴自動配置你的Spring應用。例如,如果你的classpath下存在HSQLDB,并且你沒有手動配置任何數據庫連接beans,那么我們將自動配置一個內存型(in-memory)數據庫”。你可以將@EnableAutoConfiguration或者@SpringBootApplication :添加到一個@Configuration類上來選擇自動配置。如果發現應用了你不想要的特定自動配置類,你可以使用@EnableAutoConfiguration注解的排除屬性來禁用它們。
@ComponentScan
:
表示將該類自動發現掃描組件。個人理解相當于,如果掃描到有@Component、@Controller、@Service等這些注解的類,并注冊為Bean,可以自動收集所有的Spring組件,包括@Configuration類。我們經常使用@ComponentScan注解搜索beans,并結合@Autowired注解導入。可以自動收集所有的Spring組件,包括@Configuration類。我們經常使用@ComponentScan注解搜索beans,并結合@Autowired注解導入。如果沒有配置的話,Spring Boot會掃描啟動類所在包下以及子包下的使用了@Service,@Repository等注解的類。
@Configuration
:
相當于傳統的xml配置文件,如果有些第三方庫需要用到xml文件,建議仍然通過@Configuration類作為項目的配置主類——可以使用@ImportResource注解加載xml配置文件。
@Import
:用來導入其他配置類。
@ImportResource
:用來加載xml配置文件。
@Service
:一般用于修飾service層的組件
@Repository
:使用@Repository注解可以確保DAO或者repositories提供異常轉譯,這個注解修飾的DAO或者repositories類會被ComponetScan發現并配置,同時也不需要為它們提供XML配置項。
@Value
:注入Spring boot application.properties配置的屬性的值。示例代碼:
@Inject
:等價于默認的@Autowired,只是沒有required屬性;
@Component
:泛指組件,當組件不好歸類的時候,我們可以使用這個注解進行標注。
@Bean
:相當于XML中的,放在方法的上面,而不是類,意思是產生一個bean,并交給spring管理。
@AutoWired
:
自動導入依賴的bean。byType方式。把配置好的Bean拿來用,完成屬性、方法的組裝,它可以對類成員變量、方法及構造函數進行標注,完成自動裝配的工作。當加上(required=false)時,就算找不到bean也不報錯。
@Qualifier
:
當有多個同一類型的Bean時,可以用@Qualifier(“name”)來指定。與@Autowired配合使用。@Qualifier限定描述符除了能根據名字進行注入,但能進行更細粒度的控制如何選擇候選者,具體使用方式如下:
@Resource(name=”name”,type=”type”)
:沒有括號內內容的話,默認byName。與@Autowired干類似的事。
5、spring boot開啟的兩種方式
? ① 繼承spring-boot-starter-parent項目
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.6.RELEASE</version>xml</parent>
? ② 導入spring-boot-dependencies項目依賴
<dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>1.5.6.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencyManagement>
Spring Boot依賴注意點
1、屬性覆蓋只對繼承有效
? Spring Boot依賴包里面的組件的版本都是和當前Spring Boot綁定的,如果要修改里面組件的版本,只需要添加如下屬性覆蓋即可,但這種方式只對繼承有效,導入的方式無效。
<properties><slf4j.version>1.7.25<slf4j.version></properties>
如果導入的方式要實現版本的升級,達到上面的效果,這樣也可以做到,把要升級的組件依賴放到Spring Boot之前。
<dependencyManagement><dependencies><!-- Override Spring Data release train provided by Spring Boot --><dependency><groupId>org.springframework.data</groupId><artifactId>spring-data-releasetrain</artifactId><version>Fowler-SR2</version><scope>import</scope><type>pom</type></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>1.5.6.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement>
需要注意,要修改Spring Boot的依賴組件版本可能會造成不兼容的問題。
2、資源文件過濾問題
使用繼承Spring Boot時,如果要使用Maven resource filter過濾資源文件時,資源文件里面的占位符為了使${}和Spring Boot區別開來,此時要用@...@包起來,不然無效。另外,@...@占位符在yaml文件編輯器中編譯報錯,所以使用繼承方式有諸多問題,坑要慢慢趟。
6、Spring Boot的幾種運行方式
- 打包用命令或者放到容器中運行
- 用 Maven/ Gradle 插件運行
- 直接執行 main 方法運行
7、Spring Boot實現熱部署
第一種引用devtools依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional></dependency>
第二種自定義配置熱部署
# 熱部署開關,false即不啟用熱部署spring.devtools.restart.enabled: true# 指定熱部署的目錄#spring.devtools.restart.additional-paths: src/main/java# 指定目錄不更新spring.devtools.restart.exclude: test/**
第三種Intellij Idea修改
? 1、勾上自動編譯或者手動重新編譯
? File > Settings > Compiler-Build Project automatically
? 2、注冊
? ctrl + shift + alt + / > Registry > 勾選Compiler autoMake allow when app running
注意事項:
1、生產環境devtools將被禁用,如java -jar方式或者自定義的類加載器等都會識別為生產環境。
2、打包應用默認不會包含devtools,除非你禁用SpringBoot Maven插件的 excludeDevtools
屬性。
3、Thymeleaf無需配置 spring.thymeleaf.cache:false
,devtools默認會自動設置,參考完整屬性。
4、devtools會在windows資源管理器占用java進程,在開發工具里面殺不掉,只能手動kill掉,不然重啟會選成端口重復綁定報錯。
更多詳細用法,參考發官方文檔:https://docs.spring.io/spring-boot/docs/current/reference/html/using-boot-devtools.html
8、保護Spring Boot的應用方法
- 在生產中使用HTTPS
- 使用Snyk檢查你的依賴關系
- 升級到最新版本
- 啟動CSRF保護
- 使用內容安全策略防止XSS攻擊
- 使用OpenID Connect進行身份驗證
- 管理密碼使用面密碼哈希
- 安全地存儲秘密
- 使用OWASP的ZAP測試應用程序
- 安全團隊進行代碼審查
9、Spring Boot配置加載順序(2.0)
1、properties文件;
2、YAML文件;
3、系統環境變量;
4、命令行參數;
等等……
配置屬性加載的順序如下:
1、開發者工具 `Devtools` 全局配置參數;2、單元測試上的 `@TestPropertySource` 注解指定的參數;3、單元測試上的 `@SpringBootTest` 注解指定的參數;4、命令行指定的參數,如 `java -jar springboot.jar --name="Java技術棧"`;5、命令行中的 `SPRING_APPLICATION_JSONJSON` 指定參數, 如 `java -Dspring.application.json='{"name":"Java技術棧"}' -jar springboot.jar`6、`ServletConfig` 初始化參數;7、`ServletContext` 初始化參數;8、JNDI參數(如 `java:comp/env/spring.application.json`);9、Java系統參數(來源:`System.getProperties()`);10、操作系統環境變量參數;11、`RandomValuePropertySource` 隨機數,僅匹配:`ramdom.*`;12、JAR包外面的配置文件參數(`application-{profile}.properties(YAML)`)13、JAR包里面的配置文件參數(`application-{profile}.properties(YAML)`)14、JAR包外面的配置文件參數(`application.properties(YAML)`)15、JAR包里面的配置文件參數(`application.properties(YAML)`)16、`@Configuration`配置文件上 `@PropertySource` 注解加載的參數;17、默認參數(通過 `SpringApplication.setDefaultProperties` 指定);
10、Spring Boot日志集成
Spring Boot日志框架
Spring Boot支持Java Util Logging,Log4j2,Lockback作為日志框架,如果你使用starters啟動器,Spring Boot將使用Logback作為默認日志框架。無論使用哪種日志框架,Spring Boot都支持配置將日志輸出到控制臺或者文件中。
spring-boot-starter啟動器包含spring-boot-starter-logging啟動器并集成了slf4j日志抽象及Logback日志框架。
11、Spring Boot讀取配置的幾種方式
讀取application文件
在application.yml或者properties文件中添加:
info.address=USA
info.company=Spring
info.degree=high
@Value注解讀物方式
import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Componentpublic class InfoConfig1 {@Value("${info.address}")private String address;@Value("${info.company}")private String company;@Value("${info.degree}")private String degree;public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}public String getDegree() {return degree;}public void setDegree(String degree) {this.degree = degree;}}
@ConfigurationProperties注解讀取方式
@Component@ConfigurationProperties(prefix = "info")public class InfoConfig2 {private String address;private String company;private String degree;public String getAddress() {return address;}public void setAddress(String address) {this.address = address;}public String getCompany() {return company;}public void setCompany(String company) {this.company = company;}public String getDegree() {return degree;}public void setDegree(String degree) {this.degree = degree;}}
讀取指定文件
資源目錄下建立config/db-config.properties:
db.username=root
db.password=123456
@PropertySource+@Value注解讀取方式
@Component@PropertySource(value = { "config/db-config.properties" })public class DBConfig1 {@Value("${db.username}")private String username;@Value("${db.password}")private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
注意:@PropertySource不支持yml文件讀取。
@PropertySource+@ConfigurationProperties注解讀取方式
@Component@ConfigurationProperties(prefix = "db")@PropertySource(value = { "config/db-config.properties" })public class DBConfig2 {private String username;private String password;public String getUsername() {return username;}public void setUsername(String username) {this.username = username;}public String getPassword() {return password;}public void setPassword(String password) {this.password = password;}}
Environment讀取方式
以上所有加載出來的配置都可以通過Environment注入獲取到。
@Autowiredprivate Environment env;// 獲取參數String getProperty(String key);
總結:從以上示例來看,Spring Boot可以通過@PropertySource,@Value,@Environment,@ConfigurationProperties來綁定變量。
12、Spring Boot自動配置原理
① SpringBoot啟動的時候加載主配置類,開啟了自動配置功能@EnableAutoConfiguration。
② @EnableAutoConfiguration的作用是利用AutoConfigurationImportSelector給容器中導入一些組件。
③ 可以查看public String[] selectImports(AnnotationMetadata annotationMetadata)方法的內容。
④ 通過protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes)獲取候選的配置,這個是掃描所有jar包類路徑下"META-INF/spring.factories";
⑤ 然后把掃描到的這些文件包裝成Properties對象。
⑥ 從properties中獲取到EnableAutoConfiguration.class類名對應的值,然后把他們添加在容器中。
⑦ 整個過程就是將類路徑下"META-INF/spring.factories"里面配置的所有EnableAutoConfiguration的值加入到 容器中。
⑧ 每一個這樣XXAutoConfiguration類都是容器中的一個組件都加入到容器中,用他們來做自動配置。
每一個自動配置類進行自動配置功能,以HttpEncodingAutoConfiguration為例解釋自動配置原理
⑨根據當前不同的條件判斷,決定這個配置是否生效。
13、Spring Boot中的starter
? 首先,這個 Starter 并非什么新的技術點,基本上還是基于 Spring 已有功能來實現的。首先它提供了一個自動化配置類,一般命名為 XXXAutoConfiguration ,在這個配置類中通過條件注解來決定一個配置是否生效(條件注解就是 Spring 中原本就有的),然后它還會提供一系列的默認配置,也允許開發者根據實際情況自定義相關配置,然后通過類型安全的屬性注入將這些配置屬性注入進來,新注入的屬性會代替掉默認屬性。正因為如此,很多第三方框架,我們只需要引入依賴就可以直接使用了。 當然,開發者也可以自定義 Starter,自定義 Starter 可以參考:徒手擼一個 Spring Boot 中的 Starter ,解密自動化配置黑魔法!
14、Spring Boot跨域問題
? 跨域可以在前端通過 JSONP 來解決,但是 JSONP 只可以發送 GET 請求,無法發送其他類型的請求,在 RESTful 風格的應用中,就顯得非常雞肋,因此我們推薦在后端通過 (CORS,Cross-origin resource sharing) 來解決跨域問題。這種解決方案并非 Spring Boot 特有的,在傳統的 SSM 框架中,就可以通過 CORS 來解決跨域問題,只不過之前我們是在 XML 文件中配置 CORS ,現在則是通過 @CrossOrigin 注解來解決跨域問題。關于 CORS ,小伙伴們可以參考:Spring Boot 中通過 CORS 解決跨域問題
15、Spring Boot定時任務
? 使用spring boot創建定時任務主要有以下三種創建方式:
? 一、基于注解@Scheduled
? 默認為單線程,開啟多個任務時,任務的執行時機會受上一個任務執行時 間的影響。
? ① 創建定時器
@Configuration //1.主要用于標記配置類,兼備Component的效果。
@EnableScheduling // 2.開啟定時任務
public class SaticScheduleTask {//3.添加定時任務@Scheduled(cron = "0/5 * * * * ?")//或直接指定時間間隔,例如:5秒//@Scheduled(fixedRate=5000)private void configureTasks() {System.err.println("執行靜態定時任務時間: " + LocalDateTime.now());}
}
②啟動測試

? 顯然,使用@Scheduled 注解很方便,但缺點是當我們調整了執行周期的時候,需要重啟應用才能生效,這多少有些不方便。為了達到實時生效的效果,可以使用接口來完成定時任務。
? 二、基于接口(SchedulingConfigurer)
? 1、導入依賴包:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.0.4.RELEASE</version></parent><dependencies><dependency><!--添加Web依賴 --><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><!--添加MySql依賴 --><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><!--添加Mybatis依賴 配置mybatis的一些初始化的東西--><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>1.3.1</version></dependency><dependency><!-- 添加mybatis依賴 --><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.4.5</version><scope>compile</scope></dependency></dependencies>
? 2、添加數據庫記錄
開啟本地數據庫mysql,隨便打開查詢窗口,然后執行腳本內容,代碼如下:
DROP DATABASE IF EXISTS `socks`;
CREATE DATABASE `socks`;
USE `SOCKS`;
DROP TABLE IF EXISTS `cron`;
CREATE TABLE `cron` (`cron_id` varchar(30) NOT NULL PRIMARY KEY,`cron` varchar(30) NOT NULL
);
INSERT INTO `cron` VALUES ('1', '0/5 * * * * ?');
然后在項目中的application.yml添加數據源:
spring:datasource:url: jdbc:mysql://localhost:3306/socksusername: rootpassword: 123456
3、創建定時器
? 數據庫準備好數據之后,我們編寫定時任務,注意這里添加的是TriggerTask,目的是循環讀取我們在數據庫設置好的執行周期,以及執行相關定時任務的內容。
具體代碼如下:
@Configuration //1.主要用于標記配置類,兼備Component的效果。
@EnableScheduling // 2.開啟定時任務
public class DynamicScheduleTask implements SchedulingConfigurer {@Mapperpublic interface CronMapper {@Select("select cron from cron limit 1")public String getCron();}@Autowired //注入mapper@SuppressWarnings("all")CronMapper cronMapper;/*** 執行定時任務.*/@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {taskRegistrar.addTriggerTask(//1.添加任務內容(Runnable)() -> System.out.println("執行動態定時任務: " + LocalDateTime.now().toLocalTime()),//2.設置執行周期(Trigger)triggerContext -> {//2.1 從數據庫獲取執行周期String cron = cronMapper.getCron();//2.2 合法性校驗.if (StringUtils.isEmpty(cron)) {// Omitted Code ..}//2.3 返回執行周期(Date)return new CronTrigger(cron).nextExecutionTime(triggerContext);});}}
4、啟動測試
啟動應用后,查看控制臺,打印時間是我們預期的每10秒一次:

然后打開Navicat ,將執行周期修改為每6秒執行一次,如圖:

查看控制臺,發現執行周期已經改變,并且不需要我們重啟應用,十分方便。如圖:

注意: 如果在數據庫修改時格式出現錯誤,則定時任務會停止,即使重新修改正確;此時只能重新啟動項目才能恢復。
三、基于注解設定多線程定時任務
1、創建多線程定時任務
//@Component注解用于對那些比較中立的類進行注釋;
//相對與在持久層、業務層和控制層分別采用 @Repository、@Service 和 @Controller 對分層中的類進行注釋
@Component
@EnableScheduling // 1.開啟定時任務
@EnableAsync // 2.開啟多線程
public class MultithreadScheduleTask {@Async@Scheduled(fixedDelay = 1000) //間隔1秒public void first() throws InterruptedException {System.out.println("第一個定時任務開始 : " + LocalDateTime.now().toLocalTime() + "rn線程 : " + Thread.currentThread().getName());System.out.println();Thread.sleep(1000 * 10);}@Async@Scheduled(fixedDelay = 2000)public void second() {System.out.println("第二個定時任務開始 : " + LocalDateTime.now().toLocalTime() + "rn線程 : " + Thread.currentThread().getName());System.out.println();}}
2、啟動測試查看控制臺

從控制臺可以看出,第一個定時任務和第二個定時任務互不影響;
并且,由于開啟了多線程,第一個任務的執行時間也不受其本身執行時間的限制,所以需要注意可能會出現重復操作導致數據異常
16、Spring Boot 全局異常處理
1、基于@ControllerAdvice注解的Controller層
創建 MyControllerAdvice,并添加 @ControllerAdvice注解。
package com.shsxt.demo.controller;import org.springframework.ui.Model;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;/*** controller 增強器* @author sam* @since 2017/7/17*/
@ControllerAdvice
public class MyControllerAdvice {/*** 應用到所有@RequestMapping注解方法,在其執行之前初始化數據綁定器* @param binder*/@InitBinderpublic void initBinder(WebDataBinder binder) {}/*** 把值綁定到Model中,使全局@RequestMapping可以獲取到該值* @param model*/@ModelAttributepublic void addAttributes(Model model) {model.addAttribute("author", "Magical Sam");}/*** 全局異常捕捉處理* @param ex* @return*/@ResponseBody@ExceptionHandler(value = Exception.class)public Map errorHandler(Exception ex) {Map map = new HashMap();map.put("code", 100);map.put("msg", ex.getMessage());return map;}}
啟動應用后,被 @ExceptionHandler、@InitBinder、@ModelAttribute 注解的方法,都會作用在 被 @RequestMapping 注解的方法上。
@ModelAttribute:在Model上設置的值,對于所有被 @RequestMapping 注解的方法中,都可以通過 ModelMap 獲取,如下:
@RequestMapping("/home")
public String home(ModelMap modelMap) {System.out.println(modelMap.get("author"));
}//或者 通過@ModelAttribute獲取@RequestMapping("/home")
public String home(@ModelAttribute("author") String author) {System.out.println(author);
}
@ExceptionHandler 攔截了異常,我們可以通過該注解實現自定義異常處理。其中,@ExceptionHandler 配置的 value 指定需要攔截的異常類型,上面攔截了 Exception.class 這種異常。
2、基于Springboot自身的全局異常統一處理,主要是實現ErrorController接口或者繼承AbstractErrorController抽象類或者繼承BasicErrorController類
Controller層代碼:
@Controller
@RequestMapping(value = "error")
@EnableConfigurationProperties({ServerProperties.class})
public class ExceptionController implements ErrorController {private ErrorAttributes errorAttributes;@Autowiredprivate ServerProperties serverProperties;/*** 初始化ExceptionController* @param errorAttributes*/@Autowiredpublic ExceptionController(ErrorAttributes errorAttributes) {Assert.notNull(errorAttributes, "ErrorAttributes must not be null");this.errorAttributes = errorAttributes;}/*** 定義404的ModelAndView* @param request* @param response* @return*/@RequestMapping(produces = "text/html",value = "404")public ModelAndView errorHtml404(HttpServletRequest request,HttpServletResponse response) {response.setStatus(getStatus(request).value());Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));return new ModelAndView("error/404", model);}/*** 定義404的JSON數據* @param request* @return*/@RequestMapping(value = "404")@ResponseBodypublic ResponseEntity<Map<String, Object>> error404(HttpServletRequest request) {Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));HttpStatus status = getStatus(request);return new ResponseEntity<Map<String, Object>>(body, status);}/*** 定義500的ModelAndView* @param request* @param response* @return*/@RequestMapping(produces = "text/html",value = "500")public ModelAndView errorHtml500(HttpServletRequest request,HttpServletResponse response) {response.setStatus(getStatus(request).value());Map<String, Object> model = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));return new ModelAndView("error/500", model);}/*** 定義500的錯誤JSON信息* @param request* @return*/@RequestMapping(value = "500")@ResponseBodypublic ResponseEntity<Map<String, Object>> error500(HttpServletRequest request) {Map<String, Object> body = getErrorAttributes(request,isIncludeStackTrace(request, MediaType.TEXT_HTML));HttpStatus status = getStatus(request);return new ResponseEntity<Map<String, Object>>(body, status);}/*** Determine if the stacktrace attribute should be included.* @param request the source request* @param produces the media type produced (or {@code MediaType.ALL})* @return if the stacktrace attribute should be included*/protected boolean isIncludeStackTrace(HttpServletRequest request,MediaType produces) {ErrorProperties.IncludeStacktrace include = this.serverProperties.getError().getIncludeStacktrace();if (include == ErrorProperties.IncludeStacktrace.ALWAYS) {return true;}if (include == ErrorProperties.IncludeStacktrace.ON_TRACE_PARAM) {return getTraceParameter(request);}return false;}/*** 獲取錯誤的信息* @param request* @param includeStackTrace* @return*/private Map<String, Object> getErrorAttributes(HttpServletRequest request,boolean includeStackTrace) {RequestAttributes requestAttributes = new ServletRequestAttributes(request);return this.errorAttributes.getErrorAttributes(requestAttributes,includeStackTrace);}/*** 是否包含trace* @param request* @return*/private boolean getTraceParameter(HttpServletRequest request) {String parameter = request.getParameter("trace");if (parameter == null) {return false;}return !"false".equals(parameter.toLowerCase());}/*** 獲取錯誤編碼* @param request* @return*/private HttpStatus getStatus(HttpServletRequest request) {Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");if (statusCode == null) {return HttpStatus.INTERNAL_SERVER_ERROR;}try {return HttpStatus.valueOf(statusCode);}catch (Exception ex) {return HttpStatus.INTERNAL_SERVER_ERROR;}}/*** 實現錯誤路徑,暫時無用* @see ExceptionMvcAutoConfiguration#containerCustomizer()* @return*/@Overridepublic String getErrorPath() {return "";}}
3、基于AOP也可以實現異常的全局處理
在執行切點中配置的路徑中的方法有異常時,可以被捕獲到
建議使用該方式:選用AOP方式主要是因為AOP不只可以做全局異常統一處理還可以統一打印接口請求入參和返回結果日志,打印接口訪問性能日志,處理sql注入攻擊以及處理入參特殊字符等問題
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.common.base.Stopwatch;
import com.google.common.collect.Lists;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;/*** @Author: smy* @Description: 調用接口打印性能日志以及接口報錯之后記錄錯誤日志* @Date: 2018/9/20* @Time: 15:16*/
@Component
@Aspect
public class InterfaceRequestErrrorAndPerformanceLog {public static final Logger logger = LoggerFactory.getLogger(InterfaceRequestErrrorAndPerformanceLog.class);@Value("${dc.log.bad.value:3000}")private int performanceBadValue;@Resourceprivate RabbitMQService rabbitMQService;@Resourceprivate InterfaceErrorService interfaceErrorService;@Pointcut("execution(* test.test.test.test.test.controller.*.*.*(..))")public void pointCut(){}@Around("pointCut()")public APIResponse handleControllerMethod(ProceedingJoinPoint pjp) throws Throwable{Stopwatch stopwatch = Stopwatch.createStarted();APIResponse apiResponse;try {logger.info("執行Controller開始: " + pjp.getSignature() + " 參數:" + Lists.newArrayList(pjp.getArgs()).toString());//處理入參特殊字符和sql注入攻擊checkRequestParam(pjp);//執行訪問接口操作apiResponse = (APIResponse) pjp.proceed(pjp.getArgs());try{logger.info("執行Controller結束: " + pjp.getSignature() + ", 返回值:" + JSONObject.toJSONString(apiResponse));//此處將日志打印放入try-catch是因為項目中有些對象實體bean過于復雜,導致序列化為json的時候報錯,但是此處報錯并不影響主要功能使用,只是返回結果日志沒有打印,所以catch中也不做拋出異常處理}catch (Exception ex){logger.error(pjp.getSignature()+" 接口記錄返回結果失敗!,原因為:{}",ex.getMessage());}Long consumeTime = stopwatch.stop().elapsed(TimeUnit.MILLISECONDS);logger.info("耗時:" + consumeTime + "(毫秒).");//當接口請求時間大于3秒時,標記為異常調用時間,并記錄入庫if(consumeTime > performanceBadValue){DcPerformanceEntity dcPerformanceEntity = new DcPerformanceEntity();dcPerformanceEntity.setInterfaceName(pjp.getSignature().toString());dcPerformanceEntity.setRequestParam(Lists.newArrayList(pjp.getArgs()).toString());dcPerformanceEntity.setConsumeTime(consumeTime + "毫秒");RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_PERFORMANCE, new String[] { ProjectConstants.DC_KEY_QUEUE_PERFORMANCE});rabbitMQService.send(mqTarget, JSON.toJSONString(dcPerformanceEntity));}} catch (Exception throwable) {apiResponse = handlerException(pjp, throwable);}return apiResponse;}/*** @Author: smy* @Description: 處理接口調用異常* @Date: 15:13 2018/10/25*/private APIResponse handlerException(ProceedingJoinPoint pjp, Throwable e) {APIResponse apiResponse;if(e.getClass().isAssignableFrom(ProjectException.class) ){//ProjectException為自定義異常類,項目中Controller層會把所有的異常都catch掉,并手工封裝成ProjectException拋出來,這樣做的目的是ProjectException會記錄拋出異常接口的路徑,名稱以及請求參數等等,有助于錯誤排查ProjectException projectException = (ProjectException)e;logger.error("捕獲到ProjectException異常:",JSONObject.toJSONString(projectException.getDcErrorEntity()));RabbitMQMessageTarget mqTarget = RabbitMQMessageTarget.createFanoutTarget(ProjectConstants.DC_KEY_EXCHANGE_INTERFACE_ERROR, new String[] { ProjectConstants.DC_KEY_QUEUE_INTERFACE_ERROR});rabbitMQService.send(mqTarget, JSON.toJSONString(dataCenterException.getDcErrorEntity()));apiResponse = new APIResponse(APIResponse.FAIL,null,projectException.getDcErrorEntity().getErrorMessage());} else if (e instanceof RuntimeException) {logger.error("RuntimeException{方法:" + pjp.getSignature() + ", 參數:" + pjp.getArgs() + ",異常:" + e.getMessage() + "}", e);apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());} else {logger.error("異常{方法:" + pjp.getSignature() + ", 參數:" + pjp.getArgs() + ",異常:" + e.getMessage() + "}", e);apiResponse = new APIResponse(APIResponse.FAIL,null,e.getMessage());}return apiResponse;}/*** @Author: gmy* @Description: 處理入參特殊字符和sql注入攻擊* @Date: 15:37 2018/10/25*/private void checkRequestParam(ProceedingJoinPoint pjp){String str = String.valueOf(pjp.getArgs());if (!IllegalStrFilterUtil.sqlStrFilter(str)) {logger.info("訪問接口:" + pjp.getSignature() + ",輸入參數存在SQL注入風險!參數為:" + Lists.newArrayList(pjp.getArgs()).toString());DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"輸入參數存在SQL注入風險!");throw new DataCenterException(dcErrorEntity);}if (!IllegalStrFilterUtil.isIllegalStr(str)) {logger.info("訪問接口:" + pjp.getSignature() + ",輸入參數含有非法字符!,參數為:" + Lists.newArrayList(pjp.getArgs()).toString());DcErrorEntity dcErrorEntity = interfaceErrorService.processDcErrorEntity(pjp.getSignature() + "",Lists.newArrayList(pjp.getArgs()).toString(),"輸入參數含有非法字符!");throw new DataCenterException(dcErrorEntity);}}}
IllegalStrFilterUtil代碼:
import org.slf4j.LoggerFactory;import java.util.regex.Matcher;
import java.util.regex.Pattern;/*** @Author: gmy* @Description: 特殊字符檢測工具(防止傳入非法字符和sql注入攻擊)* @Date: 2018/10/25* @Time: 15:08*/
public class IllegalStrFilterUtil {private static final org.slf4j.Logger Logger = LoggerFactory.getLogger(IllegalStrFilterUtil.class);private static final String REGX = "!|!|@|◎|#|#|($)|¥|%|%|(^)|……|(&)|※|(*)|×|(()|(|())|)|_|——|(+)|+|(|)|§ ";/*** 對常見的sql注入攻擊進行攔截** @param sInput* @return* true 表示參數不存在SQL注入風險* false 表示參數存在SQL注入風險*/public static Boolean sqlStrFilter(String sInput) {if (sInput == null || sInput.trim().length() == 0) {return false;}sInput = sInput.toUpperCase();if (sInput.indexOf("DELETE") >= 0 || sInput.indexOf("ASCII") >= 0 || sInput.indexOf("UPDATE") >= 0 || sInput.indexOf("SELECT") >= 0|| sInput.indexOf("'") >= 0 || sInput.indexOf("SUBSTR(") >= 0 || sInput.indexOf("COUNT(") >= 0 || sInput.indexOf(" OR ") >= 0|| sInput.indexOf(" AND ") >= 0 || sInput.indexOf("DROP") >= 0 || sInput.indexOf("EXECUTE") >= 0 || sInput.indexOf("EXEC") >= 0|| sInput.indexOf("TRUNCATE") >= 0 || sInput.indexOf("INTO") >= 0 || sInput.indexOf("DECLARE") >= 0 || sInput.indexOf("MASTER") >= 0) {Logger.error("該參數怎么SQL注入風險:sInput=" + sInput);return false;}Logger.info("通過sql檢測");return true;}/*** 對非法字符進行檢測** @param sInput* @return* true 表示參數不包含非法字符* false 表示參數包含非法字符*/public static Boolean isIllegalStr(String sInput) {if (sInput == null || sInput.trim().length() == 0) {return false;}sInput = sInput.trim();Pattern compile = Pattern.compile(REGX, Pattern.CASE_INSENSITIVE);Matcher matcher = compile.matcher(sInput);Logger.info("通過字符串檢測");return matcher.find();}
}
17、spring-boot-starter-parent 的作用
- 定義了 Java 編譯版本為 1.8 。
- 使用 UTF-8 格式編碼。
- 繼承自 spring-boot-dependencies,這個里邊定義了依賴的版本,也正是因為繼承了這個依賴,所以我們在寫依賴時才不需要寫版本號。
- 執行打包操作的配置。
- 自動化的資源過濾。
- 自動化的插件配置。
- 針對 application.properties 和 application.yml 的資源過濾,包括通過 profile 定義的不同環境的配置文件,例如 application-dev.properties 和 application-dev.yml。