上一篇帶領你們初步瞭解瞭如何使用 Spring Boot 搭建框架,經過 Spring Boot 和傳統的 SpringMVC 架構的對比,咱們清晰地發現 Spring Boot 的好處,它使咱們的代碼更加簡單,結構更加清晰。html
從這一篇開始,我將帶領你們更加深刻的認識 Spring Boot,將 Spring Boot 涉及到東西進行拆解,從而瞭解 Spring Boot 的方方面面。學完本文後,讀者能夠基於 Spring Boot 搭建更加複雜的系統框架。前端
咱們知道,Spring Boot 是一個大容器,它將不少第三方框架都進行了集成,咱們在實際項目中用到哪一個模塊,再引入哪一個模塊。好比咱們項目中的持久化框架用 MyBatis,則在 pom.xml 添加以下依賴:java
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.40</version> </dependency>
yaml/properties 文件mysql
咱們知道整個 Spring Boot 項目只有一個配置文件,那就是 application.yml,Spring Boot 在啓動時,就會從 application.yml 中讀取配置信息,並加載到內存中。上一篇咱們只是粗略的列舉了幾個配置項,其實 Spring Boot 的配置項是不少的,本文咱們將學習在實際項目中經常使用的配置項(注:爲了方便說明,配置項均以 properties 文件的格式寫出,後續的實際配置都會寫成 yaml 格式)。web
下面是我參與的某個項目的 application.yml 配置文件內容:spring
server: port: 8080 context-path: /api tomcat: max-threads: 1000 min-spare-threads: 50 connection-timeout: 5000 spring: profiles: active: dev http: multipart: maxFileSize: -1 datasource: url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true username: root password: root driverClassName: com.mysql.jdbc.Driver jpa: database: MYSQL showSql: true hibernate: namingStrategy: org.hibernate.cfg.ImprovedNamingStrategy properties: hibernate: dialect: org.hibernate.dialect.MySQL5Dialect mybatis: configuration: #配置項:開啓下劃線到駝峯的自動轉換. 做用:將數據庫字段根據駝峯規則自動注入到對象屬性。 map-underscore-to-camel-case: true
以上列舉了經常使用的配置項,全部配置項信息均可以在官網中找到,本課程就不一一列舉了。sql
多環境配置數據庫
在一個企業級系統中,咱們可能會遇到這樣一個問題:開發時使用開發環境,測試時使用測試環境,上線時使用生產環境。每一個環境的配置均可能不同,好比開發環境的數據庫是本地地址,而測試環境的數據庫是測試地址。那咱們在打包的時候如何生成不一樣環境的包呢?json
這裏的解決方案有不少:api
1.每次編譯以前手動把全部配置信息修改爲當前運行的環境信息。這種方式致使每次都須要修改,至關麻煩,也容易出錯。
2.利用 Maven,在 pom.xml 裏配置多個環境,每次編譯以前將 settings.xml 裏面修改爲當前要編譯的環境 ID。這種方式會事先設置好全部環境,缺點就是每次也須要手動指定環境,若是環境指定錯誤,發佈時是不知道的。
3.第三種方案就是本文重點介紹的,也是做者強烈推薦的方式。
首先,建立 application.yml 文件,在裏面添加以下內容:
spring: profiles: active: dev
含義是指定當前項目的默認環境爲 dev,即項目啓動時若是不指定任何環境,Spring Boot 會自動從 dev 環境文件中讀取配置信息。咱們能夠將不一樣環境都共同的配置信息寫到這個文件中。
而後建立多環境配置文件,文件名的格式爲:application-{profile}.yml,其中,{profile} 替換爲環境名字,如 application-dev.yml,咱們能夠在其中添加當前環境的配置信息,如添加數據源:
spring: datasource: url: jdbc:mysql://localhost:3306/database?useUnicode=true&characterEncoding=UTF-8&useSSL=true username: root password: root driverClassName: com.mysql.jdbc.Driver
這樣,咱們就實現了多環境的配置,每次編譯打包咱們無需修改任何東西,編譯爲 jar 文件後,運行命令:
java -jar api.jar --spring.profiles.active=dev
其中 --spring.profiles.active 就是咱們要指定的環境。
經常使用註解
咱們知道,Spring Boot 主要採用註解的方式,在上一篇的入門實例中,咱們也用到了一些註解。
本文,我將詳細介紹在實際項目中經常使用的註解。
@SpringBootApplication
咱們能夠注意到 Spring Boot 支持 main 方法啓動,在咱們須要啓動的主類中加入此註解,告訴 Spring Boot,這個類是程序的入口。如:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
若是不加這個註解,程序是沒法啓動的。
咱們查看下 SpringBootApplication 的源碼,源碼以下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { /** * Exclude specific auto-configuration classes such that they will never be applied. * @return the classes to exclude */ @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "exclude") Class<?>[] exclude() default {}; /** * Exclude specific auto-configuration class names such that they will never be * applied. * @return the class names to exclude * @since 1.3.0 */ @AliasFor(annotation = EnableAutoConfiguration.class, attribute = "excludeName") String[] excludeName() default {}; /** * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses} * for a type-safe alternative to String-based package names. * @return base packages to scan * @since 1.3.0 */ @AliasFor(annotation = ComponentScan.class, attribute = "basePackages") String[] scanBasePackages() default {}; /** * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to * scan for annotated components. The package of each class specified will be scanned. * <p> * Consider creating a special no-op marker class or interface in each package that * serves no purpose other than being referenced by this attribute. * @return base packages to scan * @since 1.3.0 */ @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses") Class<?>[] scanBasePackageClasses() default {}; }
在這個註解類上有3個註解,以下:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
所以,咱們能夠用這三個註解代替 SpringBootApplication,如:
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
其中,SpringBootConfiguration 表示 Spring Boot 的配置註解,EnableAutoConfiguration 表示自動配置,ComponentScan 表示 Spring Boot 掃描 Bean 的規則,好比掃描哪些包。
@Configuration
加入了這個註解的類被認爲是 Spring Boot 的配置類,咱們知道能夠在 application.yml 設置一些配置,也能夠經過代碼設置配置。
若是咱們要經過代碼設置配置,就必須在這個類上標註 Configuration 註解。以下代碼:
@Configuration public class WebConfig extends WebMvcConfigurationSupport{ @Override protected void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); registry.addInterceptor(new ApiInterceptor()); } }
不過 Spring Boot 官方推薦 Spring Boot 項目用 SpringBootConfiguration 來代替 Configuration。
@Bean
這個註解是方法級別上的註解,主要添加在 @Configuration 或 @SpringBootConfiguration 註解的類,有時也能夠添加在 @Component 註解的類。它的做用是定義一個Bean。
請看下面代碼:
@Bean public ApiInterceptor interceptor(){ return new ApiInterceptor(); }
那麼,咱們能夠在 ApiInterceptor 裏面注入其餘 Bean,也能夠在其餘 Bean 注入這個類。
@Value
一般狀況下,咱們須要定義一些全局變量,都會想到的方法是定義一個 public static 變量,在須要時調用,是否有其餘更好的方案呢?答案是確定的。下面請看代碼:
@Value("${server.port}") String port; @RequestMapping("/hello") public String home(String name) { return "hi "+name+",i am from port:" +port; }
其中,server.port 就是咱們在 application.yml 裏面定義的屬性,咱們能夠自定義任意屬性名,經過 @Value 註解就能夠將其取出來。
它的好處不言而喻:
1.定義在配置文件裏,變量發生變化,無需修改代碼。
2.變量交給Spring來管理,性能更好。
注: 本課程默認針對於對 SpringMVC 有所瞭解的讀者,Spring Boot 自己基於 Spring 開發的,所以,本文再也不講解其餘 Spring 的註解。
注入任何類
本節經過一個實際的例子來說解如何注入一個普通類,而且說明這樣作的好處。
假設一個需求是這樣的:項目要求使用阿里雲的 OSS 進行文件上傳。
咱們知道,一個項目通常會分爲開發環境、測試環境和生產環境。OSS 文件上傳通常有以下幾個參數:appKey、appSecret、bucket、endpoint 等。不一樣環境的參數均可能不同,這樣便於區分。按照傳統的作法,咱們在代碼裏設置這些參數,這樣作的話,每次發佈不一樣的環境包都須要手動修改代碼。
這個時候,咱們就能夠考慮將這些參數定義到配置文件裏面,經過前面提到的 @Value 註解取出來,再經過 @Bean 將其定義爲一個 Bean,這時咱們只須要在須要使用的地方注入該 Bean 便可。
首先在 application.yml 加入以下內容:
appKey: 1 appSecret: 1 bucket: lynn endPoint: https://www.aliyun.com
其次建立一個普通類:
public class Aliyun { private String appKey; private String appSecret; private String bucket; private String endPoint; public static class Builder{ private String appKey; private String appSecret; private String bucket; private String endPoint; public Builder setAppKey(String appKey){ this.appKey = appKey; return this; } public Builder setAppSecret(String appSecret){ this.appSecret = appSecret; return this; } public Builder setBucket(String bucket){ this.bucket = bucket; return this; } public Builder setEndPoint(String endPoint){ this.endPoint = endPoint; return this; } public Aliyun build(){ return new Aliyun(this); } } public static Builder options(){ return new Aliyun.Builder(); } private Aliyun(Builder builder){ this.appKey = builder.appKey; this.appSecret = builder.appSecret; this.bucket = builder.bucket; this.endPoint = builder.endPoint; } public String getAppKey() { return appKey; } public String getAppSecret() { return appSecret; } public String getBucket() { return bucket; } public String getEndPoint() { return endPoint; } }
而後在 @SpringBootConfiguration 註解的類添加以下代碼:
@Value("${appKey}") private String appKey; @Value("${appSecret}") private String appSecret; @Value("${bucket}") private String bucket; @Value("${endPoint}") private String endPoint; @Bean public Aliyun aliyun(){ return Aliyun.options() .setAppKey(appKey) .setAppSecret(appSecret) .setBucket(bucket) .setEndPoint(endPoint) .build(); }
最後在須要的地方注入這個 Bean 便可:
@Autowired private Aliyun aliyun;
攔截器
咱們在提供 API 的時候,常常須要對 API 進行統一的攔截,好比進行接口的安全性校驗。
本節,我會講解 Spring Boot 是如何進行攔截器設置的,請看接下來的代碼。
建立一個攔截器類:ApiInterceptor,並實現 HandlerInterceptor 接口:
public class ApiInterceptor implements HandlerInterceptor { //請求以前 @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { System.out.println("進入攔截器"); return true; } //請求時 @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { } //請求完成 @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { } }
@SpringBootConfiguration 註解的類繼承 WebMvcConfigurationSupport 類,並重寫 addInterceptors 方法,將 ApiInterceptor 攔截器類添加進去,代碼以下:
@SpringBootConfiguration public class WebConfig extends WebMvcConfigurationSupport{ @Override protected void addInterceptors(InterceptorRegistry registry) { super.addInterceptors(registry); registry.addInterceptor(new ApiInterceptor()); } }
異常處理
咱們在 Controller 裏提供接口,一般須要捕捉異常,並進行友好提示,不然一旦出錯,界面上就會顯示報錯信息,給用戶一種很差的體驗。最簡單的作法就是每一個方法都使用 try catch 進行捕捉,報錯後,則在 catch 裏面設置友好的報錯提示。若是方法不少,每一個都須要 try catch,代碼會顯得臃腫,寫起來也比較麻煩。
咱們可不能夠提供一個公共的入口進行統一的異常處理呢?固然能夠。方法不少,這裏咱們經過 Spring 的 AOP 特性就能夠很方便的實現異常的統一處理。
@Aspect @Component public class WebExceptionAspect { private static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class); //凡是註解了RequestMapping的方法都被攔截 @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)") private void webPointcut() { } /** * 攔截web層異常,記錄異常日誌,並返回友好信息到前端 目前只攔截Exception,是否要攔截Error需再作考慮 * * @param e * 異常對象 */ @AfterThrowing(pointcut = "webPointcut()", throwing = "e") public void handleThrowing(Exception e) { e.printStackTrace(); logger.error("發現異常!" + e.getMessage()); logger.error(JSON.toJSONString(e.getStackTrace())); //這裏輸入友好性信息 writeContent("出現異常"); } /** * 將內容輸出到瀏覽器 * * @param content * 輸出內容 */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); } catch (IOException e) { e.printStackTrace(); } writer.print(content); writer.flush(); writer.close(); } }
這樣,咱們無需每一個方法都添加 try catch,一旦報錯,則會執行 handleThrowing 方法。
優雅的輸入合法性校驗
爲了接口的健壯性,咱們一般除了客戶端進行輸入合法性校驗外,在 Controller 的方法裏,咱們也須要對參數進行合法性校驗,傳統的作法是每一個方法的參數都作一遍判斷,這種方式和上一節講的異常處理一個道理,不太優雅,也不易維護。
其實,SpringMVC 提供了驗證接口,下面請看代碼:
@GetMapping("authorize")//GetMapping是RequestMapping(method=method.GET)的組合 public void authorize(@Valid AuthorizeIn authorize, BindingResult ret){ if(result.hasFieldErrors()){ List<FieldError> errorList = result.getFieldErrors(); //經過斷言拋出參數不合法的異常 errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage())); } } public class AuthorizeIn extends BaseModel{ @NotBlank(message = "缺乏response_type參數") private String responseType; @NotBlank(message = "缺乏client_id參數") private String ClientId; private String state; @NotBlank(message = "缺乏redirect_uri參數") private String redirectUri; public String getResponseType() { return responseType; } public void setResponseType(String responseType) { this.responseType = responseType; } public String getClientId() { return ClientId; } public void setClientId(String clientId) { ClientId = clientId; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getRedirectUri() { return redirectUri; } public void setRedirectUri(String redirectUri) { this.redirectUri = redirectUri; } }
在 controller 的方法須要校驗的參數後面必須跟 BindingResult,不然沒法進行校驗。可是這樣會拋出異常,對用戶而言不太友好!
那怎麼辦呢?
很簡單,咱們能夠利用上一節講的異常處理,對報錯進行攔截:
@Component @Aspect public class WebExceptionAspect implements ThrowsAdvice{ public static final Logger logger = LoggerFactory.getLogger(WebExceptionAspect.class); //攔截被GetMapping註解的方法 @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)") private void webPointcut() { } @AfterThrowing(pointcut = "webPointcut()",throwing = "e") public void afterThrowing(Exception e) throws Throwable { logger.debug("exception 來了!"); if(StringUtils.isNotBlank(e.getMessage())){ writeContent(e.getMessage()); }else{ writeContent("參數錯誤!"); } } /** * 將內容輸出到瀏覽器 * * @param content * 輸出內容 */ private void writeContent(String content) { HttpServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()) .getResponse(); response.reset(); response.setCharacterEncoding("UTF-8"); response.setHeader("Content-Type", "text/plain;charset=UTF-8"); response.setHeader("icop-content-type", "exception"); PrintWriter writer = null; try { writer = response.getWriter(); writer.print((content == null) ? "" : content); writer.flush(); writer.close(); } catch (IOException e) { e.printStackTrace(); } } }
這樣當咱們傳入不合法的參數時就會進入 WebExceptionAspect 類,從而輸出友好參數。
咱們再把驗證的代碼單獨封裝成方法:
protected void validate(BindingResult result){ if(result.hasFieldErrors()){ List<FieldError> errorList = result.getFieldErrors(); errorList.stream().forEach(item -> Assert.isTrue(false,item.getDefaultMessage())); } }
這樣每次參數校驗只須要調用 validate 方法就好了,咱們能夠看到代碼的可讀性也大大的提升了。
接口版本控制
一個系統上線後會不斷迭代更新,需求也會不斷變化,有可能接口的參數也會發生變化,若是在原有的參數上直接修改,可能會影響線上系統的正常運行,這時咱們就須要設置不一樣的版本,這樣即便參數發生變化,因爲老版本沒有變化,所以不會影響上線系統的運行。
通常咱們能夠在地址上帶上版本號,也能夠在參數上帶上版本號,還能夠再 header 裏帶上版本號,這裏咱們在地址上帶上版本號,大體的地址如:http://api.example.com/v1/test,其中,v1 即表明的是版本號。具體作法請看代碼:
@Target({ElementType.METHOD,ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Mapping public @interface ApiVersion { /** * 標識版本號 * @return */ int value(); } public class ApiVersionCondition implements RequestCondition<ApiVersionCondition> { // 路徑中版本的前綴, 這裏用 /v[1-9]/的形式 private final static Pattern VERSION_PREFIX_PATTERN = Pattern.compile("v(\\d+)/"); private int apiVersion; public ApiVersionCondition(int apiVersion){ this.apiVersion = apiVersion; } @Override public ApiVersionCondition combine(ApiVersionCondition other) { // 採用最後定義優先原則,則方法上的定義覆蓋類上面的定義 return new ApiVersionCondition(other.getApiVersion()); } @Override public ApiVersionCondition getMatchingCondition(HttpServletRequest request) { Matcher m = VERSION_PREFIX_PATTERN.matcher(request.getRequestURI()); if(m.find()){ Integer version = Integer.valueOf(m.group(1)); if(version >= this.apiVersion) { return this; } } return null; } @Override public int compareTo(ApiVersionCondition other, HttpServletRequest request) { // 優先匹配最新的版本號 return other.getApiVersion() - this.apiVersion; } public int getApiVersion() { return apiVersion; } } public class CustomRequestMappingHandlerMapping extends RequestMappingHandlerMapping { @Override protected RequestCondition<ApiVersionCondition> getCustomTypeCondition(Class<?> handlerType) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(handlerType, ApiVersion.class); return createCondition(apiVersion); } @Override protected RequestCondition<ApiVersionCondition> getCustomMethodCondition(Method method) { ApiVersion apiVersion = AnnotationUtils.findAnnotation(method, ApiVersion.class); return createCondition(apiVersion); } private RequestCondition<ApiVersionCondition> createCondition(ApiVersion apiVersion) { return apiVersion == null ? null : new ApiVersionCondition(apiVersion.value()); } } @SpringBootConfiguration public class WebConfig extends WebMvcConfigurationSupport { @Bean public AuthInterceptor interceptor(){ return new AuthInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()); } @Override @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { RequestMappingHandlerMapping handlerMapping = new CustomRequestMappingHandlerMapping(); handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); return handlerMapping; } }
Controller 類的接口定義以下:
@ApiVersion(1) @RequestMapping("{version}/dd") public class HelloController{}
這樣咱們就實現了版本控制,若是增長了一個版本,則建立一個新的 Controller,方法名一致,ApiVersion 設置爲2,則地址中 v1 會找到 ApiVersion 爲1的方法,v2 會找到 ApiVersion 爲2的方法。
自定義 JSON 解析
Spring Boot 中 RestController 返回的字符串默認使用 Jackson 引擎,它也提供了工廠類,咱們能夠自定義 JSON 引擎,本節實例咱們將 JSON 引擎替換爲 fastJSON,首先須要引入 fastJSON:
<dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency>
其次,在 WebConfig 類重寫 configureMessageConverters 方法:
@Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); /* 1.須要先定義一個convert轉換消息的對象; 2.添加fastjson的配置信息,好比是否要格式化返回的json數據 3.在convert中添加配置信息 4.將convert添加到converters中 */ //1.定義一個convert轉換消息對象 FastJsonHttpMessageConverter fastConverter=new FastJsonHttpMessageConverter(); //2.添加fastjson的配置信息,好比:是否要格式化返回json數據 FastJsonConfig fastJsonConfig=new FastJsonConfig(); fastJsonConfig.setSerializerFeatures( SerializerFeature.PrettyFormat ); fastConverter.setFastJsonConfig(fastJsonConfig); converters.add(fastConverter); }
單元測試
Spring Boot 的單元測試很簡單,直接看代碼:
@SpringBootTest(classes = Application.class) @RunWith(SpringJUnit4ClassRunner.class) public class TestDB { @Test public void test(){ } }
模板引擎
在傳統的 SpringMVC 架構中,咱們通常將 JSP、HTML 頁面放到 webapps 目錄下面,可是 Spring Boot 沒有 webapps,更沒有 web.xml,若是咱們要寫界面的話,該如何作呢?
Spring Boot 官方提供了幾種模板引擎:FreeMarker、Velocity、Thymeleaf、Groovy、mustache、JSP。
這裏以 FreeMarker 爲例講解 Spring Boot 的使用。
首先引入 FreeMarker 依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>
在 resources 下面創建兩個目錄:static 和 templates,如圖所示:
其中 static 目錄用於存放靜態資源,譬如:CSS、JS、HTML 等,templates 目錄存放模板引擎文件,咱們能夠在 templates 下面建立一個文件:index.ftl(freemarker 默認後綴爲 .ftl),並添加內容:
<!DOCTYPE html> <html> <head> </head> <body> <h1>Hello World!</h1> </body> </html>
而後建立 PageController 並添加內容:
@Controller public class PageController { @RequestMapping("index.html") public String index(){ return "index"; } }
啓動 Application.java,訪問:http://localhost:8080/index.html,就能夠看到如圖所示: