前言

有触摸过starter组件吗?

信任我们在触摸Spring Boot的项目时,都遇见过像 spring-boot-starter-webspring-boot-starter-amqpmybatis-spring-boot-starter 等诸如此类的starter组件了吧。用过Spring Boot的会发现它最大的特色便是主动安装,凭借这一特色可以简化依靠,快速搭建项目。那么除了运用之外有没有想了解过怎么自界说一个这样的starter组件呢?

为什么会想自界说starter组件呢?

在实践开发中经常会发现现在开发的功用如同之前项目也开发过呢,那么这些功用能不能封装在一起,让每个项目都可以运用呢?

根据这样的需求,为了不重复写相同的功用,可以将需求封装的功用做成一个starter组件,这样在每次需求运用时只需求运用Maven依靠进来即可。

文章阐明

下面文章将会介绍的自界说starter组件是一个很简略很简略的starter组件完成。

这篇文章的意图在于让一些像我相同还没有触摸过自界说starter组件的读者们,领会一下自界说starter组件的编写进程。对于后续内容如果不感爱好可以直接跳过文章,终究希望这篇文章能对读者起到一点协助。


方针

当时自界说stater组件的运用场景

如何手写一个SpringBoot starter组件

如上图所示,这个自界说starter组件的方针是经过注解办法对指定的接口参数进行指定办法的校验

文章后续介绍的内容

  1. 怎么自界说注解以及元注解的相关常识
  2. 怎么结合注解与Spring AOP的切面办法完成校验
  3. 怎么让starter组件主动安装,怎么让starter组件可装备

完成

放一张项目整体结构图

如何手写一个SpringBoot starter组件

项目中的完整代码后续文章都会有,仔细看项目代码-XXX加粗字体下方便是完整代码。


1. 创立工程

命名标准:对于SpringBoot自带的starter一般命名为spring-boot-starter-xxx,对于第三方供给(自界说)的starter一般命名为xxx-spring-boot-starter。所以工程名取为check-spring-boot-starter

项目代码-项目依靠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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.lucas.check</groupId>
    <artifactId>check-spring-boot-starter</artifactId>
    <version>1.0</version>
    <name>check-spring-boot-starter</name>
    <description>接口参数校验组件</description>
    <packaging>jar</packaging>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 生成装备元数据 支撑 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <!-- 主动装备 支撑 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <!-- Spring AOP 支撑 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
    </dependencies>
</project>

2. 自界说校验注解

自界说校验注解相关代码设计

项目代码-校验注解代码

/**
 * 接口参数校验注解
 * @author 单程车票
 */
@Target(ElementType.METHOD) // 方针效果在办法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保存
public @interface DoCheck {
    // 校验办法 (枚举)
    Check value() default Check.Email;
    // 校验参数
    String arg() default "";
    // 过错信息
    String msg() default "";
}

其间的@Target@Retention是元注解,经过元注解限定自界说注解的润饰方针以及保存时刻。(有关元注解的运用后续会解说)

这儿自界说的校验注解有三个特点,分别是枚举类型的校验办法String类型的需求校验的参数名String类型的校验失利后回来的过错信息

项目代码-校验办法枚举类代码

/**
 * 校验枚举类
 * @author 单程车票
 */
public enum Check {
    // 邮箱校验办法
    Email("参数应为Email地址", CheckUtil::isEmail);
    public String msg;
    // 函数式接口 Object为传入参数类型,Boolean为回来类型
    public Function<Object, Boolean> function;
    Check(String msg, Function<Object, Boolean> function) {
        this.msg = msg;
        this.function = function;
    }
}

这儿为了简化代码,只完成了测验接口需求运用的校验办法(邮箱校验),有爱好的可以拓宽增加其他需求校验的枚举办法。

当时枚举类有两个特点,分别为 校验过错时回来的固定过错信息(也便是说如果注解中没有指定过错信息msg时,会运用枚举办法中自带的固定过错信息) 和 函数式接口(指定该接口入参为Object类型,回来参数为Boolean类型)

这样设计枚举类的原因是,经过完成函数式接口的办法界说校验逻辑(把Object类型的校验参数作为入参,校验完成后回来Boolean类型的回来成果)。这儿完成办法封装在了校验东西类中。

项目代码-校验东西类代码

/**
 * 校验东西类
 * @author 单程车票
 */
public class CheckUtil {
    /**
     * 运用正则表达式判别是否是邮箱格局
     */
    public static Boolean isEmail(Object value) {
        if(value == null) {
            return Boolean.FALSE;
        }
        if(value instanceof String) {
            String regEx = "^([a-z0-9A-Z]+[-|\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\.)+[a-zA-Z]{2,}$";
            Pattern p = Pattern.compile(regEx);
            Matcher m = p.matcher((String) value);
            if (m.matches()) {
                return Boolean.TRUE;
            }
        }
        return Boolean.FALSE;
    }
}

看完这个东西类,可以发现上述代码正是经过isEmail()办法完成的函数式接口,isEmail()办法内容便是邮箱校验逻辑。

总结

当需求运用校验逻辑时,会先经过注解信息(关于怎么获取注解信息后续会解说)获取到校验办法枚举类,然后经过校验办法枚举类获取到函数式接口,进而运用函数式接口的apply()办法完成校验逻辑。

// 注解信息(annotation)获取校验办法枚举类(value())获取函数式接口(function)调用apply()
// 实践上就相当于调用了CheckUtil的isEmial()办法。
Boolean res = annotation.value().function.apply(argValue);

经过这样的设计办法完成动态地获取注解中枚举办法然后获取需求的校验逻辑


弥补常识:元注解

元注解可以看作是润饰注解的注解类,罗列如下:

元注解 阐明
@Target 描绘注解的运用规模:分为ElementType.TYPE(类)、ElementType.FIELD(变量)、ElementType.METHOD(办法)、ElementType.PARAMETER(参数)、ElementType.CONSTRUCTOR(构造办法)、ElementType.LOCAL_VARIABLE(局部变量)、ElementType.ANNOTATION_TYPE(注解类)、ElementType.PACKAGE(包)、ElementType.TYPE_PARAMETER(类型参数)、ElementType.TYPE_USE(运用类型的任何地方)。
@Retention 描绘注解保存的时刻规模,分为:RetentionPolicy.SOURCE(源文件保存)、RetentionPolicy.CLASS(编译期保存)、RetentionPolicy.RUNTIME(运行期保存)。后者规模大于前者,一般运用运行期保存。
@Documented 描绘在运用javadoc东西为类生成帮组文档时是否需求保存注解信息。
@Inherited 效果是被它润饰的Annotation将具有继承性。如果某个类运用了被@Inherited润饰的Annotation,则其子类将主动具有该注解。
@Repeatable 效果是答应在同一申明类型(类,特点,或办法)的屡次运用同一个注解。
@Native 被其润饰的变量可以被本地代码引证。

弥补阐明一下元注解@Repeatable

Java8以后的新元注解(重复注解),如果想要在一个办法进行两次相同的注解会报错,可以经过该注解完成。接下来展现运用和不运用该注解的比照。

在Java8之前处理在一个办法上运用两次注解如下:

public @interface DoCheck {
    Check value() default Check.Email;
    String arg() default "";
}
public @interface DoChecks {
    DoCheck[] value();
}
// 运用@DoChecks包括@DoCheck的办法进行两次注解
@DoChecks({
    @DoCheck(value = Check.Email, argN = "email"),
    @DoCheck(value = Check.NotEmpty, arg = "email"),
})
@GetMapping("/test")
public R<String> sendEmail(@RequestParam("email") String email) {
    return R.success("发送成功");
}

在Java8后处理在一个办法上运用两次注解如下:

@Repeatable(DoChecks.class)
public @interface DoCheck {
    Check value() default Check.Email;
    String arg() default "";
}
public @interface DoChecks {
    DoCheck[] value();
}
// 只需求经过@Repeatable润饰@DoCheck即可直接在办法上进行屡次注解
@DoCheck(value = Check.Email, argN = "email")
@DoCheck(value = Check.NotEmpty, arg = "email")
@GetMapping("/test")
public R<String> sendEmail(@RequestParam("email") String email) {
    return R.success("发送成功");
}

经过@Repeatable可以进步代码可读性,仅此而已,仍然需求声明@DoChecks存储@DoCheck注解。

考虑到项意图主要意图仍是在于手写一个starter组件的进程,所以当时自界说的校验注解并未完成重复注解功用,有爱好的可以自行拓宽开发。


3. 结合Spring AOP切面办法完成注解校验

项目代码-切面代码

/**
 * aop切面办法(履行校验作业)
 * @author 单程车票
 */
@Aspect
public class DoCheckPoint {
    // 记载日志
    private final Logger log = LoggerFactory.getLogger(DoCheckPoint.class);
    /**
     * 自界说切入点
     * 切入点阐明:这样指定的切入点是任何一个履行的办法有一个 @DoCheck 注解的连接点(这儿连接点可以看是办法)
     */
    @Pointcut("@annotation(cn.lucas.check.annotation.DoCheck)")
    private void doCheckPoint() {}
    /**
     * 界说盘绕告诉
     */
    @Around("doCheckPoint()")
    public Object doCheck(ProceedingJoinPoint jp) throws Throwable {
        // 获取被润饰的办法信息
        Method method = getMethod(jp);
        // 获取办法的一切参数值
        Object[] args = jp.getArgs();
        // 获取办法的一切参数名
        String[] paramNames = getParamName(jp);
        // 获取注解信息
        DoCheck annotation = method.getAnnotation(DoCheck.class);
        // 获取需求校验的参数名
        String argName = annotation.arg();
        // 获取需求的校验办法枚举类
        Check value = annotation.value();
        // 获取需求回来的报错信息
        String msg = annotation.msg();
        // 判别是否未装备msg,未装备则直接运用枚举类的固定提示
        if ("".equals(msg)) msg = value.msg;
        // 获取需求校验的参数值
        Object argValue = getArgValue(argName, args, paramNames);
        // 记载日志
        log.info("校验办法:{} 校验值:{}", method.getName(), argValue);
        // 如果找不到需求校验的参数直接放行
        if (argValue == null) return jp.proceed();
        // 经过函数式接口传入需求校验的值, 内部会调用东西类的isEmail办法进行校验
        Boolean res = value.function.apply(argValue);
        if (res) {
            return jp.proceed(); // 校验成功则放行
        }else {
            // 校验失利抛出反常(带上过错信息msg)并交给调用方捕获(调用方:运用该注解的项目可以界说大局反常捕获,遇到IllegalArgumentException反常则回来对应报错信息)
            throw new IllegalArgumentException(msg);
        }
    }
    /**
     * 获取办法信息
     */
    public Method getMethod(ProceedingJoinPoint jp) throws NoSuchMethodException {
        MethodSignature methodSignature = (MethodSignature) jp.getSignature();
        return jp.getTarget().getClass() // 获取切入点的方针(被润饰办法)的Class方针
                .getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); // 经过办法名和办法参数类型运用反射获取到办法方针
    }
    /**
     * 获取办法的一切参数名字
     */
    private String[] getParamName(ProceedingJoinPoint jp) {
        MethodSignature methodSignature = (MethodSignature) jp.getSignature();
        return methodSignature.getParameterNames();
    }
    /**
     * 获取需求查验的参数值
     * @param target 需求校验的参数名
     * @param args 被润饰的办法中一切的参数
     * @param paramNames 被润饰的办法中一切的参数名
     */
    private Object getArgValue(String target, Object[] args, String[] paramNames){
        // 符号当时遍历的索引(由于args和paramNames是一一对应的)
        int idx = 0;
        // 遍历参数名
        for (String name : paramNames) {
            // 匹配对应的参数名则直接回来对应的参数值
            if (name.equals(target)) {
                return args[idx];
            }
            idx++;
        }
        return null;
    }
}

一眼看到上面这么臭的代码估量读者们也会感到厌烦导致一时半会无法了解校验内容,接下来会经过一点点的深化渐渐了解。


需求了解的常识一:AOP几个重要的编程术语

术语 介绍
切面(Aspect) 切面指穿插事务逻辑。常用的切面是告诉(Advice)。实践便是对主事务逻辑的一种增强。
连接点(JoinPoint) 连接点指可以被切面织入的具体办法。一般事务接口中的办法均为连接点。
切入点(Ponitcut) 切入点指声明的一个或多个连接点的调集。切入点指定切入的方位。经过切入点指定一组办法。被符号为final的办法是不能作为连接点与切入点的。由于终究的是不能被修正的,不能被增强的。
方针方针(Target) 方针方针指将要被增强的方针 。 即包括主事务逻辑的类的方针。
告诉(Advice) 告诉标明切面的履行时刻,Advice也叫增强。告诉界说了增强代码切入到方针代码的时刻点,是方针办法履行之前履行,仍是之后履行等。告诉类型不同,切入时刻不同。切入点界说切入的方位,告诉界说切入的时刻

需求了解的常识二:AOP的注解

注解 阐明
@Aspect 用来界说一个切面(Aspect)
@Pointcut 用于界说切入点(Ponitcut)表达式。在运用时还需求界说一个包括名字和恣意参数的办法签名来标明切入点名称,这个办法签名便是一个回来值为void,且办法体为空的一般办法。
@Before 用于界说前置告诉,在运用时,一般需求指定一个value特点的切入点表达式。前置告诉用于在某连接点履行之前履行的告诉
@AfterReturning 用于界说后置告诉,在运用时可以指定value和returning特点,其间value用于指定切入点表达式。后置告诉用于在某连接点正常履行完后履行的告诉
@Around 用于界说盘绕告诉,在运用时需求指定一个value特点,该特点用于指定切入点表达式。盘绕告诉用于包围一个连接点的告诉,可以在办法调用前后完成自界说的行为。它也会挑选是否继续履行连接点或直接回来它自己的回来值或抛出反常来完毕履行。
@AfterThrowing 用于界说反常告诉来处理程序中未处理的反常。在运用时可指定value和throwing特点。其间value用于指定切入点表达式,而throwing特点值用于指定一个形参名来标明Advice办法中可界说与此同名的形参,该形参可用于拜访方针办法抛出的反常。反常告诉是在办法抛出反常退出时履行的告诉。
@After 用于界说终究告诉,不管是否反常,在某连接点退出时履行的告诉。运用时需求指定一个value特点用于指定切入点表达式。

经过上面两个AOP的常识结合代码,就可以了解代码中的 @Aspect润饰该类标明该类是一个切面@Pointcut注解用于自界说切入点表达式,方便后续运用,@Around("doCheckPoint()")用于界说盘绕告诉,也便是切面的主要履行逻辑。


自界说切入点代码了解

/**
 * 自界说切入点表达式
 * 切入点阐明:这样指定的切入点是任何一个履行的办法有一个 @DoCheck 注解的连接点(这儿连接点可以看是办法)
 */
@Pointcut("@annotation(cn.lucas.check.annotation.DoCheck)")
private void doCheckPoint() {}

这儿运用@Pointcut自界说切入点表达式,在后续需求运用切入点表达式时只需求运用doCheckPoint()替代即可。@Pointcut中放入的是切入点表达式,这样自界说切入点就像是做一个大局切入点表达式,可以使得后续不必重复写切入点表达式。

这儿自界说的切入点表达式指定的切入方位是每一个被@DoCheck注解润饰的办法都是切入方位,都会履行切面事务。

有关切入点表达式这儿只能简略介绍,需求具体了解的可以查阅相关材料

// execution(拜访权限 办法回来值 办法声明(参数) 反常类型)
execution(modifiers-pattern ret-type-pattern declaring-type-pattern name-pattern(param-pattern) throws-pattern)
/*
对应的参数阐明:
    modifiers-pattern 拜访权限类型(可选(即可以不写))
    ret-type-pattern 回来值类型
    declaring-type-pattern 包名类名(可选)
    name-pattern(param-pattern) 办法名(参数类型和参数个数)
    throws-pattern 抛出反常类型(可选)
*/
// 下面放一些表达式比如
// 指定切入点为:恣意公共办法。
execution(public * *(..)) 
// 指定切入点为:任何一个以“set”开始的办法。
execution(* set*(..)) 
// 指定切入点为:界说在 service 包里的恣意类的恣意办法。
execution(* cn.lucas.service.*.*(..)) 
 // 指定一切包下的 serivce 子包下一切类(接口)中一切办法为切入点
execution(* *..service.*.*(..))
// 方针方针中有一个 @DoCheck 注解的恣意连接点
@target(cn.lucas.check.annotation.DoCheck)
// 任何一个履行的办法有一个 @DoCheck 注解的连接点
@annotation(cn.lucas.check.annotation.DoCheck)
// 任何一个只承受一个参数,而且运行时所传入的参数类型具有@DoCheck 注解的连接点
@args(cn.lucas.check.annotation.DoCheck)

盘绕告诉代码了解

/**
 * 界说盘绕告诉
 */
@Around("doCheckPoint()")
public Object doCheck(ProceedingJoinPoint jp) throws Throwable {

首要可以看到盘绕告诉@Around()的value值是"doCheckPoint()",即运用自界说的切入点标明式声明切面代码的切入方位,运用盘绕告诉声明切面代码的切入机遇

办法中带着一个参数ProceedingJoinPoint,可以了解成连接点(JoinPoint)的界说,在这儿经过它可以拿到注解润饰的办法Method信息,也可以拿到办法的参数名信息,参数值信息等。

获取注解润饰的办法信息,回来的是Method方针。

/**
 * 获取办法信息
 */
public Method getMethod(ProceedingJoinPoint jp) throws NoSuchMethodException {
    MethodSignature methodSignature = (MethodSignature) jp.getSignature();
    return jp.getTarget().getClass() // 获取切入点的方针(被润饰办法)的Class方针
            .getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); // 经过办法名和办法参数类型运用反射获取到办法方针
}

获取注解润饰的办法的一切参数名,回来的是String数组。

/**
 * 获取办法的一切参数名字
 */
private String[] getParamName(ProceedingJoinPoint jp) {
    MethodSignature methodSignature = (MethodSignature) jp.getSignature();
    return methodSignature.getParameterNames();
}

整理校验逻辑的进程

  1. 经过参数ProceedingJoinPoint参数可以拿到注解润饰的办法Method信息办法的一切参数名办法的一切参数值

    // 获取被润饰的办法信息
    Method method = getMethod(jp);
    // 获取办法的一切参数值
    Object[] args = jp.getArgs();
    // 获取办法的一切参数名
    String[] paramNames = getParamName(jp);
    
  2. 经过Method方针可以拿到注解信息Annotation方针(这是由于Method接口源码中完成了反射包下的AnnotatedElement接口的getAnnotation()办法,此办法可以获取到润饰Method办法的注解Annotation信息)。拿到注解信息即可动态的获取注解中的特点信息:校验办法枚举类、校验参数名、校验过错回来信息。

    // 获取注解信息
    DoCheck annotation = method.getAnnotation(DoCheck.class);
    // 获取需求校验的参数名
    String argName = annotation.arg();
    // 获取需求的校验办法
    Check value = annotation.value();
    // 获取需求回来的报错信息
    String msg = annotation.msg();
    
  3. 经过拿到的办法一切参数名、一切参数值、以及需求校验的参数名即可获取到需求校验的参数值。这儿经过循环比照参数名与需求校验的参数名共同时回来对应的参数值。逻辑很简略,后续读者想要拓宽可以考虑当传入参数为方针,而需求校验参数名为方针的特点时,怎么获取对应的参数值。

    /**
     * 获取需求查验的参数值
     * @param target 需求校验的参数名
     * @param args 被润饰的办法中一切的参数值
     * @param paramNames 被润饰的办法中一切的参数名
     */
    private Object getArgValue(String target, Object[] args, String[] paramNames){
        // 符号当时遍历的索引(由于args和paramNames是一一对应的)
        int idx = 0;
        // 遍历参数名
        for (String name : paramNames) {
            // 匹配对应的参数名则直接回来对应的参数值
            if (name.equals(target)) {
                return args[idx];
            }
            idx++;
        }
        return null;
    }
    
  4. 经过获取到需求校验的参数值,调用注解信息获取到的校验办法枚举类拿到对应的function函数式接口调用apply()即可进行参数值校验。终究经过是否校验成功进行判别是放行仍是抛出反常。

    // 经过函数式接口传入需求校验的值, 内部会调用东西类的isEmail办法进行校验
    Boolean res = value.function.apply(argValue);
    if (res) {
        return jp.proceed(); // 校验成功则放行
    }else {
        // 校验失利抛出反常由调用方捕获
        throw new IllegalArgumentException(msg);
    }
    

以上便是一切校验的逻辑以及怎么结合Spring AOP完成对注解的切面(校验)事务。


4. 完成starter组件主动安装以及可装备

Spring Boot主动安装原理

前面说过Spring Boot的特色便是主动安装,信任运用过Spring的读者们都领会过XML装备的杂乱,而运用Spring Boot会发现除了导入依靠之外,无需过多的装备即可运用,就算需求装备时也只需求经过装备类或装备文件进行简略装备即可,这样的便利归功于Spring Boot的主动安装。接下来就了解一下Spring Boot终究怎么完成了主动安装。

学过Java的应该都听过SPI机制(JDK内置的一种服务发现机制),而Spring boot正是根据SPI机制的办法对外供给了一套接口标准(当Spring Boot项目启动时,会扫描外部引入的Jar中的META-INF/spring.factories文件,将文件中装备的类信息安装到Spring容器中),让引入的Jar完成这套标准即可主动安装进Spring Boot中。

所以想要自界说的starter组件完成主动安装,只需求在项意图 resources 中创立 META-INF 目录,并在此目录下创立一个 spring.factories 文件,将starter组件的主动装备类的类途径写在文件上即可。

项目代码-META-INF/spring.factories

// 增加项意图主动装备类的类途径
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.lucas.check.config.CheckAutoConfigure

尽管已经处理了自界说starter完成主动安装,但是有爱好的仍是接着了解一下底层主动安装的进程。那么Spring Boot是怎么找到 META-INF/spring.factories 的文件并进行主动装备的呢?

深化源码解析主动安装进程

  1. Spring Boot项目在启动类上都会有一个 @SpringBootApplication 注解,这个注解可以看作是@Configuration@EnableAutoConfiguration@ComponentScan的调集,而主动安装原理就在其间的 @EnableAutoConfiguration 中,这个注解效果是敞开主动装备机制。
  2. @EnableAutoConfiguration 中完成主动安装中心功用的是@Import,经过加载主动安装类AutoConfigurationImportSelector完成主动安装功用。
  3. 深化 AutoConfigurationImportSelector 代码发现该类完成了ImportSelector接口同时完成了该接口的selectImports办法,这个办法用来获取一切契合条件的类的全限定名(为什么是契合条件,只在后续按需安装中阐明),而且将这些类加载进Spring容器中。
  4. 再深化selectImports办法中会发现这个getAutoConfigurationEntry()办法才是终究获取到 META-INF/spring.factories 文件的办法。
    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        // 第一步:先判别是否敞开了主动安装机制
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            // 第二步:获取@SpringBootApplication中需求扫除的类(exclude和excludeName特点),经过这两个特点扫除指定的不需求主动安装的类
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            // 第三步:获取需求主动安装的一切装备类
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }
    

到这儿便是SpringBoot的主动安装原理整个进程(可能描绘的不完整,有爱好的可以去找找材料看看)。

抛出问题:刚刚提到的Spring Boot只会加载契合条件的类是怎么回事?

Spring Boot其实并不会加载 META-INF/spring.factories 文件下的一切类,而是按需加载,怎么个按需加载呢?

Spring Boot可以经过@ConditionalOnXXX满足条件时加载(即按需加载),下面罗列一些常用的注解:

  • @ConditionalOnBean:当容器里存在指定Bean时,实例化(加载)当时Bean。
  • @ConditionalOnMissingBean:当容器里不存在指定Bean时,实例化(加载)当时Bean。
  • @ConditionalOnClass:当类途径下存在指定类时,实例化(加载)当时Bean。
  • @ConditionalOnMissingClass:当类途径下不存在指定类时,实例化(加载)当时Bean。
  • @ConditionalOnProperty:装备文件中指定的value特点是指定的havingValue特点值时,实例化(加载)当时Bean。

项目代码-主动装备类

/**
 * 主动装备类
 * @author 单程车票
 */
@Configuration
// 留意@ConditionalOnProperty注解要放在后面两个注解的前面,这样才会优先经过装备文件判别是否要敞开主动安装。
@ConditionalOnProperty(value = "check.enabled", havingValue = "true") 
@ConditionalOnClass(CheckProperties.class)
@EnableConfigurationProperties(CheckProperties.class)
public class CheckAutoConfigure {
    /**
     * 运用装备Bean的办法运用DoCheckPoint切面
     */
    @Bean
    @ConditionalOnMissingBean
    public DoCheckPoint point() {
        return new DoCheckPoint();
    }
}

解说代码:

  1. @Configuration注解:标注这个注解的类可以看成是装备类(也可以看为是Bean方针的工厂),主要效果便是将Bean方针注入容器中。
  2. @ConditionalOnProperty(value = "check.enabled", havingValue = "true")注解:当引入当时starter的项意图装备文件中呈现check.enabled=true(不呈现也行,由于默认便是true)时,主动安装当时装备类。当装备文件中呈现的是check.enabled=false时,则不安装该类,则无法运用切面,则starter组件失效。(坑点:该注解一定要放前面,优先经过装备文件判别是否需求敞开主动安装,而后再判别其他条件)
  3. @ConditionalOnClass(CheckProperties.class)注解:上面解说过,便是当CheckProperties呈现在类途径下时主动安装当时装备类。
  4. @EnableConfigurationProperties(CheckProperties.class)注解:让运用了@ConfigurationProperties注解的类收效。
  5. 这儿运用@Bean的办法将切面注入Spring容器中,使得切面收效得以运用。(还有另外一种将切面注入容器的办法,即在切面类上加@Component注解,而且在主动装备类上增加@ComponentScan(basePackages = "cn.lucas.check.*")用于扫描切面类的@Component并将切面Bean增加进容器中)具体运用哪种办法注入切面类Bean都行。

项目代码-装备文件读取类

/**
 * 读取装备文件的信息类
 * @author 单程车票
 */
@ConfigurationProperties("check") // 用于读取装备文件前缀为check的特点
public class CheckProperties {
    // 默以为true,标明敞开校验
    private boolean enabled = true;
    public boolean isEnabled() {
        return enabled;
    }
    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }
}

到此完成了对自界说Spring Boot starter的主动安装以及可经过装备文件的check.enabled操控是否敞开starter组件(是否加载该starter装备类)。


测验

完成了对自界说starter的编写后,还需求对starter进行测验,确保功用能正常运用,所以接下来新建一个项目可以命名为check-spring-boot-starter-test

需求先将编写好的自界说的starter的jar包安装到本地maven库房中。

如何手写一个SpringBoot starter组件


测验项目编写过程

  1. 引入依靠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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>cn.lucas.check</groupId>
    <artifactId>check-spring-boot-starter-test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>check-spring-boot-starter-test</name>
    <description>校验测验服务</description>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.5.RELEASE</version>
        <relativePath/>
    </parent>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!-- 引入自界说的starter -->
        <dependency>
            <groupId>cn.lucas.check</groupId>
            <artifactId>check-spring-boot-starter</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 创立共同封装成果类
/**
 * 共同封装成果类
 * @author 单程车票
 */
@Data
public class R<T> implements Serializable {
    private Integer code;
    private String msg;
    private T data;
    public static <T> R<T> success(int code, String msg, T data) {
        R<T> result = new R<>();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    public static <T> R<T> fail(int code, String msg, T data) {
        R<T> result = new R<>();
        result.setCode(code);
        result.setMsg(msg);
        result.setData(data);
        return result;
    }
    public static <T> R<T> success(T data) {
        return success(200, "操作成功", data);
    }
    public static <T> R<T> fail(String msg) {
        return fail(500, msg, null);
    }
}
  1. 创立大局反常处理类(用来处理查验失利捕获参数反常)
/**
 * 大局反常处理
 * @author 单程车票
 */
@Slf4j
@ResponseBody
@ControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
    /**
     * 捕获参数反常信息
     * @param ex 反常信息
     * @return R
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public R<String> illegalArgumentException(IllegalArgumentException ex){
        log.info(ex.getMessage());
        return R.fail(ex.getMessage());
    }
}
  1. 创立TestController用于接口测验
/**
 * 测验
 * @author 单程车票
 */
@Slf4j
@RestController
public class TestController {
    @DoCheck(value = Check.Email, arg = "email", msg = "邮箱格局不正确!")
    @GetMapping("/test")
    public R<String> sendEmail(@RequestParam("email") String email) {
        log.info("校验参数:{}", email);
        return R.success("发送成功");
    }
}
  1. 装备文件application.yml
# 端口号
server:
  port: 8888
# 操控校验开关
check:
  enabled: true

测验一:装备文件中敞开校验,传入接口参数为正确邮箱格局

经过postman东西测验接口,校验成功用够成功发送,预期成果共同。

postman成果图

如何手写一个SpringBoot starter组件

操控台信息图

如何手写一个SpringBoot starter组件

操控台打印starter的日志以及接口的日志,阐明校验成功而且履行了接口办法。


测验二:装备文件中敞开校验,传入接口参数为过错邮箱格局

经过postman东西测验接口,校验失利不可以成功发送,预期成果共同。

postman成果图

如何手写一个SpringBoot starter组件

操控台信息图

如何手写一个SpringBoot starter组件

操控台打印报错信息,阐明校验失利。


测验三:装备文件中关闭校验,传入接口参数为过错邮箱格局

# 端口号
server:
  port: 8888
# 操控校验开关
check:
  enabled: false

经过postman东西测验接口,无法校验,直接发送成功,预期成果共同。

postman成果图

如何手写一个SpringBoot starter组件

操控台信息图

如何手写一个SpringBoot starter组件

可以看到只打印了接口的日志,并没有打印starter中的日志,阐明starter未启用。