一、前言
1.传统Javaweb开发的困惑
我们用一段常见的Javaweb开发代码为例:
这段代码在开发中有两个问题
1.1 问题一:层与层之间紧密耦合、接口与具体实现类紧密耦合。
在本方法中当我们在后期开发时如果需要切换接口实现则需要修改源码,不利于后期的开发与运维。
解决思路:不在程序中手动new对象,使用第三方来提供程序需要的bean对象。
bean对象:在java中我们将符合特定规范的java类称为bean。一般来说此种类用于封装数据、有默认的构造方法
1.2 问题二:在完成此方法本来的目的之前重复的操作较多。
在本方法中我们仅仅需要userDao.updateUserInfo(user)
这段话来更改数据库内容,但是却添加了开启事务、关闭事务、记录异常等代码,这些代码在其他类似方法中也会以相似度极高的形式出现。
解决思路:不在程序中new对象,使用第三方根据程序需求生成对象并且根据程序的需求生成该Bean对象的代理对象(增强对象的方法)
代理(Proxy):在java中代理是一种设计模式。代理用于提供特定对象间接访问。
代理类可以在调用被代理类的基础上新加一些功能。
比如说我们有一个负责修改数据库的A类。A类中有一个查询数据的方法searchData()。现在我们需要在每次调用A类查询数据方法之前新加一段日志记录的功能。那么我们就可以新建一个A类的代理类B类,B类中新写一个方法此方法中完成日志记录后在调用A类中的searchData()方法。
实现方法如下:
public Class B{
//被代理对象
A a;
public B(A a){
this.a = a;
}
public Object searchData(){
...
新加记录日志功能代码
...
//调用目标对象方法
a.searchData();
...
}
}
2.IoC、DI和AOP思想的提出
IoC(Inversion of Control):控制反转,强调的是将原来程序中创建Bean以及销毁bean的权利反转给第三方。
DI(Dependency Injection):依赖注入,强调Bean之间的关系由第三方去设置。
AOP(Aspect Oriented Programming):面向切面编程,功能的横向抽取与扩展,主要实现的方式是代理(Proxy)。主要思想是对某一个bean进行增强。
二、Spring框架
Spring是一个轻量级的Java开发框架,可以简化我们的开发过程。针对与Javaweb开发中遇到的问题,Spring框架提供了IoC容器、AOP、数据访问、Web开发、集成等模块。
Spring官网 : https://spring.io/
1.BeanFactory快速入门
此处我们将要了解的BeanFactory便是控制生成与销毁Bean的第三方。
1.1 快速使用BeanFactory的案例
本案例主要模拟一个简单的Bean在BeanFactory中配置以及在程序中生成bean的过程。
- 创建一个普通的Maven项目
- 在pom文件中导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.7</version>
</dependency>
- 创建一个简单的bean
创建一个UserService以及UserServiceImpl。
UserService:
public interface UserService {
public void fun();
}
Userserviceimpl:
public class UserServiceImpl implements UserService {
@Override
public void fun() {
System.out.println("方法成功被调用...");
}
}
- 创建Bean配置文件beans.xml,并且在beans.xml中配置bean
<?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的配置清单,在BeanFactory中配置bean-->
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl"></bean>
</beans>
我们可以看到,在bean标签中我们输入了id以及class两个属性,它们分别可以理解为bean的名字以及bean对应的类。
- 创建一个测试类,在测试类中生成bean并且测试是否可行
package com.itheima.test;
import com.itheima.service.UserService;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
public class BeanFactoryTest {
public static void main(String[] args) {
// 创建BeanFactory对象
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 创建一个XML读取器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// 读取配置清单给BeanFactory
reader.loadBeanDefinitions("beans.xml");
// 获取bean对象并且调用其方法查看是否创建成功
UserService userService = (UserService) beanFactory.getBean("userService");
if (userService != null){
userService.fun();
}
}
}
运行结果:
我们可以看到我们在程序中使用BeanFactory有三步:
1.创建一个BeanFactory对象(DefaultListableBeanFactory
)
2.创建一个配置读取对象(XmlBeanDefinitionReader
),使用配置对象读取配置清单
3.获取Bean
1.2 BeanFactory中的依赖注入(DI)
我们都知道,在我们一般的程序开发中,Service层是需要调用Dao层,Service与Dao层是紧密链接的。因此我们可以在上述的案例中做一个小小的补充:模拟日常开发,创建一个Dao层的bean,在beans.xml中配置好这个bean之后将这个Dao层的bean‘注入’到userService的bean中。
- 创建一个Dao层的bean
我们创建的类为UserDao以及UserDaoImpl
UserDao:
public interface UserDao {
public void fun();
}
UserDaoImpl:
public class UserDaoImpl implements UserDao {
@Override
public void fun() {
System.out.println("正在运行UserDao的方法...");
}
}
- 在beans.xml中添加bean的配置,修改UserService中的代码以及将依赖导入
UserService
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
System.out.println("BeanFactory调用此方法,将UserDao设置到此处...");
this.userDao = userDao;
}
@Override
public void fun() {
System.out.println("UserService方法成功被调用...");
userDao.fun();
}
}
<!-- bean的配置清单,在BeanFactory中配置bean-->
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userDao" ref="userDao"></property>
</bean>
<bean id="userDao" class="com.itheima.Dao.impl.UserDaoImpl"></bean>
我们在配置UserDao的bean时也在UserService的bean中添加了一个<property>
标签,在此标签中我们配置了UserDao,但是需要注意的是我们如此配置的前提是在UserService中编写了setUserDao()的方法。也就是说在BeanFactory中当我们给一个bean添加Property标签时,FactoryBean会自己根据标签中的name在bean中寻找对应的set方法,并且运行它。
3. 运行测试类进行测试
现在我们稍微修改测试类,添加日志显示,并且获取UserService的bin并且运行他的fun()方法:
BeanFactoryTest:
public class BeanFactoryTest {
public static void main(String[] args) {
System.out.println("BeanFactoryTest测试类开始测试...");
// 创建BeanFactory对象
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
// 创建一个XML读取器
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(beanFactory);
// 读取配置清单给BeanFactory
reader.loadBeanDefinitions("beans.xml");
System.out.println("开始获取UserService bean...");
// 获取bean对象并且调用其方法查看是否创建成功
UserService userService = (UserService) beanFactory.getBean("userService");
if (userService != null){
userService.fun();
}
}
}
2.ApplicationContext快速入门
ApplicationContext称为Spring容器,内部封装了BeanFactory,比BeanFactory功能更加强大。并且在ApplicationContext中对bean的操作大部分情况下都是调用的BeanFactory。
针对上一个案例中的测试类,我们可以将其中的BeanFactory替换为ApplicationContext进行编写:
public class ApplicationContextTest {
public static void main(String[] args) {
System.out.println("开始ApplicationContext对象的创建...");
// 创建ApplicationContext对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
// 获取Bean
System.out.println("获取userService bean...");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.fun();
}
}
我们可以看到ApplicationContext在创建的时候要比BeanFactory更加简单一些,可以直接将xml文件路劲作为构造参数创建,而不需要和BeanFactory一样创建再创建一个加载XML的类对象。
运行结果:
面试题:BeanFactory和ApplicationContext之间的关系
答:BeanFactory和ApplicationContext都是Spring框架中用于配置和管理bean的容器。
BeanFactory提供了基础的IoC功能,即用配置文件或者注解的形式对bean进行创建、销毁、组装和配置等过程。
ApplicationContext是BeanFactory的子接口,在BeanFactory的基础上提供了更多企业级的功能,相较于BeanFactory,ApplicationContext更加适用于提供应用程序级别的功能,例如:事件发布、资源加载、国际化等。另外ApplicationContext还支持自动注入、AOP代理、事务管理等高级功能,这些都是BeanFactory不具有的。
在对bean进行加载时,ApplicationContext会在容器启动时就将bean加载完毕,因此ApplicationContext可以更加快速的获取bean,BeanFactory会在程序获取bean的时候才将bean进行实例化,因此BeanFactory在容器启动时速度更快一些。
总的来时,BeanFacotry和ApplicationContext都是Spring框架中两个重要的容器接口,BeanFactory提供了基础的IoC功能,ApplicationContext在BeanFactory的基础上提供了更多企业级的功能。
3.基于xml的Spring应用
3.1 Spring配置详解
Spring bean的常用配置如下
xml配置方式 | 功能描述 |
---|---|
<bean id = “” class = “” > | Bean的id以及对应的类 |
<bean name=“” > | 使用name定义bean的别名,我们也可以通过别名来获取bean实例 |
<bean scope=“” > | Bean的作用范围,BeanFactory作为容器的时候,取值singleton和prototype |
<bean lazy-init=“” > | Bean的实例化时间,可以使bean延迟加载。但是在BeanFactory作为容器的时候无效。 |
<bean init-method=“” > | Bean实例化时自动执行初始化方法,method对应方法名 |
<bean destory-method > | Bean的销毁前执行的方法,method指定方法名 |
<bean autowire=“” > | 设置自动注入模式,常用的模式有 byType:按照类型、byName:按照方法 |
<bean factory-bean=“” factory-method=“” > | 指定哪个工程Bean的哪个方法完成bean的创建 |
3.1.1 bean的id和name配置
在xml中配置bean的时候,我们一般配置一个id就可以了,之后使用getBean方法根据id获取bean。
<bean id="userDao" class="com.itheima.Dao.impl.UserDaoImpl"></bean>
在bean标签中我们还可以配置name,可以使用getBean根据name获取,name可以配置多个,配置多个name的时候使用,
分隔。
<bean id="userDao" name="dao,Userdao2" class="com.itheima.Dao.impl.UserDaoImpl"></bean>
另外我们也可以name和id都不配置,在这种情况下,我们直接使用getBean根据类名来获取bean。
<bean class="com.itheima.Dao.impl.UserDaoImpl"></bean>
这种情况下获取bean:
applicationContext.getBean("com.itheima.Dao.impl.UserDaoImpl");
3.1.2 bean的作用域范围
在基础的Spring框架中,bean配置的scope只有两个值:singleton(单例)和prototype(原型)。
singleton : 单例,只会在容器启动的时候创建一个,并且存储在容器当中,在获取bean时容器返回,每次返回的bean都是同一个。
prototype : 原型,不会在容器启动时创建,在获取bean的时候容器将实例化一个bean返回,每次返回的bean都不是同一个。
在一般情况下,我们不需要配置scope,使用默认值singleton即可。
3.1.3 bean的延迟加载
当lazy-init的值为true时,那么这个bean只有在被获取时才会被加载。可以用此方法节省资源。
<bean id="userDao" class="com.itheima.Dao.impl.UserDaoImpl" lazy-init="true"></bean>
3.1.4 bean的初始化和销毁方法
可以在bean中配置init-method和destory-method方法来指定创建以及销毁方法。
例如:
public class UserServiceImpl implements UserService {
public void init(){
System.out.println("UserServiceImpl初始化方法被调用...");
}
public void destory(){
System.out.println("UserServiceImpl销毁方法被调用...");
}
@Override
public void fun() {
System.out.println("UserServiceImpl方法被调用...");
}
}
beans.xml:
<bean id="userService" class="com.itheima.service.impl.UserServiceImpl" init-method="init" destroy-method="destory"></bean>
编写测试方法:
public class BeanTest {
public static void main(String[] args) {
//ApplicationContext没有close方法,只能使用其子类ClassPathXmlApplicationContext
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
UserService userService = (UserService) context.getBean("userService");
userService.fun();
// 只有在程序中调用容器的销毁方法时,容器才会调用其中bean的销毁方法
context.close();
}
}
测试结果:
需要注意的是,如果我们没有调用容器的close方法,直接关闭程序,那么容器就来不及调用其中bean的销毁方法就‘挂掉’了。所以我们要分清楚bean的销毁以及bean的销毁方法的区别,bean的销毁是指销毁这个bean,而bean的销毁方法是指在销毁bean之前调用的方法。
3.1.5 InitializingBean接口
要实现bean的初始化方法,我们也可以把bean的类实现InitializingBean接口,实现接口的afterPropertiesSet方法,那么容器就会在bean实例化注入依赖之后调用此方法。
3.1.6 实现bean实例化的方法
我们常用的bean实例化的方法是由Spring容器直接生成bean的实例,但是我们也可以自己对bean实例化的方法进行定义。
1)生成有参构造的bean
在我们没有特定对bean进行配置时,Spring容器会默认调用bean的无参构造方法对bean进行实例化。我们也可以给bean配置有参构造方法,在bean标签中配置对应的参数即可。
例如,我们接下来创建一个简单的bean,他的有参构造方法中的参数为String类型的name以及int类型的age:
public class BeanCreateTestService {
public BeanCreateTestService(String name , int age){
System.out.println("BeanCreateTest获取到的参数" + name + " " + age);
}
}
在Spring容器中进行配置
<bean id="beanCreateTestService" class="com.itheima.BeanTest.BeanCreateTestService">
<!-- 配置有参构造的参数-->
<constructor-arg name="name" value="BugMaker"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>
运行结果:
2)静态工厂实例化
以上实例化方法都是由Spring容器实例化,除此之外我们也可以自己编写bean的实例化方法,将bean实例化后存入Spring容器当中。
我们先自己编写一个将bean实例化的工厂类,起名为MyBeanFactorySt:
public class MyBeanFactorySt {
public static BeanCreateTestService CreateBeanCreateTestService(String name,int age){
// 编写bean的实例化方法,我们可以在bean实例化之前编写一些业务逻辑
System.out.println("静态工厂实例化之前的业务逻辑...");
return new BeanCreateTestService(name,age);
}
}
之后配置bean
<!-- 静态工厂构造bean class:bean工厂的类|factory-method:bean工厂实例化bean的方法-->
<bean id="beanCreateTestServiceStaticFactory" class="com.itheima.BeanTest.MyBeanFactorySt" factory-method="CreateBeanCreateTestService">
<!-- 配置bean工厂中实例化方法中的参数-->
<constructor-arg name="name" value="BugMakerStatic"></constructor-arg>
<constructor-arg name="age" value="24"></constructor-arg>
</bean>
运行结果:
需要注意的是,我们之前学习的’init-method’以及将类实现InitializingBean接口使用接口的afterPropertiesSet()方法这两个都是在bean实例化之后才执行的,而现在我们自定义bean的实例化方法可以在bean实例化之前执行相关业务逻辑。
bean实例化时,方法的执行顺序:
3)动态工厂实例化
我们出了使用静态的方法实例化之外,也可以使用动态的方法进行实例化,也就是说我们将一个bean作为Bean工厂,使用bean里面的方法实例化一个bean。相当于bean中嵌套了一个bean。
我们编写一个简单的动态bean工厂:
public class MyBeanFactoryDy {
public BeanCreateTestService CreateBeanCreateTestService(String name,int age){
// 编写bean的实例化方法,我们可以在bean实例化之前编写一些业务逻辑
System.out.println("动态工厂实例化之前的业务逻辑...");
return new BeanCreateTestService(name,age);
}
}
配置动态bean工厂以及其生产的bean:
<!-- 动态工厂实例化-->
<bean id="DyBeanFactory" class="com.itheima.BeanTest.MyBeanFactoryDy"></bean>
<bean id="beanCreateTestServiceDy" factory-bean="DyBeanFactory" factory-method="CreateBeanCreateTestService">
<constructor-arg name="name" value="BugMakerStatic"></constructor-arg>
<constructor-arg name="age" value="24"></constructor-arg>
</bean>
其实这些实例化bean的方法更加强调了Spring容器的“容器”性质,我们可以直接把一个类交给Spring容器让它来管理这个类的bean实例化;也可以告诉Spring容器一个实例化对象的方法,让Spring容器调用此方法来实例化一个bean。
4)实现FactoryBean规范延迟实例化方法
我们在将bean实例化出了上述的将bean的实例方法交给Spring容器之外,我们也可以配置一个简单的工厂bean,Spring容器先加载简单的工厂bean,当我们获取这个bean的时候再使用工厂bean将bean实例化。
创建一个实现FactoryBean的方法:
public class FactoryBeanImpl implements FactoryBean<UserService> {
@Override
public UserService getObject() throws Exception {
return new UserServiceImpl();
}
@Override
public Class<?> getObjectType() {
return UserService.class;
}
}
配置bean:
<!--FactoryBean延迟加载-->
<bean id="userServiceF" class="com.itheima.BeanTest.FactoryBeanImpl">
</bean>
测试方法:
public class FactoryBeanTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
UserService userServiceF = (UserService) applicationContext.getBean("userServiceF");
userServiceF.fun();
}
}
测试结果
继承FactoryBean与一般方法不同的是此方法在Spring容器创建的时候加载的类是FactoryBean的实现类而非我们需要实例化的类。
我们通过debug模式,在ApplicationContext刚刚创建之后查看ApplicationContext中的数据可以看出,在ApplicationContext->beanFactory->singletoneObjects中存放的键值对中userServiceF对应的是FactoryBean的实现类。
在我们getBean之后,在ApplicationContext中会生成对应bean的实例并且将bean存放在ApplicationContext->beanFactory->factoryBeanObjectCache中。
3.1.7 Bean的依赖注入配置
1)String注入
<property name="属性名称" value="String值"></property>
2)类注入
<property name="属性名称" ref="注入类bean的id"></property>
或者:
<property name="属性名称">
<bean class="注入依赖的类"></bean>
</property>
3)String List注入
<property name="属性名称">
<list>
<value>String值1</value>
<value>String值2</value>
<value>String值3</value>
</list>
</property>
4)类List注入
<property name="属性名称">
<list>
<ref bean="注入的bean1"></ref>
<ref bean="注入的bean2"></ref>
<ref bean="注入的bean3"></ref>
</list>
</property>
5)String Set注入
<!-- string set-->
<property name="属性名称">
<set>
<value>Sring1</value>
<value>Sring2</value>
<value>Sring3</value>
</set>
</property>
6)类Set注入
<property name="属性名称">
<set>
<ref bean="注入的bean1"></ref>
<ref bean="注入的bean2"></ref>
<ref bean="注入的bean3"></ref>
</set>
</property>
7)Map类型注入
<property name="属性名称">
<map>
<entry key="1" value-ref="注入的bean1"></entry>
<entry key="2" value-ref="注入的bean2"></entry>
<entry key="3" value-ref="注入的bean3"></entry>
</map>
</property>
8)Propertie注入
<property name="属性名称">
<props>
<prop key="配置名称1">配置值1</prop>
<prop key="配置名称2">配置值2</prop>
</props>
</property>
9)依赖自动注入
在bean标签中可以设置autowire的值。
autowire="byname"时,会自动在Spring容器中查找名字和bean中set方法名字相同的bean进行自动注入。
autowire="byType"时,会在Spring容器中查找bean类型和bean中属性相同的bean进行依赖注入,但是当bean中有两个以上类型相同的bean时,程序会错。
3.1.8 Spring的其他配置标签
beans标签
用于改变开发环境。嵌套在根标签内的beans标签中的bean只有在程序中设置对应的profiles才可以在程序中被引用。
import标签
导入其他配置文件。
alias标签
可以给bean一个别名。
3.2 Spring的getBean方法
方法定义 | 返回值和参数 |
---|---|
Object getBean(String beanName) | 根据beanName获取bean实例,返回的值为Object |
T getBean(Class type) | 根据Bean的类型获取bean,要求容器中此类型的bean尽可有一个,返回值类型为Class类型的实例,无需强转 |
T getBean(String beanName,Class Type) | 根据beanName从容器中获取bean实例,返回值为Class类型的实例无需强转 |
3.3 Spring配置非自定义Bean
以上我们都介绍的是自己创建的bean在Spring中进行配置。在实际开发中我们有时会在Spring中配置非自定义的bean,例如在jar包中的类进行配置。
配置非自定义的bean需要注意:bean的实例化方法是什么(无参还是有参构造、静态还是动态工厂实例化);bean创建时需要注入哪些必要的属性。
我们举一个简单的例子:
在我们日常开发中将日期格式转化一般使用的代码为:
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateString = "2023-04-01 10:20:30";
Date parsedDate = formatter.parse(dateString);
那么我们如果将转换日期的Date作为一个bean就需要将SimpleDateFormat类作为一个bean工厂,调用其parse方法实例化一个Date的bean:
<!-- 配置SimpleDateFormat 动态工厂-->
<bean id="dateFormat" class="java.text.SimpleDateFormat">
<constructor-arg name="pattern" value="yyyy-MM-dd HH:mm:ss"></constructor-arg>
</bean>
<!-- 使用动态工厂将Date实例化-->
<bean id="date" factory-bean="dateFormat" factory-method="parse">
<constructor-arg name="source" value="2023-04-01 10:20:30"></constructor-arg>
</bean>
此外我们也可以将常用一些类在Spring容器中添加配置,比如:数据库source、数据库链接等。
3.4 Bean实例化的基本流程
bean的实例化流程如下:
1.Spring容器初始化时,加载xml配置文件,获取每一个bean的信息,将bean封装为BeanDefinition对象
2.将所有的BeanDefinition对象存储在Spring容器的BeanDefinitionMap<String,BeanDefinition>中
3.ApplicationContext(ApplicationContext会在创建之后立马将bean实例化)遍历BeanDefinitionMap,根据BeanDefinition中的信息生成bean的实例对象,将bean的实例对象存放在singletonObjects<String,Objects>的Map集合中
4.当程序获取Bean的实例化对象时,Spring容器会在singletonObjects中找到对应的实例对象并且返回
3.5 Spring的后处理器(Post Processor)
post processor:后处理器
3.5.1 BeanFactoryPostProcessor – 在bean实例化之前执行(对BeanFactory的后处理)
Spring会在加载xml配置之后会调用BeanFactoryPostProcessor接口的实现类。
因此我们可以在BeanFactoryPostProcessor的实现类中自己在BeanFactory添加bean。
1.创建一个类实现BeanFactoryPostProcessor接口
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// 自行注册一个BeanDefinition
BeanDefinition beanDefinition = new RootBeanDefinition();
beanDefinition.setBeanClassName("com.itheima.service.impl.UserServiceImpl");
// 将自行创建的BeanDefinition加入BeanFactory当中
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) beanFactory;
defaultListableBeanFactory.registerBeanDefinition("myUserServiceimpl",beanDefinition);
}
}
2.在xml文件中配置这个类的bean
<bean class="com.itheima.factory.MyBeanFactoryPostProcessor"></bean>
3.测试能否正常获取这个bean
public class BeanFactoryPostProcessorTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService myUserService = (UserService) applicationContext.getBean("myUserServiceimpl");
System.out.println(myUserService);
}
}
测试结果:
此外BeanFactoryPostProcessor还有一个子接口BeanDefinitionRegistryPostProcessor。在Spring容器启动的时候,当所有的BeanDefinition都已经加载到内存中,Spring容器会调用BeanDefinitionRegistryPostProcessor的postProcessorBeanDefinitionRegistry方法。
3.5.2 BeanPostProcessor – 在bean实例化之后执行(对Bean的后处理)
BeanPostProcessor是在Bean实例化之后存入SingletonObjects之前做出处理的,且处理针对的对象是实例化的Bean。
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;
}
}
@Nullable 注释是表明这个方法的返回值可以为空以防空指针报错
default 表明这个方法可以不被实现类实现
BeanPostProcessor接口有两个方法postProcessBeforeInitialization和postProcessAfterInitialization,这两个方法在类实例化的过程中调用顺序如下:
BeanPostProcessor案例 :对Bean的方法的时间日志功能增强
要求如下:在bean的方法开始以及结束时增加日志显示功能
1.编写一个简单的bean:
public class UserServiceImpl implements UserService {
@Override
public void fun() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("UserServiceImpl方法被调用...");
}
}
2.编写一个BeanPostProcessor类,此类中postProcessAfterInitialization中会对实例化之后的bean进行动态代理,对其方法进行加强,在方法执行前后增加日志显示
public class BeanFuncationTimeLogPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
// 生产bean的加强类
Object beanProxy = Proxy.newProxyInstance(
bean.getClass().getClassLoader(),
bean.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss.SSS");
// 1.输出方法开始时间
System.out.println(beanName + "." + method.getName() + "---开始时间 : " + simpleDateFormat.format(new Date()));
// 2.执行方法
Object result = method.invoke(bean, args);
// 3.输出方法结束时间
System.out.println(beanName + method.getName() + "---结束时间 : " + simpleDateFormat.format(new Date()));
return result;
}
}
);
return beanProxy;
}
}
lambda表达式:lambda表达式可以理解为匿名内部类的一个更简洁的编写方式
例如文中动态代理的方法中我们需要一个InvocationHandler的内部类:Proxy.newProxyInstance( ClassLoader, Interfaces, new InvocationHandler(){ ... 内部类代码 ... } )
我们可以使用lambda表达式写为
Proxy.newProxyInstance( ClassLoader, Interfaces, () -> { ... 内部类代码 ... } )
3.测试结果
当bean方法被调用时:
bean实例化的过程
3.6 Spring Bean的生命周期
Spring生命周期是指Bean实例化之后到Bean存储到单例池的过程:
1.Bean的实例化阶段:Spring判断Bean的属性(是否是单例singleton、是否是FactoryBean),之后将普通单例通过反射实例化
2.Bean的初始化过程:Spring对Bean实例属性的填充,执行Aware接口方法、BeanPostProcessor、进行InitializingBean接口的初始化方法、执行自定义的init方法等一系列的初始化方法。
2.Bean的完成阶段:在Bean完成了初始化阶段,Bean成为了一个完整的Spring Bean,Spring容器会把它存放在单例池(SingletonObjects)之中。那么我们将Bean存放到单例池(SIngletonObjects)称为Bean完成了整个生命周期。
Spring在进行属性注入的时候我们分为以下三种情况:
1.注入普通属性
例如String、int等基本的存储类型的集合时,直接通过类的set方法反射设置进去。
2.注入单向对象引用属性:
单向对象是指当前的Bean仅对属性对象引用,而引入的对象不对当前Bean进行引用。
例如:
Class A{
...
}
Class B{
A a;
public void setA(A a){
this.a = a;
}
}
在上述代码中仅有B单向的把A当为属性。
那么在这种情况下,在B的Bean属性注入的时候,Spring会使用getBean()获取A对应的bean,之后使用set方法设置进去,如果此时A在容器中没有实例化,那么容器会先实例化A(完成整个生命周期),之后进行注入操作。
3.注入双向对象引用属性****(重点)****
双向对象引用是指两个类互相为对方的属性。
例如:
Class A{
B b;
public void setA(B b){
this.b = b;
}
}
Class B{
A a;
public void setA(A a){
this.a = a;
}
}
上述代码中A既把b作为属性又是B的属性,B也同样如此。
了解这种情况我们需要先了解一下三级缓存机制
三级缓存
public class DefaultSingletonBeanRegistry ... {
...
//一级缓存,单例池,里面存放的是已经完成生命周期的Bean
private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);
//二级缓存,早期单例池,缓存半成品对象,里面存放的对象已经被其他对象引用过
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16);
//三级缓存,单例Bean的工厂池,缓存半成品对象,里面存放的是没有被其他对象引用过的对象
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16);
...
}
注意:三级缓存中Map的结构是<String , ObjectFactory>, ObjectFactory是一个函数式接口,其中仅有getObject()函数,Bean在刚被实例化之后,会“包进”ObjectFactory中,通过get方法获取。
当两个Bean相互为引用属性时,Spring容器创建Bean实例化的逻辑流程:
我们需要知道一个Bean的创建到存入单例池的流程是实例化->初始化方法->单例池
。
当Spring执行getBean(“A”)的方法时,会先把Bean A实例化,此时创建的对象仅仅只是被创建出来并不成熟,不可以被直接引用,同时A的创建工厂会存入三级缓存。之后Spring会执行属性注入、初始化等一系列方法。当Bean A执行属性注入的时候如果发现在一二三级缓存中都没有Bean B的实例化对象,那么对于A的初始化工作会被“挂起”,Spring开始对于Bean B的创建。当Bean B创建到初始化阶段的属性注入时,会在三级缓存中找到创建Bean A的创建工厂,此时会根据Bean A是否被AOP代理创建代理对象或者普通对象交由Bean B(此时将Bean A放入二级缓存),使得Bean B可以完成一整个的创建流程。当Bean B被完全创建完成之后Spring会回到Bean A的创建工作继续Bean A的创建。
我们主要需要了解的是三级缓存的作用:
一级缓存(SIngletonObjects):存储的是已经“成熟的Bean”可以直接被用户使用。
二级缓存 (EarlySIngletonObjects):存储的Bean处于中间状态,Bean尚未完成依赖注入,但是已经被创建。
三级缓存(SIngletonFactories):是一个工厂,里面存放的是正在被创建的Bean,存放的是Lambda表达式,会根据Bean是否被AOP而创建Bean的代理对象或者普通对象。
*** 问题:为什么是三级缓存而不是二级缓存?
其实如果不考虑AOP的话。二级缓存是可以处理绝大多互相依赖的情况的。
但是当相互依赖的A、B两个bean中的A类是被AOP代理的话,Spring要求属性注入时Bean B需要注入的必须是Bean A的代理对象,但是AOP代理对象的生成是在Bean的初始化阶段才生成的。所以,我们必须借助可以直接生成AOP代理对象的三级缓存来处理相互依赖且有AOP代理的情况。
具体可以参考博客:Spring 的三级缓存机制
对于非代理Bean,singletonFactories中的工厂对象直接创建Bean的实例并将其放入一级缓存singletonObjects中。而对于需要被AOP代理的Bean,singletonFactories中的工厂对象会先创建一个原始的Bean实例,然后通过AOP代理来生成代理对象,并将代理对象放入一级缓存singletonObjects中。
3.7 Spring IoC整体流程总结
SpringBean生成的整个阶段可以总体分为两个大阶段:BeanDefinition阶段以及Bean的生命周期。
BD阶段中,Spring读取配置文件中Bean的配置信息,创建BD并且将它们存储在BDMap之中。
Bean的生命周期阶段中,Spring依次获取BDMap中的BD,根据BD的信息将Bean实例化(此时Bean不是完整的Bean),将Bean初始化(此时Bean变为完整),将Bean存入SingletonObjects中。
3.8 Spring xml方式整合第三方框架
3.8.1 Mybatis原始代码使用
1.配置Mybatis信息
创建Mybatis-config.xml文件,其内容如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<!-- 设置数据源类型。由于表格存储JDBC驱动需要主动关闭后才能让进程退出,请根据实际使用选择合适的数据源类型。-->
<!-- 如果程序常驻执行,则您可以使用POOLED维护一个连接池;如果希望程序完成数据查询后退出,则只能使用UNPOOLED。-->
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<mappers>
<!-- 设置为映射配置文件的路径。-->
<package name="com.itheima.mapper"/>
</mappers>
</configuration>
在MySQL是8.0以上的版本时,driver为com.mysql.cj.jdbc.Driver
;在MySQL是5.0版本的时候driver是com.mysql.jdbc.driver
。以及在MySQL是8.0以上的版本时URL有时是需要添加时区等参数的,否则无法建立和数据库的链接,此次测试不需要添加这些参数,原因未知,可能原因是mysql驱动或者mybatis版本是新的所以解决了这个问题。
2.创建实体类
根据数据库的结构创建一个实体类。
数据库的结构如图:
实体类:
package com.itheima.pojo;
public class User {
private int id;
private String username;
private int age;
private String address;
/*
...
getter和setter方法,toString方法
...
*/
}
3.创建Mapper以及SQL语句
创建一个简单的Mapper接口:
public interface UserMapper {
List<User> findAll();
}
根据Mapper接口创建一个mapper的xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace: mapper接口的以用路径-->
<mapper namespace="com.itheima.mapper.UserMapper">
<!-- id: mapper接口中使用的方法
resultType: SQL返回的数据类型-->
<select id="findAll" resultType="com.itheima.pojo.User">
select * from USER_INFO
</select>
</mapper>
4.编写测试类
package com.itheima.test;
import com.itheima.mapper.UserMapper;
import com.itheima.pojo.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class MybatisTest {
public static void main(String[] args) throws IOException {
// 1.获取mybatis配置文件
InputStream in = Resources.getResourceAsStream("mybatis-config.xml");
// 2.根据mybatis配置文件创建sqlSessionFactory,并且获取一个sqlSession
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory sqlSessionFactory = builder.build(in);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 3.根据sqlSession生成userMapper的对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 4.执行userMapper中的方法并且将结果输出
List<User> all = userMapper.findAll();
for (User user : all){
System.out.println(user.toString());
}
}
}
MyBaits原始的使用方法比较繁琐,并且一般情况下是不会这么使用的,所以不需要刻意记住。
执行结果:
3.8.2 Spring整合Mybatis
1.导入Mybatis整合Spring的相关坐标
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
2.编写Mapper和Mapper.xml
上个案例中已经写了对应的mapper以及mapper.xml,可以去上个案例查看。
3.配置SqlSessionFactoryBean和MapperScannerConfigurer
<!-- 数据源-->
<bean name="DataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/mybatis_learn"></property>
<property name="username" value="root"></property>
<property name="password" value="root"></property>
</bean>
<!-- SqlSessionFactory bean-->
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="DataSource"></property>
</bean>
<!-- MapperScannerConfigurer 扫描指定包,自动生成Mapper对象存入Spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.itheima.mapper"></property>
</bean>
SqlSessionFactoryBean需要数据源作为参数才可以创建。
4.编写测试代码
我们编写一个简单的UserService接口,它有一个简单的fun()方法,之后编写一个实现类UserServiceImpl实现这个接口。
UserServiceImpl中设置一个简单的Mapper属性,fun方法中使用Mapper中的方法实现数据库的查询功能。
public interface UserService {
public void fun();
}
public class UserServiceImpl implements UserService {
private UserMapper userMapper;
public void setUserMapper(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public void fun() {
List<User> all = userMapper.findAll();
// lambda表达式实现遍历
all.forEach(
(user) -> {
System.out.println(user.toString());
}
);
}
}
之后在Spring的配置文件中添加对于这个Bean的配置信息:
<bean name="userService" class="com.itheima.service.impl.UserServiceImpl">
<property name="userMapper" ref="userMapper"></property>
</bean>
我们在从头到尾的过程中都没有配置UserMapper相关的Bean,但是在对UserService进行属性创建的时候却可以将userMapper作为bean传入,这是因为MapperScannerConfigurer会自己遍历对应路径下的mapper类并且生成bean存入Spring容器之中。
编写测试方法,调用userService的bean:
public class MybatisSpringTest {
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) applicationContext.getBean("userService");
userService.fun();
}
}
测试结果:
3.8.3 Spring整合MyBatis的原理解析
整合包里提供了SqlSessionFactoryBean和一个扫描Mapper的配置对象。SqlSessionFactoryBean一旦被实例化,就开始扫描Mapper并且通过动态代理产生Mapper的实现类存储到Spring容器之中。其中主要相关的包括一下四个类:
SqlSessionFactoryBean:需要配置,用于提供SqlSessionFactory;
MapperScannerConfigurer:需要进行配置,用于扫描指定的mapper注册BeanDefiniftion;
MapperFactoryBean:Mapper的FactoryBean,获得指定Mapper时调用getObject方法(因为mybatis中mapper均是接口形式,想要生成对应的对象就要使用FactoryBean)
ClassPathMapperScanner:会将扫描到的Mapper Bean的注入状态是自动注入状态definition.setAutowireMode(2)
,所以MapperFactoryBean中的SqlSessionFactory会自动注入进去。
SqlSessionFactoryBean
SqlSessionFactoryBean在初始化阶段调用afterPropertiesSet()方法,创建SqlSessionFactory并且设置在属性中,创建的方法和原始的MyBatis代码相同,都是先创建SqlSessionFactoryBuilder之后使用SqlSessionFactoryBuilder创建SqlSessionFactory。
MapperScannerConfigurer的原理解析
Mapper扫描配置器。
我们在看Spring整合MyBatis时难免会有一个疑问:Mapper接口都是接口,不可以直接生成对象Bean,Spring容器是怎么生成的MapperBean呢?
答: 当Spring容器扫描到了Mapper文件,并且根据Mapper文件创建了BeanDefinition之后,会统一将Mapper的BeanDefinition文件中的BeanClass设置为MapperFactoryBean,并且将注入模式设置设置为自动注入。MapperFactoryBean是FactoryBean的子类,专门负责生成Mapper Bean。MapperFactoryBean将sqlSessionFactory作为属性,这个类是MyBatis原代码生成Mapper对象的工厂,之后Spring容器会自动将创建好的sqlSessionFactory Bean作为属性注入其中。之后在Spring容器的初始化阶段时,根据BeanDefinition创建实例时,会根据BeanDefinition将对应Mapper接口的Class传入作为构建参数,创建出这个Mapper专用的MapperFactoryBean,之后调用MapperFactoryBean的getObject方法,生成对应Mapper的代理bean。
MapperSessionFactoryBean的getObject方法:
MapperBean的创建过程:
3.8.4 Spring框架处理自定义命名空间原理解析
Spring框架处理XML中的自定义命名空间的步骤如下:
1. 获取XML中的自定义命名空间标签(element)
2. 根据元素自定义命名空间的URI获取自定义命名空间的解析器,使用获取到的解析器解析自定义的命名空间标签
1.获取XML中的自定义命名空间标签(element)
在Spring扫描完XML配置文件时,会将配置文件中的标签都获取到,并且依次解析(parse)这些标签。
这个获取XML中标签并且分析这些标签的过程处理代码如下:(此类为DefaultBeanDefinitionDocumentReader.java
)
/*类名: DefaultBeanDefinitionDocumentReader */
...
...
...
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for(int i = 0; i < nl.getLength(); ++i) {
Node node = nl.item(i);//获取xml中的节点信息
if (node instanceof Element) {
//如果节点类型为element则获取并且进行下一步处理
Element ele = (Element)node;
if (delegate.isDefaultNamespace(ele)) {
/*如果element为默认的命名空间(import、alias、bean、beans),
就使用默认命名空间解析的方式*/
this.parseDefaultElement(ele, delegate);
} else {
/*如果element不是默认的命名空间,
那么就启用自定义命名空间的处理方式*/
delegate.parseCustomElement(ele);
}
}
}
} else {
delegate.parseCustomElement(root);
}
}
...
...
...
2.根据元素自定义命名空间的URI获取自定义命名空间的解析器,使用获取到的解析器解析自定义的命名空间标签
Spring根据自定义命名空间的URI获取到此标签对应的解析器并且完成解析(parseDefaultElement()
)。
其代码实现如下(DefaultNamespaceHandlerResolver.java
):
...
@Nullable
public BeanDefinition parseCustomElement(Element ele) {
//调用自身类对应的parseCustomElement()方法
return this.parseCustomElement(ele, (BeanDefinition)null);
}
@Nullable
public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) {
//获取标签对应的命名空间
String namespaceUri = this.getNamespaceURI(ele);
if (namespaceUri == null) {
return null;
} else {
//获取命名空间对应的处理器(NamespaceHandler)
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
this.error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
} else {
//使用命名空间处理器来解析标签
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
}
}
...
其中,一般而言自定义命名空间的jar包中会在META-INF/spring.handlers
目录文件中存放命名空间URI对应的类(Spring会自动加载):
每个命名空间处理器(NamespaceHandler)都起码会有俩个方法:init() 和 parse()。
init()方法:来注册命名空间中每个标签的处理器
parse()方法:用来解析标签
3.8.5 案例:自制自定义命名空间
我们根据上述原理分析知道自定义一个命名空间需要以下几个条件:
1.在META-INF
目录下的spirng.handlers
以及spring.schemas
文件中有命名空间的键值对
2.拥有对应的xsd约束文件
3.有对应的解析器handler
4.各个对应标签均有对应的解析器
那么我们就不难根据以上条件来自己创建一个自定标签。此标签做一个简单的功能:将一个自定的BeanPostProcessor注册入Spring容器。
1.创建META-INF
目录,在其中创建spring.handlers
以及spring.schemas
文件,在其中填写好自定义命名空间对应的处理器以及对应的约束文件。
spring.handlers
文件:
spring.schemas
文件:
约束文件BugMakerAnno.xsd
对应的内容:
<?xml version="1.0" encoding="UTF-8" ?>
<xsd:schema xmlns="http://www.itheima.com/BugMaker"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.itheima.com/BugMakerAnno">
<xsd:element name="annotation-driven"></xsd:element>
</xsd:schema>
targetNamespace
属性必须为之前的自定义命名空间。
2.创建自定义命名空间对应的处理器类BugMakerAnnoHandler
BugMakerAnnoHandler.java:
package com.itheima.handlers;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
public class BugMakerAnnoHandler extends NamespaceHandlerSupport {
@Override
public void init() {
// 注册解析器
// “annotation-driven”为自定义命名空间中的标签名称,后面的时标签对应的处理类
this.registerBeanDefinitionParser("annotation-driven",new BugMakerAnnoParser());
}
}
我们可以看到,这个类结构十分简单,仅仅是在init()
方法中注册了标签对应的解析器。这是因为此类继承的父类NamespaceHandlerSupport
类,这个类的parse()
方法会根据传入的标签名称获取到已经在这个类中注册过的解析器,并且调用解析器的parse()
方法进行解析。
3.编写解析类
BugMakerAnnoParser
类:
public class BugMakerAnnoParser implements BeanDefinitionParser {
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
//在Spring容器中注册一个容器
// 1.创建BeanDefinition
BeanDefinition annoBeanDefinition = new RootBeanDefinition();
annoBeanDefinition.setBeanClassName("com.itheima.postProcessor.AnnoBeanPostProcessor");
// 2.将BeanDefiniton注册入Spring容器之中
parserContext.getRegistry().registerBeanDefinition("BugMakerAnnoBD",annoBeanDefinition);
return annoBeanDefinition;
}
}
BugMakerAnnoParser的代码逻辑即为创建一个BeanDefition,并且将创建的BeanDefition文件注册入Spring容器之中。
AnnoBeanPostProcessor
(BeanPostProcessor类):
public class AnnoBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("自定义标签注入的BeanPostProcessor已经运行");
return bean;
}
}
AnnoBeanPostProcessor
类的逻辑也很简单,只是在每个类实例化之前输入一个简单的日志。
4.在XML配置文件添加自定义命名空间以及对应的标签
5.运行项目