正如上一章节所述,Spring Framework提供了如下的功能:
自动创建、保存组件对象(即组件对象实例化)
自动进行组件对象的生命周期管理
自动进行组件的组装(DI依赖注入)
自动进行事务管理(TX)
与Spring全家桶的其他框架进行整合交互
而Spring要求的组件称为 Spring Bean
,其是在Java Bean要求的基础上进行规定的。
在Spring文档中, Spring Bean
被如下定义:
In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and otherwise managed by a Spring IoC container.
即:构成应用程序主干并由Spring IoC容器管理的对象称为bean。
要求 | 任何符合要求的类都可以称之为Java Bean。 | Spring Bean是在Spring IoC容器中被实例化、管理和维护的对象。 一个Bean可以是任何普通的Java对象,例如 POJO、Service、Respository、Controller等。 |
通常来说Spring项目由如下三层组成:
通常来说:
XxController
。XxService
。XxMapper
或者 XxDao
。每一层都由组件构成,并且这些组件必须被放入Spring容器中才能使用一些由Spring提供的特性。而各层之间存在的有依赖关系,例如控制层在接收到请求后,会逐层调用业务逻辑层、持久化层后才能响应该请求。
而Spring可以管理并负责这些组件之间的依赖并完成装配。但是依赖信息需要由程序员按照如下三种方式之一来配置:
在Spring中, org.springframework.beans.factory
包中定义了Spring IoC容器接口 BeanFactory
。在这个接口的定义下,Spring还提供了:
ClassPathXmlApplicationContext |
通过读取类路径下(src下)的xml格式的配置文件创建IoC容器对象,即: 1. 配置方式为xml 2. xml文件在类路径下 时使用此接口。 |
FileSystemXmlApplicationContext |
[不常用]通过文件系统路径下(其他路径)读取xml格式的配置文件创建IoC容器对象,即: 1. 配置方式为xml 2.xml文件在系统中的其他路径 时使用此接口。 |
AnnotationConfigApplicationContext |
通过读取Java配置类创建IoC容器对象,即: 1. 配置文件使用的是Java类 时使用此接口。 |
WebApplicationContext |
专门为Web应用准备,基于Web环境创建IoC容器对象, 并将对象引入存入ServletContext域中,即: 1. 当前项目为Web项目 时使用此接口。 |
等常用接口,这些接口都是 BeanFactory
的拓展,提供了更多的特性和功能(即上述表格中"简介"的功能)。
本章节只做简单介绍,具体实现可见章节Spring IoC容器创建和使用。
基本概念:
而在使用IoC容器管理组件时,需要执行如下的步骤:
仿照Spring框架中的三层组件分层,可以先假设一个如下的组件依赖情况:
则上图的依赖关系为上一级依赖下一级,即:
UserController
依赖 UserService
UserService
依赖 UserMapper
UserMapper
需要指定若干参数而上述的依赖关系可以通过DI依赖注入完成,依赖注入有如下几种方法:
setter
方法传参在后续子章节中,均假设:
UserMapper
注入到 UserService
时使用的是构造函数传参。UserService
注入到 UserController
时:
setter
接口。并且给定Java代码如下:
UserMapper.java
:
package indi.h13.mappers;
public class UserMapper {
public UserMapper(String dbUserName, String dbPassword) {
System.out.println("Created a UserMapper using a constructor with dbUserName " + dbUserName + ", dbPassword " + dbPassword);
}
}
UserService.java
:
package indi.h13.services;
import indi.h13.mappers.UserMapper;
public class UserService {
private UserMapper mapper;
public UserService(UserMapper mapper){
this.mapper = mapper;
}
}
UserController.java
:
package indi.h13.controllers;
import indi.h13.services.UserService;
public class UserController {
private UserService service;
public UserController() {}
}
在工程中引入Spring相关的组件后,非社区板的IDEA就可以直接在 resource
目录下创建IoC组件的配置模板:
若没有该选项,则可以直接使用如下的基本模板:
<?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">
</beans>
xml文件放置于resources文件夹后,经过编译后均会出现在 target/classes
文件夹下。
xml文件名可以随意命名,因为还需要调用对应的接口指定xml路径才能让Spring完成IoC组件实例化。具体见章节Spring IoC容器创建和使用。
随后在 beans
块中完成各bean的实例化配置即可。
实例化配置有如下几种方法:
对应的实例化demo如下:
<?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">
<!--
1. 使用无参构造函数实例化组件
等效伪代码为:
UserMapper userMapper1 = UserMapper();
id为组件标识,要求唯一
class为组价的全限定符
-->
<bean id="userMapper1" class="indi.h13.mappers.UserMapper"/>
<!--
一个类可以被实例化为多个不同id的组件
-->
<bean id="userMapper2" class="indi.h13.mappers.UserMapper"/>
<!--
2. 使用静态工厂类(静态方法)实例化组件
等效伪代码为:
UserService userService1 = UserService.createUserService();
id为组件标识,要求唯一
class为组价的全限定符
factory-method为静态实例化方法
-->
<bean id="userService1" class="indi.h13.services.UserService" factory-method="createUserService"/>
<!--
3. 基于实例工厂方法(非静态方法)实例化
需要先创建一个能生成该类的对象,随后用该对象生成目标对象。 等效伪代码为:
ServiceLocator serviceLocator = new ServiceLocator(); UserService userService2 = serviceLocator.createUserService();
id为Bean标识,要求唯一
factory-bean为生成该方法的Bean的id
factory-method为实例化该类的方法
-->
<bean id="serviceLocator" class="indi.h13.ServiceLocator" />
<bean id="userService2" factory-bean="serviceLocator" factory-method="createUserService" />
</beans>
使用注解完成IoC容器中组件的实例化步骤有:
IoC为组件提供的注解有:
@Component |
该注解用于描述Spring中的组件。 Spring中的任意组件均可使用该注解,包括但不限于Spring三层架构中的任意一层。 在非三层架构的组件开发中常用。 该注解用于标注于类上。 |
@Repository |
功能与 @Component 没有任何区别。但是在程序可读性上用于向程序员表示该组件属于数据访问层。 |
@Service |
功能与 @Component 没有任何区别。但是在程序可读性上用于向程序员表示该组件属于业务逻辑层。 |
@Controller |
功能与 @Component 没有任何区别。但是在程序可读性上用于向程序员表示该组件属于控制层。 |
因此,上述三个实例Java类用注解实例化的示例为:
UserController.java
:
package indi.h13.controllers;
import indi.h13.services.UserService;
@Controller
public class UserController {
private UserService service;
public UserController() {}
}
注:
@Controller
注解等效于xml配置 <bean id="userController" class="indi.h13.controllers.UserController />"
userController
,修改方式可见下例。UserMapper.java
:
package indi.h13.mappers;
@Repository(value="userMapper01")
public class UserMapper {
public UserMapper(String dbUserName, String dbPassword) {
System.out.println("Created a UserMapper using a constructor with dbUserName " + dbUserName + ", dbPassword " + dbPassword);
}
}
注:
userMapper01
@Repository("userMapper01")
UserService.java
:
package indi.h13.services;
import indi.h13.mappers.UserMapper;
@Service
public class UserService {
private UserMapper mapper;
public UserService(UserMapper mapper){
this.mapper = mapper;
}
}
随后执行第二步,配置注解生效包。配置注解生效包需要在xml文件中进行。
和上一小节中使用xml完成实例化相同,通常在 resources
文件夹下创建一个Spring Config模板文件,并向其中添加若干可选配置语句即可。
可选的配置语句有:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 以下Demo为独立Demo,根据需求选用一个即可 -->
<!-- Demo 1: 扫描包indi.h13下所有关于组件的注解 -->
<context:component-scan base-package="indi.h13"/>
<!-- Demo 2: 扫描包indi.h13下除了Service以外的注解 -->
<context:component-scan base-package="indi.h13">
<!-- 屏蔽Service注解 -->
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Service"/>
</context:component-scan>
<!-- Demo 3: 扫描包indi.h13下所有Controller注解(只要Controller) -->
<!-- 注意多了个 use-default-filters 属性!!! -->
<context:component-scan base-package="indi.h13" use-default-filters="false">
<!-- 只扫描(保留)Controller注解 -->
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>
在章节使用IoC完成容器中组件的实例化中给出了实例化的若干方法。在这些方法的基础之上完成了如下的两种DI依赖注入方式及其若干实现方法:
<?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="userMapper" class="indi.h13.mappers.UserMapper">
<!--
构造函数注入,方式一:直接顺序填写参数
value为直接引用值。不管实际使用的是什么类型,在xml中均要配置为字符串。
ref为引用其他的spring bean。
-->
<constructor-arg value="root"/> <!-- dbUserName = "root" -->
<constructor-arg value="pswd"/> <!-- dbPassword = "pswd" -->
</bean>
<bean id="userService" class="indi.h13.services.UserService">
<!--
构造函数注入,方式二[推荐]:指定参数名称并给定值
value为直接引用值。不管实际使用的是什么类型,在xml中均要配置为字符串。
ref为引用其他的spring bean。
-->
<constructor-arg name="mapper" ref="userMapper"/>
</bean>
<bean id="userMapper2" class="indi.h13.mappers.UserMapper">
<!--
构造函数注入,方式三:指定参数角标并赋值
-->
<constructor-arg index="0" value="root"/> <!-- dbUserName = "root" -->
<constructor-arg index="1" value="pswd"/> <!-- dbPassword = "pswd" -->
</bean>
<bean id="userController" class="indi.h13.controllers.UserController">
<!--
setter接口注入[重要]:使用setter接口注入。此时使用property标签
name为setter实际操作的私有变量的名称
-->
<property name="service" ref="userService"/>
</bean>
</beans>
需要注意的是,上述例子中的最后一种使用setter接口的方法需要实现setter方法:
UserController.java
:
package indi.h13.controllers;
import indi.h13.services.UserService;
import lombok.Setter;
@Setter
public class UserController {
private UserService service;
public UserController() {}
}
与xml配置中一样,使用注解的依赖注入也可以分为引用类型装配和值类型装配。
值类型的装配使用注解:
@Value
@Autowired
注解@Qualifier
注解@Resource
注解@Autowired
注解的特性有:
在父章节中的例子中,UserMapper.java
需要指定数据库用户名和密码,在使用注解完成依赖注入时可以按照如下的方式处理:
方式一,直接给定值(不常用,和直接代码里面写死没太大差异):
package indi.h13.mappers;
@Repository(value="userMapper01")
public class UserMapper {
@Value("root")
private String dbUserName;
@Value("pswd")
private String dbPassword;
public UserMapper() {
System.out.println("Created a UserMapper using a constructor with dbUserName " + this.dbUserName + ", dbPassword " + this.dbPassword);
}
}
方式二,引用配置文件(推荐):
配置文件可以存放于 resources
文件夹下,在SpringConfig的xml配置文件中添加 context:property-placeholder
块即可完成配置,示例如下:
<?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">
<context:property-placeholder location="classpath:jdbc.properties"/>
</beans>
上述示例中, jdbc.properties
为配置文件名,且通常使用此名称。
随后可以在上述文件中写入如下配置,即可使用 Value
注解读取并注入依赖:
jdbc.username=root
jdbc.password=pswd
随后使用如下的注解即可完成注入:
package indi.h13.mappers;
@Repository(value="userMapper01")
public class UserMapper {
@Value("${jdbc.username:admin}")
private String dbUserName;
@Value("${jdbc.password}")
private String dbPassword;
public UserMapper() {
System.out.println("Created a UserMapper using a constructor with dbUserName " + this.dbUserName + ", dbPassword " + this.dbPassword);
}
}
注:
@Value("${jdbc.username:admin}")
表示若配置文件中不含 jdbc.username
项,则填入默认值 admin
。@Autowired
注解的特性有:
使用Demo为:
UserService.java
:
package indi.h13.services;
import indi.h13.mappers.UserMapper;
@Service
public class UserService {
private UserMapper mapper;
@Autowired
public UserService(UserMapper userMapper){
this.mapper = mapper;
}
}
注:
UserMapper
类型寻找组件并装配UserController.java
:
package indi.h13.controllers;
import indi.h13.services.UserService;
@Controller
public class UserController {
@Autowired(required=false)
private UserService service;
public UserController() {}
}
注:
required=false
,找不到满足要求的组件时可以跳过。@Qualifier
注解的特性有:
假设组件 UserMapper
的Bean ID为 userMapper1
和 userMapper2
,则可以通过使用 @Qualifier
指定ID的方式来进行指定装配。
UserService.java
:
package indi.h13.services;
import indi.h13.mappers.UserMapper;
@Service
public class UserService {
private UserMapper mapper;
@Qualifier(value="userMapper1")
public UserService(UserMapper userMapper){
this.mapper = mapper;
}
}
注:
@Autowired
在多例模式下,也可以通过搜索属性/参数名的方式搜索对应的BeanID,即下方代码也可以实现上述效果:@Service
public class UserService {
private UserMapper mapper;
// 下方注解会先搜索UserMapper类的组件
// 若找到多例,则根据userMapper1这个参数名来搜索BeanID
@Autowired
public UserService(UserMapper userMapper1){
this.mapper = mapper;
}
}
```
@Resource
注解是由Java定义的一种规范,由Spring实现的注解。该注解的功能是先使用Autowire进行尝试装配,若失败后再尝试使用Qualifier装配的一种注解。
功能逻辑如下:
引入第三方Bean组件也是开发中常用的一个需求或实现方式,而第三方的Bean组件在装配的时候也需要指定Bean ID,并且往往也需要DI依赖注入。
这里以引入druid连接池 com.alibaba.druid
为例,完成以下内容:
com.alibaba.druid.pool.DruidDataSource
实例化为Bean ID为 dataSource
的组件。url
:存放于 jdbc.properties
中的 jdbc.url
中。driverClassName
:存放于 jdbc.properties
中的 jdbc.driver
中。username
:存放于 jdbc.properties
中的 jdbc.username
中。password
:存放于 jdbc.properties
中的 jdbc.password
中。在SpringConfig中完成如下内容:
<?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">
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="{jdbc.url}" />
<property name="driverClassName" value="{jdbc.driver}" />
<property name="username" value="{jdbc.username}" />
<property name="password" value="{jdbc.password}" />
</bean>
</beans>
即可实现引入第三方组件。
在上述使用注解进行组件实例化和依赖注入时,都需要对xml进行配置,指定组件扫描和设置外部配置文件。而配置类就是用于替代xml操作的一种方法。
通常的操作是创建一个 config
包,编写 JavaConfiguration
类(类名包名随意,在IoC容器创建时引用即可),具体操作为:
JavaConfiguration
类添加 @Configuration
注解、@ComponentScan
)@PropertySource
)Demo如下:
// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration {
}
此时的配置类与如下的xml等效:
<?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">
<context:component-scan base-package="indi.h13.package1"/>
<context:component-scan base-package="indi.h13.package2"/>
<context:property-placeholder location="classpath:jdbc.properties"/>
</beans>
本章节中注解配置类的基本假设同引入第三方Bean组件。
为满足上述要求,则应在上一子章节的注解配置类中添加如下代码和 @Bean
注解:
// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration {
@Value("{jdbc.url}")
private String url;
@Value("{jdbc.driver}")
private String driver;
@Value("{jdbc.username}")
private String username;
@Value("{jdbc.password}")
private String password;
@Bean
public DruidDataSource dataSource() {
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl(this.url);
dataSource.setDriverClassName(this.driver);
dataSource.setUsername(this.username);
dataSource.setPassword(this.password);
return dataSource;
}
}
在上述示例中:
@Bean是Spring中的一种用于直接生成Bean组件的注解,其最标准的使用方式是定义于@Configuration所注解的配置类中。其拥有如下的配置参数:
name
& value
:指定Bean ID。缺省时生成的组件ID与方法名相同。initMethod
& destroyMethod
:指定初始化和销毁方法。默认时调用Bean类型的初始化和销毁方法。autowireCandidate
:是否允许该组件被其他组件引用或修改。@Import注解的作用就是将一个配置类导入到另一个配置类中,其效果等效于将多个配置类合并为一个配置类。
Demo:
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration1 {
}
@ComponentScan(value = { "indi.h13.package3", "indi.h13.package4", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration2 {
}
随后可以使用@Import注解将 JavaConfiguration2
合并进 JavaConfiguration1
:
@Import(JavaConfiguration2.class)
@ComponentScan(value = { "indi.h13.package1", "indi.h13.package2", ...})
@PropertySource(value = "classpath:jdbc.properties")
@Configuration
public class JavaConfiguration1 {
}
然后在创建IoC容器时指定 JavaConfiguration1
即可创建容器。创建IoC容器的具体操作见后续章节。
Spring的组件作用域是指在IoC容器中被创建、存活以及被访问的规则。
Spring组件作用域主要有如下几种:
getBean()
时(见:获取容器中的组件)上述章节介绍了在IoC中注册组件和管理组件的方式,本章节将具体讲解IoC容器的创建和使用。
如章节3.2所述,Spring IoC容器提供了如下的实现类:
在本章主要给出如子章节所述的几种实例化容器的方法。
下方提供了两种读取xml配置文件实例化IoC容器的方法:
// 1. [重要]直接指定类路径下的xml文件名实例化
ApplicationContext context = new ClassPathXmlApplicationContext("spring-01.xml");
// 2. [了解]先创建容器,随后指定配置文件并刷新
// 在Spring框架中使用的是本方法
ApplicationContext context = new ClassPathXmlApplicationContext();
context.setConfigLocations("spring-01.xml");
context.refresh();
下方也提供了两种使用配置类实例化IoC容器的方法,重点是第一种。
// 1. [重要]使用配置类实例化容器
ApplicationContext context = new AnnotationConfigApplicationContext(JavaConfiguration.class);
// 2. [了解]先创建容器,随后指定配置类并刷新
ApplicationContext context = new AnnotationConfigApplicationContext();
context.register(JavaConfiguration.class);
context.refresh();
// 方式1[不推荐]:使用 `beanId` 获取,返回值类型为Object,需要使用强制类型转换操作对象。
Object userMapperBean = context.getBean("userMapper");
UserMapper userMapper = (UserMapper)userMapperBean;
// 方式2[推荐]:使用 `beanId` 获取,同时指定类型
// 此时不需要强转
userMapper = context.getBean("userMapper", UserMapper.class);
// 方式3:只使用类型获取,此时注意:
// 1. 此方法只支持单例,即同一个类型在容器中只有一个Bean。否则运行时出错(NoUniqueBeanDefinitionException)
// 2. 当使用父类或接口作为类型去获取时,也可以正常获取(只要 bean instanceof A.class == true即可,且整个项目中该父类只有一个实例)
userMapper = context.getBean(UserMapper.class);
在ioc容器中,"组件周期方法"的"周期"特指"声明周期",而非"定时或重复发生的周期"。
而Spring Bean的生命周期主要有如下几个阶段:
BeanNameAware
接口,Spring 容器将调用 setBeanName
方法传递Bean ID。BeanFactoryAware
或 ApplicationContextAware
接口,Spring容器将调用 setBeanFactory
或 setApplicationContext
方法传递当前的应用上下文。BeanPostProcessor
的实现)的 postProcessBeforeInitialization
方法被调用。InitializingBean
接口,调用 afterPropertiesSet
方法。另外,如果Bean的定义包含 init-method
,该方法也会被调用。BeanPostProcessor
的实现)的 postProcessAfterInitialization
方法被调用。DisposableBean
接口,调用 destroy
方法。如果 Bean 的定义包含 destroy-method
,该方法也会被调用。IoC容器为上述生命周期的几乎所有阶段中提供了配置组件周期方法的回调,也有许多种实现或注册的方式,具体见各子章节。
使用SpringConfig可以配置如下两个生命周期方法的回调:
配置初始化方法的Demo:
package indi.h13.controllers;
import indi.h13.services.UserService;
public class UserController {
public UserController() {}
public void init() {
System.out.println("UserController inited.");
}
}
init-method
属性,并指向该初始化方法。<bean id="userController" class="indi.h13.controllers.UserController" init-method="init" />
此外,添加 destroy-method
属性也可以注册销毁方法。
在上述生命周期的若干声明周期中,分别可以通过如下方法完成方法回调的配置:
BeanNameAware
接口setBeanName
方法接收Bean IDBeanFactoryAware
或 ApplicationContextAware
接口setBeanFactory
或 setApplicationContext
方法接收当前的应用上下文BeanPostProcessor
接口postProcessBeforeInitialization
方法接收回调InitializingBean
接口afterPropertiesSet
接收回调BeanPostProcessor
接口postProcessAfterInitialization
方法接收回调DisposableBean
接口destroy
接收回调Demo如下:
package indi.h13.controllers;
import indi.h13.services.UserService;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
public class UserController implements InitializingBean, DisposableBean {
private UserService service;
public UserController() {}
@Override
public void afterPropertiesSet() throws Exception {
System.out.println("UserController inited.");
}
@Override
public void destroy() throws Exception {
System.out.println("UserController destoried.");
}
}
考虑一个场景,现在工程需要一个负责对接数据库读写的组件,该组件的实例化需要如下步骤:
new SqlSessionFactoryBuilder
)stream = getResourceAsStream("xx")
)SqlSessionFactoryBuilder.builder(stream)
)SqlSessionFactory.openSession()
)上述的例子 SqlSession
相对复杂,因此其套了两层构造来实现。假设现在有一个新的目标对象 WebSession
,则可以考虑实现一个 WebSessionFactory
的构造器工具来构造这个 WebSession
对象。
注:
FactoryBean
和 BeanFactory
。面向切面编程(Aspect-Oriented Programming,简称 AOP),正如其名所述,其主要功能是通过代理劫持某个函数调用或者方法调用,并在这个方法调用之前或者之后去增加一个切面,并在这个切面中加入自己的业务逻辑。
几个常见的业务场景:
如上所述,面向切面编程主要是通过代理劫持某个函数调用,并为其增加和劫持若干切面,完成切面处业务逻辑的一种程序设计模式。
这种代理通常有两种实现方式:
静态代理就是指直接通过实现代理类的方式完成添加切面与增加切面功能的方式。这种方式的代理类在编译前就已经被明确定义,且通常需要程序员手动完成。因此在这里静态代理仅会给出一个静态代理的demo并思考其所存在的问题。
// 定义服务接口
public interface IService {
void serve();
}
// 实现服务接口的具体类
public class Service implements IService {
@Override
public void serve() {
System.out.println("Serving...");
}
}
// 代理类,也实现IService接口
public class ServiceProxy implements IService {
private IService service; // 内部持有一个IService的引用,通常是目标对象
public ServiceProxy(IService service) {
this.service = service;
}
@Override
public void serve() {
System.out.println("Before serving"); // 添加额外的逻辑
service.serve(); // 调用原服务方法
System.out.println("After serving"); // 添加额外的逻辑
}
}
public class Controller {
@Autowired
// ServiceProxy和Service本质并不是同一个类,因此需要使用接口类型接值
private IService serviceProxy;
@GetMapping("/controller")
public void controller(HttpServletRequest request, HttpServletResponse response) {
serviceProxy.serve();
}
}
在上述静态代理的Demo中,其本质就是定义一个代理类,并在代理类中的同名方法劫持并封装为新的同名代理方法,并在后续程序中使用代理类完成面向切面编程。
但是上述代码存在的问题是代理类和代理方法需要手动生成,并且较为麻烦。而动态代理解决了这一问题。
动态代理就是不需要程序员手动实现、代理类在编译前未被定义、在编译中自动生成代理类的编程方式。
常用的动态代理实现方式有两种:
通常来说,有接口的使用JDK,无接口的使用cglib。但是往往不需要额外注意这些,因为Spring AOP会自动处理这些。实际使用时直接使用Spring AOP即可。
OOP是指面向对象编程,AOP是指面向切面编程。AOP本质基于OOP,且是对OOP编程思路的一种补充。OOP是针对父类与子类的纵向编程思路,AOP是若干基于同一父类(父接口)的子类之间的纵向封装关系。
横切关注点:
AOP面相切面编程所需要关注的,非核心代码的关注点。例如token校验、日志输出等。
通知和通知方法:
每一个横切关注点上所插入的方法就叫做通知方法
通知是指调用通知方法的这个动作
通知主要有如下几种类型:
@Before
):@AfterReturning
):@AfterThrowing
):@After
):@Around
):try...catch...finally
逻辑了,把接口丢给你让你自己执行。具体可见环绕通知。连接点:
被代理拦截到的点,是一个逻辑概念
切入点:
被选中切入的连接点;切点一定是连接点。
切面:
切面=切入点+通知
目标:
指被代理的目标
代理:
即生成的代理对象
织入:
切点被配置的动作
如上一章节所述,通知主要有以下五种类型:
@Before
):@AfterReturning
):@AfterThrowing
):@After
):@Around
):try...catch...finally
逻辑了,把接口丢给你让你自己执行。具体可见环绕通知。其调用逻辑如下方伪代码所示:
try {
@Before
method();
@AfterReturning
} catch {
@AfterThrowing
} finally {
@After
}
使用注解方式进行面相切面编程的步骤主要如下:
步骤1,定义通知方法:
按照需求定义方法即可,通常会将类名命名为 ${功能}Advance
,并放置于 advance
包下,例如增加日志功能:
public class LogAdvance {
public void before() {
System.out.println("@Before ...");
}
public void after() {
System.out.println("@After ...");
}
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
步骤2,使用注解配置和选中这些目标方法:
public class LogAdvance {
@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void before() {
System.out.println("@Before ...");
}
@After("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
注:
"execution(* indi.h13.ssserver.service.impl.*.*(..))"
中的:
*
表示忽略方法返回值类型indi.h13.ssserver.service.impl
为包名*
表示匹配该包下的所有类*
表示匹配所有方法(..)
表示忽略方法参数步骤3,补全注解,以加入容器和配置切面:
主要需要注意以下内容:
@Aspect
表示该类是个切面@Component
将其放置于IoC容器中@Component
@Aspect
public class LogAdvance {
@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void before() {
System.out.println("@Before ...");
}
@After("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
advance
包。步骤4,开启Aspect注解支持:
<aop:aspectj-autoproxy />
即可:<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 开启Aspectj注解支持 -->
<aop:aspectj-autoproxy />
</beans>
@EnableAspectJAutoProxy
注解即可:// 配置包扫描注解,`value = ` 可以省略
@ComponentScan(value = {...})
// 配置配置文件名注解,`value = ` 可以省略
@PropertySource(value = ...)
@Configuration
@EnableAspectJAutoProxy
public class JavaConfiguration {
}
在完成上述章节的通知方法定义后,会直接面对如下需求:
获取目标方法信息是在任何一个增强方法中都可以执行的。
具体步骤是:
JoinPoint
类的对象。JoinPoint
对象即可获取目标方法信息。@Component
@Aspect
public class LogAdvance {
@Before("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void before(JoinPoint joinPoint) {
// 1. 获取所属类的简称
System.out.println("Class name: " + joinPoint.getTarget().getClass().getSimpleName());
// 2. 获取所属类的全称
System.out.println("Class name: " + joinPoint.getTarget().getClass().getName());
// 3. 获取方法名
System.out.println("Function name: " + joinPoint.getSignature().getName());
// 4. 获取参数
Object args[] = joinPoint.getArgs();
// 5. 获取访问修饰符
int modifiers = joinPoint.getSignature().getModifiers();
System.out.println("Function modifiers: " + Modifier.toString(modifiers));
}
}
获取方法返回值仅能在@AfterReturning增强中才可以获取。
具体步骤如下:
Object result
。returning
为 result
:@AfterReturning(value="...", returning=result)
@Component
@Aspect
public class LogAdvance {
@AfterReturning(
value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
returning = result
)
public void afterReturning(Object result) {
// ...
}
}
获取方法异常对象仅能在@AfterThrowing增强中才可以获取。
具体步骤如下:
Throwable throwable
。throwing
为 throwable
:@AfterThrowing(value = "...", throwing = throwable)
@Component
@Aspect
public class LogAdvance {
@AfterThrowing(
value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
throwing = throwable
)
public void afterThrowing(Throwable throwable) {
System.out.println("@AfterThrowing ...");
}
}
切点表达式的基本结构为:
注:
${权限+返回类型} ${方法所在全类名} ${方法名} (${参数})
,其中:
${权限+返回类型}
段不能只指定返回类型而通配修饰符,也不能指定修饰符通配返回类型,要通配就全通配,要指定就全指定,不能出现 * int
或 prvate *
。${方法所在全类名}
段有以下特性:
indi.h13.ssserver.*.services
indi..services
,但是 ..
不能开头,如果需要模糊匹配开头,可以将二者结合,写为 *..services
。(${参数})
段有如下特殊写法:
(..)
表示任意参数,有没有都行()
表示无参(String..)
表示以String开头,后面有没有无所谓(..int)
表示以int结尾,开头有没有无所谓(String, int)
等表示具体参数通常来说,在一个工程中会有多个通知方法被插入到同一个切点表达式所限定的一类切点中(例如上述章节的Demo中多次使用表达式 "execution(* indi.h13.ssserver.service.impl.*.*(..))"
)。为了避免重复编写,统一管理,就有了切点表达式的提取和复用。
复用方式有如下几种:
Demo如下:
@Component
@Aspect
public class LogAdvance {
@Pointcut("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void pc() { }
// 同一类内可以直接指定方法名
@Before("pc()")
public void before() {
System.out.println("@Before ...");
}
@After("pc()")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("pc()")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
通常考虑创建一个单独维护切点的类,统一维护切点。通常放置于 pointcuts
包中。
package indi.h13.pointcuts;
@Component
public class LogPointCut {
@Pointcut("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public void serviceImplPc() { }
}
注:
随后在增强类中使用即可:
@Component
@Aspect
public class LogAdvance {
// 在类外时使用方法名的全限定符
@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void before() {
System.out.println("@Before ...");
}
@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
前面四个通知类型是基于 try { } catct { } finally { }
结构进行的,而环绕通知是不自带这些基本结构,把句柄丢给环绕通知方法,让开发者在环绕通知方法内自行调用随意切片的一种设计。
public class LogAroundAdvance {
@Around("execution(* indi.h13.ssserver.service.impl.*.*(..))")
public Object transaction(ProceedingJoinPoint joinPoint) {
Object result;
// Do anything...
try {
// Do anything...
// Even without calling the objective function...
result = joinPoint.proceed(joinPoint.getArgs());
// Do anything...
} catch (Throwable t) {
// Do anything...
throw t;
} finally {
// Do anything...
}
// Do anything...
return result;
}
}
无论对同一个组件方法添加了多少个增强代码,不同切面之间的优先级是固定的,即:
try catch
一致的包裹结构:但是同一切面内的不同增强方法之间可以使用 @Order(value)
设置优先级,value值越低的越靠外执行,即:
@Before
)比value值高的前置切面先执行@Before
)比value值高的后置切面后执行@Component
@Aspect
@Order(10)
public class Advance1 {
// 在类外时使用方法名的全限定符
@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void before() {
System.out.println("@Before ...");
}
@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
@Component
@Aspect
@Order(7)
public class Advance2 {
// 在类外时使用方法名的全限定符
@Before("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void before() {
System.out.println("@Before ...");
}
@After("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void after() {
System.out.println("@After ...");
}
@AfterThrowing("indi.h13.pointcuts.LogPointCut.serviceImplPc()")
public void afterThrowing() {
System.out.println("@AfterThrowing ...");
}
}
注:
@Order
被省略时默认为int类型的最大值,即优先级最低Advavce2.before()
Advavce1.before()
method()
Advavce1.after()
Advavce2.after()
基本不用,了解即可。
步骤:
public class LogAdvance {
public void before() {
System.out.println("@Before ...");
}
public void after() {
System.out.println("@After ...");
}
@AfterThrowing(
value = "execution(* indi.h13.ssserver.service.impl.*.*(..))",
throwing = throwable
)
public void afterThrowing(Throwable throwable) {
System.out.println("@AfterThrowing ...");
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 1. 注册组件 -->
<bean id="logAdvance" class="indi.h13.advances.LogAdvance"/>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 1. 注册组件 -->
<bean id="logAdvance" class="indi.h13.advances.LogAdvance"/>
<!-- 2. 声明切点标签、引用增强对象 -->
<!-- 等效于@Aspect注解 -->
<aop:config>
<!-- 2.1 声明切点标签 -->
<!-- 等效于@Pointcut注解及其切点类 -->
<aop:pointcut id="pc" expression="execution(* indi.h13.ssserver.service.impl.*.*(..))" />
<!-- 2.2 引用增强对象 -->
<!-- 等效于@Order注解 -->
<aop:aspect ref="logAdvance" order="7">
<!-- 等效于@Before注解 -->
<aop:before method="before" pointcut-ref="pc"/>
<!-- 也可以选择不引用切点 -->
<!-- 等效于@AfterThrowing(value="",throwing="throwable")注解 -->
<aop:after-throwing method="afterThrowing" pointcut="execution(* indi.h13.ssserver.service.impl.*.*(..))" throwing="throwable"/>
</aop:aspect>
</aop:config>
</beans>
考虑如下的一个业务逻辑:
// 用户注册操作
public void regist(String userName, String email, String phoneNumber) {
try {
User user = userMapper.createNewUser();
user.updateUserName(userName);
user.updateEmail(email);
user.updatePhoneNumber(phoneNumber);
} catch (Exception e) {
if(e instanceof InvalidUserNameException) {
userMapper.deregistration(userName);
} else if(e instanceof IncorrectEmailException) {
user.clearEmail();
} else if(e instanceof IncorrectPhoneNumberException) {
user.clearPhoneNumber();
}
}
}
可以发现这种连续的,且有先后要求的请求出现rollback时的逻辑较为复杂(尽管注册用户的时候一般不需要这么做),但是总体来说许多业务都可能用得到如下逻辑:
0. 响应超时回滚,以及撤销所有操作
此外,再考虑一个场景:
regist
正在依次执行上述四个操作的同时,还有其他的组件也在操作目标用户的数据。那如何解决并行化问题?而Spring为了解决上述若干问题,给出了事务管理的一个框架(事务管理器)。
这个框架在项目中往往仅在数据库操作时使用,也就是项目中只需要一个事务管理器实例(不过也有多例的情况)。
事务管理器的使用步骤:
先以单例的数据库操作事务为例,可以将上述的代码进行如下的实现:
@Configuration
@ComponenScan("indi.h13")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
// 实例化DruidDataSource,略
@Bean
public DataSource dataSource(...) { }
/**
* 实例化JdbcTemplate对象,并在UserMapper中用于数据库操作
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
/**
* 装配事务管理实现对象
*/
@Bean
public TransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
public classs UserService {
// 用户注册操作
@Transactional
public void regist(String userName, String email, String phoneNumber) {
}
}
public classs UserService {
// 用户注册操作
@Transactional
public void regist(String userName, String email, String phoneNumber) {
User user = userMapper.createNewUser();
user.updateUserName(userName);
user.updateEmail(email);
user.updatePhoneNumber(phoneNumber);
}
}
此时就已经实现了rollback操作,当其中某一步发生异常时,数据库就会自动回滚到全部请求开始之前的状况。
注意点:
@Transactional
可以标注到类前,也可以标注到方法前。当标注到类前时,其下所有方法均为事务方法。通常会将事务注解加到类前。@Transactional
会自动指定项目中唯一的 TransactionManager
。@Transactional(transactionManager="transactionManager")
。在注解内指定Bean ID即可,即:
public classs UserService {
// 用户注册操作
@Transactional(transactionManager="transactionManager")
public void regist(String userName, String email, String phoneNumber) {
User user = userMapper.createNewUser();
user.updateUserName(userName);
user.updateEmail(email);
user.updatePhoneNumber(phoneNumber);
}
}
一般来说进行只读事务操作时,也不太需要事务管理器进行rollback。但是使用事务管理器可以保证该事务中只有只读事务。其特性有:
readOnly = true
即可:public classs UserService {
// 用户注册操作
@Transactional(readOnly = true)
public void getUserInfo(int targetUid) {
UserVo userVo = new UserVo();
// BeanUtils.copyProperties(userService.getById(uid), userVo);
userVo.userName = userMapper.getUserName();
userVo.avatarId = userMapper.getAvatarId();
...
}
}
由于通常会将事务注解加到类前,因此需要再在方法前加一个只读限定的注解以优化性能。
在注解中增加 timeout=t
即可,单位为秒,类型为整数。默认值为-1,即永不超时。
当超时时抛出 TransactionTimeOutException
异常。
public classs UserService {
// 用户注册操作
@Transactional(readOnly = true, timeout = 1)
public void getUserInfo(int targetUid) {
UserVo userVo = new UserVo();
// BeanUtils.copyProperties(userService.getById(uid), userVo);
userVo.userName = userMapper.getUserName();
userVo.avatarId = userMapper.getAvatarId();
...
}
}
需要注意的是,由于timeout自带默认值-1,因此若在类上标注了timeout,而在方法上因为其他方式又重新标注了一次Transactional注解,则此时的timeout会以方法上的注解为准。
即此种情况下:
如上述章节所述,事务内部发生异常时事务管理器会自动进行回滚。但是默认时并不是所有异常都会出发回滚!默认时触发RuntimeException时才会进行回滚。
而注解中可以使用 rollbackFor=<? extends Throwable>[]
来指定需要回滚的异常,例如 rollbackFor=Throwable.class
。
此外,还可以用 noRollbackFor
来取消特定异常的回滚。noRollbackFor
在 rollbackFor
后判定。
考虑如下几个并发问题:
而对于上述三个并发问题,并不是在所有场景中都需要完全避免上述三个问题,有些问题是可以接受的。对应的四个隔离级别分别为:
Spring中的默认隔离级别为可重复读,但是推荐修改为读提交,这样性能会好一些。
具体的设置方法是在注解中设置 isolation
属性,可选项分别是:
Isolation.DEFAULT
:可重复读Isolation.READ_UNCOMMITTED
Isolation.READ_COMMITTED
Isolation.REPEATABLE_READ
Isolation.SERIALIZABLE
现在一个事务内调用了两个方法,一个是业务方法,另外一个也是业务方法。
考虑如下的情况:
@Transactional
public void doResponse() {
transactionA();
transactionB();
}
并且假设事务 transactionB
执行时一定会失败,并思考 transactionA
的执行结果是否会被回退、以及回退与否是否可以选择。
而事务传播行为就是用于设定 transactionA
是否会被 transactionB
影响的一个特性,该选项通过注解中的 propagation
属性进行设置,其通常使用如下两个选项:
Propagation.REQUIRED
:默认值,表示:
Propagation.REQUIRES_NEW
:表示:接下来考虑如下的调用过程:
此时,若
通常来说,子事务一般使用 Propagation.REQUIRED
设置事务传播行为,而父事务可以选择使用 try catch
来捕获子事务的异常,达到可控回滚。
总体来说,当前子事务期望前置事务回滚与否可以通过 propagation
进行设置,而子事务是否期望被后置事务回滚也是通过 propagation
设置。
而父事务可以通过 try catch
来强行控制回滚与否。
通常来说该考点会在面试时考察,实际使用中多数为默认回滚。