SpringBoot 自定义starter的三种方式
阅读原文时间:2021年04月21日阅读:1

虽然自定义的starter与版本无关,但还是说明一下版本

SpringBoot 版本2.1.4.RELEASE

1、命名问题

由于官方提供的starter,命名格式为spring-boot-starter-xxx,为与官方的starter区分开来,官方建议自定义的starter命名方式为xxx-spring-boot-starter,也仅仅是建议。

2、starter的实现原理

SpringBoot官方的starter,和自定义的starter,基本都是利用java的SPI思想。在SpringBoot的自动装配过程中,最终会加载classpath目录下所有的META-INF/spring.factories文件,查看任一个starter,应该都有该文件。加载spring.factories文件的代码定位:

1、SpringApplication构造函数 
2、this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
   this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
3、getSpringFactoriesInstances
4、SpringFactoriesLoader.loadFactoryNames(静态方法)
5、loadSpringFactories
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") :ClassLoader.getSystemResources("META-INF/spring.factories");

在spring.factories文件中查看所有的ApplicationContextInitializer.class和ApplicationListener.class文件。之所以说是基本是利用SPI思想,是因为不配置spring.factories文件,也是可以完成starter开发的。

3、自定义starter的方式

3.1、SPI机制

在spring.factories文件中配置EnableAutoConfiguration,如下所示:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wit.sc.support.configuration.SupportAutoConfiguration

其中SupportAutoConfiguration是starter配置文件的类限定名,多个之间逗号分割。

3.2、实现ImportSelector接口

定义一个Enable注解,如下所示,类似@EnableEurekaClient、@EnableAsync等注解。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableSupportImportSelector.class)
@Documented
public @interface EnableSupport {

    @AliasFor("autowired")
    boolean value() default true;

    /**
     * 是否注入
     * @return
     */
    @AliasFor("value")
    boolean autowired() default true;
}

在注解中使用Import注解(spring4.2之后的注解),导入相应的bean到spring容器中,该类需要实现ImportSelector接口,在该接口中让SupportAutoConfiguration配置类生效,到目前为止,和在spring.factories文件中配置此类的效果是一样的。

public class EnableSupportImportSelector implements ImportSelector {

    private static final Log logger = LogFactory.getLog(EnableSupportImportSelector.class);

    /**
     * support配置类
     */
    public static final String SUPPORT_DEFAULT_CONFIGURATION = "com.wit.sc.support.configuration.SupportAutoConfiguration";

    /**
     * support启动配置
     */
    public static final String SUPPORT_ENABLE_ANNOTATION = "com.wit.sc.support.configuration.annotation.EnableSupport";

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        MultiValueMap<String, Object> valueMap =  importingClassMetadata.getAllAnnotationAttributes(SUPPORT_ENABLE_ANNOTATION);
        List<Object> enableFalgList = valueMap.get("value");
        boolean enableFlag = (boolean) enableFalgList.get(0);
        if(!enableFlag) {
            return new String[]{};
        }
        Set<String> configuration = new HashSet<>();
        configuration.add(SUPPORT_DEFAULT_CONFIGURATION);
        String[] configComponent =new String[configuration.size()];
        configuration.toArray(configComponent);
        return enableFlag ? new String[]{SUPPORT_DEFAULT_CONFIGURATION} : new String[]{};
    }
}

这样使用虽然比较麻烦,但是starter可以控制,也就是通过以设置参数,让starter生效或失效。比如设置value设置为false,即可让start失效或者不加注解,而配置spring.factories,只要导入相应的包,一定会生效,不想用的办法,只能删除starter包

3.3、SPI机制结合Enable注解

但似乎越来越多的starter使用的是这种方式,这种方式的简单,也容易控制,相当于结合了前面两种的优势,具体实现如下:
···
1、配置spring.factories
2、定义Enable注解,不用实现ImportSelector接口的复杂逻辑,只要通过注解导入的配置类,将一个bean注入到spring容器中即可
···
以配置中心的注解@EnableConfigServer为例。
1、spring.factories配置EnableAutoConfiguration

org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.springframework.cloud.config.server.config.ConfigServerAutoConfiguration

2、定义Enable注解,引入配置类,在配置中将Mark注入到spring容器,mark不用实现任何逻辑

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ConfigServerConfiguration.class})
public @interface EnableConfigServer {
}

@Configuration
public class ConfigServerConfiguration {
    public ConfigServerConfiguration() {
    }
    @Bean
    public ConfigServerConfiguration.Marker enableConfigServerMarker() {
        return new ConfigServerConfiguration.Marker();
    }
    class Marker {
        Marker() {
        }
    }
}

3、用ConditionalOnBean注解实现开关的效果

4、starter能实现的功能

starter有以下功能
1、提供公共接口。
2、为引入starter的模块,提供被component注解的单例
3、实现aop切面,完成系统日志的功能
4、添加过滤器,实现公共的逻辑处理
5、如果确定引入starter的服务的spring容器中,会有某一类型的bean,starter模块也可以引用(通过Resource注解),即starter和引入他的应用之,bean是可以相互注入的

4.1、EnableAutoConfiguration

@ComponentScan(basePackages = {"com.wit.sc.support"})
@EnableConfigurationProperties(value = SupportConstant.class)
public class SupportAutoConfiguration {

    Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource
    private SupportConstant supportConstant;

    @Bean
    public SupportCommandLineRunner supportCommandLineRunner() {
        return new SupportCommandLineRunner(this.supportConstant);
    }

    @Bean
    public RequestContextListener requestContextListenerBean() {
        return new RequestContextListener();
    }

    /**
     * 从filters变量中获取的过滤器名称为pioneerFilter
     * @return
     */
    @Bean
    public FilterRegistrationBean pioneerFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new PioneerFilter());
        FilterConstant pioneerFilterConstant = new FilterConstant();
        if(!CollectionUtils.isEmpty(supportConstant.getFilters())) {
            pioneerFilterConstant = supportConstant.getFilters().get(PioneerFilter.FILTE_RNAME);
        }
        registration.setEnabled(pioneerFilterConstant.isEnable());
        if(!StringUtils.isEmpty(pioneerFilterConstant.getFilterName())) {
            //设置过滤器名称
            registration.setName(pioneerFilterConstant.getFilterName());
        }
        if(!StringUtils.isEmpty(pioneerFilterConstant.getPattern())) {
            //设置过滤器拦截路径
            List<String> urlPattern = Arrays.asList(StringUtils.split(pioneerFilterConstant.getPattern(), FilterConstant.COMMA));
            registration.setUrlPatterns(urlPattern);
        }
        if(pioneerFilterConstant.getOrder() != null) {
            registration.setOrder(pioneerFilterConstant.getOrder());
        }
        Map<String, String> properties = pioneerFilterConstant.getProperties();
        if(!CollectionUtils.isEmpty(properties)) {
            registration.setInitParameters(properties);
        }
        return registration;
    }
}

  EnableAutoConfiguration是整个starter的关键,通过这个文件,可以使用注解@EnableConfigurationProperties导入其他的属性文件,也可以通过注解@ComponentScan扩大spring容器扫描包的范围,还可以直接将bean注入到spring容器中。
  由于注解ComponentScan,让在starter中定义接口成为可能。在该文件中注入bean,还可以添加过滤器等。
  过滤器可以用来拦截请求的特定参数,比如分页参数,分页查询的pageSize不得大于50,可以设置在过滤器中,而不用每个接口都去做逻辑判断。
  但这是starter,会被很多服务使用的,可能设置的最大分页50,不符合其他系统的要求,必须可以进行覆盖设置。
而EnableConfigurationProperties导入的类,就可以存放属性。

4.2、maven依赖

 <dependency>
    <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-autoconfigure</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-configuration-processor</artifactId>
     <optional>true</optional>
 </dependency>

有了这两个依赖,让引用starter的人知道,你这个starter有哪些属性,在设置属性时,会有提示。效果如下图所示

提示括号中的描述,就是属性对应的注释,毕竟,别人导入jar包,使用的时,去看源码会显得不太方便,最好是能通过注释,让使用的人知道如何使用,属性是什么含义。

被EnableConfigurationProperties导入的属性类代码如下

@ConfigurationProperties(prefix = "support")
public class SupportConstant {
    private Method method = new Method();

    /**
     * filter
     * PioneerFilter -> pioneerFilter
     * support.filters[pioneerFilter].filterName=pioneerFilter
     * support.filters[pioneerFilter].enable=false
     * support.filters[pioneerFilter].properties[pageSize]=51
     * support.filters[pioneerFilter].properties[whiteUrls]=127.0.0.1,192.168.1.1
     */
    private Map<String, FilterConstant> filters = new HashMap<>();
}

建议

参数尽量设置默认值,这也是springboot核心思想,约定大于配置。比如有一个变量,需要根据实际情况,确定参数设置成什么值最好,让别人尽量可以不配置
一个参数就可以使用这个starter模块,而当别人发现默认的配置无法满足自己要求的时候,自己又可以进行设置。比如有一个参数pageSize,默认设置成100,
如果用户配置了,会默认覆盖,不用做任何逻辑。我们先做 int pageSize = 100,使用者后做pageSize=50,所以是可以覆盖的。如果是整型,不设置初始化
都无法使用,难道非要用户设置一个值,程序才可以运行吗,这就违背了springboot的初衷。比如引入redis模块,甚至不用设置一个参数,就可以使用,
极其方便
private int database = 0;
private String url;
private String host = "localhost";
private String password;
private int port = 6379;

自定义的starter结构如下图所示:

实现了切面、过滤器、自定义日志入库、CommandLineRunner、接口等。实现了两个接口、只要引入了这个starter的,都会含有这两个接口,也可以通过

 @Autowired
 InterfaceHandler interfaceHandler;

引入starter中的实例。

@RestController
public class SupportController {

    @Autowired
    InterfaceHandler interfaceHandler;

    /**
     * 返回所有接口
     * @param request
     * @return
     */
    @GetMapping("interfaces")
    public Object interfaces(HttpServletRequest request) {
        ServletContext servletContext = request.getSession().getServletContext();
        return interfaceHandler.getAllRequestToMethodItems(servletContext);
    }

    /**
     * 返回所有接口路径
     * @param request
     * @return
     */
    @GetMapping("interfaceUrls")
    public Object interfaceUrls(HttpServletRequest request) {
        ServletContext servletContext = request.getSession().getServletContext();
        return interfaceHandler.getAllInterfaceUrls(servletContext);
    }
}

接口访问效果如下图所示


两个接口的功能是查询当前应用的所有接口,可见也包含interfaceUrls和interfaces接口。
自定义starter github地址(第二种方式)

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器

你可能感兴趣的文章