1.6 案例:需求
自定义redis-starter。要求当导入redis坐标时,SpringBoot自动创建Jedis的Bean
案例:实现步骤
- 创建 redis-spring-boot-autoconfigure 模块
- 导入依赖
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>redis-spring-boot-autoconfigure</artifactId> <version>0.0.1-SNAPSHOT</version> <name>redis-spring-boot-autoconfigure</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!--引入jedis依赖--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- 创建 redis-spring-boot-starter 模块,依赖 redis-springboot-autoconfigure的模块
- 导入依赖
<dependency> <groupId>com.example</groupId> <artifactId>redis-spring-boot-autoconfigure</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
- 在 redis-spring-boot-autoconfigure 模块中初始化 Jedis 的 Bean。并定义META-INF/spring.factories 文件
package com.example.redisspringbootautoconfigure.config; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.Jedis; @Configuration @EnableConfigurationProperties(RedisProperties.class) public class RedisAutoConfiguration { @Bean public Jedis jedis(RedisProperties redisProperties){ return new Jedis(redisProperties.getHost(),redisProperties.getPort()); } }
package com.example.redisspringbootautoconfigure.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @ConfigurationProperties(prefix = "redis") public class RedisProperties { private String host = "localhost"; private int port = 6379; public String getHost() { return host; } public void setHost(String host) { this.host = host; } public int getPort() { return port; } public void setPort(int port) { this.port = port; } }
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.redisspringbootautoconfigure.config.RedisAutoConfiguration
META-INF / spring.properties
- 在测试模块中引入自定义的 redis-starter 依赖,测试获取 Jedis 的Bean,操作 redis。
package com.example.springbootday02enable; import com.example.springbootday02enableother.config.EnableUser; import com.example.springbootday02enableother.config.MyImportBeanDefinitionRegistrar; import com.example.springbootday02enableother.config.MyImportSelector; import com.example.springbootday02enableother.config.UserConfig; import com.example.springbootday02enableother.domain.User; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Import; import org.springframework.stereotype.Component; import redis.clients.jedis.Jedis; /** * @ComponentScan 扫描范围:当前引导类所在包及其子包 * ```java com.itoldlu.springbootenable com.itoldlu.config //1.使用@ComponentScan扫描com.itoldlu.config包 //2.可以使用@Import注解,加载类。这些类都会被Spring创建,并放入IOC容器 //3.可以对Import注解进行封装。 Import4中用法: 1. 导入Bean 2. 导入配置类 3. 导入ImportSelector的实现类。 4. 导入ImportBeanDefinitionRegistrar实现类 @SpringBootApplication //@ComponentScan(basePackages="com.example.springbootday02enableother") //@Import(UserConfig.class) //@EnableUser //@Import(User.class) //@Import(UserConfig.class) //@Import(MyImportSelector.class) //@Import(MyImportBeanDefinitionRegistrar.class) public class SpringbootDay02EnableApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(SpringbootDay02EnableApplication.class, args); // Object user = run.getBean(User.class); // System.out.println(user); // Object user1 = run.getBean("user"); // System.out.println(user1); Jedis bean = run.getBean(Jedis.class); System.out.println(bean); bean.set("test","oldlu"); System.out.println(bean.get("test")); } @Bean public Jedis jedis(){ return new Jedis("localhost",6379); } }
package com.example.redisspringbootautoconfigure.config; 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; import redis.clients.jedis.Jedis; @Configuration @EnableConfigurationProperties(RedisProperties.class) @ConditionalOnClass(Jedis.class) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(Jedis.class) public Jedis jedis(RedisProperties redisProperties){ System.out.println("RedisAutoConfiguration...."); return new Jedis(redisProperties.getHost(),redisProperties.getPort()); } }
2 自定义start
SpringBoot核心是maven依赖管理和start,我们自定义的start可以引入第三方依赖,并且提供自定义bean对象交给Spring容器管理。其他项目直接引入自定义start,就可以省去引入第三方依赖和手动创建bean的繁琐操作,其实这里每个第三方公司想要更好的兼容springboot,一般每导入一个第三方的依赖,除了本身的jar包以外,还会有一个 xxx-spring-boot-autoConfigure,这个就是第三方依赖自己编写的自动配置类,然后都会通过注解被自动扫描后注入bean,自定义start没什么实际意义只是加以区别即可!
2.1 如何区分自定义start和官方start
官方start的命名规范是spring-boot-starter-**
比如下面的这个web依赖,而自定义start名字一般是**-spring-boot-start
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
2.2 自定义start的原理
SpringBoot在启动的时候会扫描所有依赖或者说jar包中resources/META-INF/spring.factories文件,并会将该文件中配置的类注入到IOC容器中
步骤
1.在自定义的start项目中声明一个配置类,在该配置类中配置一个bean
2.将该配置类通过pringframework.boot.autoconfigure.EnableAutoConfiguration配置到spring.factories文件中即可
2.3 自定义start功能描述及实现需求
现在需要自定义个start,功能是提供一个Person对象。在其他Springboot模块中引入该start,并且获取该bean对象
1.创建一个配置文件
package com.example.config; import com.example.Cat; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class CatAutoConfiguration { @Bean public Cat cat(){ return new Cat("tom",12); } }
2.添加resources/META-INF/spring.factories配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.config.CatAutoConfiguration
2.4 代码升级
升级原因
cat()方法中返回的对象的属性是写死的,最好改成可配置的
升级步骤
1.在Cat类上添加@ConfigurationProperties(prefix = “cat”)注解,引用配置文件中的数据,代码如下
import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.Objects; @ConfigurationProperties(prefix = "cat") public class Cat { private String name; private Integer age; //..get/set.. }
2.在配置类中使用EnableConfigurationProperties引入Cat配置文件,并将cat注入到cat方法中代码如下
@Configuration @EnableConfigurationProperties(Cat.class) public class CatAutoConfiguration { @Bean public Cat cat(Cat cat){ return new Cat(cat.getName(),cat.getAge()); } }
注意事项:此种方式只有配置文件在SpringBoot项目才生效,在当前start项目的配置文件中不生效
2.5 注意事项
start项目的pom中不能有build标签,否则引入该start的springboot项目不能正常打包
2.6 总结
SpringBoot启动时会自动搜索包含spring.factories文件的JAR包;
根据spring.factories文件加载自动配置类AutoConfiguration;
通过AutoConfiguration类,加载满足条件(@ConditionalOnXxx)的bean到Spring IOC容器中;
使用者可以直接使用自动加载到IOC的bean。
小伙伴们是否想起曾经被 SSM 整合支配的恐惧?相信很多小伙伴都是有过这样的经历的,一大堆配置问题,各种排除扫描,导入一个新的依赖又得添加新的配置。自从有了 SpringBoot 之后,咋们就起飞了!各种零配置开箱即用,而我们之所以开发起来能够这么爽,自动配置的功劳少不了,今天我们就一起来讨论一下 SpringBoot 自动配置原理。
3 SpringBoot 源码常用注解拾遗
这部分主要讲一下 SpringBoot 源码中经常使用到的注解,以扫清后面阅读源码时候的障碍。
3.1 组合注解
当可能大量同时使用到几个注解到同一个类上,就可以考虑将这几个注解到别的注解上。被注解的注解我们就称之为组合注解。
- 元注解:可以注解到别的注解上的注解。
- 组合注解:被注解的注解我们就称之为组合注解。
3.2 @Value 【Spring 提供】
@Value 就相当于传统 xml 配置文件中的 value 字段。
假设存在代码:
@Component public class Person { @Value("i am name") private String name; }
上面代码等价于的配置文件:
<bean class="Person"> <property name ="name" value="i am name"></property> </bean>
我们知道配置文件中的 value 的取值可以是:
- 字面量
- 通过
${key}
方式从环境变量中获取值 - 通过
${key}
方式全局配置文件中获取值 #{SpEL}
所以,我们就可以通过 @Value(${key})
的方式获取全局配置文件中的指定配置项。
3.3 @ConfigurationProperties 【SpringBoot 提供】
如果我们需要取 N 个配置项,通过 @Value 的方式去配置项需要一个一个去取,这就显得有点 low 了。我们可以使用 @ConfigurationProperties。
标有 @ConfigurationProperties 的类的所有属性和配置文件中相关的配置项进行绑定。(默认从全局配置文件中获取配置值),绑定之后我们就可以通过这个类去访问全局配置文件中的属性值了。
下面看一个实例:
1.在主配置文件中添加如下配置
person.name=kundy person.age=13 person.sex=male
2.创建配置类,由于篇幅问题这里省略了 setter、getter 方法,但是实际开发中这个是必须的,否则无法成功注入。另外,@Component 这个注解也还是需要添加的。
@Component @ConfigurationProperties(prefix = "person") public class Person { private String name; private Integer age; private String sex; }
这里 @ConfigurationProperties
有一个 prefix 参数,主要是用来指定该配置项在配置文件中的前缀。
3.测试,在 SpringBoot 环境中,编写个测试方法,注入 Person 类,即可通过 Person 对象取到配置文件的值。
3.4 @Import 【Spring 提供】
@Import 注解支持导入普通 java 类,并将其声明成一个bean。主要用于将多个分散的 java config 配置类融合成一个更大的 config 类。
- @Import 注解在 4.2 之前只支持导入配置类。
- 在4.2之后 @Import 注解支持导入普通的 java 类,并将其声明成一个 bean。
@Import 三种使用方式
- 直接导入普通的 Java 类。
- 配合自定义的 ImportSelector 使用。
- 配合 ImportBeanDefinitionRegistrar 使用。
1. 直接导入普通的 Java 类
1.创建一个普通的 Java 类。
public class Circle { public void sayHi() { System.out.println("Circle sayHi()"); } }
2.创建一个配置类,里面没有显式声明任何的 Bean,然后将刚才创建的 Circle 导入。
@Import({Circle.class}) @Configuration public class MainConfig { }
3.创建测试类。
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class); Circle circle = context.getBean(Circle.class); circle.sayHi(); }
4.运行结果:
Circle sayHi()
可以看到我们顺利的从 IOC 容器中获取到了 Circle 对象,证明我们在配置类中导入的 Circle 类,确实被声明为了一个 Bean。
2. 配合自定义的 ImportSelector 使用
ImportSelector 是一个接口,该接口中只有一个 selectImports 方法,用于返回全类名数组。所以利用该特性我们可以给容器动态导入 N 个 Bean。
1.创建普通 Java 类 Triangle。
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class); Circle circle = context.getBean(Circle.class); circle.sayHi(); }
2.创建 ImportSelector 实现类,selectImports 返回 Triangle 的全类名。
public class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { return new String[]{"annotation.importannotation.waytwo.Triangle"}; } }
3.创建配置类,在原来的基础上还导入了 MyImportSelector。
@Import({Circle.class,MyImportSelector.class}) @Configuration public class MainConfigTwo { }
4.创建测试类
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(MainConfigTwo.class); Circle circle = context.getBean(Circle.class); Triangle triangle = context.getBean(Triangle.class); circle.sayHi(); triangle.sayHi(); }
5.运行结果:
Circle sayHi()
Triangle sayHi()
可以看到 Triangle 对象也被 IOC 容器成功的实例化出来了。
3. 配合 ImportBeanDefinitionRegistrar 使用
ImportBeanDefinitionRegistrar 也是一个接口,它可以手动注册bean到容器中,从而我们可以对类进行个性化的定制。(需要搭配 @Import 与 @Configuration 一起使用。)
1.创建普通 Java 类 Rectangle。
public class Rectangle { public void sayHi() { System.out.println("Rectangle sayHi()"); } }
2.创建 ImportBeanDefinitionRegistrar 实现类,实现方法直接手动注册一个名叫 rectangle 的 Bean 到 IOC 容器中。
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) { RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Rectangle.class); // 注册一个名字叫做 rectangle 的 bean beanDefinitionRegistry.registerBeanDefinition("rectangle", rootBeanDefinition); } }
3.创建配置类,导入 MyImportBeanDefinitionRegistrar 类。
@Import({Circle.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class}) @Configuration public class MainConfigThree { }
4.创建测试类。
public static void main(String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(MainConfigThree.class); Circle circle = context.getBean(Circle.class); Triangle triangle = context.getBean(Triangle.class); Rectangle rectangle = context.getBean(Rectangle.class); circle.sayHi(); triangle.sayHi(); rectangle.sayHi(); }
Circle sayHi()
Triangle sayHi()
Rectangle sayHi()
嗯对,Rectangle 对象也被注册进来了。