starter介绍 Spring Boot Starter是什么?
Spring Boot Starter可以被理解为一种依赖的集合,也可以看作是一个空的项目,它由pom.xml 文件配置了一堆jar包的组合。
Spring Boot Starter解决了什么问题?
Spring Boot Starter解决了手动配置大量依赖项和参数的问题。在Spring Boot之前,如果要开发一个Web应用程序,需要手动添加很多依赖项,如Servlet、JSP、JSTL等,并且还需要配置很多参数,如数据源、事务管理器等。而通过使用Spring Boot Starter,开发者只需要添加一个Starter依赖,就可以轻松地集成各种不同的功能模块,而无需关心底层的配置和集成细节。
Spring Boot Starter的价值是什么?
Spring Boot Starter的价值在于它能够提高开发效率和代码质量,同时减少开发成本和复杂度。通过使用Starter,开发者可以专注于业务逻辑的实现,而不需要关心底层的配置和集成细节。另外,Starter还支持更快的迭代和部署,因为它们通常包含了一些可重用的依赖库和自动配置类。
SpringBoot starter封装方法 第1步:定义一个XXXProperties的类文件 用于抽象化原有的配置属性与增加新的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @ConfigurationProperties(GrowingioProperties.GROWINGIO_PREFIX) @Data public class GrowingioProperties { public static final String GROWINGIO_PREFIX = "growingio" ; private String apiHost; private String projectId; private Integer sendMsgInterval = 100 ; private Integer sendMsgThread = 3 ; private Integer msgStoreQueueSize = 500 ; private Boolean compress = true ; private String loggerLevel = "debug" ; private String loggerImplemention = "com.my.growingio.log.GrowingioLogger" ; private String runMode = "test" ; private Integer connectionTimeout = 2000 ; private Integer readTimeout = 2000 ; private Boolean enable = false ; }
第2步:定义XXXAutoConfiguration的类文件 将核心的业务处理类,初始化核心业务处理类并注入到IOC中,通常写在XXXAutoConfiguration的类文件文件中。
GrowingioAutoConfiguration这个类主要是方便做bean的注册,@ComponentScan这个注解会扫描并加载属性basePackages指定的包路径下所有bean,就不用在spring.factories文件逐个写了,只用写这个类就行了。@SpringBootApplication启动类注解也使用了@ComponentScan。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Configuration @EnableConfigurationProperties(GrowingioProperties.class) @ComponentScan("com.my.growingio") public class GrowingioAutoConfiguration { private static final Logger logger = LoggerFactory.getLogger(GrowingioAutoConfiguration.class); @Autowired protected GrowingioProperties growingioProperties; @Bean public GrowingioService growingioService () { return new GrowingioServiceImpl (); } public void checkProperties () { if (StringUtils.isEmpty(growingioProperties.getApiHost())){ throw new RuntimeException ("growing properties api.host must be defined" ); } if (StringUtils.isEmpty(growingioProperties.getProjectId())){ throw new RuntimeException ("growing properties project.id must be defined" ); } } @PostConstruct private void init () { this .checkProperties(); this .initGrowingioApiProperties(); } private void initGrowingioApiProperties () { Properties properties = new Properties (); properties.setProperty(GrowingioConstant.API_HOST_KEY, growingioProperties.getApiHost()); properties.setProperty(GrowingioConstant.PROJECT_ID_KEY, growingioProperties.getProjectId()); properties.setProperty(GrowingioConstant.SEND_MSG_INTERVAL_KEY, growingioProperties.getSendMsgInterval().toString()); properties.setProperty(GrowingioConstant.SEND_MSG_THREAD_KEY, growingioProperties.getSendMsgThread().toString()); properties.setProperty(GrowingioConstant.MSG_STORE_QUEUE_SIZE_KEY, growingioProperties.getMsgStoreQueueSize().toString()); properties.setProperty(GrowingioConstant.COMPRESS_KEY, growingioProperties.getCompress().toString()); properties.setProperty(GrowingioConstant.LOGGER_LEVEL_KEY, growingioProperties.getLoggerLevel()); properties.setProperty(GrowingioConstant.LOGGER_IMPL_KEY, growingioProperties.getLoggerImplemention()); properties.setProperty(GrowingioConstant.RUN_MODE_KEY, growingioProperties.getRunMode()); properties.setProperty(GrowingioConstant.CONNECTION_TIMEOUT_KEY, growingioProperties.getConnectionTimeout().toString()); properties.setProperty(GrowingioConstant.READ_TIMEOUT_KEY, growingioProperties.getReadTimeout().toString()); ConfigUtils.init(properties); logger.info("init load growingio starter api properties success,url:{},runmode:{},enable:{}" , growingioProperties.getApiHost(),growingioProperties.getRunMode(),growingioProperties.getEnable()); } }
第3步:声明一个spring.factories的文件 为了防止使用者与Starter中包名路径不一致,声明一个spring.factories的文件,来提供一种扫描类到IOC中的途径。
resources包下手动创建一个META-INF文件夹,并且在包下创建一个spring.factories文件,文件内容写,注意空格(使用Idea会有提示)
1 org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.my.growingio.config.GrowingioAutoConfiguration
在Spring Boot 2.7后,这个文件过时了,后续版本会取消,那么新版本的约定是怎么样的规则呢,这里以wxjava的spring-boot-starter组件为例,仓库地址如下:
https://github.com/Wechat-Group/WxJava/tree/develop/spring-boot-starters/wx-java-miniapp-spring-boot-starter
在该工程示例中,在resources文件下定义了如下文件:
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
在该文件中直接定义了实现类,接口声明在体现在了文件名字上。
1 com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration
简化配置: 新的方法可能旨在简化配置过程,使得自动配置和服务的管理更加直观和易于理解。
性能优化: 改变 SPI 文件的规则可能是为了提高应用启动和运行时的性能。
增加灵活性: 新的机制可能提供了更大的灵活性,允许更精细的控制和定制。
SpringBoot starter 新玩法 定义一个XXXXEnable模式+@Import模式的注解 用于控制Starter是否生效与动态注册对象Bean到IOC容器中。这里以rocketmq的spring-boot-starter为例,
首先我们在自定义好一个autoconfiguration类后,如果不想让客户端自动装配上,可以提供一个Enable命名为开头的类,来通过这种方式启用装配,代码样例如下:
1 2 3 4 5 6 7 8 9 10 11 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(RocketMQAutoConfiguration.class) public @interface EnableRocketMQ {}
我们定义好这样的一个注解,使用时在启动类上面进行声明即可。然后再Auto装配类中,我们还可以结合ConditionalOnProperty注解来表达,某个属性等于某个值的时候,才触发某些装配,代码如下所示:
1 2 3 4 5 6 7 8 @Bean @ConditionalOnClass(DefaultMQProducer.class) @ConditionalOnMissingBean(DefaultMQProducer.class) @ConditionalOnProperty(prefix = "spring.rocketmq", value = {"nameServer", "producer.group"}) public DefaultMQProducer mqProducer (RocketMQProperties rocketMQProperties) {return producer;}
注解的使用 使用多种注解,来区分当前starter组件中的先后顺序、环境区分、兼容性等等。
@Profile注解:用于区分环境来加载不同的自动装配类
@EnableConfigurationProperties注解:用于装配导入一个属性配置文件,通常结合@ConfigurationProperties来使用
@ConditionalOnClass注解:用于标识当前类路径中存在某个类的时候,才触发自动装配类
@ConditionalOnMissingClass注解:用于标识当前类路径中不存在某个类的时候,才触发
@ConditionalOnMissingBean注解:用于标识当前IOC容器中不存在某个Bean的时候,才触发
@AutoConfigureAfter、@AutoConfigureBefore注解:用于控制先后顺序的注解
基于 AOP 注解实现对某些接口或配置的自动拦截、代码增强 自定义一个AOP类和一个注解,用于动态标识哪些方法进行业务埋点操作,避免一定程度上的代码侵入。
这里以一个Redis的ratelimiter-spring-boot-starter的限流的Starter组件为例,仓库地址如下:
https://github.com/taptap/ratelimiter-spring-boot-starter
首先,可以定义一个自定义注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Target(value = {ElementType.METHOD}) @Retention(value = RetentionPolicy.RUNTIME) public @interface RateLimit { Mode mode () default Mode.TIME_WINDOW; int rate () ; }
然后,为这个注解定义一个AOP拦截类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Aspect @Component @Order(0) public class RateLimitAspectHandler { private static final Logger logger = LoggerFactory.getLogger(RateLimitAspectHandler.class); private final RateLimiterService rateLimiterService; private final RuleProvider ruleProvider; public RateLimitAspectHandler (RateLimiterService lockInfoProvider, RuleProvider ruleProvider) { this .rateLimiterService = lockInfoProvider; this .ruleProvider = ruleProvider; } @Around(value = "@annotation(rateLimit)") public Object around (ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { Rule rule = ruleProvider.getRateLimiterRule(joinPoint, rateLimit); Result result = rateLimiterService.isAllowed(rule); boolean allowed = result.isAllow(); if (!allowed) { logger.info("Trigger current limiting,key:{}" , rule.getKey()); if (StringUtils.hasLength(rule.getFallbackFunction())) { return ruleProvider.executeFunction(rule.getFallbackFunction(), joinPoint); } long extra = result.getExtra(); throw new RateLimitException ("Too Many Requests" , extra, rule.getMode()); } return joinPoint.proceed(); } }
这段的核心配置其实是@Around(value = “@annotation(rateLimit)”)这个代码,通过这个环绕通知的切面拦截,可以实现一种AOP的Starter的自动增强处理。
然后在AutoConfiguration类中,去导入这个AOP类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration @ConditionalOnProperty(prefix = RateLimiterProperties.PREFIX, name = "enabled", havingValue = "true") @AutoConfigureAfter(RedisAutoConfiguration.class) @EnableConfigurationProperties(RateLimiterProperties.class) @Import({RateLimitAspectHandler.class, RateLimitExceptionHandler.class}) public class RateLimiterAutoConfiguration { private final RateLimiterProperties limiterProperties; public final static String REDISSON_BEAN_NAME = "rateLimiterRedissonBeanName" ; public RateLimiterAutoConfiguration (RateLimiterProperties limiterProperties) { this .limiterProperties = limiterProperties; } }
SpringBoot 自动装配 @SpringBootApplication的源码如下:
1 2 3 4 5 6 7 8 9 10 @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 {}
@ComponentScan的作用是扫描被 @Component @Service @Controller注解的bean,注解会默认扫描该类所在包下的所有类
@SpringBootConfiguration 的源码如下:
它的核心就是 @Configuration,允许在上下文中注册额外的bean或导入其他配置类
1 2 3 4 5 6 7 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration @Indexed public @interface SpringBootConfiguration {}
@EnableAutoConfiguration是启用SpringBoot的自动配置机制的关键
可以看到,@EnableAutoConfiguration注解通过 Spring 提供的 @Import 注解导入了 AutoConfigurationImportSelector 类
AutoConfigurationImportSelector 类中的 getCandidateConfigurations 方法会将所有自动配置类信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration" ; Class<?>[] exclude() default {}; String[] excludeName() default {}; }
1 2 3 4 5 6 7 protected List<String> getCandidateConfigurations (AnnotationMetadata metadata, AnnotationAttributes attributes) { List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), 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; }
有了自动配置信息以后,自动配置还差 @Conditional 注解。
拿 Spring Security 的自动配置举个例子:SecurityAutoConfiguration 中导入了 WebSecurityEnablerConfiguration类,WebSecurityEnablerConfiguration源代码如下:
1 2 3 4 5 6 7 8 @Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN) @ConditionalOnClass(EnableWebSecurity.class) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) @EnableWebSecurity class WebSecurityEnablerConfiguration {}
WebSecurityEnablerConfiguration 类中使用了 @ConditionalOnClass 指定了容器中必须还有 EnableWebSecurity 类。