几个月之前,我接触了SpringBoot,不知不觉就沉浸在他的自动配置所带来的便利中,基本上已经做到的零配置就集成了web的所有主流框架,但是 前几天在头看Spring,怕了怕了,忘记了… 所以就很有必要重新温习spring,spring博大精深,短时间内,想在源码的层面上把它整的明明白白,简直是不可能的,我计划是这个学期大概会整理四篇博客,1.上手 2. AOP原理 3. 声明式事务原理 4. Spring容器
Spring 第一篇
- Spring带给了我们什么便利?
- 注解版本的IOC如何玩?
- 组件注册
- 组件注册的过程中有哪些过滤规则?
- 如何控制组件的作用域(单例多例)?
- 六种注册组件的方式?
- 生命周期
- 什么是bean的生命周期
- 在bean的生命周期的各个阶段我们可以插手做什么?
- 属性赋值
- 我们有哪些手段给bean的属性赋值?
- 自动装配
- 什么是自动装配?
- Spring提供哪些注解可以帮我们实现自动装配?
- java规范提供了哪些注解帮助我们实现了自动装配?
- Spring提供的
@Profile
实现适配多环境?
- 组件注册
- 注解版本的AOP如何玩?
Spring带给了我们什么?
假如我们是第一次学习Spirng,我们大可不必关心spring带给我了我们什么便利,因为spring大概是web阶段筑基需要学习的第一个主流框架,初学难免会遇见各种各样的错误,所以尽管放心大胆的去学习如何使用就行了.先会用,其它的不用多想
过几天,用着熟悉了如何使用,再考虑spring究竟带给了我们什么便利也不迟, 那么,spring究竟带给了我们什么便利呢?
IOC(Inverse of Control),把对象的创建权反转给Spring容器,实现了解耦
我们使用Spring提供给我们的注解,把bean注册进IOC容器,进而把bean之间的依赖关系全部交给Spring管理,现在想想这绝对是一件超级赞的事情,可能原来的我会想,我自己可以new对象,干嘛非让Spring插一脚,是,假如我的项目就是个小demo全文一共三对象,完全顾的上来,那么spring对我来说就是累赘,但是!慢慢的接触的工程大了就会发现,离开了Spring,自己去管理那些对象之间的依赖会累死人的,而且SpringIOC的诞生也应正了bean的管理,完全不需要我们关系,我们关心的就是,我们如何向Spring索取依赖的Bean就行了,所以说Spring真的是web爱好者的小春天.
AOP(Aspect Oriented Programming),spring的aop
面向切面编程,aop说白了就是代码复用,把大量的重复代码从我们的业务逻辑中抽取出去,等程序运行的时候再动态的植入抽取出去的代码,这是件要多优雅就多优雅的事情!
声明式事务
后端天天和数据库打交道,事务安全这样的不可能遇不见,我们这一代学习的人还真的是幸运,因为spring提供的声明式事务,我们不用再去苦苦的去写编程式事务,而且,现在spring4全面支持注解开发,我们甚至连配置文件都不用写,加一个注解就引入了spring的事务模块,激动不?
方便继承其他开源框架
spring最擅长的事情就是整合这使它的生态变的超级庞大
例外给大家推荐一篇很棒的博客,里面详细图文论述了,spring究竟带给了我们什么?
注解版Spring-IOC怎么玩?
组件注册
只使用IOC,导入Spring-Context就可以
1 | <dependency> |
这个过程,我们要注意IOC以下几点
- 组件注册的过程中有哪些过滤规则?
- 如何控制组件的作用域(单例多例)?
- 六种注册组件的方式?
组件注册及其过滤规则我串联在下面相邻的两部分里,这里先提一下Spring提供的所有过滤规则类型
在spring启动,开始扫描包,注册组件时,如果我们想按照我们的需求往IOC里面添加组件,就需要指定过滤规则,下面这五种类型的过滤规则,都源于我们在主配置类(相当于配置文件)上添加的@ComponentScan()
包扫描注解
- 按照注解过滤
ANNOTATION
1 | // 这个注解替换了原来配置文件中的包扫描 ( |
- 按照给定的类型过滤
1 |
|
按照切面 (ASPECTJ) 基本不用
按照正则 REGEX
自定义规则 CUSTOM
1 |
|
注解版本中,配置类替换了原来的配置文件
@Configuration
注解标记本类为配置类
那么我不加
@Configuration
注解,这个类就不能成为配置类吗? 里面通过@Bean
注解就添加不进bean来了? 不是的,@Configuration
标记会被Spring使用Cglib技术拦截下来,换言之我们得到的bean,不再是我们自己new 的那种小白bean,而是被代理过的Bean
那么什么才是百分百的配置类呢? 配置类是我们调用如下代码所需要的那个类
1
2
3 > AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
> applicationContext.register(配置类.class);
>
如下代码! 原来的包扫描被@ComponentScan
取代!!! 里面可以配置多个信息
- 基础包信息
value="com.XXX"
- 排除被哪个注解标记的类
excludeFilters = @Filter(type... classes...)
- 只要被哪个注解标记的类,(配置文件版本的分两步!第一步禁用掉默认的过滤器),注解版同样分两步` useDefaultFilters=false,
includeFilters = @Filter(classes= Service.class)`
excludeFilters 把bean排除出IOC容器!!! 一般都是按照注解, 后面是 !!注解!!的Class数组,
另外,过滤器Filter的过滤类型,默认为按照注解过滤!!!
还可以在配置类中往IOC容器中组成对象!!!@Bean
,用的时候 @Autowired
默认情况下,无论获取多少次,都是单实例的!!!
1 | /** |
此外在java8中,有如下代码,@ComponentSacn
的倒数第二个注解是@Repeatble
意味着我们可以在主配置类上写多个@ComponentScan
1 | */ |
自定义TypeFilter
的过滤规则
自定义注解,要求实现FilterType
接口,实现他的match()
,该方法里面的两个参数都是接口类型的,我们可以从它里面取出关于IOC所扫描到的所有类的源信息,满足我们的预期,返回true,表示允许注册进IOC
1 | public class MyTypeFilter implements TypeFilter { |
@Scope, 设置组件的作用域
容器中的对象默认都是单实例的!!!,我们使用@Scope
注解改变这种状态
1 | /* |
观察在单实例和多实例情况下,bean创建的时机
- 默认在单实例的情况下,ioc容器创建完,直接加载bean
但是以后每次获取都会都是从IOC容器中获取
懒加载
@Lazy
! 针对单实例Bean,我们知道,容器启动的时候,就会创建一个唯一的bean,我们使用懒加载可以做到,在第一次使用的时候加载bean
- 多实例情况下,只有在获取类时,IOC容器才会创建bean,!!!
以后每次获取Bean,ioc容器都会调用那个方法去new Bean
六种给容器注册组件的方法
着重看一种,按照条件注册bean @Conditional
springboot底层大量的使用!!!
它的实现方式和自定义过滤条件相似
@Conditional : 按照条件注册bean
1 | // 既可以标在类上,也可以标在方法上 |
解释一下怎么用:
- 它是个接口,需要的属性名为
value
, 值是Class数组
,可以进一步看到它的泛型是Condition接口
我们自己实现接口,重写里面的方法match()
,根据方法提供的参数可以获取到我们想要的任何信息,返回true表示满足条件,注册bean,否则不注册
其他注册组件的方法
- 方式1: 包扫描+注解 (@Cotroller @Service @Repository等等注解的类可以被IOC扫描到,添加进容器)
这种方法有局限性!!!, 只能是我们自己写的类, id为首字母小写的类名
- 方式2: 在配置类内部 使用:
@Bean
注解,
更多的使用它注册第三方bean
- 方式3: 在配置类头上使用
@Import(XXX.class,YYY.class)
解放了第二种通过@Bean还的自己new的缺陷!!! 组册进IOC中的组件的默认id,是组件的全类名
- 方式4:
@Import({Color.class, MyImportSelector.class})
实现ImportSelector接口: 返回需要导入的组件的全类名数组,完成批量导入
- 方式5: 手动注册
@Import({Color.class,MyImportSelector.class, MyImportBeanDefinitionRegisrar.class})
实现ImportBeanDefinitionRegistrar接口: 它里面的
registerBeanDefinitions()
方法的第二个参数就是BeanDefinitionRegistry
, 所有bean注册进IOC容器都经由他完成,因此我们可以手动注册bean, 还可以通过第一个参数获取当前标注@Import
注解的类的全部注解信息,加上第二个参数可以获取当前IOC容器的全部信息,动态判断是否要注入类到IOC
同时,第五种在SpringBoot中得到了大量的使用,实现SpringBoot的自动配置
- 方式6 : 使用Spring提供的
FactoryBean
(工厂bean)
- 自己实现FactoryBean接口,重写三个方法
- @Bean,把自己实现的工厂bean添加到IOC
- 测试自己实现的BeanFactory的类型,是我们指定的泛型的类型的!!!
- 想获取到工厂的话, 需要添加前缀&
1 | AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyConfig02.class); |
Bean的生命周期
通过第一阶段的学习,我们顺利把bean的创建权反转给了Spring,下面就来看看,IOC是如何控制Bean的生命周期的!
什么是bean的生命周期?
其实,就是下面的过程
Bean创建——> 初始化——> 销毁
在IOC管理bean生命周期的过程中,我们可以插手做什么?
- 我们可以自定义bean的初始化和销毁方法,bean在到达相应的生命周期时,ioc会调用我们指定的方法,施加在bean上
原来的配置文件版本,需要我们写 init-method 和distroy-method
如何实现?
方法1: 使用@Bean
注解完成
1 | "init",destroyMethod = "destory") (initMethod= |
测试类
1 |
|
单实例情况下: 对象创建后并赋值好了后,执行init方法,容器关闭对象销毁
多实例情况下: 对象创建后并赋值好了后,执行init方法,IOC不会管理bean,需要我们自己去销毁bean
用处?
在配置数据源是大量使用,对象创建后需要初始话很多的数据,对象销毁了,很多资源要释放
方法2: 让Bean实现spring提供的两个接口InitializingBean
和DisposableBean
,重写这两个接口的方法
1 | public interface InitializingBean { |
- 单例模式下: 在IOC一初始化就进行bean构造,并且执行完了的
afterPropertiesSet()
初始化,容器关闭,bean执行销毁DisposableBean()
方法3: 使用JS205规范提供的两个注解@PostConstruct和@PreDestory
完成
- @PostConstruct :在bean创建完成并且属性赋值好了后执行本初始化方法
- @PreDestory: 在容器销毁bean之前,通知我们进行清理工作
这两个注解都是标注在方法上的!!!
1 |
|
方法4: 使用Spring提供给我们的组件,接口BeanPostProcessor
这个组件很重要,Spring的底层,尤其是AOP底层大量使用它
里面有两个方法
1 | public interface BeanPostProcessor { |
实现自己的BeanPostprocessor
1 |
|
测试结果如下图
划重点!!! —
BeanPostprocessor
意味bean的后置处理器,前面也说了,它是spring提供给我们的一个辅助类型的组件,我们可以自定义BeanPostProcessor
,在所有满足加入IOC容器的bean初始化方法前后执行BeanPostProcessor
接口里的postProcessAfterInitialization()
和postProcessBeforeInitialization()
方法,完成对bean的增强,AOP的底层使用的 后置处理器 是spring自动给我们加载进来的,他的这种特性,为AOP,动态植入代码的实现,提供的前提
接口BeanPostProcessor
运行原理
- 遍历得到容器中所有的
BeanPostProcessor
,挨个执行beforeInitialization
,一旦返回值为null,说明ioc在没有这个对象,直接跳出for循环,不会执行BeanPostProcessor.postProcessBeforeInitialization()
方法进行处理
1 | populateBean(beanName,mdb,instanceWrapper);给bean的属性赋值 |
通过这个继承图,可以看到,
BeanPostProcessor
作为后置处理器的顶级接口存在,程序运行打上断点,也能看到我们自定义的MyBeanPostProcessor
,另外需要我们关心的一个实现是InstantiationAwareBeanPosProcessor
这个子接口,AOP的实现,就应用到了它(第二篇博客会记录)
使用配置文件给bean赋值
1 | <bean id="person" class="com.changwu.bean" scope="prototype"> |
使用注解的方法,给bean赋值
@Value
1 | 1. 基本数值 |
其中取出配置文件中的值,要在主配置类上添加
@PropertySource(value = "classpath:/bean.properties")
指明配置文件的路径
@Value+@PropertySource
前者使用
${}
取出环境变量中的属性(程序运行后配置文件会加载进环境变量),后者给前者提供定位
1 | (ElementType.TYPE) |
自动装配?
- 什么是自动装配?
自动装配就是,Spring利用依赖注入DI,完成对IOC容器中的各个组件依赖关系的赋值
@Autowired & @Qualifier & @Primary
@Autowired
是Spring自己的注解
那些Bean,都在IOC中,通过@Autowired
注解可以完成自动装配
- 默认按照类型(XXX.class)去IOC中找到对应的组件
- 如果存在多个bean,就按照id找(
@Autowired
标记的引用名) 使用
@Autowired
,默认如果IOC中不存在该bean,就会报错通过设置
@Autowired(required=false)
设置组件不必需存在于,IOC@Autowired
+@Qualifier
明确指定装配的bean的id
1 | "bookDao2") ( |
再强调一遍,如果是包扫描的话,Bean在IOC的id是类名首字母小写,
@Qualifier("XXX")
不能乱写,要么是类名首字母小写,要么是我们通过@Bean("XXX")
指定的id
@Primary
标记在我们手动添加进去的bean上,强制,首选注入!!!
但是,如果同时存在
@Primary
和@Qualifier
依然会装配我们明确指定的Bean
1 | // 构造器, 方法,参数,属性,全能!!! |
@Autowired
标注在方法上,方法的参数,默认从容器中获取@Autowired
标注在构造器上,构造器需要的参数,默认从容器中获取@Bean
标注的方法,方法的参数,默认从容器中获取,在参数位置的@Autowired
可以省略不写
@Resources
(JSR205) & @Inject
(JSR330)
java规范注解
@Resources
- 作用: 默认按照组件名称装配
- 缺点: 不能和
@Qulifier
和@Primary
一起使用
@Inject
- 还麻烦! 需要我们导入依赖
1 | <dependency> |
@Profile
Spring为我们提供的可以根据当前的环境,动态的激活和切换一系列组件的功能
比如在开发环境下,我们使用A数据库, 测试环境使用B数据库, 生产环境使用C数据库
1 | (RetentionPolicy.RUNTIME) |
支持写到方法上!满足
@profile
指定的环境下,方法的中组件的注入才会生效
支持写到类上!满足
@profile
指定的环境下,整个类的中组件的注入才会生效
- 准备工作,注册三个数据源
1 |
|
2 . 结合@Profile
注解,在组件上做一个标识,只有满足@Profile
标识条件的才会被注册进IOC
- 加了环境标识的bean,只有那个环境被激活,才会注册到容器中,默认是
@Profile("default")
- 没有环境标识的bean,任何条件下都会加载进容器
如何改变环境?
- 使用代码的方法
1 |
|
注解版AOP怎么玩?
AOP面向切面编程,指的是在程序运行期间,动态的将某段代码,切入到指定方法的指定位置额运行的编程方式
导入AOP模块
1 | <dependency> |
创建业务逻辑类
- 我们要做的就是在动态的在div运行前,运行后,出现异常时,正常结束,异常结束等不同情况下打印日志
1 | public class MathAop { |
定义一个日志切面类
切面类上的方法会动态的感知到,切入点div的状态,在其指定的状态上,做出不同的反应而无序更改仍和div的代码
五种通知方法,对应五种不同的注解,具体的细节,在下面的代码中有注释
1 |
|
抽取切入表达式
- 注解
@Pointcut
- 表达式可以使用* 通配
- execution(pulbic com.changwu.tryspring.aop.MathAop.*(int,int))
- execution(* com.changwu.tryspring.aop.MathAop.div(..))
- 本类使用 : 直接用方法名–> pointCut()
- 其他类使用: 使用带包名的全路径–> com.changwu.tryspring.aop.AopLog.pointCut()
1 | "execution(* com.changwu.tryspring.aop.MathAop.div(..))") ( |
前置通知
- 注解
@Before("切入点表达式")
- 调用时机: 在目标方法执行前执行
- 可选参数: JoinPoint,里面封装着切入点方法的全部信息,比如方法名,参数等等
1 | "com.changwu.tryspring.aop.AopLog.pointCut()") ( |
后置通知
- 注解
@After("切入点表达式")
- 调用时机:无论方法正常结束还是异常结束,都调用
1 | "execution(* com.changwu.tryspring.aop.MathAop.div(..))") ( |
返回通知
- 注解
@AfterReturning(value="切入点表达式",returning="XXX")
- 调用时机: 方法正常返回执行
- 注意点: 函数的入参,参数名和注解中的XXX要相同,里面封装着,函数的返回值结果
1 | "com.changwu.tryspring.aop.AopLog.pointCut()",returning = "result") (value = |
异常通知
- 注解
@AfterThrowing(value="切入点表达式",throwing="XXX")
- 调用时机: 切入点出现异常
- 注意点:函数的入参,参数名和注解中的XXX要相同,里面封装着,函数的返回值结果
- XXX里面封装着方法的异常信息
1 | "com.changwu.tryspring.aop.AopLog.pointCut()",throwing = "expection") (value = |
环绕通知
- 注解
@Around("切入点表达式")
- 调用时机: 切入点执行前后都会执行
- 参数:
ProceedingJoinPoint
里面封装了关于切入点的所有信息 - proceedingJoinPoint.proceed();返回值: 为切入点的返回值, 必须返回
- 环绕通知里面的所有异常全部抛出去,,一旦我们try起来了,异常通知就获取不到异常,进而返回通知就认为方法是正常结束的,结果为NULL
1 | "com.changwu.tryspring.aop.AopLog.pointCut()") ( |
注意点: 业务逻辑类,切面类都要添加进IOC
注意点: 切面类都要添加注解
@Aspects
补充: JointPoint 可以获取的切入点的信息 而且,必须在参数的第一位
好继续准备,回顾xml版本的spring开发方式需要我们添加如下的配置
1 | <aop:aspectj-autoproxy></aop:aspectj-autopeoxy> |
基于注解,开启切面—>@EnableAspectjAutoProxy
1 |
|
测试:
1 |
|