Spring原理篇
一、IOC部分
1.1 spring框架核心特征
三种思想总结
1)IoC控制赖注入,某个完整Bean需要依赖于其他Bean(或属性)的注入;
3)AOP面向切面编程,用横向抽取方法(属性、对象等)思想,组装成一个功能性切面。
spring框架
https://www.spring.io
1.2 BeanFactory与ApplicationContext的关系
-
BeanFactory是Spring的早期接口,称为Sprin
ory基础上对功能进行了扩展,例如:监听功能
在BeanFactory中,ApplicationContext不仅继承了BeanFactory,而且ApplicationContext内部还维护着BeanFactory的引用,所以,ApplicationContextBeanFactory既有继承关系,又有融合关系。
-
Bean的
blisher (事件发布器)、ResouresPatternResolver (资源解析器)、 MessageSource (消息资源)等。但是 ApplicationContext 的核心功能还是 BeanFactory 。
在Spring 的 web 环境下,常用的两个 ApplicationContext 作用如下:
1.
1.3.1 SpringBean 的配置详解
1.3.1.1 基于xml方式的bean配置
对象创建之后,有什么初始化的方法:
-
写一个初始化方法,并通过init-method=""指定
-
使对象实现InitializingBean接口,复写afterPropertiesSet方法
Bean 的实例化配置:
Spring的实例化方式主要如下两种:
-
构造方式实例化:底层通过构造方法对Bean 进行实例化
构造方式实例化Bean 又分为无参构造方法实例化和有参构造方法实例化, Spring 中配置的 < bean>几乎都是无参构造该方式,此处不在赘述。下面讲解有参构造方法实例化 Bean
有参构造在实例化Bean 时,需要参数的注入,通过 <constructor-arg> 标签,嵌入在 <bean> 标签内部提供构造参数,如下:
public UserDaoImpl(String name){}
<bean id="userService" name="aaa,test" class="com.learn.service.impl.UserServiceImpl">
<constructor-arg name="name" value="haha"></constructor-arg>
<property name="userDao" ref="userDao"></property>
</bean>
-
工厂方式实例化:底层通过调用自定义的工厂方法对Bean 进行实例化
工厂方式实例化Bean ,又分为如下三种
-
静态工厂方法实例化Bean
-
实例工厂方法实例化Bean
-
实现FactoryBean 规范延迟实例化 Bean
-
静态工厂方法实例化Bean
静态工厂方法实例化Bean ,其实就是定义一个工厂类,提供一个静态方法用于生产 Bean 实例,在将该工厂类及其静态方法配置给 Spring 即可
public class MyBeanFactory1 {
public static UserDao userDao() {
// 可以在创建之前执行操作
return new UserDao();
}
}
<bean id="userDao1" class="com.learn.factory.MyBeanFactory1" factory-method="userDao"/>
<bean id ="userDao" class ="com.itheima.factory.UserDaoFactoryBean" factory-method="getUserDao">
<constructor-arg name ="name" value ="haohao/>
</bean>
PS:<constructor-arg>标签不仅仅是为构造方法传递参数,只要是为了实例化对象而传递的参数都可以通过<constructor-arg>标签完成,例如上面通过静态工厂方法实例化 Bean 所传递的参数也是要通过 <constructor-arg>进行传递的
-
实例工厂方法实例化Bean
实例工厂方法,也就是非静态工厂方法产生Bean 实例,与静态工厂方式比较,该方式需要先有工厂对象,在用工厂对象去调用非静态方法,所以在进行配置时,要先配置工厂 Bean ,再配置目标 Bean
public class MyBeanFactory2 {
public UserDao userDao() {
// 可以在创建之前执行操作
return new UserDao();
}
}
<bean id="myBeanFactory2" class="com.learn.factory.MyBeanFactory2"/>
<bean id="userDao2" factory-bean="myBeanFactory2" factory-method="userDao"/>
通过断点观察单例池singletonObjects ,发现单例池中既有工厂 Bean 实例,也有目标 Bean 实例,且都是在 Spring容器创建时,就完成了 Bean 的实例化
无论是静态工厂方法还是实例工厂方法实例化 Bean,都可以在创建 Bean 的实例之前执行自定义逻辑(例如初始化第三方库、加载配置、验证环境等)。这两种方式在 Spring 中均支持通过自定义代码控制对象的创建过程,包括对第三方依赖的初始化操作。
-
实现FactoryBean 规范延迟实例化 Bean
上面不管是静态工厂方式还是非静态工厂方式,都是自定义的工厂方法,Spring 提供了 FactoryBean 的接口规范,FactoryBean 接口定义如下:
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
定义工厂实现FactoryBean
public class MyBeanFactory3 implements FactoryBean<UserDao> {
@Override
public UserDao getObject() throws Exception {
System.out.println("getObject被调用");
return new UserDao();
}
@Override
public Class<?> getObjectType() {
return null;
}
}
配置FactoryBean 交由 Spring 管理即可
<bean id="userDao3" class="com.learn.factory.MyBeanFactory3"></bean>
通过断点观察发现Spring 容器创建时, FactoryBean 被实例化了,并存储到了单例池 singletonObjects 中,但是getObject () 方法尚未被执行, UserDaoImpl 也没被实例化,当首次用到 UserDaoImpl 时,才调用 getObject ()此工厂方式产生的 Bean 实例不会存储到单例池 singletonObjects 中,会存储到 factoryBeanObjectCache 缓存池中,并且后期每次使用到 userDao 都从该缓存池中返回的是同一个 userDao 实例。
简单而言,在加载配置文件后,只是创建了BeanFactory,但是没有创建useDao实例。只有当使用到时,才会调用getObject方法创建实例,并存储在factoryBeanObjectCache中,此后再次调用则不会再创建实例,只用从缓存中查找。
Bean的依赖注入配置
即ref为注入其他bean,value为注入值
依赖注入的数据类型有如下三种:
-
普通数据类型,例如:String、int、boolean等,通过value属性指定。
-
引用数据类型,例如:UserDaoImpl、DataSource等,通过ref属性指定。
-
集合数据类型,例如:List、Map、Properties等。
扩展:自动装配方式
如果被注入的属性类型是Bean引用的话,那么可以在<bean> 标签中使用 autowire 属性去配置自动注入方式,属
性值有两个:
-
byName:通过属性名自动装配,即去匹配 setXxx 与 id="xxx"(name="xxx")是否一致;
-
byType:通过Bean的类型从容器中匹配,匹配出多个相同Bean类型时,报错。
<bean id="userService" name="aaa,test" class="com.learn.service.impl.UserServiceImpl" autowire="byName"></bean>
Spring 的其他配置标签
Spring的 xml 标签大体上分为两类,一种是默认标签,一种是自定义标签
-
默认标签:就是不用额外导入其他命名空间约束的标签,例如<bean> 标签
-
自定义标签:就是需要额外引入其他命名空间约束,并通过前缀引用的标签,例如
context:property-placeholder/>
标签
Spring的默认标签用到的是 Spring 的默认命名空间
-
<beans>标签,除了经常用的做为根标签外,还可以嵌套在根标签内,使用 profile 属性切换开发环境
<beans profile="dev">
<bean id="userService1" class="com.learn.service.impl.UserServiceImpl"></bean>
</beans>
<beans profile="test">
<bean id="userDao1" class="com.learn.dao.UserDao"></bean>
</beans>
可以使用以下两种方式指定被激活的环境:
-
使用命令行动态参数,虚拟机参数位置加载
Dspring.profiles.active =test
-
使用代码的方式设置环境变量
System.setProperty(spring.profiles.active","test")
<?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 id="userService" name="aaa,test" class="com.learn.service.impl.UserServiceImpl" autowire="byName"></bean>
<bean id="userDao" class="com.learn.dao.UserDao"/>
<beans profile="dev">
<bean id="userService1" class="com.learn.service.impl.UserServiceImpl"></bean>
</beans>
<beans profile="test">
<bean id="userDao1" class="com.learn.dao.UserDao"></bean>
</beans>
</beans>
上面黄色高亮为公共部分,无论是否指定环境都会生效,而下述beans中只用指定了profile才会生效。具体而言,指定了dev则dev生效而test不生效,指定了test则test生效。
-
<import>
标签,用于导入其他配置文件,项目变大后,就会导致一个配置文件内容过多,可以将一个配置文件根
据业务某块进行拆分,拆分后,最终通过<import>
标签导入到一个主配置文件中,项目加载主配置文件就连同<import>
导入的文件一并加载了
-
<alias>
标签是为某个Bean
添加别名,与在<bean>
标签上使用name
属性添加别名的方式一样,我们为
UserServiceImpl
指定四个别名:aaa
、bbb
、 xxx
、yyy
<bean id="userService" name="aaa,test" class="com.learn.service.impl.UserServiceImpl" autowire="byName"></bean>
<bean id="userDao" class="com.learn.dao.UserDao"/>
<alias name="userDao" alias="xxx"></alias>
<alias name="userDao" alias="yyy"></alias>
Spring的自定义标签需要引入外部的命名空间,并为外部的命名空间指定前缀,使用<前缀:标签>
形式的标签,称
之为自定义标签,自定义标签的解析流程也是 Spring xml 扩展点方式之一,在 《 Spring 整合其他框架 》章节进行详细介绍
1.3.2 Spring的get方法
1.3.3 Bean 实例化的基本流程
Spring
容器在进行初始化时,会将xml
配置的<bean>
的信息封装成一个BeanDefinition
对象,所有的BeanDefinition
存储到一个名为beanDefinitionMap
的Map
集合中去,Spring
框架在对该Map
进行遍历,使用反射创建Bean
实例对象,创建好的Bean
对象存储在一个名为singletonObjects
的 Map 集合中,当调用getBean
方法时则最终从该Map
集合中取出Bean
实例对象返回。
Bean 实例化的基本流程
-
加载xml配置文件,解析获取配置中的每个<bean>的信息,封装成一个个的BeanDefinition对象;
-
将BeanDefinition存储在一个名为beanDefinitionMap的Map<String,BeanDefinition>中;
-
ApplicationContext底层遍历beanDefinitionMap,创建Bean实例对象;
-
创建好的Bean实例对象,被存储到一个名为singletonObjects的Map<String,Object>中;
-
当执行applicationContext.getBean(beanName)时,从singletonObjects去匹配Bean实例返回。
1.3.4 Spring的后处理器
Spring的后处理器是Spring对外开发的重要扩展点,允许我们介入到Bean的整个实例化流程中来,以达到动态注册BeanDefinition,动态修改BeanDefinition,以及动态修改Bean的作用。Spring主要有两种后处理器:
-
BeanFactoryPostProcessor:Bean工厂后处理器,在BeanDefinitionMap填充完毕,Bean实例化之前执行;
-
BeanPostProcessor:Bean后处理器,一般在Bean实例化之后,填充到单例池singletonObjects之前执行。
1.3.4.1 Bean工厂后处理器 – BeanFactoryPostProcessor
BeanFactoryPostProcessor是一个接口规范,实现了该接口的类只要交由Spring容器管理的话,那么Spring就会回调该接口的方法,用于对BeanDefinition注册和修改的功能。
public interface BeanFactoryPostProcessor {
void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory);
}
postProcessBeanFactory 参数本质就是 DefaultListableBeanFactory,拿到BeanFactory的引用,自然就可以对beanDefinitionMap中的BeanDefinition进行操作了 ,例如对UserDaoImpl的BeanDefinition进行修改操作
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)throws BeansException {
BeanDefinition userDaoBD = beanFactory.getBeanDefinition(“userDao”);//获得 UserDao定义对象
userDaoBD.setBeanClassName("com.itheima.dao.impl.UserDaoImpl2"); //修改 class
//userDaoBD.setInitMethodName(methodName); //修改初始化方法
//userDaoBD.setLazyInit(true); //修改是否懒加载
}
}
上面已经对指定的BeanDefinition进行了修改操作,下面对BeanDefiition进行注册操作
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
//强转成子类 DefaultListableBeanFactory
if(configurableListableBeanFactory instanceof DefaultListableBeanFactory){
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) configurableListableBeanFactory;
BeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClassName("com.itheima.dao.UserDaoImpl2");
//进行注册操作
beanFactory.registerBeanDefinition("userDao2",beanDefinition);
}
}
}
Spring 提供了一个BeanFactoryPostProcessor的子接口BeanDefinitionRegistryPostProcessor专门用于注册BeanDefinition操作
public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
BeanDefinition beanDefinition=new RootBeanDefinition();
beanDefinition.setBeanClassName("com.learn.dao.Person");
beanDefinitionRegistry.registerBeanDefinition("person",beanDefinition);
}
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}
-
BeanDefinitionRegistryPostProcessor
:额外 提供了一个在 BeanDefinition 加载完成后、但还没来得及转换成 BeanDefinition 对象之前的“注册”阶段,可以向容器动态地 新增 Bean 定义。 -
BeanFactoryPostProcessor
:在所有 BeanDefinition 加载完成、Bean 实例化之前,对 已有 的 Bean 定义(BeanDefinition) 做修改,例如属性注入、替换、校验等。
1.3.4.2 Bean后处理器 – BeanPostProcessor
Bean被实例化后,到最终缓存到名为singletonObjects单例池之前,中间会经过Bean的初始化过程,例如:属性的
填充、初始方法init的执行等,其中有一个对外进行扩展的点BeanPostProcessor,我们称为Bean后处理。跟上面的
Bean工厂后处理器相似,它也是一个接口,实现了该接口并被容器管理的BeanPostProcessor,会在流程节点上被
Spring自动调用。
BeanPostProcessor的接口定义如下:
public interface BeanPostProcessor {
@Nullable
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Nullable
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
在 Spring 容器中,一个 Bean 从定义到可用,大致会经历以下几个阶段:
-
实例化 (Instantiation)
-
容器通过无参构造器或工厂方法创建一个 Bean 对象。
-
-
属性填充 (Populate Properties)
-
将配置的依赖(属性、@Autowired、@Value 等)注入到刚刚实例化的 Bean 上。
-
-
postProcessBeforeInitialization
回调 (Before Initialization)-
所有注册的
BeanPostProcessor
的postProcessBeforeInitialization(...)
方法先被调用,允许在初始化逻辑之前对 Bean 做进一步处理或增强。
-
-
初始化 (Initialization)
-
如果实现了
InitializingBean
,执行afterPropertiesSet()
; -
如果在配置中声明了
init-method
(或使用@PostConstruct
),执行相应的初始化方法。
-
-
postProcessAfterInitialization
回调 (After Initialization)-
容器再一次遍历所有
BeanPostProcessor
,依次调用它们的postProcessAfterInitialization(...)
方法,允许返回包装后的代理对象(如 AOP 代理)或做其他增强。 -
这一步常用于生成 AOP 代理、做最终包装或记录日志等。
-
-
Bean 就绪 (Ready for Use)
-
经过上述所有步骤之后,Bean 最终被放入单例缓存(或按作用域管理),可供应用程序使用。
-
案例——对Bean方法进行执行时间日志增强
要求如下:
-
Bean的方法执行之前控制台打印当前时间;
-
Bean的方法执行之后控制台打印当前时间。
分析:
-
对方法进行增强主要就是代理设计模式和包装设计模式;
-
由于Bean方法不确定,所以使用动态代理在运行期间执行增强操作;
-
在Bean实例创建完毕后,进入到单例池之前,使用Proxy代替真是的目标Bean
编写BeanPostProcessor,增强逻辑编写在 after方法中
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 使用动态代理增强
Object beanProxy = Proxy.newProxyInstance(
bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(),
(proxy, method, args) -> {
System.out.println("方法:" + method.getName() + "开始时间" + new Date());
Object result = method.invoke(bean, args);
System.out.println("方法:" + method.getName() + "结束时间" + new Date());
return result;
}
);
// 返回代理对象
return beanProxy;
}
1.3.5 Spring Bean的生命周期
Spring Bean的生命周期是从 Bean 实例化之后,即通过反射创建出对象之后,到Bean成为一个完整对象,最终存储到单例池中,这个过程被称为Spring Bean的生命周期。Spring Bean的生命周期大体上分为三个阶段:
-
Bean的实例化阶段:Spring框架会取出BeanDefinition的信息进行判断当前Bean的范围是否是singleton的,是否不是延迟加载的,是否不是FactoryBean等,最终将一个普通的singleton的Bean通过反射进行实例化;
-
Bean的初始化阶段:Bean创建之后还仅仅是个"半成品",还需要对Bean实例的属性进行填充、执行一些Aware接口方法、执行BeanPostProcessor方法、执行InitializingBean接口的初始化方法、执行自定义初始化init方法等。该阶段是Spring最具技术含量和复杂度的阶段,Aop增强功能,后面要学习的Spring的注解功能等、spring高频面试题Bean的循环引用问题都是在这个阶段体现的;
-
Bean的完成阶段:经过初始化阶段,Bean就成为了一个完整的Spring Bean,被存储到单例池singletonObjects中去了,即完成了Spring Bean的整个生命周期。
由于Bean的初始化阶段的步骤比较复杂,所以着重研究Bean的初始化阶段
Spring Bean的初始化过程涉及如下几个过程:
-
Bean实例的属性填充
-
Aware接口属性注入
-
BeanPostProcessor的before()方法回调
-
InitializingBean接口的初始化方法回调
-
自定义初始化方法init回调
-
BeanPostProcessor的after()方法回调
1.3.5.1 Bean实例属性填充
BeanDefinition 中有对当前Bean实体的注入信息通过属性propertyValues进行了存储,例如UserService的属性信息如下:
Spring在进行属性注入时,会分为如下几种情况:
-
注入普通属性,String、int或存储基本类型的集合时,直接通过set方法的反射设置进去;
-
注入单向对象引用属性时,从容器中getBean获取后通过set方法反射设置进去,如果容器中没有,则先创建被注入对象Bean实例(完成整个生命周期)后,在进行注入操作;
-
注入双向对象引用属性时,就比较复杂了,涉及了循环引用(循环依赖)问题,下面会详细阐述解决方案。
🌟🌟🌟🌟🌟🌟循环依赖
多个实体之间相互依赖并形成闭环的情况就叫做"循环依赖",也叫做"循环引用"
代码验证后,分析出UserService与UserDao实例化与初始化的顺序如下:
Spring提供了三级缓存存储 完整Bean实例 和 半成品Bean实例 ,用于解决循环引用问题
在DefaultListableBeanFactory的上四级父类DefaultSingletonBeanRegistry中提供如下三个Map:
public class DefaultSingletonBeanRegistry ... {
//1、最终存储单例 Bean成品的容器,即实例化和初始化都完成的 Bean,称之为"一级缓存"
Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//2、早期 Bean单例池,缓存半成品对象,且当前对象已经被其他对象引用了,称之为"二级缓存"
Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
//3、单例 Bean的工厂池,缓存半成品对象,对象未被引用,使用时在通过工厂创建 Bean,称之为"三级缓存"
Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
}
UserService和UserDao循环依赖的过程结合上述三级缓存描述一下
-
UserService 实例化对象,但尚未初始化,将UserService存储到三级缓存;
-
UserService 属性注入,需要UserDao,从缓存中获取,没有UserDao;
-
UserDao实例化对象,但尚未初始化,将UserDao存储到到三级缓存;
-
UserDao属性注入,需要UserService,从三级缓存获取UserService,UserService从三级缓存移入二级缓存;
-
UserDao执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存;
-
UserService 注入UserDao;
-
UserService执行其他生命周期过程,最终成为一个完成Bean,存储到一级缓存,删除二三级缓存。
常用的Aware接口
Aware接口是一种框架辅助属性注入的一种思想,其他框架中也可以看到类似的接口。框架具备高度封装性,我们接触到的一般都是业务代码,一个底层功能API不能轻易的获取到,但是这不意味着永远用不到这些对象,如果用到了,就可以使用框架提供的类似Aware的接口,让框架给我们注入该对象。
1.3.5.2 Spring IoC 整体流程总结
1.3.6 Spring xml方式整合第三方框架
xml整合第三方框架有两种整合方案:
-
不需要自定义名空间,不需要使用Spring的配置文件配置第三方框架本身内容,例如:MyBatis;
-
需要引入第三方框架命名空间,需要使用Spring的配置文件配置第三方框架本身内容,例如:Dubbo。
Spring整合MyBatis的原理剖析
整合包里提供了一个SqlSessionFactoryBean和一个扫描Mapper的配置对象,SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并通过动态代理产生Mapper的实现类存储到Spring容器中。相关的有如下四个类:
-
SqlSessionFactoryBean:需要进行配置,用于提供SqlSessionFactory;
-
MapperScannerConfigurer:需要进行配置,用于扫描指定mapper注册BeanDefinition;
-
MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法;
-
ClassPathMapperScanner:definition.setAutowireMode(2) 修改了自动注入状态,所以MapperFactoryBean中的setSqlSessionFactory会自动注入进去。
配置SqlSessionFactoryBean作用是向容器中提供SqlSessionFactory,SqlSessionFactoryBean实现了FactoryBean和InitializingBean两个接口,所以会自动执行getObject() 和afterPropertiesSet()方法
配置MapperScannerConfigurer作用是扫描Mapper,向容器中注册Mapper对应的MapperFactoryBean,MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor和InitializingBean两个接口,会在postProcessBeanDefinitionRegistry方法中向容器中注册MapperFactoryBean
Spring 整合其他组件时就不像MyBatis这么简单了,例如Dubbo框架在于Spring进行整合时,要使用Dubbo提供的命名空间的扩展方式,自定义了一些Dubbo的标签
引入context命名空间,在使用context命名空间的标签,使用SpEL表达式在xml或注解中根据key获得value
其实,加载的properties文件中的属性最终通过Spring解析后会被存储到了Spring容器的environment中去,不仅自己定义的属性会进行存储,Spring也会把环境相关的一些属性进行存储
原理剖析解析过程,只能从源头ClassPathXmlApplicationContext入手,经历复杂的源码追踪,找到如下两个点:
1)在创建DefaultNamespaceHandlerResolver时,为处理器映射地址handlerMappingsLocation属性赋值,并加载命名空间处理器到Map<String, Object> handlerMappings 中去
第一点完成后,Map集合handlerMappings就被填充了很多XxxNamespaceHandler,继续往下追代码
2)在DefaultBeanDefinitionDocumentReader的parseBeanDefinitions方法中,发现如下逻辑:
如果是默认命名空间,则执行parseDefaultElement方法
如果是自定义命名空间,则执行parseCustomElement方法
在执行resovle方法时,就是从Map<String, Object> handlerMappings中根据命名空间名称获得对应的处理器对象,此处是ContextNamespaceHandler,最终执行NamespaceHandler的parse方法
ContextNamespaceHandler源码如下,间接实现了NamespaceHandler接口,初始化方法init会被自动调用。由于context命名空间下有多个标签,所以每个标签又单独注册了对应的解析器,注册到了其父类
NamespaceHandlerSupport的Map<String, BeanDefinitionParser> parsers中去了
通过上述分析,我们清楚的了解了外部命名空间标签的执行流程,如下:
-
将自定义标签的约束 与 物理约束文件与网络约束名称的约束 以键值对形式存储到一个spring.schemas文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
-
将自定义命名空间的名称 与 自定义命名空间的处理器映射关系 以键值对形式存在到一个叫spring.handlers文件里,该文件存储在类加载路径的 META-INF里,Spring会自动加载到;
-
准备好NamespaceHandler,如果命名空间只有一个标签,那么直接在parse方法中进行解析即可,一般解析结果就是注册该标签对应的BeanDefinition。如果命名空间里有多个标签,那么可以在init方法中为每个标签都注册一个BeanDefinitionParser,在执行NamespaceHandler的parse方法时在分流给不同的BeanDefinitionParser进行解析(重写doParse方法即可)。
1.4 基于注解的Spring应用
1.4.1 Bean基本注解开发
基本Bean注解,主要是使用注解的方式替代原有xml的 <bean> 标签及其标签属性的配置
使用@Component 注解替代<bean>标签
可以通过@Component注解的value属性指定当前Bean实例的beanName,也可以省略不写,不写的情况下为当前类名首字母小写
使用注解对需要被Spring实例化的Bean进行标注,但是需要告诉Spring去哪找这些Bean,要配置组件扫描路径
@Component就单纯一个value属性,那么xml配置 <bean> 时那些属性怎么进行配置呢?Spring 是通过注解方式去配置的之前 <bean> 标签中的那些属性,例如:@Scope
由于JavaEE开发是分层的,为了每层Bean标识的注解语义化更加明确@Component又衍生出如下三个注解:
1.4.2 Bean依赖注入注解开发
Bean依赖注入的注解,主要是使用注解的方式替代xml的 <property> 标签完成属性的注入操作
Spring主要提供如下注解,用于在Bean内部进行属性注入的:
通过@Value 直接注入普通属性
@Autowired注解,用于根据类型进行注入
当容器中同一类型的Bean实例有多个时,会尝试自动根据名字进行匹配:
当容器中同一类型的Bean实例有多个时,且名字与被注入Bean名称不匹配时会报错
@Qualifier配合@Autowired可以完成根据名称注入Bean实例,使用@Qualifier指定名称
@Resource注解既可以根据类型注入,也可以根据名称注入,无参就是根据类型注入,有参数就是根据名称注入
PS:@Resource注解存在与 javax.annotation 包中,Spring对其进行了解析
1.4.3 非自定义Bean注解开发
非自定义Bean不能像自定义Bean一样使用@Component进行管理,非自定义Bean要通过工厂的方式进行实例化,使用@Bean标注方法即可,@Bean的属性为beanName,如不指定为当前工厂方法名称
PS:工厂方法所在类必须要被Spring管理
如果@Bean工厂方法需要参数的话,则有如下几种注入方式:
-
使用@Autowired 根据类型自动进行Bean的匹配,@Autowired可以省略
-
使用@Qualifier 根据名称进行Bean的匹配;
-
使用@Value 根据名称进行普通数据类型匹配。
1.4.4 Bean配置类的注解开发
@Component等注解替代了<bean>标签,但是像<import>、<context:componentScan> 等非<bean> 标签怎样去使用注解替代呢?
定义一个配置类替代原有的xml配置文件,<bean>标签以外的标签,一般都是在配置类上使用注解完成的
@Configuration注解标识的类为配置类,替代原有xml配置文件,该注解第一个作用是标识该类是一个配置类,第二个作用是具备@Component作用
@ComponentScan 组件扫描配置,替代原有xml文件中的<context:component-scan base-package=""/>
base-package的配置方式:
-
指定一个或多个包名:扫描指定包及其子包下使用注解的类
-
不配置包名:扫描当前@componentScan注解配置类所在包及其子包下的类
@PropertySource 注解用于加载外部properties资源配置,替代原有xml中的 <context:property-placeholder location=“”/> 配置
@Import 用于加载其他配置类,替代原有xml中的<import resource=
“classpath:beans.xml”/>配置
二、AOP部分
2.1 AOP 简介
2.1.1 AOP的概念
AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是横向的对不同事物的抽象,属性与属性、方法与方法、对象与对象都可以组成一个切面,而用这种思维去设计编程的方式叫做面向切面编程。
2.1.2 AOP思想的实现方案
动态代理技术,在运行期间,对目标对象的方法进行增强,代理对象同名方法内可以执行原有逻辑的同时嵌入执行其他增强逻辑或其他对象的方法。
2.1.3 AOP相关概念
2.2 基于xml配置的AOP
2.2.1 xml方式AOP快速入门
xml方式配置AOP的步骤:
-
导入AOP相关坐标;
-
准备目标类、准备增强类,并配置给Spring管理;
-
配置切点表达式(哪些方法被增强);
-
配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)。
1、导入AOP相关坐标
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.22</version>
</dependency>
2、准备目标类、准备增强类,并配置给Spring管理
3、配置切点表达式(哪些方法被增强)
4、配置织入(切点被哪些通知方法增强,是前置增强还是后置增强)
2.2.2 xml方式AOP配置详解
xml配置AOP的方式还是比较简单的,下面看一下AOP详细配置的细节:
-
切点表达式的配置方式
-
切点表达式的配置语法
-
通知的类型
-
AOP的配置的两种方式
1、切点表达式的配置方式
切点表达式的配置方式有两种,直接将切点表达式配置在通知上,也可以将切点表达式抽取到外面,在通知上进行引用
2、切点表达式的配置语法
切点表达式是配置要对哪些连接点(哪些类的哪些方法)进行通知的增强,语法如下:
execution([访问修饰符]返回值类型 包名.类名.方法名(参数))
其中,
-
访问修饰符可以省略不写;
-
返回值类型、某一级包名、类名、方法名 可以使用 * 表示任意;
-
包名与类名之间使用单点. 表示该包下的类,使用双点.. 表示该包及其子包下的类;
-
参数列表可以使用两个点.. 表示任意参数。
AspectJ的通知由以下五种类型
环绕通知
异常通知,当目标方法抛出异常时,异常通知方法执行,且后置通知和环绕后通知不再执行
最终通知,类似异常捕获中的finally,不管目标方法有没有异常,最终都会执行的通知
通知方法在被调用时,Spring可以为其传递一些必要的参数
JoinPoint 对象
ProceedingJoinPoint对象
Throwable对象
AOP的另一种配置方式,该方式需要通知类实现Advice的子功能接口
例如:通知类实现了前置通知和后置通知接口
切面使用advisor标签配置
使用aspect和advisor配置区别如下:
1)配置语法不同:
2)通知类的定义要求不同,advisor 需要的通知类需要实现Advice的子功能接口:
aspect 不需要通知类实现任何接口,在配置的时候指定哪些方法属于哪种通知类型即可,更加灵活方便:
3)可配置的切面数量不同:
-
一个advisor只能配置一个固定通知和一个切点表达式;
-
一个aspect可以配置多个通知和多个切点表达式任意组合,粒度更细。
4)使用场景不同:
-
如果通知类型多、允许随意搭配情况下可以使用aspect进行配置;
-
如果通知类型单一、且通知类中通知方法一次性都会使用到的情况下可以使用advisor进行配置;
-
在通知类型已经固定,不用人为指定通知类型时,可以使用advisor进行配置,例如后面要学习的Spring事务控制的配置;
-
xml方式AOP原理剖析
通过xml方式配置AOP时,我们引入了AOP的命名空间,根据讲解的,要去找spring-aop包下的META-INF,在去找spring.handlers文件
最终加载的是 AopNamespaceHandler,该Handler的init方法中注册了config标签对应的解析器
以ConfigBeanDefinitionParser作为入口进行源码剖析,最终会注册一个AspectJAwareAdvisorAutoProxyCreator进入到Spring容器中,那该类作用是什么呢?看一下集成体系图
AspectJAwareAdvisorAutoProxyCreator 的上上级父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法
通过断点方式观察,当bean是匹配切点表达式时,this.wrapIfNecessary(bean, beanName, cacheKey)返回的是一个JDKDynamicAopProxy
可以在深入一点,对wrapIfNecessary在剖析一下,看看是不是我们熟知的通过JDK的
Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler h) 的方式创建的代理对象呢?经过如下一系列源码跟踪
动态代理的实现的选择,在调用getProxy() 方法时,我们可选用的 AopProxy接口有两个实现类,如上图,这两种都是动态生成代理对象的方式,一种就是基于JDK的,一种是基于Cglib的
JDK的动态代理代码,之前已经写过了,下面看一下Cglib基于超类的动态代理
2.3 基于注解配置的AOP
2.3.1 注解方式AOP基本使用
Spring的AOP也提供了注解方式配置,使用相应的注解替代之前的xml配置,xml配置AOP时,我们主要配置了三部分:目标类被Spring容器管理、通知类被Spring管理、通知与切点的织入(切面),如下:
目标类被Spring容器管理、通知类被Spring管理
配置aop,其实配置aop主要就是配置通知类中的哪个方法(通知类型)对应的切点表达式是什么
注解@Aspect、@Around需要被Spring解析,所以在Spring核心配置文件中需要配置aspectj的自动代理
如果核心配置使用的是配置类的话,需要配置注解方式的aop自动代理
2.3.2 注解方式AOP配置详解
各种注解方式通知类型
切点表达式的抽取,使用一个空方法,将切点表达式标注在空方法上,其他通知方法引用即可
@Component
@Aspect
public class AnnoAdvice {
//切点表达式抽取
@Pointcut("execution(* com.itheima.aop.*.*(..))")
public void pointcut(){}
//前置通知
@Before("pointcut()")
public void before(JoinPoint joinPoint){}
//后置通知
@AfterReturning("AnnoAdvice.pointcut()")
public void AfterReturning(JoinPoint joinPoint){}
// ... 省略其他代码...
}
2.3.3 注解方式AOP原理剖析
之前在使用xml配置AOP时,是借助的Spring的外部命名空间的加载方式完成的,使用注解配置后,就抛弃了<aop:config>标签,而该标签最终加载了名为AspectJAwareAdvisorAutoProxyCreator的BeanPostProcessor ,最终,在该BeanPostProcessor中完成了代理对象的生成。
同样,从aspectj-autoproxy标签的解析器入手
this.registerBeanDefinitionParser("aspectj-autoproxy", new
AspectJAutoProxyBeanDefinitionParser());
<!--开启 aop aspectj的自动代理-->
<aop:aspectj-autoproxy/>
同样,从aspectj-autoproxy标签的解析器入手
this.registerBeanDefinitionParser("aspectj-autoproxy", new
AspectJAutoProxyBeanDefinitionParser());
而AspectJAutoProxyBeanDefinitionParser代码内部,最终也是执行了和xml方式AOP一样的代码
registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source)
如果使用的是核心配置类的话
@Configuration
@ComponentScan("com.itheima.aop")
@EnableAspectJAutoProxy
public class ApplicationContextConfig {
}
查看@EnableAspectJAutoProxy源码,使用的也是@Import导入相关解析类
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;
boolean exposeProxy() default false;
}
使用@Import导入的AspectJAutoProxyRegistrar源码,一路追踪下去,最终还是注册了AnnotationAwareAspectJAutoProxyCreator 这个类
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
}
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, (Object)null);
}
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class,registry, source);
}
2.4 基于AOP的声明式事务控制
2.4.1 Spring事务编程概述
事务是开发中必不可少的东西,使用JDBC开发时,我们使用connnection对事务进行控制,使用MyBatis时,我们使用SqlSession对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化,Spring 就将这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制 和 声明式事务控制
Spring事务编程相关的类主要有如下三个
虽然编程式事务控制我们不学习,但是编程式事务控制对应的这些类我们需要了解一下,因为我们在通过配置的方式进行声明式事务控制时也会看到这些类的影子
2.4.2 基于xml声明式事务控制
结合上面我们学习的AOP的技术,很容易就可以想到,可以使用AOP对Service的方法进行事务的增强。
-
目标类:AccountServiceImpl
-
切点:service业务类中的所有业务方法
-
通知类:Spring提供的,通知方法已经定义好,只需要配置即可
我们分析:
-
通知类是Spring提供的,需要导入Spring事务的相关的坐标;
-
配置目标类AccountServiceImpl;
-
使用advisor标签配置切面。
导入Spring事务的相关的坐标,spring-jdbc坐标已经引入的spring-tx坐标
配置目标类AccountServiceImpl
使用advisor标签配置切面
疑问:Spring提供的通知类是谁?是spring-tx包下的advice标签配置提供的
对上述配置进行详解一下
-
首先,平台事务管理器
PlatformTransactionManager
是Spring提供的封装事务具体操作的规范接口,封装了事务的提交和回滚方法
不同的持久层框架事务操作的方式有可能不同,所以不同的持久层框架有可能会有不同的平台事务管理器实现,例如,MyBatis作为持久层框架时,使用的平台事务管理器实现是DataSourceTransactionManager。Hibernate作为持久层框架时,使用的平台事务管理器是HibernateTransactionManager。
-
其次,事务定义信息配置,每个事务有很多特性,例如:隔离级别、只读状态、超时时间等,这些信息在开发时可以通过connection进行指定,而此处要通过配置文件进行配置
<tx:attributes>
<tx:method name="方法名称"
isolation="隔离级别"
propagation="传播行为"
read-only="只读状态"
timeout="超时时间"/>
</tx:attributes>
其中,name属性名称指定哪个方法要进行哪些事务的属性配置,此处需要区分的是切点表达式指定的方法与此处指定的方法的区别?切点表达式,是过滤哪些方法可以进行事务增强;事务属性信息的name,是指定哪个方法要进行哪些事务属性的配置
方法名在配置时,也可以使用 * 进行模糊匹配,例如:
<tx:advice id="myAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!--精确匹配 transferMoney方法-->
<tx:method name="transferMoney"/>
<!--模糊匹配以 Service结尾的方法-->
<tx:method name="*Service"/>
<!--模糊匹配以 insert开头的方法-->
<tx:method name="insert*"/>
<!--模糊匹配以 update开头的方法-->
<tx:method name="update*"/>
<!--模糊匹配任意方法,一般放到最后作为保底匹配-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
isolation属性:指定事务的隔离级别,事务并发存在三大问题:脏读、不可重复读、幻读/虚读。可以通过设置事务的隔离级别来保证并发问题的出现,常用的是READ_COMMITTED
和 REPEATABLE_READ
read-only属性:设置当前的只读状态,如果是查询则设置为true,可以提高查询性能,如果是更新(增删改)操作则设置为false
timeout属性:设置事务执行的超时时间,单位是秒,如果超过该时间限制但事务还没有完成,则自动回滚事务,不在继续执行。默认值是-1,即没有超时时间限制
propagation属性:设置事务的传播行为,主要解决是A方法调用B方法时,事务的传播方式问题的,例如:使用单方的事务,还是A和B都使用自己的事务等。事务的传播行为有如下七种属性值可配置
xml方式声明式事务控制的原理浅析一下
<tx:advice>标签使用的命名空间处理器是TxNamespaceHandler,内部注册的是解析器是TxAdviceBeanDefinitionParser
this.registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser());
TxAdviceBeanDefinitionParser中指定了要注册的BeanDefinition
protected Class<?> getBeanClass(Element element) {
return TransactionInterceptor.class;
}
TxAdviceBeanDefinitionParser二级父类AbstractBeanDefinitionParser的parse方法将TransactionInterceptor以配置的名称注册到了Spring容器中
parserContext.registerComponent(componentDefinition);
TransactionInterceptor中的invoke方法会被执行,跟踪invoke方法,最终会看到事务的开启和提交
2.4.3 基于注解声明式事务控制
注解就是对xml的替代
同样,使用的事务的注解,平台事务管理器仍然需要配置,还需要进行事务注解开关的开启
如果使用全注解的话,使用如下配置类的形式代替配置文件
三、Web部分
3.1 Spring整合web环境
3.1.1 Javaweb三大组件及环境特点
在Java语言范畴内,web层框架都是基于Javaweb基础组件完成的,所以有必要复习一下Javaweb组件的特点
3.1.2 Spring整合web环境的思路及实现
在进行Java开发时要遵循三层架构+MVC,Spring操作最核心的就是Spring容器,web层需要注入Service,service层需要注入Dao(Mapper),web层使用Servlet技术充当的话,需要在Servlet中获得Spring容器
web层代码如果都去编写创建AnnotationConfigApplicationContext的代码,那么配置类重复被加载了,Spring容器也重复被创建了,不能每次想从容器中获得一个Bean都得先创建一次容器,这样肯定是不允许。
针对以上诉求我们给出解决思路,如下:
-
在
ServletContextListener
的contextInitialized方法中执行ApplicationContext的创建。或在Servlet的init
方法中执行ApplicationContext的创建,并给Servlet的load-on-startup属性一个数字值,确保服务器启动Servlet就创建; -
将创建好的ApplicationContext存储到ServletContext域中,这样整个web层任何位置就都可以获取到了
3.1.3 Spring的web开发组件spring-web
到此,就将一开始的诉求都解决了,当然我们能想到的Spring 框架自然也会想到,Spring其实已经为我们定义好了一个ContextLoaderListener,使用方式跟我们上面自己定义的大体一样,但是功能要比我们强百倍,所以,遵循Spring "拿来主义" 的精神,我们直接使用Spring提供的就可以了,开发如下:
先导入Spring-web的坐标:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.7</version>
</dependency>
在web.xml中去配置ContextLoaderListener,并指定配置文件的位置
在Servlet中直接使用
如果核心配置类使用的是注解形式的,那么Spring容器是AnnotationConfigWebApplicationContext,如下配置方式
3.2 web层MVC框架思想与设计思路
Java程序员在开发一般都是MVC+三层架构,MVC是web开发模式,传统的Javaweb技术栈实现的MVC如下
原始Javaweb开发中,Servlet充当Controller的角色,Jsp充当View角色,JavaBean充当模型角色,后期Ajax异步流行后,在加上现在前后端分离开发模式成熟后,View就被原始Html+Vue替代。原始Javaweb开发中,Service充当Controller有很多弊端,显而易见的有如下几个:
负责共有行为的Servlet称之为前端控制器,负责业务行为的JavaBean称之为控制器Controller
四、SpringMVC
4.1 SpringMVC简介
4.1.1 SpringMVC概述
SpringMVC是一个基于Spring开发的MVC轻量级框架,Spring3.0后发布的组件,SpringMVC和Spring可以无缝整合,使用DispatcherServlet作为前端控制器,且内部提供了处理器映射器、处理器适配器、视图解析器等组件,可以简化JavaBean封装,Json转化、文件上传等操作。
4.1.2 SpringMVC快速入门
导入Spring整合SpringMVC的坐标
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.7</version>
</dependency>
编写一个控制器Controller,配置映射信息
@Controller
public class UserController {
@RequestMapping("/show")
public String show(){
System.out.println("show 执行....");
//视图跳转到 index.jsp
return "/index.jsp";
}
}
在web.xml中配置SpringMVC的前端控制器ServletDispatcher
创建springMVC的核心配置文件 spring-mvc.xml,并配置组件扫描web层
4.1.3 Controller中访问容器中的Bean
DispatcherServlet在进行初始化时,加载的spring-mvc.xml配置文件创建的SpringMVC容器,那么web层Controller被扫描进入到了容器中,而之前Spring容器中的Service是否可以获取到呢?下面搭建Spring的web环境进行验证
创建一个applicationContext.xml文件
在web.xml中配置ContextLoaderListener
编写UserService和UserServiceImpl
修改UserController,从Spring容器中匹配Service进行注入
4.1.4 SpringMVC关键组件浅析
上面已经完成的快速入门的操作,也在不知不觉中完成的Spring和SpringMVC的整合,我们只需要按照规则去定义Controller和业务方法就可以。但是在这个过程中,肯定是很多核心功能类参与到其中,这些核心功能类,一般称为组件。当请求到达服务器时,是哪个组件接收的请求,是哪个组件帮我们找到的Controller,是哪个组件帮我们调用的方法,又是哪个组件最终解析的视图?
先简单了解一下以上三个重要组件的关系
SpringMVC的默认组件,SpringMVC 在前端控制器 DispatcherServlet加载时,就会进行初始化操作,在进行初始化时,就会加载SpringMVC默认指定的一些组件,这些默认组件配置在 DispatcherServlet.properties 文件中,该文件存在与spring-webmvc-5.3.7.jar包下的org\springframework\web\servlet\DispatcherServlet.properties
这些默认的组件是在DispatcherServlet中进行初始化加载的,在DispatcherServlet中存在集合存储着这些组件,SpringMVC的默认组件会在 DispatcherServlet 中进行维护,但是并没有存储在与SpringMVC的容器中
配置组件代替默认组件,如果不想使用默认组件,可以将替代方案使用Spring Bean的方式进行配置,例如,在spring-mvc.xml中配置RequestMappingHandlerMapping
当我们在Spring容器中配置了HandlerMapping,则就不会再加载默认的HandlerMapping策略了,原理比较简单,DispatcherServlet 在进行HandlerMapping初始化时,先从SpringMVC容器中找是否存在HandlerMapping,如果存在直接取出容器中的HandlerMapping,在存储到 DispatcherServlet 中的handlerMappings集合中去。