先了解一下什么是 starter:一个 starter其实就是对一个功能的集成封装,然后对外提供一个依赖,让业务去使用,像我们熟悉的 Redis,mongo,mybatis 等均属于。
编写完starter后,可以提交到公司私有仓库供其他项目组进行调用。
由于任何人都可以编写自己的 starter,为了区分官方的 starter 和个人的 starter,通常在命名上面会有一个规范。SpringBoot 官方提出规范如下:
作为前缀:spring-boot-starter-xxx
比如:spring-boot-starter-web…
作为后缀:xxx-spring-boot-starter
比如:test-spring-boot-starter
自动装配首先要有一个配置类,其次还要有 spring.factories 文件,所以这两步是必不可少的
编写配置类首先要添加一个自动装配的依赖,然后再编写对应的配置类和业务实现类,在 pom 中添加如下依赖
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.4.RELEASEversion>
parent>
<modelVersion>4.0.0modelVersion>
<groupId>com.examplegroupId>
<artifactId>mydateutil-spring-boot-starterartifactId>
<version>1.0version>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-autoconfigureartifactId>
<version>2.7.1version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.9version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-loggingartifactId>
dependency>
dependencies>
我们要编写一个提供被springboot管理的自动配置的stater,首先我们要做的是编写配置类
package com.example.starter.config;
import com.example.starter.service.MyStaterTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnClass({MyStaterTemplate.class})
@EnableConfigurationProperties(MyStaterProperties.class)
public class MyStaterAutoConfiguration {
@Autowired
MyStaterProperties myStaterProperties;
@Bean
@ConditionalOnMissingBean(MyStaterTemplate.class)
public MyStaterTemplate myJsonService() {
MyStaterTemplate myStaterTemplate = new MyStaterTemplate();
myStaterTemplate.setPrefix(myStaterProperties.getProPre());
return myStaterTemplate;
}
}
package com.example.starter.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "nata.prac")
public class MyStaterProperties {
private String proPre;
public String getProPre() {
return proPre;
}
public void setProPre(String proPre) {
this.proPre = proPre;
}
}
编写一个 指定年月获取月份最后一天日期 的功能,同时返回调用
package com.example.starter.service;
import com.example.starter.utils.BaseController;
import org.apache.commons.lang3.StringUtils;
import java.text.SimpleDateFormat;
import java.util.Calendar;
public class MyStaterTemplate extends BaseController {
public static final String FORMAT_DATE_YEAR_MONTH_DAY = "yyyy-MM-dd";//年月日
private String prefix;
/**
* 指定年月获取月份最后一天日期
* @param dateTime
* @return
*/
public String getLastDayOfMonth(String dateTime) {
if (StringUtils.isBlank(dateTime) || dateTime.length() > 7) {
return "时间为空或不符合规范,时间格式应为:YYYY-MM";
}
//格式化日期
SimpleDateFormat sdf = new SimpleDateFormat(FORMAT_DATE_YEAR_MONTH_DAY);
Calendar calendar = Calendar.getInstance();
try {
int year = Integer.parseInt(dateTime.substring(0,4));
int month = Integer.parseInt(dateTime.substring(5,7));
//设置年份
calendar.set(Calendar.YEAR, year);
//设置月份
calendar.set(Calendar.MONTH, month-1);
//获取某月最大天数
int last_day = calendar.getActualMaximum(Calendar.DATE);
//设置日历中月份的最大天数
calendar.set(Calendar.DAY_OF_MONTH, last_day);
} catch (Exception e) {
logger.error("指定年月获取月份最后一天日期获取异常" + e.getMessage());
}
return this.prefix + "---" + sdf.format(calendar.getTime());
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
}
简单说明一下上面三个类都是干什么的
(1)MyHashTemplate 该类是实际业务需要注入的类,用来对入参进行 MD5 摘要,然后返回一个拼接了前缀的字符串给业务。这个前缀是通过 application.properties 中配置
ziyou.hash.hashPre=JavaGeekTech666 配置后传递过来的。
(2)MyHashProperties是接受属性值的类
(3)MyHashAutoConfiguration 则是自动装配类,这个类会根据条件进行 MyHashTemplateBean 的初始化,并将前缀进行赋值
定义完配置类后,需要将这个bean暴露出来。这么做的原因也是因为被@SpringbootApplication标注的类,除非定义了扫描的包路径,不然默认是扫描被@SpringbootApplication标注的类所在的包路径。
这也是为什么官方建议启动类放置在最外层的包,是为了保证当前项目中定义的bean都能被springboot扫描并加载。
最后还需要在 resource 文件中编写一个 META-INF/spring.factories 文件,内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.starter.config.MyStaterAutoConfiguration
前面的 Key 是固定写法,后面的 value 就是配置类的全路径引用地址。
1、为什么加了**@Configuration注解还是要配置META-INF/spring.factories呢?因为springboot项目默认只会扫描本项目下的带@Configuration注解的类**,如果自定义starter,不在本工程中,是无法加载的,所以要配置META-INF/spring.factories配置文件。
2、为什么配置了META-INF/spring.factories配置文件就可以加载?这里才是springboot实现starter的关键点,springboot的这种配置加载方式是一种类SPI(Service
Provider
Interface)的方式,SPI可以在META-INF/services配置接口扩展的实现类,springboot中原理类似,只是名称换成了spring.factories而已。
编写完了 starter 过后,我们再创建一个新的 web 应用,在其中增加我们编写的 starter 来验证是否成功。
<dependency>
<groupId>com.examplegroupId>
<artifactId>mydateutil-spring-boot-starterartifactId>
<version>1.0version>
dependency>
package com.example.demo.controller;
import com.example.demo.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@GetMapping(value = "/getLastDayOfMonth")
public String getLastDayOfMonth(@RequestParam("dateTime") String dateTime) {
return helloService.getLastDayOfMonth(dateTime);
}
}
package com.example.demo.service;
import com.example.starter.service.MyStaterTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class HelloService {
@Autowired
private MyStaterTemplate myStaterTemplate;
public String getLastDayOfMonth(String dateTime) {
return myStaterTemplate.getLastDayOfMonth(dateTime);
}
}
nata.prac.proPre=PropertiesInfo
我们访问地址 http://127.0.0.1:8080/getLastDayOfMonth?dateTime=2022-08
可以看到效果如下。
自己编写的 starter 已经成功生效了,只不过功能目前很单一,我们完全可以根据自己需要的实际功能来实现一个复杂一点的 starter 来提供开箱即用的效果。
在一些大公司特别是一些中间件或者中台团队,很多时候都是提供各种工具的 starter 来给业务团队使用的,毕竟很多重复的功能如果每个业务团队都自己开发的话,完全是浪费资源,通过中台团队统一封装 starter,让各个业务团队拿来就用快速搭建业务即可。
(1)发送邮件、短信验证码等常用功能可以统一成一个starter,其他项目要使用时直接依赖即可调用
(2)数据库mapper层可以抽取封装成一个starter,其他项目需要使用数据库时直接依赖即可,不在需要每个项目单独提供数据库链接配置类
(3)登录、鉴权之类的公共功能同样可以抽取封装成一个starter,这样就不用每个项目都写一遍了
@ConditionalOnBean 仅在当前上下文中存在某个bean时,才会实例化这个bean
@ConditionalOnClass 某个class位于类路径上,才会实例化这个Bean
@ConditionOnExpression 当表达式为true的时候,才会实例化这个Bean
@ConditionalOnMissBean 仅在上下文中不存在某个Bean时才会实例化这个Bean
@ConditionalOnMissClass 某个class类路径上不存在,才会实例化这个Bean
@ConditionalOnNotWebApplication,不是web应用时才会实例化这个Bean
@ConditionalOnProperty,指定的属性是否有指定的值
@ConditionalOnResource,类路径是否有指定的值
@AutoConfigureAfter,在某个bean完成自动配置后实例化这个bean
@AutoConfigureBefore,在某个bean完成自动配置前实例化这个bean
@EnableConfigurationProperties(MyStaterProperties.class),自动注入属性文件
Starter的核心主要就是@Import和@Conditional
(1) @Import负责引入配置类
(2) @Conditional负责引入的类,初始化成对象的条件.