Java – SpringBoot原理
Bean 创建方式
xml创建Bean
Spring中提供一种Bean的管理,通过XML方式声明类,可以在Spring中载入创建类的对象
<?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 -->
<bean id="book" class="java.awt.print.Book"></bean>
<!-- 创建第三方 Bean -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.Driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
</beans>
在代码中取得这些Bean时,使用【ClassPathXmlApplicationContext】获取等理Bean
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
ApplicationContext xml = new ClassPathXmlApplicationContext("applicationContext.xml");
xml.getBean("book");
xml.getBean(DruidDataSource.class);
}
}
注解创建Bean
如果使用xml创建Bean,那么在项目的xml中会写入非常多的Bean,xml管理Bean的优点是清晰明了,可以清楚地知道有什么类型的Bean,而缺点则是定义非常麻烦。
Spring 还支持使用注解的方式对Bean进行管理,只需要在类之前加入【@Component】、【@Service】、【@Repository】、【@Configuration】注解即可,但需要在xml中设置Spring在启动时对包中进行扫描,以得到所有定义为Bean的类
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<context:component-scan base-package="cn.unsoft.controller"></context:component-scan>
</beans>
@Component 表示是一类定义为普通Bean类的Bean注解
@Service 表示是一类在项目中充当业务层的Bean
@Repository 表示是一类在项目中充当DAO数据库操作的Bean
@Configuration 表示是一类Spring 配置的Bean
@Component
public class Person { }
全注解创建Bean
使用注解时,Spring难免需要对指定的包进行扫描以得出那些类是属于Bean类,因此需要在xml中定义 context:component-scan.
在Spring中,通过【@ComponentScan】注解,可以把xml中的 context:component-scan 也省略
@ComponentScan({"cn.unsoft.domain","cn.unsoft.controller"})
public class SpringConfig { }
@ComponentScan 注解的类默认会被定义为Bean,所以不需要在带有@ComponentScan注解的类中加入 @Component 注解
启动SpringContext就不需要使用 ClassPathXmlApplicationContext 定义了,使用 AnnotationConfigApplicationContext
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
ApplicationContext xml = new AnnotationConfigApplicationContext(SpringConfig.class);
xml.getBean("book");
xml.getBean(DruidDataSource.class);
}
}
FactoryBean
FactoryBean 是用于创建某一个Bean类的工厂方法,调用它不会返回它自身的对象,而是返回它对应创建的Bean类,Spring提供了FactoryBean的方式,需要实现FactoryBean<T> 接口,其中泛型T则是表示,这个FactoryBean是用来创建T类的Bean对象
FactoryBean 接口需要实现三个方法
public class BookFactoryBean implements FactoryBean<Book> {
/**
* FactoryBean 创建 Bean 的实现方法
* 它是用于工厂创建指定Bean类的方法
* 在这里可以创建一个Bean类,并对这个Bean类做初始化的工作,做完后再返回Spring进行管理
* @return
* @throws Exception
*/
@Override
public Book getObject() throws Exception {
return new Book();
}
/**
* 用于返回这个Bean的类型,因为Spring不知道你的Bean工厂生产的对象是什么类型,
* 所以在这里返回Bean类型给Spring管理
* @return
*/
@Override
public Class<?> getObjectType() {
return Book.class;
}
/**
* 是否是单例
* Spring 管理的Bean多数是单例
* 这个方法是要告诉Spring,管理这个Bean时,这个Bean是否为单例,如果为true,
* 那么在多次调用时Spring只会从它的Bean库中取回原来的对象
* 在Spring中,建议为单例
* @return
*/
@Override
public boolean isSingleton() {
return true;
}
}
proxyBeanMethods
proxyBeanMethods 参数是【@Configuration】中的一个参数,@Configuration 是一个用于声明定义Spring配置的注解,在不使用xml配置第三方Bean时,如果需要声明第三方的Bean,就需要在代码中创建一个注解Bean.
注解Bean中,会new 产生新的Bean类,而 proxyBeanMethods 则是表示是在调用配置第三方的创建方法时,是否返回Spring中已存在的相同Bean
@Configuration(proxyBeanMethods = false)
public class SpringConfig {
/**
* 在创建第三方Bean时,比如 DruidDataSource 数据源的Bean
* 会new一个新的DruidDataSource数据源
* 如果 proxyBeanMethods = false
* 那每一次调用这个配置方法时,所产生的 Bean 都不是同一个 Bean
* 这使得在项目使用中,产生多个 DruidDataSource 对象
* 是不合理的,因为项目中我们只需要一个 DruidDataSource 对象使用就可以
* @return
*/
@Bean
public DruidDataSource dataSource(){
return new DruidDataSource();
}
}
proxyBeanMethods 默认为 true
@ImportResource 导入 xml 的Bean
如果遇到旧项目中,Bean的管理是使用xml声明的,而在新项目中又改用了注解来声明Bean的,那么我们可以兼容xml文件管理Bean的方法
使用【@ImportResource】导入旧项目中的 xml 文件,也可以把 xml 文件中的 Bean导入进来。
@ImportResource({"applocationContext.xml"})
public class SpringConfig { }
@Import 导入非管理 Bean
如果一些类,它本身不是Bean,在不修改这个类任何代码的情况下,使它变成一个Bean
除了可以使用【@Bean】方法引入外,还可以使用【@Import】注解导入
@Import({Book.class, Class.class})
public class SpringConfig { }
即使目标类没有加入【@Component】等等的注解的情况下,也可以使目标变成Spring管理的Bean类。
关于 @Import 可顺带参考以下文章的 @Import 章节:
ApplicationContext 上下文配置创建Bean
在创建完applicationContext上下文时,也可以通过上下文对Bean进行创建,具体方法如下
public static void main(String[] args) {
// 创建完 ApplicationContext 后
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
// 在 ApplicationContext 上下文中加入Bean
// 注意:ApplicationContext 接口没有 registerBean 的方法,所以只能用 AnnotationConfigApplicationContext
applicationContext.registerBean("druid",DruidDataSource.class,...有参构造参数);
// 不使用自定义名称创建 Bean
applicationContext.register(DruidDataSource.class);
}
注意:register 方法中,是没有定义Bean的名称的,这时,Spring 就会在类中,寻找是否有定义Bean名称(如加了【@Component("xxx")】),如果定义了Bean名称,则Spring中的Bean为该名称,如果没有定义名称,则Spring默认使用【类名首字母】小写作为Bean名称。
@Import 导入 ImportSelector 接口创建Bean
ImportSelector 接口是Spring在内部中大量使用的创建Bean的方式,它提供了一个实现类,在实现类中,对使用了【@Import】注解的类,提供了源数据,通过判断源数据,我们可以对创建Bean做各种处理。
// 使用了 @Import 注解时,可使 MyImportSelector 类调用 ImportSelector 实现方法
@Import(MyImportSelector.class)
public class SpringConfigImport { }
在MyImportSelector类中,就可以获取到 SpringConfigImport 类中的所有源数据
源数据包括【类名】、【是否存在xxx注解】、【是否包含xxx注解中的某个参数】、【取得xxx注解中的参数值】等等
通过判断源数据,来自定义需要创建的Bean类,把需要创建的Bean类的全类名数组返回,Spring将对这些类进行Bean创建
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
Map<String, Object> attr = importingClassMetadata.getAnnotationAttributes("org.springframework.context.annotation.Import");
System.out.println(attr);
// 通过对调用 @import 注解的类的各种源数据,来自定义加载需要什么Bean
return new String[]{"cn.unsoft.domain.Dog","cn.unsoft.domain.Cat"};
}
}
@Import 导入 ImportBeanDefinitionRegistrar 接口创建Bean
ImportBeanDefinitionRegistrar 接口比 ImportSelector 多了一种创建Bean的方式,ImportSelector 只提供了调用 @Import 注解的类的源数据,而ImportBeanDefinitionRegistrar 接口除了提供调用 @Import 注解的类的源数据外,还提供了用于创建Bean类的注册器,注册器可以更细节上对Bean的属性进行设置(如设置Bean是否单例等等)
@Import(SpringConfigImportBeanDefinitionRegisirar.class)
public class SpringConfigImport { }
ImportBeanDefinitionRegistrar 实现类
public class SpringConfigImportBeanDefinitionRegisirar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// importingClassMetadata 是对调用@Import注册中的类的各种源数据
// ... 略
// BeanDefinitionRegistry 是定义创建Bean对象的注册器,它可以定义Bean对象的细节
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class);
// 可以设置这个Bean的许多细节
beanDefinition.setDescription("this is a dog class");
// 注册为Bean
registry.registerBeanDefinition("dog", beanDefinition);
}
}
@Import 导入 BeanDefinitionRegistryPostProcessor 接口创建Bean
BeanDefinitionRegistryPostProcessor 接口属于一种后置创建Bean对象的方法
它是执行在所有创建Bean对象方法以后,最后对创建Bean对象收尾的一个接口,因此它可以对Spring中已创建的所有Bean进行处理,包括覆盖Bean,甚至删除Bean
@Import(SpringConfigBeanDefinitionRegistryPostProcessor.class)
public class SpringConfigImport { }
实现类
public class SpringConfigBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
// 如果在别的地方已经对 yellowDog : Dog.class 进行Bean创建,那么在这里将会对之前创建的进行覆盖
BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class);
beanDefinitionRegistry.registerBeanDefinition("yellowDog",beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
Bean 的加载控制
通过上面的Bean创建与加载的八种方法,我们可以在不同的环境下,对Bean的创建。
但是如果我们需要对这个Bean进行判断,在什么时候创建Bean,什么时候不创建Bean这个问题上,只有4种方式可以用
分别是
-> ApplicationContext 上下文配置创建Bean
-> @Import 导入 ImportSelector 接口创建Bean
-> @Import 导入 ImportBeanDefinitionRegistrar 接口创建Bean
-> @Import 导入 BeanDefinitionRegistryPostProcessor 接口创建Bean
通过上面4种方式,我们可以在创建Bean之前,进行对其它环境进行判断,当符合条件时,再创建Bean(例:如发现MySQL包时才创建DruidDataSource)
使用 ImportSelector 接口判断条件创建Bean
本例只选取了 ImportSelector 接口做编程式判断条件创建Bean的案例。
举例:判断Mouse类Bean是否存在,如果存在,则创建Cat类Bean
引入实现ImportSelector接口的方法
@Import(MyImportSelector.class)
public class MySpringConfig { }
实现类
/**
* 这里使用了 ImportSelector 判断什么情况创建 Bean
*
*/
public class MyImportSelector implements ImportSelector{
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
/**
* 判断是否存在类 cn.unsoft.domain.Mouse
* 如果存,则创建 cn.unsoft.domain.Cat 的 Bean
* 如果不存在,则不创建
*/
try {
Class<?> mouse = Class.forName("cn.unsoft.domain.Mouse");
if (mouse != null){
return new String[]{"cn.unsoft.domain.Cat"};
}
} catch (ClassNotFoundException e) {
return new String[0];
}
return null;
}
}
缺点:如果有非常多的Bean需要被判断时,则通过编程式定义判断的话,会非常麻烦。
使用注解判断条件创建Bean
通过使用注解定义每一个Bean在什么时候什么条件成立时,才会被创建。
SpringBoot 中引入了一种通过注解判断类是否需要加载的方式
注解大多以【@ConditionalOnXXXX】为主
举例:如果Mouse类存在,且Dog类不存在时,加载Cat类为Bean
public class MySpringConfig {
@Bean
// 若 Mouse 类存在
@ConditionalOnClass(name = "cn.unsoft.domain.Mouse")
// 若 Dog 类不存在
@ConditionalOnMissingClass("cn.unsoft.domain.Dog")
// 就加载 Cat 类
public Cat cat(){
return new Cat();
}
}
【@ConditionalOnXXXX】 注解还能直接定义在对应的类上面,效果一样
// 若 Mouse 类存在
@ConditionalOnClass(name = "cn.unsoft.domain.Mouse")
// 若 Dog 类不存在
@ConditionalOnMissingClass("cn.unsoft.domain.Dog")
// 就加载 Cat 类
public class Cat { }
举例:若没发现《MySQL驱动类》,就不加载《Druid数据源》
public class MySpringConfig {
@Bean
@ConditionalOnClass(name = "com.mysql.cj.jdbc.Driver")
public DruidDataSource dataSource(){
return new DruidDataSource();
}
}
相关的【@ConditionalOnXXXX】注解有如下
Bean 自动属性配置
在SpringBoot中,我们在调用三方技术时,很多配置不需要我们自己去配置,但为何SpringBoot能做到三方技术自动配置?
1.收集Spring开发者的编程习惯,整理开发过程使用的常用技术列表——>(技术集A)
2.收集常用技术(技术集A)的使用参数,整理开发过程中每个技术的常用设置列表——>(设置集B)
3.初始化SpringBoot基础环境,加载用户自定义的bean和导入的其他坐标,形成初始化环境
4.将技术集A包含的所有技术都定义出来,在Spring/SpringBoot启动时默认全部加载
5.将技术集A中具有使用条件的技术约定出来,设置成按条件加载,由开发者决定是否使用该技术(与初始化环境比对)
6.将设置集B作为默认配置加载(约定大于配置),减少开发者配置工作量
@SpringBootApplication 启动过程(了解)
@SpringBootApplication 是由多个注解合并的组合注解,@SpringBootApplication注解中包含如下注解
@SpringBootConfiguration
@Configuration
@Indexed
@EnableAutoConfiguration
@AutoConfigurationPackage
@Import({AutoConfigurationPackages.Registrar.class})
@Import({AutoConfigurationImportSelector.class})
@ComponentScan(
excludeFilters = {@Filter( type = FilterType.CUSTOM,classes = {TypeExcludeFilter.class}), @Filter(type = FilterType.CUSTOM,classes = {AutoConfigurationExcludeFilter.class})})
其中 @Import({AutoConfigurationPackages.Registrar.class}) 与 @Import({AutoConfigurationImportSelector.class}) 是启动SpringBoot程序的主要入口
AutoConfigurationPackages.Registrar.class(了解)
AutoConfigurationPackages.Registrar.class 是用于取得Application.java所在的包,取出该包的全名称,并对该包进行扫描
AutoConfigurationPackages.Registrar.class
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 取出Application.java中定义的注解属性
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}
AutoConfigurationImportSelector.class
AutoConfigurationImportSelector.class 是用于加载所有默认配置项,并对技术集加载默认配置(技术集会通过@ConditionalOnClass等方式判断是否需要加载)
AutoConfigurationImportSelector实现了接口【DeferredImportSelector->process方法】
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());
});
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);
}
}
getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
}
getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
loadFactoryNames
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();
try {
// 取出自动配置包中的配置项
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
后续执行代码:略
自定义自动配置功能
通过上面的基础代码跟进我们知道,自动配置的方式就是SpringBoot先把多数三方技术开发自动配置整合,并把所有自动配置存放在META-INF/spring.factories文件中,spring.factories文件包含了所有该技术的自动配置类,在启动时将被加载,并通过读取application.yml 配置文件来自动配置,如配置文件中没有该配置项,那么自动配置类中已包含默认配置。
基于上述原理,我们也可以通过在自定义的包中创建 META-INF/spring.factories 文件,定义启动时用于存储自动配置的类,当SpringBoot启动时会寻找该文件并自动加载自定义自动配置类。
# 自动配置文件 spring.factories 格式
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.autoconfigure.data.redis.RedisUrlSyntaxFailureAnalyzer,\
org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinitionFailureAnalyzer,\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\
org.springframework.boot.autoconfigure.jooq.NoDslContextBeanFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.MissingR2dbcPoolDependencyFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.MultipleConnectionPoolConfigurationsFailureAnalyzer,\
org.springframework.boot.autoconfigure.r2dbc.NoConnectionFactoryBeanFailureAnalyzer,\
org.springframework.boot.autoconfigure.session.NonUniqueSessionRepositoryFailureAnalyzer
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.mustache.MustacheTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafTemplateAvailabilityProvider,\
org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvider
# DataSource initializer detectors
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDatabaseInitializerDetector
# Depends on database initialization detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.autoconfigure.batch.JobRepositoryDependsOnDatabaseInitializationDetector,\
org.springframework.boot.autoconfigure.quartz.SchedulerDependsOnDatabaseInitializationDetector,\
org.springframework.boot.autoconfigure.session.JdbcIndexedSessionRepositoryDependsOnDatabaseInitializationDetector
自动配置类,可参考Mybatis-Plus官方做法。
自定义starter
自定义一个带自动配置的starter
创建自动配置类
自动配置功能,需要有一个类,用于存储配置项,和自动配置的功能
cn.unsoft.autoconfig.IPCountConfiguration
public class IPCountConfiguration {
@Bean
public IPCountService ipCountService(){
return new IPCountService();
}
}
为后面定时任务,需要开启Spring提供的定时任务功能
@EnableScheduling
public class IPCountConfiguration { }
为后面做自动配置开启自动配置选项
@EnableScheduling
@EnableConfigurationProperties({IPProperties.class})
public class IPCountConfiguration { }
创建自定义配置文件 spring.factories
META-INF/spring.factories
// 设定SpringBoot在自动配置时,调用那一个类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.unsoft.autoconfig.IPCountConfiguration
创建存储自动配置项类
cn.unsoft.autoconfig.IPCountConfiguration
// 读取配置文件中的配置,如果没有,则使用原来的配置
@ConfigurationProperties("tools.ip")
public class IPProperties {
/**
* 设置该报表多少秒输出一次
*/
private Integer cycle = 5;
/**
* 设置该报表每次输出完后是否删除旧统计
*/
private boolean cycleReset = false;
private String model = LogModel.DETAIL.value;
/**
* 创建一个枚举型类型
*/
public enum LogModel {
SIMPLE("simple"),
DETAIL("detail");
private String value;
LogModel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
public Integer getCycle() {
return cycle;
}
public void setCycle(Integer cycle) {
this.cycle = cycle;
}
public boolean isCycleReset() {
return cycleReset;
}
public void setCycleReset(boolean cycleReset) {
this.cycleReset = cycleReset;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
}
创建Bean
创建一个用于增加访问数统计的方法
cn.unsoft.service.IPCountService
public class IPCountService {
private Map<String, Integer> ipCountMap = new HashMap<>();
@Autowired
private HttpServletRequest httpServletRequest;
/**
* 用于计算记录ip请求数
*/
public void count() {
/**
* 1.通过HttpRequest获取ip地址
* 2.把获取到的ip存到Map中
*/
String remoteAddr = httpServletRequest.getRemoteAddr();
System.out.println("------------"+remoteAddr);
Integer reqCount = ipCountMap.get(remoteAddr);
if (reqCount == null) {
// 如果没有就提交一个新的
ipCountMap.put(remoteAddr, 1);
} else {
// 如果有就增加一个值
ipCountMap.put(remoteAddr, ipCountMap.get(remoteAddr) + 1);
}
}
}
创建一个用于打印统计信息的方法
// 用于打印统计结果的方法,并开启定时任务
@Scheduled(cron = "0/5 * * * * ?")
public void print() {
Set<Map.Entry<String, Integer>> ips = ipCountMap.entrySet();
System.out.println("+------------ip统计+");
for (Map.Entry<String, Integer> ipEntry : ips) {
String key = ipEntry.getKey();
Integer value = ipEntry.getValue();
System.out.println(String.format("|%10s |%5d |", key, value));
}
System.out.println("+------------------+");
}
改造Bean类,使得Bean支持配置项设定
// 用于打印统计结果的方法,并开启定时任务
@Scheduled(cron = "0/5 * * * * ?")
public void print() {
Set<Map.Entry<String, Integer>> ips = ipCountMap.entrySet();
System.out.println("+------------ip统计+");
/**
* 对配置项输出对应信息
* - 设定输出模式
*/
if (ipProperties.getModel().equals(IPProperties.LogModel.SIMPLE.getValue())) {
// 如果是SIMPLE
for (Map.Entry<String, Integer> ipEntry : ips) {
String key = ipEntry.getKey();
System.out.println(String.format("|%15s |", key));
}
} else if (ipProperties.getModel().equals(IPProperties.LogModel.DETAIL.getValue())) {
// 如果是DETAIL
for (Map.Entry<String, Integer> ipEntry : ips) {
String key = ipEntry.getKey();
Integer value = ipEntry.getValue();
System.out.println(String.format("|%10s |%5d |", key, value));
}
}
System.out.println("+------------------+");
/**
* 对配置项输出对应信息
* - 是否清空统计数据
*/
if (ipProperties.isCycleReset()){
ipCountMap.clear();
}
}
配置项结构
tools:
ip:
cycle: 5
cycleReset: true
model: detail
关于Cycle(时间间隔)配置的问题
我们知道,时间间隔使用的是【定时任务】,其时间定义在【@Scheduled(cron = "0/5 * * * * ?")】注解中,要使注解使用自定义属性,有几种办法:
1.直接读取yml配置文件:
在@Scheduled注解中,可以使用【${}】来直接读取yml配置文件中的值
@Scheduled(cron = "0/${tools.ip.cycle:5} * * * * ?")
如果yml配置中不填写值,则使用默认值 5
缺点:如果直接访问yml配置中的值,那么IPProperties类中的配置就没有被使用了,并不建议
2.使用【#{}】读取Bean中的属性
在@Scheduled注解中,可以使用【#{}】来读取Bean中的属性
@Scheduled(cron = "0/${IPProperites.cycle} * * * * ?")
"IPProperites"这个名字从那里来,其实是源自Bean在Spring中的名称【@Component("IPProperites")】
但是本案例中,IPProperites类,使用的是【@ConfigurationProperties】和【@EnableConfigurationProperties】注解被加载成Bean的,并没有设定Bean名称。
如果要设定名称的话,那么必须要在IPProperites类中加入【@Component("IPProperites")】
那么问题来了,我们知道【@EnableConfigurationProperties】注解会直接使自动配置类作为Bean加载,如果再加上【@Component】,就会出现两个Bean.
因此本例中,为了使用【@Component("IPProperites")】,就不能使用【@EnableConfigurationProperties】注解,只能使用【@Import】与【@Component("IPProperites")】注解实现带名称加载成Bean
cn.unsoft.autoconfig.IPCountConfiguration
@EnableScheduling
// 不能使用 @EnableConfigurationProperties 因为需要加载Bean名称
//@EnableConfigurationProperties({IPProperties.class})
@Import(IPProperties.class)
public class IPCountConfiguration { }
cn.unsoft.properties.IPProperties
@Component("IPProperties")
@ConfigurationProperties("tools.ip")
public class IPProperties { }
通过以上两个加载类的更改后,就可以使用【#{}】进行配置了
@Scheduled(cron = "0/#{IPProperties.cycle} * * * * ?")
public void print() { ... }
创建过滤器
从上面的案例中可以看出,我们的请求,都是在执行后手动调用的,那有没有一种方法,可以只需要导入依赖坐标后,就能生效呢?
可以通过过滤器,使得每一次的请求都能被触发
1.定义一个处理过滤器的类
/**
* 过滤器的处理类
*/
public class IPInterceptor implements HandlerInterceptor {
@Autowired
private IPCountService ipCountService;
/**
* 添加一个过滤器,使得每一次请求都会被触发
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 每一次的触发都会调用一次
ipCountService.count();
return true;
}
}
2.为软件包增加过滤器
增加过滤器后,过滤器会在父项目中自动运行
/**
* 往Spring中注册一个过滤器
*/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 所有请求都会被这个过滤器触发
registry.addInterceptor(interceptor()).addPathPatterns("/**");
}
@Bean
public IPInterceptor interceptor(){
return new IPInterceptor();
}
}
yml提示功能
1.添加依赖坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
2.对项目进行一次install
install完成后,会在classes\META-INF中会多出一个文件【spring-configuration-metadata.json】
把这个文件复制到我们的 resources\META-INF 文件夹中
即可完成yml配置提示
注意:把文件复制后,打包前必须要把步骤 1 中的坐标注释了,否则打包后我们的【spring-configuration-metadata.json】文件会被坐标生成的给覆盖
3.实现枚举项候选功能
在【spring-configuration-metadata.json】文件中,有一个属性【hints】,它是用于定义默认可选参数
"hints": [
{
"name": "tools.ip.mode",
"values": [
{
"value": "detail",
"description": "明细模式."
},
{
"value": "simple",
"description": "极简模式."
}
]
}
]
注意:description 文本解析结尾需要加上【 . 】符号
打包
打包时,需要执行Maven命令 clean 和 install
install 是指把包打包好后,再安装到Maven库中,这样在其它项目中才可以顺利在Maven中导入坐标。
参考项目
项目下载地址:https://www.tzming.com/wp-content/uploads/2022/filedown/ip_spring_boot_starter.rar
共有 0 条评论