SpringBoot原理篇

自动配置

bean的加载方式

1.纯xml

在xml中配置自定义bean和第三方bean,通过bean标签实现,同时也能利用bean标签中的属性如scope,autowire,lazy-init等

<?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.itheima.bean.Cat"/>

</beans>

2.xml+注解

在xml中开启包扫描。需要注意的是这里需要加上context命名空间

<?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
       http://www.springframework.org/schema/context/spring-context.xsd
">
    <!--指定加载bean的位置,component-->
    <context:component-scan base-package="com.itheima.bean"/>

</beans>

使用@Component注解或其衍生注解@Controller @Service@Repository定义 自定义bean

@Component("tom")
public class Cat {
}

如果是第三方bean,使用@Bean定义第三方bean,并将所在类定义为配置类或Bean

@Component
public class DbConfig {
    @Bean
    public DruidDataSource getDataSource(){
        DruidDataSource ds = new DruidDataSource();
        return ds;
    }
}

3.纯注解

注解方式声明配置类,在这里,@Configuration可省略

@Configuration
@ComponentScan("com.itheima")
public class SpringConfig {
}
拓展1:FactoryBean
初始化实现 FactoryBean 接口的类,实现对 bean 加载到容器之前的批处理操作
public class DogFactoryBean implements FactoryBean<Dog> {
    @Override
    public Dog getObject() throws Exception {
        //可实现自定义的bean创建逻辑,如特殊初始化等
        return new Dog();
    }

    @Override
    public Class<?> getObjectType() {
        return Dog.class;
    }

    @Override
    public boolean isSingleton() {
        return false;
    }
}
public class SpringConfig {
    @Bean
    public DogFactoryBean dog(){
        return new DogFactoryBean();
    }
}

通过FactoryBean可以实现

  1. 自定义Bean创建逻辑:通过实现FactoryBean 接口,可以在getObject 方法中定义自定义的Bean创建逻辑。这对于那些需要特殊初始化、从外部资源获取数据或进行复杂计算的Bean非常有用。

  2. 抽象复杂性FactoryBean 可以用于封装复杂的对象创建和初始化过程,从而将这些复杂性隐藏在Bean的后台。

  3. 与外部资源交互:可以使用FactoryBean 与外部资源(如数据库连接池、JNDI资源、远程服务等)进行交互,以创建和管理与这些资源相关的Bean。

  4. 懒加载:通过返回falseisSingleton 方法,可以实现懒加载,即只有在首次访问Bean时才会创建它。

  5. 类型控制getObjectType 方法可以提供关于FactoryBean创建的Bean类型的信息,以便在容器中注册和查找Bean时使用。


拓展2:@ImportResource 

可以通过@ImportResource注解加载xml配置文件,可以使用这个注解来渐进式地将XML配置逐步引入到Java配置中

@Configuration
@ComponentScan("com.itheima")
@ImportResource("applicationContext-config.xml")
public class SpringConfig2 {
}

扩展3:@Configuration(proxyBeanMethods = false)

@Configuration注解中的属性proxyBeanMethods默认值为true,可以保障调用此方法得到的对象是从容器中获取的而不是重新创建的,Spring将为配置类的@Bean方法生成代理,以确保方法调用返回的是同一个实例。

如果proxyBeanMethods = false,则Spring将不会为配置类的@Bean方法生成代理,每次调用这些方法都会返回新的实例。

@Configuration(proxyBeanMethods = false)
public class SpringConfig3 {
    @Bean
    public Book book(){
        System.out.println("book init ...");
        return new Book();
    }
}

4.@Import

使用@Import注解导入要注入的bean对应的字节码
@Import({Dog.class, Cat.class})
public class SpringConfig4 {
}

这种方式下,被导入的bean无需使用注解声明为bean

public class Dog {
}

同时,使用@Import注解可以有效地降低源代码与spring技术的耦合度,实现无侵入式编程。集合第三方bean时也非常方便。

拓展4:@Import导入配置类

使用此注解导入配置类时,会自动将配置类中的bean也一同导入。同时,配置类也可省略@Configuration注解(配置类将不会成为bean)


5.上下文对象注册bean

使用上下文对象在容器初始化完毕后注入bean
public class App5 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig4.class);

        context.registerBean("tom", Cat.class);
        context.register(Dog.class);

        String[] beanDefinitionNames = context.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}

6.ImportSelector

如果配置类导入实现了ImportSelector接口的类,则可以实现对该配置类的编程式处理,如判断是否含有某注解等,并根据判断结果的不同自动导入不同的bean

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {

        boolean flag = annotationMetadata.hasAnnotation("org.springframework.context.annotation.Import");
        
        if (flag){
            return new String[]{"com.itheima.bean.Dog"};
        }
        return new String[]{"com.itheima.bean.Cat"};
    }
}

ImportSelector在Spring源码中被广泛使用,特别是在Spring的自动配置(Auto-Configuration)和条件化配置(Conditional Configuration)方面。它允许Spring框架在运行时根据条件和配置来动态选择性地加载和管理组件,以实现更灵活和可定制的应用程序配置。


7.ImportBeanDefinitionRegistrar

导入实现了 ImportBeanDefinitionRegistrar 接口的类,通过 BeanDefinition 的注册器注册实名 bean ,实现对容器中bean 的裁定,例如对现有 bean 的覆盖,进而达成不修改源代码的情况下更换实现的效果
public class MyRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        //可执行任意自定义操作
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(Dog.class).getBeanDefinition();
        registry.registerBeanDefinition("yellow",beanDefinition);
    }
}

在配置类中导入MyRegistrar

@Import(MyRegistrar.class)
public class SpringConfig7 {
}

8.BeanDefinitionRegistryPostProcessor

导入实现了 BeanDefinitionRegistryPostProcessor 接口的类,通过 BeanDefinition 的注册器注册实名 bean ,实现对容器中bean 的最终裁定
public class MyPostProcessor implements BeanDefinitionRegistryPostProcessor {
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        //可执行任意自定义操作
        BeanDefinition beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(BookServiceImpl2.class).getBeanDefinition();
        beanDefinitionRegistry.registerBeanDefinition("bookService",beanDefinition);
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
    }
}

MypostProcessor中配置的impl2会覆盖impl1

@Import({BookServiceImpl1.class, MyPostProcessor.class})
public class SpringConfig8 {
}

加载bean

public class App8 {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig7.class);
        BookService bookService = context.getBean("bookService", BookService.class);
        bookService.check();
    }
}

BeanDefinitionRegistryPostProcessor的主要作用:

  1. 动态注册Bean定义:你可以在这个阶段动态地注册新的Bean定义,这些Bean可以是通过Java代码创建的,也可以是通过外部配置加载的。

  2. 修改Bean定义:你可以在这个阶段修改已经注册的Bean定义,例如,改变Bean的作用域、属性、初始化方法等。

  3. 条件化注册:根据特定的条件或配置信息来决定是否注册某些Bean定义。这可以用于根据应用程序的配置灵活地注册不同的Bean。


总结

1. xml+<bean/>
2. xml:context+ 注解( @Component+4 @Bean
3. 配置类 + 扫描 + 注解( @Component+4 @Bean
         @Bean 定义 FactoryBean 接口
         @ImportResource
         @Configuration 注解的 proxyBeanMethods 属性
4. @Import 导入 bean 的类
         @Import 导入配置类
5. AnnotationConfigApplicationContext 调用 register 方法
6. @Import 导入 ImportSelector 接口
7. @Import 导入 ImportBeanDefinitionRegistrar 接口
8. @Import 导入 BeanDefinitionRegistryPostProcessor 接口

bean加载控制

1.编程式控制

 可以使用编程语言控制是否加载bean的方式一共有如下四种:

1. AnnotationConfigApplicationContext 调用 register 方法
2. @Import 导入 ImportSelector 接口
3. @Import 导入 ImportBeanDefinitionRegistrar 接口
4. @Import 导入 BeanDefinitionRegistryPostProcessor 接口
利用条件判断语句控制是否加载bean
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        try {
            //有Mouse便加载Cat,否则不加载
            Class<?> clazz = Class.forName("com.itheima.ebean.Mouse");
            if(clazz != null) {
                return new String[]{"com.itheima.bean.Cat"};
            }
        } catch (ClassNotFoundException e) {
            return new String[0];
        }
        return null;
    }
}

2.注解式控制

springboot提供了一系列的注解用于控制bean加载

匹配指定类,使用注解@ConditionalOnClass,当指定的类存在时,结果为true

public class SpringConfig {
    @Bean
    @ConditionalOnClass(Mouse.class)
    public Cat tom(){
        return new Cat();
    }
}

未匹配指定类,使用注解@ConditionalOnMissingClass,当指定的类不存在时,结果为true

public class SpringConfig {
    @Bean
    @ConditionalOnClass(Mouse.class)
    @ConditionalOnMissingClass("com.itheima.bean.Wolf")
    public Cat tom(){
        return new Cat();
    }
}
匹配指定名称的bean,使用注解@ConditionalOnBean,传入参数name
@Import(Mouse.class)
public class SpringConfig {
    @Bean
    @ConditionalOnBean(name="jerry")
    public Cat tom(){
        return new Cat();
    }
}
匹配指定环境,使用注解@ConditionalOnNotWebApplication,当不是web环境时,加载bean
@Configuration
@Import(Mouse.class)
public class MyConfig {
    @Bean
    @ConditionalOnClass(Mouse.class)
    @ConditionalOnMissingClass("com.itheima.bean.Dog")
    @ConditionalOnNotWebApplication
    public Cat tom(){
        return new Cat();
    }
}

bean依赖属性配置

1.将业务功能 bean 运行需要的资源抽取成独立的属性类( ******Properties),设置读取配置文件信息。这里使用@ConfigurationProperties(prefix = "cartoon")注解自动注入application.yml中的属性。
需要注意的是,@ConfigurationProperties注解需要该类是一个bean才能使用,这里为了解耦,配合使用了另一个注解@EnableConfigurationProperties(CartoonProperties.class)
@Data
@ConfigurationProperties(prefix = "cartoon")
public class CartoonProperties {
    private Cat cat;
    private Mouse mouse;
}

2.在application.yml中设置属性

cartoon:
  cat:
    name: "迪迦"
    age: 4
  mouse:
    name: "盖亚"
    age: 6

3.在Cartoon类进行属性注入,并进行判定:如果Properties类中有值,则使用该值,否则使用默认值

@EnableConfigurationProperties(CartoonProperties.class)
@Data
public class Cartoon {
    private Cat cat;
    private Mouse mouse;

    private CartoonProperties cartoonProperties1;

    public Cartoon(CartoonProperties cartoonProperties) {
        this.cartoonProperties1 = cartoonProperties;
        cat = new Cat();
        cat.setName(cartoonProperties1.getCat()!=null &&
                StringUtils.hasText(cartoonProperties1.getCat().getName())?
                cartoonProperties1.getCat().getName():"tom");
        cat.setAge(cartoonProperties.getCat()!=null &&
                cartoonProperties.getCat().getAge() != null ?
                cartoonProperties.getCat().getAge() : 4);
        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() : 1);
    }

    public void play(){
        System.out.println(cartoonProperties1.getCat().getName());
        System.out.println(cat.getAge()+"岁的"+cat.getName() + 
        "与"+mouse.getAge()+"岁的"+mouse.getName()+"打起来了");
    }
}

4.使用@Import注解解除耦合。这样当我们调用Cartoon类时,它才会成为一个bean,反之就不会。

@SpringBootApplication
@Import(Cartoon.class)
public class App {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(App.class);
        Cartoon cartoon = context.getBean(Cartoon.class);
        cartoon.play();
    }
}

自动装配原理

Spring Boot的自动装配原理是通过条件化配置和类路径扫描来实现的,它的核心思想是根据项目的依赖和配置来自动配置Spring应用程序,以尽量减少手动配置的工作。

以下是Spring Boot自动装配的关键原理:

  1. 条件化配置(Conditional Configuration): Spring Boot使用条件化注解(例如@ConditionalOnClass@ConditionalOnProperty@ConditionalOnBean等)来定义配置类在何种条件下生效。这意味着只有在满足特定条件时,自动配置类才会被加载和生效。例如,只有在类路径中存在某个特定的类时,相关的自动配置类才会生效。

  2. 类路径扫描(Classpath Scanning): Spring Boot会自动扫描项目的类路径,寻找特定的配置文件和类。这些配置文件和类可以包含各种自动配置的规则和配置信息。Spring Boot会自动识别这些配置,并在适当的条件下进行自动装配。

  3. 默认属性(Default Properties): Spring Boot会提供一组默认的配置属性,这些属性可以在项目中自动生效,而不需要手动配置。这些默认属性可以在Spring Boot的文档中找到,并可以在应用程序的配置文件中进行覆盖。

  4. 自动配置类(Auto-Configuration Classes): Spring Boot包含了大量的自动配置类,这些类根据条件化配置的原则,在特定条件下自动生效。例如,如果项目中引入了Spring Data JPA依赖,Spring Boot会自动配置一个JPA数据源。

  5. 自定义配置: 开发人员也可以创建自定义的配置类,并使用条件化注解来指定何时生效。这允许开发人员在不修改Spring Boot自动配置的情况下,为应用程序添加特定的配置。


变更自动配置

  • 自定义自动配置(META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itheima.bean.CartoonCatAndMouse
  • 控制SpringBoot内置自动配置类加载
spring:
    autoconfigure:
        exclude: 
            - org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
            - org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
@EnableAutoConfiguration(excludeName = "",exclude = {})
  • 变更自动配置:去除tomcat自动配置(条件激活),添加jetty自动配置(条件激活)
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <!--web起步依赖环境中,排除Tomcat起步依赖,匹配自动配置条件-->
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <!--添加Jetty起步依赖,匹配自动配置条件-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
</dependencies>


自定义starter

1.案例:记录系统访客独立IP访问次数(需求分析)

1. 数据记录位置:Map / Redis
2. 功能触发位置:每次web请求(拦截器)
  1.  步骤一:降低难度,主动调用,仅统计单一操作访问次数(例如查询)
  2.  步骤二:开发拦截器
3. 业务参数(配置项)
  1. 输出频度,默认10秒
  2. 数据特征:累计数据 / 阶段数据,默认累计数据
  3. 输出格式:详细模式 / 极简模式
4. 校验环境,设置加载条件

2.业务开发

public class IpCountService {

    private Map<String,Integer> ipCountMap = new HashMap<>();

    @Autowired
    //当前的request对象的注入工作由使用当前starter的工程提供自动注入
    private HttpServletRequest httpServletRequest;

    public void count(){
        //1. 获取当前操作的ip地址
        String ip = httpServletRequest.getRemoteAddr();
        //2. 根据id地址从map中取值并递增
        Integer count = ipCountMap.get(ip);
        if (count == null){
            ipCountMap.put(ip,1);
        }else {
            ipCountMap.put(ip,count + 1);
        }
    }

    //开启定时功能
    @Scheduled(cron = "0/10 * * * * ?")
    public void print(){
        System.out.println(" IP访问监控");
        System.out.println("+-----ip-address-----+--num--+");
        for(Map.Entry<String,Integer> info :ipCountMap.entrySet()){
            String key = info.getKey();
            Integer count = info.getValue();
            String lineInfo = String.format("|%18s |%6d |",key,count);
            System.out.println(lineInfo);
        }
        System.out.println("+--------------------+-------+");
    }
}
自动配置类
@EnableScheduling
@Import(IpCountService.class)
public class IpAutoConfiguration {
}

配置:spring.factories

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.itheima.autoConfig.IpAutoConfiguration
定义属性类,加载对应属性
@ConfigurationProperties(prefix = "tools.ip")
public class IpProperties {

    //日志显示周期
    private Long cycle = 10L;

    //是否周期内重置数据
    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;
        }
    }
    get...
    set...
}
设置加载 Properties 类为 bean
@EnableScheduling
@Import(IpCountService.class)
@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {
}
根据配置切换设置
//开启定时功能
    @Scheduled(cron = "0/10 * * * * ?")
    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> info :ipCountMap.entrySet()){
                String key = info.getKey();
                Integer count = info.getValue();
                String lineInfo = String.format("|%18s |%6d |",key,count);
                System.out.println(lineInfo);
            }
            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> info :ipCountMap.entrySet()){
                String lineInfo = String.format("|%18s |", info.getKey());
                System.out.println(lineInfo);
            }
            System.out.println("+--------------------+");
        }
        if(ipProperties.getCycleReset()){
            ipCountMap.clear();
        }
    }
配置信息
tools:
  ip:
    cycle: 10
    cycle-reset: true
    model: "simple"

3.读取属性cycle

IpProperties中的其他属性都已经可以在ipCountService中读取到,但这个值还没有用上

  • 自定义bean名称
@ConfigurationProperties(prefix = "tools.ip")
@Component("ipProperties")
public class IpProperties {
}
  • 放弃配置属性创建bean方式,改为手工控制
@EnableScheduling
@Import({IpCountService.class,IpProperties.class})
//@EnableConfigurationProperties(IpProperties.class)
public class IpAutoConfiguration {
}
  • 使用#{beanName.attrName}读取bean的属性
//开启定时功能
    //@Scheduled(cron = "0/${tools.ip.cycle:5} * * * * ?")
    @Scheduled(cron = "0/#{ipProperties.cycle} * * * * ?")
    public void print(){
}

4.自定义拦截器

之前开启ip记录功能的做法是在控制层中注入service,手动开启,太过繁琐。

将对应功能配置到拦截器中可实现自动对所有访问进行拦截记录

  • 开发拦截器
public class IpCountInterceptor implements HandlerInterceptor {

    @Autowired
    private IpCountService ipCountService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        ipCountService.count();
        return true;
    }
}
  • 设置核心配置类,加载拦截器
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor()).addPathPatterns("/**");
    }
    
    @Bean
    public IpCountInterceptor interceptor(){
        return new IpCountInterceptor();
    }
}

5.实现yml提示

入配置处理器坐标

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>

然后install,将target中的spring-configuration-metadata.json复制到resources中的META-INF下,然后clear并重新install(不然yml中出现的提示会重复)

并在hits中对选择值进行提示

  "hints": [
    {
      "name": "tools.ip.model",
      "values": [
        {
          "value": "detail",
          "description": "明细模式."
        },
        {
          "value": "simple",
          "description": "极简模式."
        }
      ]
    }
  ]


核心原理

SpringBoot启动流程

  1. 初始化各种属性,加载成对象
    • 读取环境属性(Environment
    • 系统配置(spring.factories
    • 参数(Argumentsapplication.properties
  2. 创建Spring容器对象ApplicationContext,加载各种配置
  3. 在容器创建前,通过监听器机制,应对不同阶段加载数据、更新数据的需求
  4. 容器初始化过程中追加各种功能,例如统计时间、输出日志等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值