前言
我們都知道可以使用SpringBoot快速的開發基於Spring框架的項目。由於圍繞SpringBoot存在很多開箱即用的Starter依賴,使得我們在開發業務代碼時能夠非常方便的、不需要過多關注框架的配置,而只需要關注業務即可。
例如我想要在SpringBoot項目中集成web,那麼我只需如下兩步:
第一步 要加入spring-boot-starter-web的依賴並簡單配置一下信息
<!-- spring-boot-starter-web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
第二步 在啓動類或配置類上增加一個註解即可完成
@GetMapping("/user/user_id")
這爲我們省去了之前很多的配置操作,有些功能的開啓只需關心業務邏輯
是不是特別方便?
也許,你可以對面試官說你熟(略)練(懂)使(皮)用(毛)SpringBoot了。
但是你有沒有想過自己開發一個炫酷功能的starter被別人拿來引用呢?比如 spring-boot-starter-cache
@Cache(key = "user_id", action = "用戶查詢", type = "redis")
只要加上這個註解,系統便會自動利用redis對數據庫進行緩存查詢優化,而不是在業務中進行代碼優化。
我相信只要大家花三分鐘看完這篇文章一定可以開發出來的。
原理淺談
從spring boot啓動類說起,衆所周知啓動類會有@SpringBootApplication註解
@SpringBootApplication
public class EdgeApplication {
public static void main(String[] args) {
SpringApplication.run(EdgeApplication.class, args);
}
}
ctrl+alt+鼠標左鍵查看註解源碼
@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 {
我們看下@EnableAutoConfiguration這個註解:
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
通過@AutoConfiguration啓用Spring應用程序上下文的自動配置,這個註解會導入一個AutoConfigurationImportSelector的類,而這個類會去讀取一個spring.factories下key爲EnableAutoConfiguration對應的全限定名的值。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
這個spring.factories裏面配置的那些類,主要作用是告訴Spring Boot這個stareter所需要加載的那些xxxAutoConfiguration類,也就是你真正的要自動註冊的那些bean或功能。然後,我們實現一個spring.factories指定的類,標上@Configuration註解,一個starter就定義完了。
概括 SpringBoot 在啓動時會去依賴的 starter 包中尋找 /META-INF/spring.factories 文件,然後根據文件中配置的路徑去掃描項目所依賴的 Jar 包,這類似於 Java 的 SPI 機制。
實戰說明
需求
之前系統前後端傳參使用VO,如果微服務場景下各個服務頻繁發版,其他服務調用方改動會很大,因此後端改用了Map<String, String> paramMap數據類型來傳遞參數,請求進入到controller層後要在每一個方法內進行參數校驗,如果我們可以開發一個starter,只需加上註解即可實現參數校驗,就會減少很多冗餘代碼。
效果
通過數組註解實現參數校驗使用示例
@GetMapping("/product")
@ParamCheck(name = "paramMap", params = {
@ParamCheck.Param(name = "requireProductCategory", type = Integer.class),
@ParamCheck.Param(name = "requireProductDetail", type = Integer.class, required = false),
@ParamCheck.Param(name = "productName", type = String.class, required = true)})
public ResultModel query(@RequestParam Map<String, String> paramMap) {
// 業務邏輯
ResultModel resultModel = new ResultModel();
resultModel.setMsg("success");
return resultModel;
}
驗證效果
請求
http://localhost:8080/product
響應
{
code: 10001,
msg: "參數校驗失敗"
}
請求
http://localhost:8080/product?productName=%E8%8B%B9%E6%9E%9C
響應
{
code: 0,
msg: "success"
}
實戰開發
爲了方便,大家可以先把demo代碼克隆下來https://github.com/aaa081215/parametercheck-spring-boot-starter.git
命名規則
官方命名:spring-boot-starter-xxxx
非官方命名:xxxx-spring-boot-starter
這裏我們使用非官方命名 parametercheck-spring-boot-starter (避免將來spring-boot官方使用你的starter而重名)
開發步驟
第一步 新建 maven 工程
相信各位小夥都非常熟練,不多說了,省略
第二步 添加 pom 依賴,完整 pom.xml 文件如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>parametercheck-spring-boot-starter</artifactId>
<groupId>9421.top</groupId>
<version>1.0.1-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>7</source>
<target>7</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
</dependencies>
</project>
僅需引入spring-boot-autoconfigure即可,由於此項目用到了aop等,所以引用了多個依賴。
第三步 編寫上文曾提到的spring.factories文件
- resource 目錄下,創建 META-INF 目錄
- META-INF 目錄下創建 spring.factories 文件
完整spring.factories文件如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
hiweek.autoconfig.VerifyAnnotationAutoConfiguration
第四步 新建自動配置類,VerifyAnnotationAutoConfiguration 類如下
類名爲spring.factories聲明的類
@Aspect
@Configuration
public class VerifyAnnotationAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public ParamAspect paramAspect() {
return new ParamAspect();
}
/**
* aop切入點
*/
@Around("@annotation(hiweek.verify.annotation.ParamCheck)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
return paramAspect().around(joinPoint);
}
}
解釋:
@Aspect聲明切面
@Configuration 相當於把該類作爲spring的xml配置文件中的,配置spring容器(應用上下文)
@ConditionalOnMissingBean 判斷是否執行初始化代碼,即如果用戶已經創建了bean,則相關的初始化代碼不再執行。
@Around("@annotation(hiweek.verify.annotation.ParamCheck)")
切入點爲hiweek.verify.annotation.ParamCheck註解
也就是說當執行到有ParamCheck註解的方法時,會執行環繞通知paramAspect().around(joinPoint);
第五步 自定義元註解
現在starter配置基本完成了,接下來開發上面代碼中的自定義註解hiweek.verify.annotation.ParamCheck
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ParamCheck {
/**
* 存放參數的變量名
*/
String name();
/**
* 要校驗的參數
*/
Param[] params();
/**
* 自定義註解,用來描述要校驗的參數信息
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@interface Param {
/**
* 參數名
*/
String name();
/**
* 是否必須
*/
boolean required() default false;
/**
* 參數類型
*/
Class type();
/**
* 參數格式
*/
String format() default "";
}
}
解釋:
@Target({ElementType.METHOD})
作用:用於描述註解的使用範圍(即:被描述的註解可以用在什麼地方)
取值(ElementType)有:
- 1.CONSTRUCTOR:用於描述構造器
- 2.FIELD:用於描述域
- 3.LOCAL_VARIABLE:用於描述局部變量
- 4.METHOD:用於描述方法
- 5.PACKAGE:用於描述包
- 6.PARAMETER:用於描述參數
- 7.TYPE:用於描述類、接口(包括註解類型) 或enum聲明
@Retention(RetentionPolicy.RUNTIME)
- 1、RetentionPolicy.SOURCE:註解只保留在源文件,當Java文件編譯成class文件的時候,註解被遺棄
- 2、RetentionPolicy.CLASS:註解被保留到class文件,但jvm加載class文件時候被遺棄,這是默認的生命週期
- 3、RetentionPolicy.RUNTIME:註解不僅被保存到class文件中,jvm加載class文件之後,仍然存在
第六步 ParamAspect 業務邏輯開發(配置環繞通知,校驗參數是否合法)
ParamAspect
完整代碼
public class ParamAspect {
/**
* 配置環繞通知。
* 校驗參數是否合法
*
* @param joinPoint 切入點
* @return 執行結果
*/
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
ResultModel resultModel = new ResultModel();
// 1 對包含ParamCheck註解的方法進行操作
if (method.isAnnotationPresent(ParamCheck.class)) {
// 2 獲取註解paramCheckName要校驗的參數名
ParamCheck paramCheck = method.getAnnotation(ParamCheck.class);
String paramCheckName = paramCheck.name();
// 3 從joinPoint中獲得獲取方法的所有參數名
String[] parameterNames = methodSignature.getParameterNames();
// 4 根據註解中要校驗的參數名與joinPoint中所有的參數名,獲取到要校驗的數據的下標
int paramMapIndex = ArrayUtils.indexOf(parameterNames, paramCheckName);
if (paramMapIndex == -1) {
resultModel.setFailed(CommonStatusCode.PARAM_INVALID, "要校驗的方法參數名應與註解字段相同");
return resultModel;
}
// 5 根據joinPoint獲取方法的所有元素與下標,拿到真實值
Object[] args = joinPoint.getArgs();
Map<String, String> realParamMap = (Map<String, String>) args[paramMapIndex];
int index = 0;
ParamCheck.Param[] params = paramCheck.params();
String[] paramArray = new String[params.length];
boolean[] requiredArray = new boolean[params.length];
Class[] classArray = new Class[params.length];
String[] format = new String[params.length];
// 6 獲得註解中定義的校驗規則
for (ParamCheck.Param paramItem : params) {
paramArray[index] = paramItem.name();
requiredArray[index] = paramItem.required();
classArray[index] = paramItem.type();
format[index] = paramItem.format();
index++;
}
try {
StringUtils.checkParam(realParamMap, paramArray, requiredArray, classArray, format);
} catch (ParamException e) {
resultModel.setFailed(CommonStatusCode.PARAM_INVALID, e.getMessage());
return resultModel;
}
}
return joinPoint.proceed();
}
}
至此開發以及完成。
結束語
在大型微服務系統開發中,會有一個總的cloud-spring-boot-starter包,內部包含項目所有用到的所有spring-boot-starter如spring-boot-starter-logging,spring-boot-starter-data-redis,org.springframework.amqp…這樣任何微服務只需引入cloud-spring-boot-starter包即可。
更多面試乾貨,互聯網內推信息請微信關注公衆號“雲計算平臺技術”