一、自动配置
1.1 bean加载方式
bean的加载方式1 - xml方式声明bean
导入依赖:
<dependencies><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.9</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.17</version></dependency>
</dependencies>
配置applicationContext1.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><!--xml方式声明自己开发的bean--><bean id="cat" class="com.ming.bean.Cat"/><bean class="com.ming.bean.Dog"/><!--xml方式声明第三方开发的bean--><bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"/><bean class="com.alibaba.druid.pool.DruidDataSource"/><bean class="com.alibaba.druid.pool.DruidDataSource"/>
</beans>
测试:
public class App1 {public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext1.xml");// Object cat = ctx.getBean("cat");// System.out.println(cat); // com.ming.bean.Cat@59494225// Dog dog = ctx.getBean(Dog.class);// System.out.println(dog); // com.ming.bean.Dog@4566e5bdString[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);/*** cat* com.ming.bean.Dog#0* dataSource* com.alibaba.druid.pool.DruidDataSource#0* com.alibaba.druid.pool.DruidDataSource#1*/}}
}
bean的加载方式2 - XML+注解方式声明bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:context="http://www.springframework.org/schema/context"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsd"><context:component-scan base-package="com.ming.bean,com.ming.config"/>
</beans>
- 使用@Component及其衍生注解@Controller 、@Service、@Repository定义bean
@Component("tom")
public class Cat {}@Controller("jerry")
public class Mouse {}@Service
public class BookServiceImpl01 implements BookService {@Overridepublic void check() {System.out.println("BookServiceImpl01 ..");}
}// ....
- 使用@Bean定义第三方bean,并将所在类定义为配置类或Bean
@Configuration
public class DbConfig {/*** 加载第三方bean* @return*/@Beanpublic DruidDataSource dataSource(){DruidDataSource druidDataSource = new DruidDataSource();return druidDataSource;}
}
测试:
public class App2 {public static void main(String[] args) {ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext2.xml");String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);/*** tom* jerry* bookServiceImpl01* dbConfig* org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* dataSource*/}}
}
bean的加载方式3 - 注解方式声明配置类
标注注解的类同上
配置类如下所示:
@ComponentScan({"com.ming.bean","com.ming.config"})
public class SpringConfig3 {}
如果通过扫描的方式加入(
new AnnotationConfigApplicationContext(SpringConfig3.class)
),
则SpringConfig3类上的 @Configuration可以省略
测试:
public class App3 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig3.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig3* tom* jerry* bookServiceImpl01* dbConfig* dataSource*/}}
}
扩展1:
初始化实现FactoryBean接口的类,实现对bean加载到容器之前的批处理操作
public class DogFactoryBean implements FactoryBean<Dog> {@Overridepublic Dog getObject() throws Exception {Dog dog = new Dog();// 进行dog对象的相关初始化return dog;}@Overridepublic Class<?> getObjectType() {return Dog.class;}@Overridepublic boolean isSingleton() {return false;}
}
使用方式:返回的不是DogFactoryBean对象,而是DogFactoryBean对象创建出来的Dog对象
@ComponentScan({"com.ming.bean","com.ming.config"})
public class SpringConfig3 {@Beanpublic DogFactoryBean dog(){return new DogFactoryBean();}
}
测试:
public class App3 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig3.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}System.out.println(ctx.getBean("dog"));}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig3* tom* jerry* bookServiceImpl01* dbConfig* dataSource* dog* com.ming.bean.Dog@65d6b83b*/
扩展2:
如何在同时加载配置文件和配置类?(系统迁移)
@ImportResource({"applicationContext3.xml"})
public class SpringConfig32 {
}
思考:若配置文件1里面加载了cat,配置文件2里面也加载cat会发生什么?
如下所示:配置文件1
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="cat" class="com.ming.bean.Cat"><property name="name" value="tom1"/></bean>
</beans>
配置文件2
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="cat" class="com.ming.bean.Cat"><property name="name" value="tom2"/></bean>
</beans>
配置类:
@ImportResource({"applicationContext3.xml","applicationContext32.xml"})
public class SpringConfig32 {
}
结果:与@ImportResource的导入顺序有关,配置文件2覆盖配置文件1的bean,另外 在配置类中声明Bean,也会被覆盖,优先级最低
public class App32 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig32.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}System.out.println("--------------");Cat cat = (Cat) ctx.getBean("cat");System.out.println(cat.getName()); // tom2}
}
扩展3:
- 使用proxyBeanMethods=true可以保障调用此方法得到的对象是从容器中获取的而不是重新创建的
@Configuration(proxyBeanMethods = true) // 默认为true
public class SpringConfig33 {@Beanpublic Cat cat(){System.out.println("cat init ...");return new Cat();}
}
测试:
public class App33 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig33.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}System.out.println("--------------------");System.out.println(ctx.getBean("springConfig33")); ///*** 当Configuration注解的proxyBeanMethods属性为true(默认)时:* 打印结果为:com.ming.config.SpringConfig33$$EnhancerBySpringCGLIB$$1939a483@21a947fe* 从打印结果可以看出,这个对象是由CGLIB动态代理生成的代理对象* 改为false后:* 打印结果为:com.ming.config.SpringConfig33@2c039ac6*/SpringConfig33 springConfig33 = ctx.getBean("springConfig33", SpringConfig33.class);System.out.println(springConfig33.cat());System.out.println(springConfig33.cat());/*** 当proxyBeanMethods属性为true(默认)时:* 打印结果都为 com.ming.bean.Cat@1dd92fe2 ,同一个对象* 改为false后:* 打印结果不同:* com.ming.bean.Cat@ed9d034* com.ming.bean.Cat@6121c9d6*/}
}
bean的加载方式4- 使用@Import()注入bean
- 使用@Import注解导入要注入的bean对应的字节码
@Import({Dog.class})
public class SpringConfig4 {}
- 被导入的bean无需使用注解声明为bean
public class Dog {
}
此形式可以有效的降低源代码与Spring技术的耦合度,在spring技术底层及诸多框架的整合中大量使用
测试:
public class App4 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig4.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}System.out.println("------------------");System.out.println(ctx.getBean(Dog.class));}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig4* com.ming.bean.Dog* ------------------* com.ming.bean.Dog@9a7504c*/
扩展4:
除了可以使用@Import()注解注入普通的Bean外,还可以加载配置类,并且在配置类中声明的bean也生效
@Import({DbConfig.class})
public class SpringConfig42 {}
- 配置类可以不加@Configuration注解
// @Configuration
public class DbConfig {@Beanpublic DruidDataSource dataSource(){DruidDataSource druidDataSource = new DruidDataSource();return druidDataSource;}
}
测试:
public class App42 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig42.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig42* com.ming.config.DbConfig* dataSource*/
bean的加载方式5 - 硬编码方式注入(register/registerBean方法)
- 使用上下文对象在容器初始化完毕后注入bean
public class App5 {public static void main(String[] args) {AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig5.class);// 上下文容器对象已经初始化完毕后,手工加载bean。 - registerBean/registerctx.registerBean("tom", Cat.class);ctx.register(Dog.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig5* tom* dog*/
bean的加载方式6 - ImportSelector接口
- 导入实现了ImportSelector接口的类,实现对导入源的编程式处理
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {return new String[]{"com.ming.bean.Cat","com.ming.bean.Dog"};}
}
配置类导入MyImportSelector
@Import(MyImportSelector.class)
public class SpringConfig6 {
}
测试:
public class App6 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig6.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig6* tom # 因为Cat类上有@Component("tom")* com.ming.bean.Dog*/
扩展:selectImports方法的使用技巧
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {System.out.println("==============");// 获取的类名System.out.println(""+annotationMetadata.getClassName()); // com.ming.config.SpringConfig6// 获取的类名上是否注解:org.springframework.context.annotation.ConfigurationSystem.out.println(annotationMetadata.hasAnnotation("org.springframework.context.annotation.Configuration")); // true// 获取类上注解的属性,如 @ComponentScan(basePackages = {"com.ming"})Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes("org.springframework.context.annotation.ComponentScan");System.out.println(attributes); // {basePackageClasses=[], basePackages=[com.ming], excludeFilters=[], includeFilters=[], lazyInit=false, nameGenerator=interface org.springframework.beans.factory.support.BeanNameGenerator, resourcePattern=**/*.class, scopeResolver=class org.springframework.context.annotation.AnnotationScopeMetadataResolver, scopedProxy=DEFAULT, useDefaultFilters=true, value=[com.ming]}System.out.println("==============");return new String[]{"com.ming.bean.Cat","com.ming.bean.Dog"};}// @Override// public String[] selectImports(AnnotationMetadata annotationMetadata) {// // 还可以进行条件的判定,判定完毕后决定装载哪个bean// boolean flag = annotationMetadata.hasAnnotation("org.springframework.context.annotation.Configuration");// if(flag){// return new String[]{"com.ming.bean.Cat"};// }else{// return new String[]{"com.ming.bean.Dog"};// }// }
}
bean的加载方式7 - ImportBeanDefinitionRegistrar接口
- 导入实现了ImportBeanDefinitionRegistrar接口的类,通过BeanDefinition的注册器注册实名bean,实现对 容器中bean的裁定,例如对现有bean的覆盖,进而达成不修改源代码的情况下更换实现的效果
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();registry.registerBeanDefinition("dog1",beanDefinition);}
}
配置类注入:
@Import(MyImportBeanDefinitionRegistrar.class)
public class SpringConfig7 {}
测试:
public class App7 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig7.class);String[] names = ctx.getBeanDefinitionNames();for (String name : names) {System.out.println(name);}}
}
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig7* dog1*/
bean的加载方式8 - BeanDefinitionRegistryPostProcessor接口
- 导入实现了BeanDefinitionRegistryPostProcessor接口的类,通过BeanDefinition的注册器注册实名bean, 实现对容器中bean的最终裁定
场景构建:在一个配置类上的@Import()注解可以导入多个bean,如果此时导入的bean名字相同,一般来说配置靠后的会覆盖前面的配置,为了最终统一得到想要的bean,可以使用实现BeanDefinitionRegistryPostProcessor的方式完成
声明BeanDefinitionRegistryPostProcessor接口实现类:
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl04.class).getBeanDefinition();beanDefinitionRegistry.registerBeanDefinition("bookService", beanDefinition);}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {}
}
- @Import({BookServiceImpl01.class })
@Service("bookService")
public class BookServiceImpl01 implements BookService {@Overridepublic void check() {System.out.println("BookServiceImpl01 ..");}
}
- @Import({MyImportBeanDefinitionRegistrar81.class, })
public class MyImportBeanDefinitionRegistrar81 implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl02.class).getBeanDefinition();registry.registerBeanDefinition("bookService",beanDefinition);}
}
- @Import({MyImportBeanDefinitionRegistrar82.class, })
public class MyImportBeanDefinitionRegistrar82 implements ImportBeanDefinitionRegistrar {@Overridepublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl03.class).getBeanDefinition();registry.registerBeanDefinition("bookService",beanDefinition);}
}
- @Import({MyPostProcessor.class, })
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {@Overridepublic void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl04.class).getBeanDefinition();beanDefinitionRegistry.registerBeanDefinition("bookService", beanDefinition);}@Overridepublic void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {}
}
导入配置类:
//@Import({BookServiceImpl01.class }) // 此时运行App8.java 打印出 BookServiceImpl01 ..
//@Import({BookServiceImpl01.class, MyImportBeanDefinitionRegistrar81.class, }) // 此时运行App8.java 打印出 BookServiceImpl02 .. ..
//@Import({BookServiceImpl01.class, MyImportBeanDefinitionRegistrar81.class, MyImportBeanDefinitionRegistrar82.class}) // 此时运行App8.java 打印出 BookServiceImpl03 .. .. ..
//@Import({BookServiceImpl01.class, MyImportBeanDefinitionRegistrar82.class, MyImportBeanDefinitionRegistrar81.class}) // 此时运行App8.java 打印出 BookServiceImpl02 .. ..
@Import({MyPostProcessor.class, BookServiceImpl01.class, MyImportBeanDefinitionRegistrar82.class, MyImportBeanDefinitionRegistrar81.class, }) // 此时运行App8.java 打印出 BookServiceImpl04 .. .. .. ..
public class SpringConfig8 {}
测试:
public class App8 {public static void main(String[] args) {ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig8.class);BookService bookService = ctx.getBean("bookService", BookService.class);bookService.check();}
}
小结:
1.2 bean加载控制
编程式
bean的加载控制指根据特定情况对bean进行选择性加载以达到适用于项目的目标。
故只有上述8种加载方式的后四种可以实现bean的加载控制
用一种方式举例:实现ImportSelector接口的方式
public class MyImportSelector implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {/*** 功能描述:如果有Mouse这个类的话,就加载cat,* 如果没有Wolf类的话,就不加载cat*/try {
// Class<?> clazz = Class.forName("com.ming.bean.Mouse");Class<?> clazz = Class.forName("com.ming.bean.Wolf");if (clazz != null){return new String[]{"com.ming.bean.Cat"};}} catch (ClassNotFoundException e) {return new String[0];}return null;}
}
配置类加载MyImportSelector类
@Import(MyImportSelector.class)
public class SpringConfig {
}
测试:前提条件com.ming.bean
包下,有Mouse,没有Wolf;
- 当满足第一种情况下,(有Mouse,加载Cat)
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig* com.ming.bean.Cat # 因为com.ming.bean包下有Mouse类*/
- 当满足第二种情况下,(没有Wolf,不加载Cat)
/*** org.springframework.context.annotation.internalConfigurationAnnotationProcessor* org.springframework.context.annotation.internalAutowiredAnnotationProcessor* org.springframework.context.annotation.internalCommonAnnotationProcessor* org.springframework.context.event.internalEventListenerProcessor* org.springframework.context.event.internalEventListenerFactory* springConfig*/
注解式
使用@Conditional注解的派生注解设置各种组合条件控制bean的加载
- 添加SpringBoot相关的jar包
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><version>2.5.3</version>
</dependency>
spring提供默认规范,springboot提供实现
前提条件com.ming.bean
包下,有Mouse,没有Wolf;
- 匹配指定类
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
public class SpringConfig2 {@Bean// com.ming.bean包下有Mouse类,则加载 cat
// @ConditionalOnClass(name = "com.ming.bean.Mouse") // com.ming.bean包下有Wolf类,则加载 cat@ConditionalOnClass(name = "com.ming.bean.Wolf") public Cat cat(){return new Cat();}
}
- 未匹配指定类
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
public class SpringConfig2 {@Bean// com.ming.bean包下没有Mouse类,则加载 cat@ConditionalOnMissingClass("com.ming.bean.Mouse")// com.ming.bean包下没有Wolf类,则加载 cat
// @ConditionalOnMissingClass("com.ming.bean.Wolf")public Cat cat(){return new Cat();}
}
- 匹配指定类型的bean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
@Import(Mouse.class)
public class SpringConfig2 {@Bean// 容器中有名称为com.ming.bean.Mouse的bean,则加载 cat2// @ConditionalOnBean(name = "com.ming.bean.Mouse")// 容器中有名称为jerry的bean,则加载 cat2// @ConditionalOnBean(name = "jerry")// 容器中有名称为com.ming.bean.Wolf的bean,则加载 cat2@ConditionalOnBean(name = "com.ming.bean.Wolf")public Cat cat2(){return new Cat();}
}
- 未匹配指定类型的bean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
@Import(Mouse.class)
public class SpringConfig2 {@Bean// 容器中没有名称为com.ming.bean.Mouse的bean,则加载 cat2// @ConditionalOnMissingBean(name = "com.ming.bean.Mouse")// 容器中没有名称为com.ming.bean.Wolf的bean,则加载 cat2@ConditionalOnMissingBean(name = "com.ming.bean.Wolf") public Cat cat2(){return new Cat();}
}
- 匹配指定环境
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
public class SpringConfig2 {@Bean// 当是web环境时,才加载// @ConditionalOnWebApplication// 当不是web环境时,才加载@ConditionalOnNotWebApplicationpublic Cat cat3(){return new Cat();}
}
小结
1.3 bean依赖属性配置
案例描述:
要求在application.yml中传入业务所需要的属性值,并在原始位置声明默认值;
为了实现这一效果,我们先来配置一个基本的环境
业务类:CartoonCatAndMouse
@Component
public class CartoonCatAndMouse {private Cat cat;private Mouse mouse;CartoonCatAndMouse(){cat = new Cat();cat.setName("tom");cat.setAge(3);mouse = new Mouse();mouse.setName("jerry");mouse.setAge(4);}public void play(){System.out.println(cat.getAge()+"岁的"+cat.getName()+"和"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了~~~");}
}
实体类:Cat / Mouse
@Data
public class Cat {private String name;private Integer age;
}@Data
public class Mouse {private String name;private Integer age;
}
主类:
@SpringBootApplication
public class App1 {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(App1.class);CartoonCatAndMouse cartoonCatAndMouse = context.getBean(CartoonCatAndMouse.class);cartoonCatAndMouse.play();}
}
// 3岁的tom和4岁的jerry打起来了~~~
改进: 增加配置文件application.yml 和 对应的属性类, (将业务功能bean运行需要的资源抽取成独立的属性类(******Properties),设置读取配置文件信息)
新增:
cartoon:cat:name: "TOM"age: 6mouse:name: "JERRY"age: 8
@Component
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonProperties {private Cat cat;private Mouse mouse;
}
修改业务类CartoonCatAndMouse读取属性的方式:
@Component
public class CartoonCatAndMouse {private Cat cat;private Mouse mouse;private CartoonProperties cartoonProperties;CartoonCatAndMouse(CartoonProperties cartoonProperties){this.cartoonProperties = cartoonProperties;cat = new Cat();cat.setName(cartoonProperties.getCat() != null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() :"tom");cat.setAge(cartoonProperties.getCat() != null && cartoonProperties.getCat().getAge() != null ? cartoonProperties.getCat().getAge() : 3);mouse = new Mouse();mouse.setName(cartoonProperties.getMouse() != null && StringUtils.hasText(cartoonProperties.getMouse().getName()) ? cartoonProperties.getMouse().getName() :"jerry");mouse.setAge(cartoonProperties.getMouse() != null && cartoonProperties.getMouse().getAge() != null ? cartoonProperties.getMouse().getAge() : 4);}public void play(){System.out.println(cat.getAge()+"岁的"+cat.getName()+"和"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了~~~");}
}
测试效果:6岁的TOM和8岁的JERRY打起来了~~~
完善:
- CartoonProperties 不应该强制声明成Bean,因为,没有在配置文件中声明的话,即用默认的即可,无需加载这个Bean
- 解决:去掉CartoonProperties类上的
@Component
注解,在调用方(CartoonCatAndMouse)类上加@EnableConfigurationProperties(CartoonProperties.class)
注解
- 解决:去掉CartoonProperties类上的
- CartoonCatAndMouse 也不应该强制声明成Bean,因为,只有在调用方(App1)使用到的时候,才应该加载该Bean
- 解决:去掉CartoonCatAndMouse 类上的
@Component
注解,在调用方(App1)类上加@Import(CartoonCatAndMouse.class)
注解
- 解决:去掉CartoonCatAndMouse 类上的
小结:
1.4 自动配置原理
- 收集Spring开发者的编程习惯,整理开发过程使用的常用技术列表——>(技术集A)
- 收集常用技术(技术集A)的使用参数,整理开发过程中每个技术的常用设置列表——>(设置集B)
- 初始化SpringBoot基础环境,加载用户自定义的bean和导入的其他坐标,形成初始化环境
- 将技术集A包含的所有技术都定义出来,在Spring/SpringBoot启动时默认全部加载
- 将技术集A中具有使用条件的技术约定出来,设置成按条件加载,由开发者决定是否使用该技术(与初始化环境比对)
- 将设置集B作为默认配置加载(约定大于配置),减少开发者配置工作量
- 开放设置集B的配置覆盖接口,由开发者根据自身需要决定是否覆盖默认配置
源码解析:
SpringBoot的自动配置是由@SpringBootApplication实现的,所以我们来先看这个注解内部构造:
/*** @SpringBootConfiguration* @Configuration # 里面有一个proxyBeanMethods 属性,控制是否开启代理模式创建bean,详解扩展3* @Component* @Indexed # 在编译时扫描 @Indexed 注解,确定 bean,生成索引文件。先看如下 Spring 官网* @EnableAutoConfiguration* @AutoConfigurationPackage* @Import({AutoConfigurationPackages.Registrar.class}) # 重点在此* @Import({AutoConfigurationImportSelector.class}) # 重点在此* @ComponentScan( # 组件扫描 排除一些不需要的Bean* excludeFilters = {* @ComponentScan.Filter(type = FilterType.CUSTOM,* classes = {TypeExcludeFilter.class}),* @ComponentScan.Filter(type = FilterType.CUSTOM,* classes = {AutoConfigurationExcludeFilter.class})* })*//*** 下面我们来看一下重点的两个:* 1. @Import({AutoConfigurationPackages.Registrar.class})* 2. @Import({AutoConfigurationImportSelector.class})*/@SpringBootApplication
public class App1 {public static void main(String[] args) {SpringApplication.run(App1.class);}
}
- @Import({AutoConfigurationPackages.Registrar.class})
public abstract class AutoConfigurationPackages {private static final Log logger = LogFactory.getLog(AutoConfigurationPackages.class);private static final String BEAN = AutoConfigurationPackages.class.getName();// 实现ImportBeanDefinitionRegistrar接口,所以推测出是要注册某个Bean到容器中static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {Registrar() {}public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 调用register方法,向容器注入一个beanAutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));}public Set<Object> determineImports(AnnotationMetadata metadata) {return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));}}// 进入 registerpublic static void register(BeanDefinitionRegistry registry, String... packageNames) {if (registry.containsBeanDefinition(BEAN)) {AutoConfigurationPackages.BasePackagesBeanDefinition beanDefinition = (AutoConfigurationPackages.BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);beanDefinition.addBasePackages(packageNames);} else {// BEAN: 当前类的全路径名 org.springframework.boot.autoconfigure.AutoConfigurationPackages// packageNames : com.ming : 当前App1类所在的包registry.registerBeanDefinition(BEAN, new AutoConfigurationPackages.BasePackagesBeanDefinition(packageNames));}}static final class BasePackagesBeanDefinition extends GenericBeanDefinition {private final Set<String> basePackages = new LinkedHashSet();// 进入BasePackagesBeanDefinition构造器BasePackagesBeanDefinition(String... basePackages) {this.setBeanClass(AutoConfigurationPackages.BasePackages.class);this.setRole(2);// 添加 扫描路径 com.mingthis.addBasePackages(basePackages);}public Supplier<?> getInstanceSupplier() {return () -> {return new AutoConfigurationPackages.BasePackages(StringUtils.toStringArray(this.basePackages));};}private void addBasePackages(String[] additionalBasePackages) {this.basePackages.addAll(Arrays.asList(additionalBasePackages));}}// .....
}
经过断点模式,发现(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames()
是我们当前的App1类所在的包名。
结论:这个@Import 的作用是设置当前配置所在的包作为扫描包,后续要针对当前的包进行扫描
- @Import({AutoConfigurationImportSelector.class})
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {// 实现DeferredImportSelector 接口, 发现 DeferredImportSelector 的 getImportGroup 方法,最终返回AutoConfigurationGroup对象,// AutoConfigurationGroup 对象又实现了 DeferredImportSelector的Group 方法,在DeferredImportSelector的Group中调用process()方法,// 所以入口是process()方法private static class AutoConfigurationGroup implements Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {// 主要看getAutoConfigurationEntry方法public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());});AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);this.autoConfigurationEntries.add(autoConfigurationEntry);Iterator var4 = autoConfigurationEntry.getConfigurations().iterator();while(var4.hasNext()) {String importClassName = (String)var4.next();this.entries.putIfAbsent(importClassName, annotationMetadata);}}}// 跟进,getCandidateConfigurations()protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {if (!this.isEnabled(annotationMetadata)) {return EMPTY_ENTRY;} else {// ....List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);}}// 跟进,loadFactoryNames()protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;}// 返回EnableAutoConfiguration的全路径类名,以便在调用loadFactoryNames时,作为参数传入protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;}
}
调用SpringFactoriesLoader类中的loadFactoryNames()方法:
public final class SpringFactoriesLoader {// 跟进,loadSpringFactories()public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {// 获取类型名,此时的factoryTypeName: org.springframework.boot.autoconfigure.EnableAutoConfigurationString factoryTypeName = factoryType.getName();// 返回 spring.factories 文件中,所有以factoryTypeName 为key的value,存入list中return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());}// 发现读取了一个 META-INF/spring.factories 文件private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {Map<String, List<String>> result = (Map)cache.get(classLoader);if (result != null) {return result;} else {HashMap result = new HashMap();try {Enumeration urls = classLoader.getResources("META-INF/spring.factories");return result;} catch (IOException var14) {throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);}}}
}
spring.factories 位置:
内容:看EnableAutoConfiguration类下的
结论:这个@Import 的作用:利用AutoConfigurationImportSelector给容器中导入一些组件,具体就是最终就是将类路径下** META-INF/spring.factories **里面配置的所有EnableAutoConfiguration的值加入到了容器中;也就是xxxAutoConfiguration,这些都是用来做自动配置的。
至此,所有的技术集已经全部加载,完成了第四步;
接下来,我们查看spring.factories
中的org.springframework.boot.autoconfigure.EnableAutoConfiguration
,选取一个redis相关的配置:
RedisAutoConfiguration.java
@Configuration(proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class}) // 当类库中存在RedisOperations接口时才加载,所以要导入redis的依赖
@EnableConfigurationProperties({RedisProperties.class}) // RedisProperties类中存 所有redis配置
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {public RedisAutoConfiguration() {}@Bean@ConditionalOnMissingBean(name = {"redisTemplate"}) // 如果 没有自定义 redisTemplate ,则使用他封装的@ConditionalOnSingleCandidate(RedisConnectionFactory.class)public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {RedisTemplate<Object, Object> template = new RedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}@Bean@ConditionalOnMissingBean@ConditionalOnSingleCandidate(RedisConnectionFactory.class)public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {StringRedisTemplate template = new StringRedisTemplate();template.setConnectionFactory(redisConnectionFactory);return template;}
}
@ConfigurationProperties(prefix = "spring.redis"
)
public class RedisProperties {private int database = 0;private String url;private String host = "localhost";private String username;private String password;private int port = 6379;private boolean ssl;private Duration timeout;private Duration connectTimeout;private String clientName;private RedisProperties.ClientType clientType;private RedisProperties.Sentinel sentinel;private RedisProperties.Cluster cluster;private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
}
对比导入redis依赖前后,容器中的bean:
- 导入redis的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId><version>3.0.4</version>
</dependency>
- 导入前:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
app1
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
com.ming.bean.CartoonCatAndMouse
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
org.springframework.boot.context.internalConfigurationPropertiesBinderFactory
org.springframework.boot.context.internalConfigurationPropertiesBinder
org.springframework.boot.context.properties.BoundConfigurationProperties
org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter
cartoon-com.ming.bean.CartoonProperties
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
propertySourcesPlaceholderConfigurer
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
mbeanExporter
objectNamingStrategy
mbeanServer
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
springApplicationAdminRegistrar
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration
forceAutoProxyCreatorToUseClassProxying
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
applicationAvailability
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
lifecycleProcessor
spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties
org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
taskExecutorBuilder
applicationTaskExecutor
spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
scheduledBeanLazyInitializationExcludeFilter
taskSchedulerBuilder
spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
org.springframework.aop.config.internalAutoProxyCreator
没有redis相关的信息
- 导入后:
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
app1
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
com.ming.bean.CartoonCatAndMouse
org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor
org.springframework.boot.context.internalConfigurationPropertiesBinderFactory
org.springframework.boot.context.internalConfigurationPropertiesBinder
org.springframework.boot.context.properties.BoundConfigurationProperties
org.springframework.boot.context.properties.EnableConfigurationPropertiesRegistrar.methodValidationExcludeFilter
cartoon-com.ming.bean.CartoonProperties
org.springframework.boot.autoconfigure.AutoConfigurationPackages
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
propertySourcesPlaceholderConfigurer
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
mbeanExporter
objectNamingStrategy
mbeanServer
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
springApplicationAdminRegistrar
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$ClassProxyingConfiguration
forceAutoProxyCreatorToUseClassProxying
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
applicationAvailability
org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration
lettuceClientResources
redisConnectionFactory
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
redisTemplate
stringRedisTemplate
spring.redis-org.springframework.boot.autoconfigure.data.redis.RedisProperties
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
lifecycleProcessor
spring.lifecycle-org.springframework.boot.autoconfigure.context.LifecycleProperties
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration
persistenceExceptionTranslationPostProcessor
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration
reactiveRedisTemplate
reactiveStringRedisTemplate
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration
keyValueMappingContext
redisCustomConversions
redisReferenceResolver
redisConverter
redisKeyValueAdapter
redisKeyValueTemplate
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
spring.info-org.springframework.boot.autoconfigure.info.ProjectInfoProperties
org.springframework.boot.autoconfigure.netty.NettyAutoConfiguration
spring.netty-org.springframework.boot.autoconfigure.netty.NettyProperties
org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
spring.sql.init-org.springframework.boot.autoconfigure.sql.init.SqlInitializationProperties
org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer$DependsOnDatabaseInitializationPostProcessor
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
taskExecutorBuilder
applicationTaskExecutor
spring.task.execution-org.springframework.boot.autoconfigure.task.TaskExecutionProperties
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration
scheduledBeanLazyInitializationExcludeFilter
taskSchedulerBuilder
spring.task.scheduling-org.springframework.boot.autoconfigure.task.TaskSchedulingProperties
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
platformTransactionManagerCustomizers
spring.transaction-org.springframework.boot.autoconfigure.transaction.TransactionProperties
org.springframework.aop.config.internalAutoProxyCreator
有redis相关的信息
扩展:
SpringFactories机制
- Java SPI机制的延伸和扩展
- Spring框架的基础机制,在Spring以及SpringBoot源码中到处可见
- 可以基于它来实现SpringBoot的自动配置功能
- 它的核心逻辑是从classpath中读取到所有jar包中的配置文件META-IF/spring.factories,然后根据指定的key从配置文件中解析出对应的value值
小结:
1.5 变更自动配置
添加自动配置
- 添加自动配置文件(META-INF/spring.factories)
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.ming.bean.CartoonCatAndMouse
- 在CartoonCatAndMouse类上添加@ConditionalOnClass
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisOperations") // 借用redis的jar, 当有这个类时,注册这个bean
@EnableConfigurationProperties(CartoonProperties.class)
public class CartoonCatAndMouse {private Cat cat;private Mouse mouse;private CartoonProperties cartoonProperties;CartoonCatAndMouse(CartoonProperties cartoonProperties){this.cartoonProperties = cartoonProperties;cat = new Cat();cat.setName(cartoonProperties.getCat() != null && StringUtils.hasText(cartoonProperties.getCat().getName()) ? cartoonProperties.getCat().getName() :"tom");cat.setAge(cartoonProperties.getCat() != null && cartoonProperties.getCat().getAge() != null ? cartoonProperties.getCat().getAge() : 3);mouse = new Mouse();mouse.setName(cartoonProperties.getMouse() != null && StringUtils.hasText(cartoonProperties.getMouse().getName()) ? cartoonProperties.getMouse().getName() :"jerry");mouse.setAge(cartoonProperties.getMouse() != null && cartoonProperties.getMouse().getAge() != null ? cartoonProperties.getMouse().getAge() : 4);}public void play(){System.out.println(cat.getAge()+"岁的"+cat.getName()+"和"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了~~~");}
}
- 测试:删除之前的手动导入Bean -
@Import(CartoonCatAndMouse.class)
;加上redis依赖;
@SpringBootApplication
//@Import(CartoonCatAndMouse.class)
public class App1 {public static void main(String[] args) {ConfigurableApplicationContext context = SpringApplication.run(App1.class);CartoonCatAndMouse cartoonCatAndMouse = context.getBean(CartoonCatAndMouse.class);cartoonCatAndMouse.play();}
}
删除系统自动配置
- 在yml 文件中配置
spring:autoconfigure:exclude:- org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
- 使用注解
@SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration")
小结:
二、自定义starter
2.1 案例:统计独立IP访问次数
功能描述:
- 每次访问网站行为均进行统计
- 后台每10s输出1次监控信息(格式:IP + 访问次数)
实现方案:
- 数据记录位置:Map / Redis
- 功能触发位置:每次web请求(拦截器)
- 步骤1:降低难度,主动调用,仅统计单一操作访问次数(例如查询)
- 步骤2:开发拦截器
- 业务参数(配置项)
- 输出频度:默认10s
- 数据特征:累计数据 / 阶段数据,默认累计数据
- 输出格式:详细模式 / 极简模式
- 校验环境:设置加载条件
具体实现:
- 环境搭建:
参照:TangGuoNiuBi/spring-boot-study
导入数据:tbl_book.sql
访问url:http://localhost:81/pages/books.html
2.2 自定义starter
新建模块及基础功能开发:ip_spring_boot_starter
导入依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
编写业务功能代码:第一版
public class IpCountService {private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();@Autowired// 当前的request对象的注入工作,由使用当前starter的工程提供自动装配private HttpServletRequest httpServletRequest;public void count(){// 每次调用当前操作,就记录当前访问IP,然后累加访问次数// 1. 获取当前操作的IP地址String ip = httpServletRequest.getRemoteAddr();System.out.println("-----------------------" + ip);// 2. 根据IP操作地址从Map取值,并递增Integer count = ipCountMap.get(ip);if (count == null){ipCountMap.put(ip,1);}else {ipCountMap.put(ip,count + 1);}}
}
自动配置类:
public class IpAutoConfiguration {@Beanpublic IpCountService ipCountService(){return new IpCountService();}
}
在META-INF/spring.factories文件中配置:
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.ming.autoconfig.IpAutoConfiguration
在本地maven仓库中安装自定义starter:
mvn clear
mvn install
切记使用之前先clean后install安装到maven仓库,确保资源更新
- 简单测试
在需要添加的计数功能的工程中导入自定义starter
<dependency><groupId>cn.ming</groupId><artifactId>ip_spring_boot_starter</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
模拟调用:
@Autowired
private IpCountService ipCountService;@GetMapping("{currentPage}/{pageSize}")
public R getPage(@PathVariable int currentPage, @PathVariable int pageSize, Book book) {ipCountService.count();IPage<Book> page = bookService.getPage(currentPage, pageSize, book);// 如果当前页面值大于总页码值,那么重新执行查询操作,使用最大页码值作为当前页码值if (currentPage > page.getPages()) {page = bookService.getPage((int) page.getPages(), pageSize, book);}return new R(true, page);
}
预想结果:每次刷新,点击,出现下面内容;
-----------------------0:0:0:0:0:0:0:1
至此已经完成基础功能,下面开发定时任务功能
功能开发之定时任务功能
添加@EnableScheduling注解,开启定时任务
@EnableScheduling
public class IpAutoConfiguration {@Beanpublic IpCountService ipCountService(){return new IpCountService();}
}
添加业务方法,打印map中存的信息
public class IpCountService {private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();@Scheduled(cron = "0/5 * * * * ?")public void print(){System.out.println(" IP访问监控");System.out.println("+-----ip-address-----+--num--+");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {String key = entry.getKey();Integer value = entry.getValue();System.out.println(String.format("|%18s |%5d |",key,value));}//System.out.println(String.format("|%18s |%5d |","abc",20));System.out.println("+--------------------+-------+");}public static void main(String[] args) {new IpCountService().print();}
}
注意:String.format()的使用
功能开发之属性动态配置
定义属性类:
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {/*** 日志显示周期*/private Long cycle = 5L;/*** 是否重置周期内数据*/private Boolean cycleReset = false;/*** 日志输出格式:detail: 详细模式;simple: 极简模式;*/private String model = LogModel.DETAIL.value;public enum LogModel {DETAIL("detail"),SIMPLE("simple");private String value;LogModel(String value) {this.value = value;}public String getValue() {return value;}}// getter / setter
}
对应配置文件:
tools:ip:cycle: 10cycle-reset: falsemodel: "simple"
测试时使用,实际引入时,要清空yaml文件
在配置类上 设置加载Properties类为bean
@EnableScheduling
@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {@Beanpublic IpCountService ipCountService(){return new IpCountService();}
}
在业务类中追加逻辑代码
public class IpCountService {private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();@Autowiredprivate IpProperties ipProperties;@Scheduled(cron = "0/5 * * * * ?")public void print(){// 判断启动模式if(ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){// 详细模式System.out.println(" IP访问监控(详细)");System.out.println("+-----ip-address-----+--num--+");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {System.out.println(String.format("|%18s |%5d |",entry.getKey(),entry.getValue()));}System.out.println("+--------------------+-------+");}else if (ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){// 极简模式System.out.println(" IP访问监控(极简)");System.out.println("+-----ip-address-----+");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {System.out.println(String.format("|%18s |",entry.getKey()));}System.out.println("+--------------------+");}// 判断是否清空数据if(ipProperties.getCycleReset()){ipCountMap.clear();}}
}
测试 tools.ip.cycle-reset
和 tools.ip.model
,输出符合预期;
对 tools.ip.cycle
的处理:
处理目标:使用属性类的方式读取cycle属性加载到@Scheduled(cron = "0/5 * * * * ?")
位置上去
使用#{beanName.属性名}
的方式,可以将IpProperties的cycle注入到注解中。
Tips:
但是,此时的IpProperties类的beanName是tools.ip-cn.ming.properties.IpProperties
,所以直接使用的话会报错。
因为当前的beanName是由配置类上的@EnableConfigurationProperties(IpProperties.class
注解自动生成的,所以,我们将这个注解删除,在IpProperties类上加上@Component("ipProperties")
注解,最后在到配置类上引入即可;
属性类:自定义bean名称
@Component("ipProperties")
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {/*** 日志显示周期*/private Long cycle = 5L;/*** 是否重置周期内数据*/private Boolean cycleReset = false;/*** 日志输出格式:detail: 详细模式;simple: 极简模式;*/private String model = LogModel.DETAIL.value;public enum LogModel {DETAIL("detail"),SIMPLE("simple");private String value;LogModel(String value) {this.value = value;}public String getValue() {return value;}}// setter / getter
}
配置类: 放弃配置属性创建bean方式,改为手工控制
@EnableScheduling
//@EnableConfigurationProperties(IpProperties.class)
@Import(IpProperties.class)
public class IpAutoConfiguration {@Beanpublic IpCountService ipCountService(){return new IpCountService();}
}
业务类使用: 使用#{beanName.attrName}
读取bean的属性。
public class IpCountService {private Map<String,Integer> ipCountMap = new HashMap<String,Integer>();@Autowired// 当前的request对象的注入工作,由使用当前starter的工程提供自动装配private HttpServletRequest httpServletRequest;public void count(){// 每次调用当前操作,就记录当前访问IP,然后累加访问次数// 1. 获取当前操作的IP地址String ip = httpServletRequest.getRemoteAddr();System.out.println("-----------------------"+ip);// 2. 根据IP操作地址从Map取值,并递增Integer count = ipCountMap.get(ip);if (count == null){ipCountMap.put(ip,1);}else {ipCountMap.put(ip,count + 1);}}@Autowiredprivate IpProperties ipProperties;@Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")public void print(){// 判断启动模式if(ipProperties.getModel().equals(IpProperties.LogModel.DETAIL.getValue())){// 详细模式System.out.println(" IP访问监控(详细)");System.out.println("+-----ip-address-----+--num--+");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {System.out.println(String.format("|%18s |%5d |",entry.getKey(),entry.getValue()));}System.out.println("+--------------------+-------+");}else if (ipProperties.getModel().equals(IpProperties.LogModel.SIMPLE.getValue())){// 极简模式System.out.println(" IP访问监控(极简)");System.out.println("+-----ip-address-----+");for (Map.Entry<String, Integer> entry : ipCountMap.entrySet()) {System.out.println(String.format("|%18s |",entry.getKey()));}System.out.println("+--------------------+");}// 判断是否清空数据if(ipProperties.getCycleReset()){ipCountMap.clear();}}
}
功能开发之添加拦截器
在ip_spring_boot_starter模块中添加拦截器:
package cn.ming.interceptor;
public class IpInterceptor implements HandlerInterceptor {@Autowiredprivate IpCountService ipCountService;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {ipCountService.count();return true;}
}
设置核心配置类,加载拦截器
@Configuration(proxyBeanMethods = true)
public class SpringMvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(ipInterceptor()).addPathPatterns("/**");}@Beanpublic IpInterceptor ipInterceptor(){return new IpInterceptor();}
}
记得删除之前测试用的
ipCountService.count();
至此,自定义starter已经全部开发完,实现,导入依赖,添加统计功能,移出依赖,不影响自身运行;
2.3 辅助功能开发
yml提示功能开发
- 导入依赖:
<!-- yml 自动提示包,target生成json文件后,复制到本项目的META-INF目录下,最后移除此依赖-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
- 执行
mvn clear
和mvn install
- 复制target/classes/META-INF目录下的
spring-configuration-metadata.json
文件到src/main/resources/META-INF/spring-configuration-metadata.json
- 删除多余的
spring-configuration-metadata.json
和 依赖 - 在json文件中 进行自定义提示功能开发
"hints": [{"name": "tools.ip.model","values": [{"value": "detail","description": "详细模式."},{"value": "simple","description": "极简模式."}]}
]
- 测试:
三、核心原理
SpringBoot启动流程
- 初始化各种属性,加载成对象
- 读取环境属性(Environment)
- 系统配置(spring.factories)
- 参数(Arguments、application.properties)
- 创建Spring容器对象ApplicationContext,加载各种配置
- 在容器创建前,通过监听器机制,应对不同阶段加载数据,更新数据的需求
- 容器初始化过程中追加各种功能,例如统计时间、输出日志等
小结:
- 加载数据
- 创建容器
ClassName[行号] -> 执行代码 // 解释
SpringbootDemo05Application[10] -> SpringApplication.run(SpringbootDemo05Application.class, args);SpringApplication[825] -> return run(new Class[]{primarySource}, args);SpringApplication[829] -> return (new SpringApplication(primarySources)).run(args);SpringApplication[829] -> SpringApplication(primarySources)// 加载各种配置信息,初始化各种配置对象SpringApplication[101] -> this((ResourceLoader)null, primarySources);SpringApplication[104] -> public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {SpringApplication[105] -> this.sources = new LinkedHashSet(); // 创建LinkedHashSetSpringApplication[106] -> this.bannerMode = Mode.CONSOLE; // 该属性是用来打印springboot启动时的图标。// Mode是org.springframework.boot.Banner接口的静态enum类型// OFF: 关闭// CONSOLE: 打印到控制台// LOG: 打印到日志SpringApplication[107] -> this.logStartupInfo = true; // true: 打印jvm的启动和运行信息。包括启动类,java版本,pid等SpringApplication[108] -> this.addCommandLineProperties = true; // true: 通过命令行参数,向application.properties文件中添加属性;例如 java -jar test.jar -server.port=80,(设置端口号)SpringApplication[109] -> this.addConversionService = true;// true: 加载默认的类型转换和格式化类(ApplicationConversionService); 详情参考 ApplicationConversionService[152] -> public static void addApplicationConverters(ConverterRegistry registry)SpringApplication[110] -> this.headless = true;// 开启java的headless模式。此模式允许java在没有显示器、鼠标等设备缺失的情况下启动服务,对于部署在服务器的程序来说是很有必要的。SpringApplication[111] -> this.registerShutdownHook = true;// 创建线程,该线程用来在Java程序关闭后释放资源SpringApplication[112] -> this.additionalProfiles = Collections.emptySet();// 默认为空,即使用springboot默认配置文件。可以调用SpringApplication的setAdditionalProfiles()方法,实现springboot中的profile属性的指定,用于切换环境SpringApplication[113] -> this.isCustomEnvironment = false;// 加载环境配置SpringApplication[114] -> this.lazyInitialization = false;// 懒加载SpringApplication[115] -> this.applicationContextFactory = ApplicationContextFactory.DEFAULT;// TODOSpringApplication[116] -> this.applicationStartup = ApplicationStartup.DEFAULT;// TODOSpringApplication[117] -> this.resourceLoader = resourceLoader;// 初始化资源加载器SpringApplication[118] -> Assert.notNull(primarySources, "PrimarySources must not be null");SpringApplication[119] -> this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));// 形参的primarySources: Class类型的SpringbootDemo05Application// 初始化配置类的类名信息(格式转换)(将可变类型转换为集合类型存入成员变量中)SpringApplication[120] -> this.webApplicationType = WebApplicationType.deduceFromClasspath();// 确认当前容器加载的类型WebApplicationType[25] -> static WebApplicationType deduceFromClasspath(){}// WebApplicationType enum类中有三种类型["NONE","SERVLET","REACTIVE"]// NONE: 需导入spring-boot-starter依赖// SERVLET: 需导入的spring-boot-starter-web依赖// REACTIVE: 需导入spring-boot-starter-webflux依赖SpringApplication[121] -> this.bootstrapRegistryInitializers = this.getBootstrapRegistryInitializersFromSpringFactories();// 获取系统配置引导信息SpringApplication[127] -> private List<BootstrapRegistryInitializer> getBootstrapRegistryInitializersFromSpringFactories() {}// 在 META-INF/spring.factories 中查找 以 Bootstrapper.class / BootstrapRegistryInitializer.class 为key的value值SpringApplication[122] -> this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));// 在 META-INF/spring.factories 中查找 以 ApplicationContextInitializer.class 为key的value值// 获取ApplicationContextInitializer.class对应的实例SpringApplication[123] -> this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));// 在 META-INF/spring.factories 中查找 以 ApplicationListener.class 为key的value值// 初始化监听器,对初始化过程及运行过程进行干预SpringApplication[124] -> this.mainApplicationClass = this.deduceMainApplicationClass();// 初始化了引导类(SpringbootDemo05Application)的类名信息,备用SpringApplication[829] -> (new SpringApplication(primarySources)).run(args);// 初始化容器,得到一个ApplicationContext对象SpringApplication[155] -> StopWatch stopWatch = new StopWatch();// 声明一个计时器,用于统计SpringApplication[156] -> stopWatch.start();// 计时器开始SpringApplication[157] -> DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();// 系统引导信息对应的上下文对象SpringApplication[194] -> private DefaultBootstrapContext createBootstrapContext() {SpringApplication[195] -> DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();SpringApplication[196] -> this.bootstrapRegistryInitializers.forEach((initializer) -> {// List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;// 遍历在之前`加载各种配置信息,初始化各种配置对象`阶段加载好的 bootstrapRegistryInitializers 集合SpringApplication[197] -> initializer.initialize(bootstrapContext);SpringApplication[198] -> });SpringApplication[199] -> return bootstrapContext;SpringApplication[200] -> }SpringApplication[158] -> ConfigurableApplicationContext context = null;// 创建容器对象SpringApplication[159] -> this.configureHeadlessProperty();// 模拟输入输出信号,避免出现因缺少外设导致的信号传递失败,进而引发错误(模拟显示器,键盘,鼠标等)SpringApplication[268] -> private void configureHeadlessProperty() {SpringApplication[269] -> System.setProperty("java.awt.headless", System.getProperty("java.awt.headless", Boolean.toString(this.headless)));// 为系统变量添加 java.awt.headless = trueSpringApplication[270] -> }SpringApplication[160] -> SpringApplicationRunListeners listeners = this.getRunListeners(args);// 获取当前注册的所有监听器SpringApplication[161] -> listeners.starting(bootstrapContext, this.mainApplicationClass);// 监听器执行了对应的操作步骤SpringApplication[163] -> try {SpringApplication[164] -> ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);// 获取参数 argsSpringApplication[165] -> ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);// 将前期读取的数据加载成了一个环境对象,用来描述信息SpringApplication[166] -> this.configureIgnoreBeanInfo(environment);// 做了一个配置,备用SpringApplication[167] -> Banner printedBanner = this.printBanner(environment);// 初始化启动图标 使用到之前配置的`SpringApplication[106] -> this.bannerMode = Mode.CONSOLE;` SpringApplication[168] -> context = this.createApplicationContext();// 根据之前的容器类型,创建一个容器上下文// 如果是NONE: 则context容器为 org.springframework.context.annotation.AnnotationConfigApplicationContext@386f0da3// 如果是SERVLET: 则context容器为 org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@8dbfffbSpringApplication[169] -> context.setApplicationStartup(this.applicationStartup);// 设置启动模式SpringApplication[170] -> this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);// 对容器进行设置,参数来源于前期的设定SpringApplication[171] -> this.refreshContext(context);// 刷新容器环境,(Spring IOC容器初始化经典原理)【spring默认的onRefresh()方法是空的,springboot将onRefresh()方法重写,加入了tomcat的启动】SpringApplication[172] -> this.afterRefresh(context, applicationArguments);// 刷新完毕后做后处理 空方法SpringApplication[173] -> stopWatch.stop();// 计时器结束SpringApplication[174] -> if (this.logStartupInfo) {// 判定是否记录启动时间的日志,默认为trueSpringApplication[175] -> (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);// 创建日志对应的对象,输出日志信息,包含启动时间// 控制台打印 : 2023-08-16 20:10:25.288 INFO 1932 --- [ main] com.ming.SpringbootDemo05Application : Started SpringbootDemo05Application in 18.095 seconds (JVM running for 20.728)SpringApplication[176] -> }SpringApplication[178] -> listeners.started(context);// 监听器执行了对应的操作步骤SpringApplication[179] -> this.callRunners(context, applicationArguments);SpringApplication[180] -> } catch (Throwable var10) {SpringApplication[181] -> this.handleRunFailure(context, var10, listeners);// 异常处理SpringApplication[182] -> throw new IllegalStateException(var10);SpringApplication[183] -> }SpringApplication[185] -> try {SpringApplication[186] -> listeners.running(context);// 监听器执行了对应的操作步骤SpringApplication[187] -> return context;SpringApplication[188] -> } catch (Throwable var9) {SpringApplication[189] -> this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);SpringApplication[190] -> throw new IllegalStateException(var9);SpringApplication[191] -> }
参考博客:
- https://blog.csdn.net/weixin_45525272/article/details/126502991
- https://blog.csdn.net/A_art_xiang/article/details/122437676
容器类型选择
监听器
监听器类型:
- 在应用运行但未进行任何处理时,将发送 ApplicationStartingEvent。
- 当Environment被使用,且上下文创建之前,将发送 ApplicationEnvironmentPreparedEvent。
- 在开始刷新之前,bean定义被加载之后发送 ApplicationPreparedEvent。
- 在上下文刷新之后且所有的应用和命令行运行器被调用之前发送 ApplicationStartedEvent。
- 在应用程序和命令行运行器被调用之后,将发出 ApplicationReadyEvent,用于通知应用已经准备处理请求。
- 启动时发生异常,将发送 ApplicationFailedEvent