初步原理分析-Spring开发

目录

1 Spring中常见的Bean注解

2 依赖注入注解

2.1 @Autowired

2.2 @Resource

3 Bean初步原理分析

3.1 项目配置文件

3.2 Bean管理

3.2.1 获取bean

3.2.2 bean创建模式

3.2.3 第三方依赖的Bean配置管理

3.3 SpringBoot起步依赖和自动装配

3.4 起步依赖-starter

3.5 第三方Bean的加载

3.5.1 Bean加载的整体过程

3.5.2 自动装配源码分析例子——SpringBoot中自动配置类的Bean加载

3.5.3 按条件加载Bean

4 总结


码字不易,喜欢就点个关注❤,持续更新技术内容。相关资料请私信。

 

相关内容:

Servlet原理和简单的案例编写_Maxlec的博客-CSDN博客

第一篇:SpringBoot项目的创建和开发_Maxlec的博客-CSDN博客

 

1 Spring中常见的Bean注解

Spring中常用的控制反转的注解:

IOC注解说明位置
@Component声明bean的基础注解,bean的名称默认使用类名或方法名首字母小写,也可以自己指定。不属于以下三类时使用此注解
@Controller封装了@Component,相当于衍生注解修饰控制器
@RestController包含@Controller和@ResponseBody修饰控制器
@Service封装了@Component,相当于衍生注解修饰业务层

以及@Repository、@RequestBody、@Index、@Import等。

其中:

  1. @Index提升@ComponentScan的效率。

  2. @Import是import标签的替换,在SpringBoot的自动装配中非常重要,也是EnableXXX的前置条件。

  3. @Repository标注在由Mybatis整合的数据访问类上。

Bean组件扫描:

  1. 前面声明bean的注解要生效好需要被@ComponentScan扫描到。

  2. @ComponentScan注解已经在启动类声明注解@SpringBootApplication中隐式配置,默认扫描范围时启动类所在的包及其子包。所以尽量按照这样的规范进行编写。

2 依赖注入注解

@Autowired和@Resource的区别。@Resource的作用相当于@Autowired,只不过@Autowired按照byType方式进行装配注入。

主要体现在以下 5 点:

  1. 来源不同;

  2. 依赖查找的顺序不同;

  3. 支持的参数不同;

  4. 依赖注入的用法不同;

  5. 编译器 IDEA 的提示不同。

@Resource和@Autowired都是做bean注入时使用的,首先它们的来源不同,@Autowired是Spring的注解。而@Resource是java中的注解。

  1. 相同点:两者都能修饰字段和setter方法上,两者都写在字段上,那么就不需要再写setter方法了。

  2. 不同点:@Autowired按照byType方式匹配进行装配注入,@Resource可以按照byName和byType的方式匹配注入。当然如果@Autowired想按照byName方式匹配注入,Spring提供了@Qualifier注解指定bean的name。

2.1 @Autowired

@Autowired为Spring提供的注解,需要从springframework导入包。@Autowired注解是按照类型(byType)装配依赖对象,默认情况下要求依赖对象必须存在,如果允许为null需要设置它的required为false。

public class TestServiceImpl {
    // 下面两种方式只需使用一种即可
    
    @Autowired // 用于字段上
    private UserMapper userMapper;
    
    @Autowired  // 用于方法属性上
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

因为@Autowired按照类型匹配进行装配注入,所以如果在注入一个类似于IUserService接口时,该接口有多个实现类,那按照类型匹配的话,注入的是哪一个bean呢?这时就会报自动装配失败的错误信息。

这时如果想按照名称(byName)来装配,可以结合@Qualififier注解一起使用:

public class TestServiceImpl {
    @Autowired
    @Qualifier("userMapper")
    private UserMapper userMapper;
}

或者在想要注入的类上再加上@Primary注解设置优先级,优先装配注入该bean。或者使用@Resource注解。

2.2 @Resource

@Resource默认按照byName自动匹配注入,由J2EE提供,需要从java导入包。@Resource有两个属性:name和type,而Spring将@Resource注解的name属性解析为bean的名称,而type属性则解析为bean的类型。所以如果使用name属性,则使用byName自动匹配注入,而使用type属性则使用byType自动匹配注入。都不定义的话,Spring会通过反射机制使用byName自动匹配注入。

public class TestServiceImpl {
    // 下面两种方式只需使用一种即可
    
    @Resource(name="userMapper") // 用于字段上
    private UserMapper userMapper;
    
    @Resource(name="userMapper")  // 用于方法属性上
    public void setUserMapper(UserMapper userMapper) {
        this.userMapper = userMapper;
    }
}

@Resource的装配注入顺序:

  1. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配注入,找不到则抛出异常。

  2. 如果指定了type,则从上下文中查找类似匹配的唯一bean进行装配注入,找不到或者找到多个都会抛出异常。

  3. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配注入,找不到则抛出异常。

  4. 如果都没指定,则自动按照byName方式进行装配注入,找不到则回退为一个原始类型进行匹配。

3 Bean初步原理分析

3.1 项目配置文件

SpringBoot中支持三种格式的配置文件:.properties、.yml、.yaml。

优先级:.properties > .yml > .yaml。

在项目开发时,推荐使用统一格式的配置文件。主流是使用.yml。

除了配置文件的方式进行端口配置,还可以在项目配置中通过java系统属性和命令行参数的方式进行属性配置。

  1. java系统属性配置:-Dserver.port=xxxx。

  2. 命令行参数:--server.port=xxxx。

如果项目打包后,还需要修改配置,可以通过执行java指令运行jar包进行配置:

java -Dserver.port=xxxx -jar xxx.jar --server.port=xxxx

3.2 Bean管理

3.2.1 获取bean

在Spring项目启动时,会把bean都创建好放在IOC容器中,如果想要主动获取这些bean,可以通过如下方式:

  1. 根据name获取bean:Object getBean(String name)

  2. 根据名称和类型获取bean:<T> T getBean(String name, Class<T> requiredType)

  3. 根据类型获取bean:<T> T getBean(Class<T> requiredType)

@SpringBootTest
class SpringBootApplicationTest {
    
    // 首先获取IOC容器
    @Autowired
    private ApplicationContext applicationContext;
    
    @Test
    public void GetBeanTest() {
        // 根据bean名称获取bean对象
        UserController bean1 = (UserController) applicationContext.getBean("userController");
        System.out.println("bean1"+bean1);
        
        // 根据bean类型获取bean对象
        UserController bean2 = applicationContext.getBean(UserController.class);
        System.out.println("bean2"+bean2);
        
        // 根据bean名称和类型获取bean对象
        UserController bean3 = applicationContext.getBean("userController", UserController.class);
        System.out.println("bean3"+bean3);
    }
}

可以看到三次获取到的bean对象的地址都是一样的,说明是同一个bean,bean对象的创建方式是单例的(分为饿汉和懒汉单例),简称单例bean。获取对象的时候,单例bean在项目启动时在内部提前创建好了,直接返回。如果我们需要每次获取的都是全新的bean,那么需要设置bean的作用域。

3.2.2 bean创建模式

Spring支持五种bean的创建方式,后三种在Web环境中生效:

作用域说明
singleton容器内同名称的bean只有一个实例。(默认饿汉单例创建方式)
prototype每次获取该bean的时都创建新的实例。
request每个请求对应创建一个新的实例。
session每个会话对应创建一个新的实例。
application每个应用对应创建一个新的实例。

Spring中默认使用singleton模式创建实例,singleton分为饿汉单例和懒汉单例。顾名思义,前者饿汉先创建好,直接获取时直接返回。后者懒汉在获取时才进行创建再在返回:

public class SingleInstanceDemo01 {
    public static void main(String[] args) {
        Singleton01 s1 = Singleton01.getInstance();
        Singleton01 s2 = Singleton01.getInstance();
        System.out.println(s1 == s2);
    }
}
​
// 饿汉单例设计模式
class Singleton01{
    //  b.定义一个静态变量存储一个对象( 在用类获取对象的时候,对象已经提前为你创建好了。)
    private static final Singleton01 INSTANCE = new Singleton01();
    //  a.定义一个类,把构造器私有。
    private Singleton01(){
    }
    // c.提供一个返回单例对象的方法。
    public static Singleton01 getInstance(){
        return INSTANCE;
    }
}
public class SingleInstanceDemo02 {
    public static void main(String[] args) {
        Singleton02 s1 = Singleton02.getInstance();
        Singleton02 s2 = Singleton02.getInstance();
        System.out.println(s1 == s2);
    }
}
​
// 懒汉单例设计模式
class Singleton02{
    //  b.定义一个静态变量存储一个对象(这里不能创建对象,需要的时候才创建,这里只是一个变量用于存储对象!)
    public static Singleton02  instance ;
​
    //   a.定义一个类,把构造器私有。
    private Singleton02(){
​
    }
    //  c.提供一个返回单例对象的方法。
    public static Singleton02 getInstance(){
        if(instance == null){
            // 第一次来拿单例对象!需要创建一次对象,以后直接返回!!
            instance = new Singleton02();
        }
        return instance;
    }
}

在Spring中想要实现懒汉单例的话只需要在类上加上@Lazy注解即可。其他非单例模式可以通过注解@Scope指示bean的创建模式。

@Scope("prototype")

在实际开发中,绝大部分的Bean是单例的,所以说大部分Bean不需要配置scope属性。

3.2.3 第三方依赖的Bean配置管理

我们自己定义Bean只需要在类上加上@Component、@RestController、@Service注解即可。而我们引入的第三方依赖中的类怎么定义为Bean呢。

如果要管理的bean对象来自于第三方(非自己定义),那么无法使用@Component注解定义Bean,需要使用到@Bean注解。

这些第三方Bean都是进行集中管理、分类配置的,可以通过@Configuration注解声明一个配置类。如在配置类中管理各种第三Bean:

@Configuration
public class CommonCofig {
    @Bean
    public Xyy xyy(){
        return new Xyy();
    }
    
    @Bean
    ...
}

通过@Bean注解还可以声明bean的名称,不指定默认bean的名称默认是类名或方法名首字母小写。

如果第三方Bean需要依赖其他Bean,直接在Bean定义方法中依赖的形参即可,容器会将其一并定义为Bean。

3.3 SpringBoot起步依赖和自动装配

Spring是目前最流行的Java框架,即Spring Framework。直接基于Spring Framework开发会很繁琐,比如依赖配置以及大量的项目配置文件。SpringBoot就是基于Spring这个基础框架开发的,它简化开发过程,比如起步依赖配置,自动装配等等,这些让项目开发更加简单、快捷。

3.4 起步依赖-starter

我们通过SpringMVC搭建一个Web应用,需要做以下工作:

  1. 配置pom.xml添加Spring 、SpringMVC框架的依赖,同时还需要考虑这些不同的框架的不同版本是否存在不兼容的问题。

  2. 配置Web.xml,加载Spring、SpringMVC。

  3. 配置Spring。

  4. 配置Spring MVC。

  5. 编写业务逻辑代码。

而使用SpringBoot搭建的话,只需要做以下工作:

  1. 配置pom.xml继承SpringBoot的pom.xml,添加 Web 起步依赖。

  2. 创建启动引导类。

  3. 编写业务逻辑代码。

使用SpringBoot的最大优点就是简化了配置的工作,并不是说使用SpringBoot就不需要这些配置过程了。而是SpringBoot帮我们把这些起步配置工作给做了。SpringBoot为我们提供了起步依赖。

这些起步依赖就是starter,在各种starter中,定义了完成该功能需要的坐标合集。大部分版本信息来自于父工程,只要我们的工程继承starter-parent,通过依赖传递,就可以简单方便获得需要的jar包,并且不会存在版本冲突等问题。

比如引入spring-boot-starter-web,那么完成Web开发所需的依赖集合都引入了,利用的就是Maven的依赖传递。

在spring-boot-starter-web中定义了各种技术的版本信息,组合了一套最优搭配的技术版本。

3.5 第三方Bean的加载

3.5.1 Bean加载的整体过程

SpringBoot的自动装配就是当spring容器启动后,一些配置类、bean对象就在容器中自动被创建,不需要我们手动去声明Bean,从而简化了开发。同样的,并不是不需要创建bean了,而是我们通过简单注解配置去让Spring帮助我们自动的创建。

如以下EurekaApplication项目启动后自动加载了很多的bean对象。可以直接使用完成相应的功能。

这就是引入了eureka的starter依赖后,在该starter中,定义了完成注册与发现服务功能需要的坐标合集。我们只需要通过简单的注解配置,Spring就能自动将依赖jar中的bean加载到IOC容器中,然后就能使用其功能了。

实现过程:

在引入依赖之后,因为SpringBoot项目启动后只能扫描启动类所在的包以及其所在包的子包,扫描不到引入的依赖中的包。

那Spring是如何扫描到依赖中定义的Bean,然后加载到IOC容器中的呢?就是简单的注解配置。

  1. 在项目启动类上添加@ComponentScan("...", "..."),指定扫描引入的依赖的包名。

  2. 在启动类上添加@Import({...})注解,其中可以导入普通类,Bean配置管理类,ImportSelector接口实现类。其中ImportSelector接口的实现类会读取每个依赖的自动配置类,最后达到扫描加载装配的目的。

  3. 在启动类上直接添加@EnableXXXX,该注解中封装了@Import注解,

如@EnableEurekaServer:

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

进入该注解,可以看到里面封装了@Import,里面定义好了导入某个Bean的配置管理类。

3.5.2 自动装配源码分析例子——SpringBoot中自动配置类的Bean加载

SpringBoot项目启动后需要自动加载org.springframework.boot包下的基础Bean,我们以此为例介绍Bean自动装配的原理。

项目的启动由项目启动类开始,我们进行源码跟踪时也可以从启动类开始。

@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}

点击进入SpringBoot项目的核心注解@SpringBootApplication,可以看到主要封装了以下三个注解:

第一个注解就是声明启动类也是一个配置类,第三个是用来进行组件扫描的注解,就是扫描有没有定义的Bean,然后加载到IOC容器中。我们主要来看第二个注解,开启自动配置功能的注解@EnableAutoConfiguration,在讲解第三方bean的加载时提到的注解,其中封装了@Import注解。

其中封装的@Import注解导入的就是ImportSelector接口实现类AutoConfigurationImportSelector。

@Import({AutoConfigurationImportSelector.class})

该类中的selectImports方法中会以字符串数组返回jar包中所有配置类的全类名,然后由@Import注解导入这些Bean的配置类,将这些类实例化后放入Spring容器中,然后Spring容器扫描其中定义的Bean,最后达到加载装配bean的目的。

这些配置类的全类名就是从依赖jar包的文件中读取来的。我们接着看,该方法中关键的对象就是autoConfigurationEntry

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
}

我们进入getAutoConfigurationEntry方法看它是怎么得到该对象的。在该方法中返回的对象包含了configurations,我们可以看到configurations是一个List集合,我们继续进入返回configurations对象的方法中。

protected AutoConfigurationImportSelector.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 AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
}

getCandidateConfigurations方法中,可以看到,该方法主要是对configurations集合进行非空判断的,还要继续进入loadFactoryNames方法。不过根据非空判断的提示信息也可以判断出要做什么。"在META-INF/spring.factories中找不到自动配置类。如果您正在使用自定义打包,请确保该文件是正确的。",所以说,loadFactoryNames方法是去META-INF文件下读取配置类的全类名的。

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;
}

如在Web开发的起步依赖中也可看到这么一个jar包,spring-boot-autoconfigure,该jar包下就包含存储Bean的配置类的全类名的META-INF文件以及Bean的配置管理类。

可以看到,该包下的META文件就包含了 spring.factories 这个文件。

spring.factories这个文件中就包含了很多自动配置类以及Bean的配置管理类的全类名。SpringBoot3 版本是将Bean的配置管理类分开放入了org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件下。spring.factories文件只剩下了配置项。目的是为了优化springboot的启动速度。

loadFactoryName 方法的作用就是读取 classpath 下的 META-INF/spring.factories 文件的配置项,将key为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 对应Bean的配置类读取出来,然后通过@Import注解导入配置类,最后通过反射机制将配置类实例化交给Spring容器,完成配置类中bean的加载。

我们在spring.factories文件中点击进入其中一个Bean的配置管理类,可以看到该配置类中定义的Bean。

然后在定义Bean的方法上还有一个注解@ConditionalXXXX,该注解是按条件加载Bean,就是当满足该注解条件时才会加载该Bean。接下来有讲解按条件加载Bean。

3.5.3 按条件加载Bean

扫描到所有 Bean 并不是都需要加载的。会根据满足的条件进行加载,避免占用内存。

@ConditionalOnXxx

作用:按照一定条件进行判断,满足条件后才会加载Bean到IOC容器中。

位置:方法,类。加载类上对整个配置类中定义的Bean都有效。

@Conditional本身是一个父注解,它派生出了以下部分子注解:

条件注解说明
@ConditionalOnClass("...")判断是否存在指定的字节码文件,存在才加载Bean到容器中。不指定时默认是方法名。
@ConditionalOnMissingBean("...")判断是否存在指定的bean,不存在才会加载bean到容器中。不指定时默认是方法名。
@ConditionalOnProperty(xx="...", xx="...")判断配置文件中是否存在对应属性和值,存在才加载Bean到容器中。
@Bean
@ConditionalOnSingleCandidate(RabbitTemplate.class)
public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
    return new RabbitMessagingTemplate(rabbitTemplate);
}

如以上Bean的定义,@ConditionalOnSingleCandidate注解用来判断RabbitMessagingTemplate类在 BeanFactory 中是否只有一个实例:

  1. 如果在 BeanFactory 中存在多个实例,则匹配失败;

  2. 如果在 BeanFactory 中仅仅存在一个实例,则匹配成功。

举一个因为配置问题而出现bean创建失败的例子:(一些 Bean 的加载条件是必须配置了某配置项信息)

我们在eureka-server项目的配置文件中将项目服务端口删除,然后运行eureka服务。

不出所料出异常了,在最下面的报错中可以看到是因为bean创建失败,bean的名称是eurekaInstanceConfigBean。

我们点击进入查看,该Bean是在EurekaClientAutoConfiguration配置类中定义的,该Bean的定义方法上有一个@ConditionalOnMissingBean注解,虽然项目中没有eurekaInstanceConfigBean这个bean对象,但是还是因为项目启动后没有找到服务端口的配置而创建失败。

所以 isSecurePortEnabled 为 false,在该方法内进行判断时出错了。所以此Bean不能被创建。

最后,我们将项目运行的服务端口加上,打开Actuator查看,可以看到EurekaClientAutoConfiguration配置下定义的EurekaInstanceConfigBean已经注册到了IOC称为eurekaInstanceConfigBean的bean对象。

打脸了,这个例子可能不太合适,因为不由于@ConditionalOnXXX而创建失败的,不过从中可以学到相关bean的创建。

4 总结

第三方Bean的加载装配:

第三方依赖的配置类中的Bean的加载可以简单总结为以下:

引入了eureka依赖后,通过以下的注解配置,将扫描到Bean配置类实例化交给容器后,自动将其中定义的Bean加载到IOC容器中。

  1. 在项目启动类上添加@ComponentScan("...", "..."),指定扫描引入的依赖的包名。

  2. 在启动类上添加@Import({...})注解,其中可以导入普通类,Bean配置管理类,ImportSelector接口实现类。其中ImportSelector接口的实现类会读取存储全类名文件,然后以字符串数组返回jar包中所有的类的全类名,最后达到扫描加载装配的目的。

  3. 在启动类上直接添加@EnableXXXX,该注解中封装了@Import注解,

在Bean加载的源码分析中,我们以SpringBoot中Bean的自动装配为例。

从核心注解@SpringBootApplication开始,其中封装配置类注解,自动配置注解以及自定义组件扫描注解。核心就是自动配置注解,其中封装了@Import注解,就是导入了ImportSelector接口的实现类,该实现类重要的方法就是selectImports,目的还是读取jar文件下所有Bean配置类的全类名,最后交给@Import注解导入配置类,通过反射机制将配置类实例化交给Spring容器,完成配置类中bean的加载。

@Import({AutoConfigurationImportSelector.class})

可以说自动装配的核心就是@Import注解。

按条件加载Bean:

因为有了按条件加载Bean,所以说,SpringBoot项目一启动,并不是所有导入的第三方依赖的配置类都会实例化交给Spring扫描,扫描的配置类中所有定义的Bean也并不是全部注册到IOC容器中。这样就不用白白浪费更多的资源。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Maxlec

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值