開發自己的Spring Boot Starter(元註解,AOP,Spring Boot Starter實戰)

前言

我們都知道可以使用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包即可。

更多面試乾貨,互聯網內推信息請微信關注公衆號“雲計算平臺技術”

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章