Spring原理
Bean的作用域
概念
在Spring IoC & DI阶段,我们学习了Spring是如何帮助我们管理对象的。
1、通过@Controller,@Service,@Reposiytory,@Compnent,@Configuration,@Bean来声明Bean对象
2、通过ApplicationContext 或者 BeanFactory来获取对象
3、通过@Autowired,Setter方法或者构造方法等来应用程序注入所依赖的Bean对象
简单回顾
通过@Bean声明bean,把bean存在Spring容器中。
public class Doge {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}
@Configuration
public class DogConfig {@Beanpublic Doge getDoge1(){Doge doge = new Doge();doge.setName("zhangsan");return doge;}
}
2、从Spring中获取Bean
@SpringBootApplication
public class SpringIocApplication {public static void main(String[] args) {//获取Spring上下⽂对象ApplicationContext context =
SpringApplication.run(SpringIocApplication.class, args);//从Spring上下⽂中获取对象Dog dog = context.getBean(Dog.class);System.out.println(dog);}
}
也可以通过在代码中注入ApplicationContext的方式来获取到Spring容器,进而获取Bean。
@SpringBootTest
class DemoApplicationTests {@Autowiredprivate ApplicationContext applicationContext; //Spring 容器@Testvoid contextLoads() {DogBean dog1 = applicationContext.getBean(DogBean.class);System.out.println(dog1);}
}
修改代码,比较这两种方式获取Bean:
@SpringBootTest
class DemoApplicationTests {@Autowiredprivate ApplicationContext applicationContext; //Spring 容器@Testvoid contextLoads() {Dog dog1 = applicationContext.getBean(Dog.class);System.out.println(dog1);Dog dog2 = applicationContext.getBean(Dog.class);System.out.println(dog2);}
}
执行结果:
通过执行结果可以发现输出的bean对象的引用时一样的,这就说明每次从Spring容器中取出的对象都是同一个(单例模式)。
默认情况下,Spring容器中的bean都是单例的,这种行为模式,我们就称之为Bean的作用域。
Bean的作用域指的时Bean在Spring框架中的某种行为模式。
当作用域为单例时,整个Spring中是只有这一个Bean的,它是全局共享的。那么当其他人修改了这个值之后,那么另一个人读取到的就是被修改的值。
修改上述代码,给UserController添加属性name。
修改测试代码:
@SpringBootTest
class DemoApplicationTests {@Autowiredprivate ApplicationContext applicationContext; //Spring 容器@Testvoid contextLoads() {Dog dog1 = applicationContext.getBean(Dog.class);dog1.setName("狗狗1");System.out.println(dog1);System.out.println(dog1.getName());Dog dog2 = applicationContext.getBean(Dog.class);System.out.println(dog2);System.out.println(dog2.getName());}
}
观察结果:
因为这里的dog1和dog2实际上为同一个对象,dog2拿到了dog1设置的值。
那么能不能将bean对象设置为非单例的(每次获取到的bean都是一个新对象呢?)
这就是Bean的不同作用域了。
Bean的作用域
在Spring中支持6种作用域,后4种在SpringMVC环境下才生效。
- singleton:单例作用域
- prototype:原型作用域(多例作用域)
- request:请求作用域
- session:会话作用域
- Application:全局作用域
- websocket:HTTP WebSocket作用域
参考文档:Bean 作用域 :: Spring 框架
代码实现:
@Component
public class DogBeanConfig {@Beanpublic Dog dog(){Dog dog = new Dog();dog.setName("旺旺");return dog;}//单例作用域@Bean@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)public Dog singleDog(){Dog dog = new Dog();return dog;}//原型作用域@Bean@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)public Dog prototypeDog(){Dog dog = new Dog();return dog;}//请求作用域@Bean@RequestScopepublic Dog requestDog() {Dog dog = new Dog();return dog;
}//会话作用域@Bean@SessionScopepublic Dog sessionDog() {Dog dog = new Dog();return dog;}//全局作用域@Bean@ApplicationScopepublic Dog applicationDog() {Dog dog = new Dog();return dog;}
}
测试不同作用域获取到的Bean是否一致(@Autowired注入和从Applicationcontext获取)。
@RestController
public class DogController {//Autowire注入@Autowiredprivate Dog singleDog;@Autowiredprivate Dog prototypeDog;@Autowiredprivate Dog requestDog;@Autowiredprivate Dog sessionDog;@Autowiredprivate ApplicationContext applicationContext;//测试单例作用域@RequestMapping("/single")public String single(){Dog contextDog = (Dog)applicationContext.getBean("singleDog");return "dog:"+singleDog.toString()+",contextDog:"+contextDog;}//测试原型作用域@RequestMapping("/prototype")public String prototype(){Dog contextDog = (Dog)applicationContext.getBean("prototypeDog");return "dog:"+prototypeDog.toString()+",contextDog:"+contextDog;}//测试请求作用域@RequestMapping("/request")public String request(){Dog contextDog = (Dog)applicationContext.getBean("requestDog");return
"dog:"+requestDog.toString()+",contextDog:"+contextDog.toString();}//测试会话作用域@RequestMapping("/session")public String session(){Dog contextDog = (Dog)applicationContext.getBean("sessionDog");return
"dog:"+sessionDog.toString()+",contextDog:"+contextDog.toString();}//测试全局作用域@RequestMapping("/application")public String application(){Dog contextDog = (Dog)applicationContext.getBean("applicationDog");return
"dog:"+applicationDog.toString()+",contextDog:"+contextDog.toString();}
}
观察Bean的作用域
单例作用域
多次访问,@Autowired和ApplicationContext得到的都是同一个对象。
原型作用域
通过Autowired注入和ApplicationContext获取的对象不同,且ApplicationContext获得的对象每次访问都是不一样的(注入的对象因为在Spring容器启动时,就已经注入了,所以多次访问获得的对象不会发生变化)。
请求作用域
在一次请求中,@Autowired和ApplicatonContext获取的对象是一样的,但是每次请求(每次访问)获取的对象都是不一样的。
会话作用域
在一个session种,多次请求,获取的对象都是同一个:
换一个浏览器继续访问(session不同):
全局作用域
在一个应用中,多次访问得到的都是同一个对象。
全局作用域对于整个web容器来说,bean的作用域是ServerletContext级别的。和singleton类似,区别在于:Appliation scope 是ServerletContext的单例,singleton是一个ApplicationContext的单例。在一个web容器中ApplicationContext可以有多个。
Bean的生命周期
生命周期指的是一个对象从诞生到销毁的整个生命过程,我们把这个过程叫做一个对象的生命周期。
Bean的生命周期分为以下5个部分:
1、实例化(为Bean分配内存空间)
2、属性注入(Bean注入和装配,如@Autowired)
3、初始化
a、执行各种通知,如:BeanNameAware,BeanFactoryAware,ApplicationContextAware的接口方法。
b、执行初始化方法
- xml定义的init-method
- 使用注解的方式@PostConstruct
- 执行初始化后置方法(BeanPostProcessor)
4、使用Bean
5、销毁Bean
- 销毁容器的各种方法,如@PreDestory,DisposableBean接口方法,destory-method
实例化和属性赋值对应构造方法和setter方法的注入。初始化和销毁时用户能自定义扩展的两个阶段。可以在实例化之后,类加载完成之前进行自定义“事件”处理。
比如现在我们需要买一套房子,流程如下:
1、先买房(实例化,从无到有)
2、装修(设置属性)
3、买家电,如:洗衣机、空调、电视等(初始化,可以入住)
4、入住(使用Bean)
5、买房(Bean销毁)
执行流程:
代码演示
@Component
public class BeanLifeComponent implements BeanNameAware {private UserComponent userComponent;public BeanLifeComponent() {System.out.println("执⾏构造函数");}@Autowiredpublic void setUserComponent(UserComponent userComponent) {System.out.println("设置属性userComponent");this.userComponent = userComponent;}@Overridepublic void setBeanName(String s) {System.out.println("执⾏了 setBeanName ⽅法:" + s);}/*** 初始化*/@PostConstructpublic void postConstruct() {System.out.println("执⾏ PostConstruct()");}public void use() {System.out.println("执⾏了use⽅法");}/*** 销毁前执⾏⽅法*/@PreDestroypublic void preDestroy() {System.out.println("执⾏:preDestroy()");}
}
执行结果:
源码分析
可参考其他博主的博文:
Spring深度学习:Bean生命周期及源码解析_bean的生命周期源码解读-CSDN博客
Spring Boot自动装配
SpringBoot的自动装配是当Spring容器启动后,一些配置类,bean对象等就自动存入到了IoC容器中,不需要我们手动去声明,从而简化了开发,省去了繁琐的配置操作。
SpringBoot自动配置,就是指SpringBoot是如何将依赖jar包中的配置以及Bean加载到SpringIoC容器中的。
问题描述
需求:我们在编写程序的过程中常常需要使用Spring管理第三方的jar包的配置。
数据准备:
1、创建新的目录org.test.xmy
2、创建模拟第三方文件(文件在新建目录下):
第三方文件代码:
@Component
public class XmyConfig {public void use(){System.out.println("xmy 使用");}
}
3、获取XmyConfig这个Bean。
写测试代码:
@SpringBootTest
class SpringAutoconfigApplicationTests {@Autowiredprivate ApplicationContext applicationContext;@Testvoid contextLoads() {XmyConfig XmyConfig = applicationContext.getBean(XmyConfig.class,
"XmyConfig");System.out.println(XmyConfig);}
}
4、运行程序:
观察日志:No qualify bean of type‘org.test.xmy.XmyConfig’available
没有org.test.xmy.XmyConfig这个类型的Bean。
原因分析
Spring通过五大注解和@Bean注解可以帮助我们把Bean加载到SpringIoC容器中,但有个前提条件:这些注解类需要和SpringBoot启动类在同一个目录下。(XmyConfig所在目录为org.test.xmy,而启动类所在目录在com.example.springprincle,因此SpringBoot没有扫描到XmyConfig)。
解决方案
我们需要指定路径或者引入文件,告诉Spring,让Spring进行扫描到。
常见解决方案:
- @ComponentScan组件扫描
- @Import导入(使用@Import导入的类会被Spring加载到IoC容器中)
代码如下:
@ComponentScan:
@SpringBootApplication
@ComponentScan("org.test.xmy")
public class SpringPrincleApplication {public static void main(String[] args) {SpringApplication.run(SpringPrincleApplication.class, args);}}
@ComponentScan里的参数可以是数组,可以指定扫描多个包。
运行程序:
可以看到,这次XmyConfig这个Bean被Spring获取到了。
@Import
@Import导入主要有以下几种形式:
- 导入类
- 导入ImportSelector接口实现类
导入类
@SpringBootApplication
@Import(XmyConfig.class)
public class SpringPrincleApplication {public static void main(String[] args) {SpringApplication.run(SpringPrincleApplication.class, args);}}
运行程序:
问题:如果我多了一个配置项XmyConfig2呢?
@Component
public class XmyConfig2 {public void use(){System.out.println("xmy2 使用");}
}
我们可以采用导入多个类:
@SpringBootApplication
@Import({XmyConfig.class,XmyConfig2.class})
public class SpringPrincleApplication {public static void main(String[] args) {SpringApplication.run(SpringPrincleApplication.class, args);}}
测试结果:
虽然这种方式能够实现,但却太过繁琐了。
导入ImportSelector接口实现类
ImportSelector实现类:
public class MyImportSelect implements ImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {//要导入的类的全限定类名return new String[]{"org.test.xmy.XmyConfig","org.test.xmy.XmyConfig2"};}
}
启动类:
@SpringBootApplication
@Import(MyImportSelect.class)
public class SpringPrincleApplication {public static void main(String[] args) {SpringApplication.run(SpringPrincleApplication.class, args);}}
运行程序:
可以看到,我们采用这种方式也可以导入第三方依赖提供的Bean。
但他们都有一个问题,就是使用者需要指导第三方依赖中有哪些Bean对象或者配置类,如果漏掉其中一些Bean,很可能导致我们的项目出现大的事故。
那么如何解决呢?
比较常见的解决方案就是第三方依赖给我们提供一个注解,这个注解一般都以@Enablexxxx开头的注解,注解中封装的就是@Import注解。
第三方依赖提供注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(MyImportSelect.class)
public @interface EnableAutoXmyConfig {
}
启动类引入第三方提供的注解:
@SpringBootApplication
@EnableAutoXmyConfig
public class SpringPrincleApplication {public static void main(String[] args) {SpringApplication.run(SpringPrincleApplication.class, args);}}
运行程序:
可以看到,这种方式也可以导入第三方依赖提供的Bean。并且这种方式更加优雅一些,SpringBpot采用的也是这种方式。
SpringBoot自动装配源码分析
参考博文:【Spring Boot】自动配置源码解析_springboot自动配置源码解析-CSDN博客