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 章节:

简介 Spring 在2.5版之后,提出了基于注解进行管理Spring,关于使用XML管理Bean可参考以下文章: ……
2023-02-28

 

 

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

如果您喜欢本站,点击这儿不花一分钱捐赠本站

这些信息可能会帮助到你: 下载帮助 | 报毒说明 | 进站必看

修改版本安卓软件,加群提示为修改者自留,非本站信息,注意鉴别

THE END
分享
二维码
打赏
海报
Java – SpringBoot原理
  Bean 创建方式 xml创建Bean Spring中提供一种Bean的管理,通过XML方式声明类,可以在Spring中载入创建类的对象 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http……
<<上一篇
下一篇>>