提到Spring Boot时,很多人想到的是它的自动化装配特性。当我们项目需要Redis、MongoDB时,只需要引入相应的 spring-boot-starter-data-redis、spring-boot-starter-data-mongodb包,Spring 自然会把这些redisTemplate、mongoTemplate 放进IOC容器管理。我们只需要在application.properties文件中关注对应的 url、password等基础配置信息。 当然。这些Starter的实现思想仍然还是约定大于配置。
在自定义Starter 之前,先了解下什么是SPI,像在Dubbo、Spring中,好多拓展性接口都基于SPI实现。
SPI 即Service provider interface,服务提供发现接口,可能还是有点抽象。 现在我们有一个场景
先定义一个接口 Person
public interface Person {
void eatFood();
void doSport();
}
然后定义它的实现类
Doctor类
public class Doctor implements Person {
[@Override](https://my.oschina.net/u/1162528)
public void eatFood() {
System.out.println("医生————>多吃蔬菜");
}
[@Override](https://my.oschina.net/u/1162528)
public void doSport() {
System.out.println("医生————>多多休息");
}
}
Student 类
public class Student implements Person {
[@Override](https://my.oschina.net/u/1162528)
public void eatFood() {
System.out.println("学生————>多喝牛奶");
}
[@Override](https://my.oschina.net/u/1162528)
public void doSport() {
System.out.println("学生————>多做运动");
}
}
现在我们怎么拿到实现Person 接口的所有 类,并选择性调用呢,
就可以通过SPI
首先在resources 下创建 META-INF 文件包,然后在META-INF下创建services文件包,再创建文件com.tcwong.service.Person ,文件名即接口全路径名称,文件内容
com.tcwong.impl.Student
com.tcwong.impl.Doctor
通过ServiceLoader加载
public class SpiDemoApplication {
public static void main(String[] args) {
ServiceLoader<Person> serviceLoader = ServiceLoader.load(Person.class);
serviceLoader.forEach(person -> {
person.doSport();
person.eatFood();
});
}
}
就可以拿到所有实现类了。我们不妨假想,ServiceLoader 通过扫描指定的META-INF下的services包,拿到里面的配置信息,通过反射就可以实现。 实际上面的就是SPI机制,即约定大于配置。
实际Spring Boot 的Starter也是这种原理,只是Spring结合IOC特性,把实现类Bean统一管理,我们注入后直接使用即可。
现在自定义一个redis-starter
引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.2.6.RELEASE</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
</dependency>
Redis操作类
public class MyRedis<V> {
private String url;
private String port;
private String password;
public MyRedis(String url, String port, String password) {
// 配置参数校验
if (url == null){
throw new RuntimeException("myRedis连接url不能为空");
}
if (port == null){
throw new RuntimeException("myRedis连接port不能为空");
}
if (password == null){
throw new RuntimeException("myRedis连接password不能为空");
}
this.url = url;
this.port = port;
this.password = password;
System.out.println(MessageFormat.format("myRedis已建立连接,地址:{0} 端口:{1} 密码:{2}",url,port,password));
}
/**
* Description 模拟redis存储
*
* [@param](https://my.oschina.net/u/2303379) key 键
* @param value 值
* @return 值 JSON
* @author tcwong
* @date 2021/3/19
*/
public String setValue(String key,V value){
String valueString = JSONObject.toJSONString(value);
System.out.println(MessageFormat.format("保存成功,key:{0} value:{1}",key,valueString));
return valueString;
}
}
Redis连接参数
@ConfigurationProperties(prefix = "myredis")
public class MyRedisProperty {
private String url;
private String port;
private String password;
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
统一交给SpringIOC容器管理
@Configuration
@ConditionalOnClass(Jedis.class)
@EnableConfigurationProperties(MyRedisProperty.class)
public class MyRedisAutoConfiguration {
@Resource private MyRedisProperty myRedisProperty;
@Bean
MyRedis myRedis(){
MyRedis<Object> myRedis = new MyRedis<>(myRedisProperty.getUrl()
, myRedisProperty.getPort()
, myRedisProperty.getPassword());
return myRedis;
}
}
其中注解 @ConditionalOnClass(Jedis.class) 只有在引入包Jedis 时,当前配置才会生效。
在resources 文件包下 创建 META-INF包,创建文件 spring.factories
文件内容
org.springframework.boot.autoconfigure.EnableAutoConfiguration =
com.tcwong.config.MyRedisAutoConfiguration
maven install 打jar包。
构建 Spring-demo 测试项目
引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.tcwong</groupId>
<artifactId>myredis-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
在 application.properties 配置 myredis 连接信息
server.port = 8080
myredis.url = 177.03.789.104
myredis.port = 9377
myredis.password = myredis*123456
定义redis 存储实体 Student 类
public class Student {
private String id;
private String name;
private Integer age;
public Student() {
}
public Student(String id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
RedisController 接口
@RestController
public class RedisController {
@Resource private MyRedis myRedis;
@PostMapping("/redis")
public String redis(@RequestBody Student student){
return myRedis.setValue(student.getId(),student);
}
}
启动项目
redis 已正常连接
接口 http://localhost:8080/redis
{
"age": 1,
"id": "123",
"name": "苏苏"
}
myredis-starter 成功。
扩展:
如何在 application.properties 文件中,增加提示
只需要在自定义starter 时,
引入包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.6.RELEASE</version>
<optional>true</optional>
</dependency>
在META-INF 下创建文件 additional-spring-configuration-metadata.json
文件内容
{
"properties": [
{
"name": "myredis.url",
"type": "java.lang.String",
"defaultValue": "127.0.0.1",
"description": "连接地址."
},
{
"name": "myredis.port",
"type": "java.lang.String",
"defaultValue": "6379",
"description": "端口号."
},
{
"name": "myredis.password",
"type": "java.lang.String",
"defaultValue": "123456",
"description": "密码."
}
]
}
既可以自定义提示信息