Spring框架
零、Spring 简介
Spring 是分层的 Java SE/EE 应用轻量级全栈开源框架,以 IoC(反转控制)和 AOP(面向切面编程)为内核。
提供了展现层 Spring MVC和持久层 Spring JDBCTemplate 以及业务层事务管理等众多的企业级应用技术,还能整合开源世界众多著名的第三方框架和类库,逐渐成为使用最多的Java EE 企业应用开源框架。
Spring 的优势
-
方便解耦,简化开发;
通过 Spring 框架提供的Bean IoC容器,可以将对象间的依赖关系交由Spring控制,从而降低耦合度。
-
AOP思想的支持;
-
声明式事务的支持;
传统的编程式事务处理繁琐、事务管理代码冗杂,spring通过声明式配置多种事务的管理,提高效率
-
方便程序的测试
-
方便集成其他优秀框架
Spring 结构 (ver 4.x
)
-
Core Container
: 核心容器- 控制反转 (Inversion of Control)
- 依赖注入 (Dependency Injection)
-
Aspect Oriental Program (AOP)
: 面向切面编程思想AspectsJ
: AOP思想的实现 -
Data Access / Data Integration
: 数据访问与数据集成(与数据库的操作有关) -
Web
: Web 开发相关 -
Test
: 测试相关
一、Spring 核心容器
1. 控制反转 IoC 与依赖注入 DI
我们先从一个简单的例子讲起:
1 | package DAO; |
1 | package Service; |
- 耦合度偏高:当我们修改
BookDaoImpl
这个类的内容时,相应的,BookServiceImpl
类需要重新编译。
此时我们就可以想到解决方法:为了使业务与数据解耦,即使用对象时,在程序中不要主动使用 new
产生对象,而是由外部提供对象。将对象的创建由程序转移到外部,这种思想称为 IoC 控制反转。
- Spring 提供了一个容器,称为 IoC 容器(核心容器),用来充当 IoC 思想中的“外部”。
- IoC 容器负责对象的创建、初始化等一系列工作,被创建或被管理的对象在 IoC 容器中统称为
Bean
graph LR subgraph "IoC容器" Sercive --依赖--> Dao end
在 IoC 容器中建立 Bean
与 Bean
之间的依赖关系的过程,被称为依赖注入 (DI)
注意 ⚠️
我们已经分别解释了控制反转和依赖注入的概念。有些人会把控制反转和依赖注入等同,但实际上它们有着本质上的不同。
- **控制反转 (IoC)**是一种思想
- **依赖注入 (DI)**是一种设计模式
IoC 入门案例
整体思路:
- 添加 Spring 的坐标(
pom.xml
)- IoC 容器管理什么?(
Bean
,即对象)- 如何将对象“放入” IoC 容器中?(通过配置文件)
- 如何得到 IoC 容器?(接口)
- 如何得到 IoC 容器中的对象?(接口方法)
-
导入 Spring 的坐标 (
pom.xml
)1
2
3
4
5<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.5.RELEASE</version>
</dependency> -
编写
Dao
接口和实现类1
2
3
4package Interface;
public interface UserDao {
public void save();
}1
2
3
4
5
6
7
8package DAO;
import Interface.UserDao;
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save running...");
}
} -
创建 Spring 配置文件(
ApplicationContext.xml
) -
在 Spring 配置文件中配置
UserDaolmpl
类1
2
3
4
5
6
7
<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="userDao" class="DAO.UserDaoImpl"></bean>
</beans> -
实现 Spring 提供的接口得到 IoC 容器,并从容器获得
Bean
实例1
2
3
4
5
6
7
8
9
10
11public class UserDaoTest {
public static void main(String[] args) {
// 获取 Ioc 容器
ApplicationContext context =
new ClassPathXmlApplicationContext("ApplicationContext.xml");
// 获取 Bean 对象
UserDao userDao = (UserDao) context.getBean("userDao");
userDao.save();
}
}
DI 入门案例
通过刚才的例子,我们已经成功地将对象的管理放在了 IoC 容器中。那么该如何才能不使用 new
关键字来进行操作对象间的依赖关系呢?
整体思路:
- 基于 IoC 管理对象
Bean
- 不使用
new
关键字来创建对象- 如何将需要依赖的对象传入当前对象呢?(提供
setXxx(Object obj)
方法)- 这种依赖关系该如何描述?(通过配置)
-
将
BookServiceImpl
类放入 IoC 容器1
<bean id="bookServiceImpl" class="Service.BookServiceImpl"></bean>
-
删去
BookServiceImpl
类中使用new
关键字创建对象的语句 -
并提供
setBookDao(BookDao bookDao)
方法传入一个BookDao
对象1
2
3
4
5
6
7
8
9
10
11
12package Service;
public class BookServiceImpl implements BookService {
// 2.
private BookDao bookDao;
// 3.
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save(){
bookDao.save();
}
} -
通过配置描述依赖关系
1
2
3
4
5<bean id="bookServiceImpl" class="Service.BookServiceImpl">
<!-- 配置依赖关系 -->
<!-- name="属性字段的标识符" ref="Bean 对象的 id" -->
<property name="bookDao" ref="bookDao"/>
</bean>
BeanFactory
的简单实现
IoC 的主要实现是得益于一个可以动态生成对象的工厂 BeanFactory
,它通过读取配置文件中的对象的配置信息,通过反射 (reflexion) 机制返回一个实体对象。
-
首先定义一个外部的配置文件
1
UserDao=fr.spring.gdai.impl.UserDaoImpl
-
Java程序读取这个配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public class BeanFactory {
private static Properties properties;
private static Map<String,Object> cache = new HashMap<>();
static {
properties = new Properties();
properties.load(BeanFactory.class.getClassLoader().
getResourceAsStream("factory.properties"))
}
// 多例 prototype
public static Object getUserDaoPrototype(String property) {
String value = properties.getProperty(property);
Class clazz = Class.forName(value);
Object userDao = clazz.getConstructor(null).newInstance(null);
return userDao;
}
// 单例 singleton,spring默认
public static Object getUserDaoSingleton(String property) {
// 先判断缓存中是否存在该对象
if(!cache.containsKey(property)){
// 在多线程下确保一致性,加锁
synchronized (BeanFactory.class){
// 双重检测缓存中是否存在该对象
if(!cache.containsKey(property)){
// 反射机制创建对象
String value = properties.getProperty(property);
Class clazz = Class.forName(value);
Object object = clazz.getConstructor(null).newInstance(null);
// 将对象存入缓存
cache.put(property,object);
}
}
}
return cache.get(property);
}
} -
测试
Object getUserDao(String property)
方法1
2
3
4
5
6
7
8
9
10
11
public void userDaoPrototypeTest() {
UserDao userDao1 = (UserDao) BeanFactory.getUserDaoPrototype("UserDao");
UserDao userDao2 = (UserDao) BeanFactory.getUserDaoPrototype("UserDao");
System.out.println(userDao1);
System.out.println(userDao2);
}
/*
fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
fr.spring.gdai.impl.UserDaoImpl2@1d3a1b2c
*/1
2
3
4
5
6
7
8
9
10
11
public void userDaoSingletonTest() {
UserDao userDao1 = (UserDao) BeanFactory.getUserDaoSingleton("UserDao");
UserDao userDao2 = (UserDao) BeanFactory.getUserDaoSingleton("UserDao");
System.out.println(userDao1);
System.out.println(userDao2);
}
/*
fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
fr.spring.gdai.impl.UserDaoImpl2@3f8d7a8c
*/
2. Bean
对象
<Bean>
标签的基本配置
1 | <bean id="唯一性标识符" class="全类名(包名+类名)" scope="singleton 或 prototype"/> |
作用范围 scope
singleton
:单例,默认值prototype
:多例request
:WEB 项目中,Spring 创建一个Bean
对象,将其放入request
域中session
:WEB 项目中,Spring 创建一个Bean
对象,将其放入session
域中global session
:WEB 项目中,Spring创建一个Bean对象,将其放入global session域中
singleton
与 prototype
的区别
实例化个数 | 实例化时机 | 生命周期 | |
---|---|---|---|
singleton 默认 |
1 | 加载 AppliContext.xml 时 实例化所有配置的 Bean 实例 |
- 创建:当应用加载,创建容器时,对象就被创建了 - 运行:只要容器在,对象一直活着 - 销毀:当应用卸载,销毁容器时,对象就被销毁了 |
prototype |
多个 | 当调用 getBean() 方法时 |
- 创建:当使用对象时,创建新的对象实例 - 运行:只要对象在使用中,就一直活着 - 销毁:当对象长时间不用时,被 Java 的垃圾回收器回收 |
Bean
生命周期配置
创建对象(内存分配) 执行构造方法 执行属性注入(setXxx()
) 执行 Bean
的初始化方法 init-method
使用 Bean
执行 Bean
的销毁方法 destroy-method
。
1 | public class UserDaoImpl implements UserDao { |
1 | <bean id="userDao" class="impl.UserDaoImpl" |
init-method
:指定类中的初始化方法destroy-method
:指定类中的销毁方法
关闭销毁 IoC 容器
那么,如何关闭销毁 IoC 容器呢?
-
调用
ClassPathXmlApplicationContext
对象的close()
方法可以暴力地关闭销毁 IoC 容器;1
((ClassPathXmlApplicationContext) context).close();
-
调用
ClassPathXmlApplicationContext
对象的registerShutdownHook()
方法可以在 JVM 结束运行前关闭销毁 IoC 容器1
((ClassPathXmlApplicationContext) context).registerShutdownHook();
Bean
实例化的三种方式
-
无参默认构造实例化
Bean
对象1
public UserDaoImpl() {}
1
<bean id="userDaoByConstructor" class="impl.UserDaoImpl"/>
-
静态工厂实例化
Bean
对象1
2
3
4
5public class StaticFactory {
public static UserDao getUserDaoInstance(){
return new UserDaoImpl();
}
}1
2
3
4
5<!-- 使用静态工厂返回Bean -->
<bean id="userDaoStaticFactory"
class="factory.StaticFactory"
factory-method="getUserDaoInstance">
</bean> -
动态工厂实例化
Bean
对象1
2
3
4
5
6
7
8
9public class DynamicFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
/**
* 因为getUserDao()不是静态方法,所以调用的时候需要实例化一个工厂对象
* 相应的,bean的配置也是如此
**/1
2
3
4
5
6
7
8
9
10<!-- 使用动态工厂返回Bean -->
<!-- 1. 先创建一个动态工厂实例 -->
<bean id="DynamicFactory"
class="factory.DynamicFactory">
</bean>
<!-- 2. 再通过动态工厂实例获得一个userDao对象 -->
<bean id="userDaoDynamicFactory"
factory-bean="DynamicFactory"
factory-method="getUserDao">
</bean> -
使用
FactoryBean
实例化Bean
对象1
2
3
4
5
6
7
8
9
10
11
12public class UserDaoFactoryBean implements FactoryBean<UserDao> {
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
public Class<?> getObjectType() {
return UserDao.class;
}
}1
<bean id="userDaoByBeanFactory" class="factory.UserDaoFactoryBean"/>
Bean
的依赖注入
因为 UserService
和 UserDao
都在 Spring容器中,而最终程序直接使用的是 UserService
,所以可以在 Spring 容器中,将 UserDao
设置到 UserService
内部 (依赖注入)
graph LR subgraph "IoC容器" Sercive --依赖--> Dao end
在编写程序时,通过控制反转,把对象的创建交给了 Spring,但是代码中不可能出现没有依赖的情况。
IoC 解耦只是降低他们的依赖关系,但不会消除。例如:业务层仍会调用持久层的方法
那这种业务层和持久层的依赖关系,在使用 Spring 之后,就让 Spring 来维护了。简单的说,就是让框架把持久层对象传入业务层,而不用我们自己去获取。
怎么将 UserDao
怎样注入到 UserService
内部呢?
依赖传入方式
思考:
依赖注入描述了在容器中建立
Bean
对象之间依赖关系的过程。那如果需要建立的依赖关系是String
和基本数据类型(如int, char
)呢?
-
setXxx()
方法 - 引用对象- 在代码中提供可访问的
setter
方法 - 在配置中使用
<u><property></u>
标签,ref
属性注入对象(可注入多个:多个<property>
即可)
- 在代码中提供可访问的
1 | <bean id="userDao1" class="impl.UserDaoImpl"></bean> |
1 | public class UserServiceImpl implements UserService { |
-
setXxx()
方法 - 引用基本数据类型- 在代码中提供可访问的
setter
方法 - 在配置中使用
<u><property></u>
标签,value
属性注入参数(可注入多个:多个<property>
即可)
1
2
3
4<bean id="userDao1" class="impl.UserDaoImpl">
<property name="username" value="gdai"></property>
<property name="age" value="24"></property>
</bean>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class UserDaoImpl implements UserDao {
private String username;
private int age;
public void setUsername(String username) { this.username = username; }
public void setAge(int age) { this.age = age; }
public UserDaoImpl() {
System.out.println("UserDaoImpl 创建成功");
}
public void save() {
System.out.println("[UserDao] : "+username+", "+age+"save running...");
} - 在代码中提供可访问的
-
有参构造方法 - 引用对象
- 构造器中存在参数
- 在配置中使用
<constructor-arg>
标签,name
是形参名,ref
属性注入对象(可注入多个:多个<property>
即可)
1
2
3
4
5<bean id="userDao1" class="impl.UserDaoImpl"></bean>-->
<bean id="userServiceByConstructor" class="service.impl.UserServiceImpl">
<!-- 注入依赖关系 name=形参名 -->
<constructor-arg name="userDao" ref="userDao1"></constructor-arg>
</bean>1
2
3
4
5
6
7
8
9
10
11public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
userDao.save();
}
} -
有参构造方法 - 引用基本数据类型
- 构造器中存在参数
- 在配置中使用
<constructor-arg>
标签,name
是形参名,value
属性注入参数(可注入多个:多个<property>
即可)
1 | <bean id="userDao1" class="impl.UserDaoImpl"> |
1 | public class UserDaoImpl implements UserDao { |
自动装配 autowire
自动装配引用对象主要有 3 种方式:按类型,按名称,按构造方法。关键字是 autowire
其中按类型 和 按名称 的自动装配基于 setter
方法:
autowire=byType
:根据setter
方法中this
参数的类型,自动装配(同类型的bean
对象要唯一);autowire=byName
:根据setter
方法中this
参数的名称,自动装配;
集合注入
对于集合对象(Arrays, List, Set, Map, Properties
等),他们的注入会相对繁琐一些。如下所述
1 | <bean id="bookDao" class="impl.BookDaoImpl"> |
1 | public class BookDaoImpl implements BookDao { |
第三方数据源 Bean
管理
我们这里使用 c3p0
连接池做示范:
-
首先我们先导入
c3p0
的maven
坐标;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- JDBC -->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency> -
然后我们想使用
bean
对象来帮我们维护一个dataSource
对象用来连接数据库。因为c3p0
提供了setter
方法,所以我们可以使用setter
注入;1
2
3
4
5
6<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"/>
<property name="user" value="root"/>
<property name="password" value="root"/>
</bean>
其实,在
c3p0
中可以使用外置配置文件.properties
来配置连接池信息。那么该如何使用bean
来维护呢?
加载 .properties
文件
c3p0.properties
1 | jdbc.driver=com.mysql.jdbc.Driver |
-
开启
context
命名空间1
2
3
4
5
6
7
8
9<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
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
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
"> -
使用
context
空间加载demo.properties
文件,可使用*.properties
加载所有配置文件1
<context:property-placeholder location="classpath:c3p0.properties"/>
-
使用
${}
占位符读取加载的属性值1
2
3
4
5
6<bean name="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
引入其他配置文件(分模块开发)
实际开发中,Spring的配置内容非常多,这就导致Spring配置很繁杂且体积很大,所以,可以将部分配置拆解到其他配置文件中,而在Spring主配置文件通过impor标签进行加载
1 | <import resource="applicationContext-xxx.xml"/> |
Spring的重点配置
<bean>
标签
id
属性:在容器中Bean实例的唯一标识,不允许重复class
属性:要实例化的Bean的全类名scope
属性:Bean的作用范国,常用是singleton
(默认)和prototype
<constructor-arg>
标签:构造器注入<property>
标签:setter
注入name
属性:厲性名称value
属性:注入的普通属性值ref
属性:注入的对象引用值<list>
标签<map>
标签<properties>
标签
<import>
标签:导入其他的 spring 的分文件
二、Spring 注解开发
Spring是轻代码而重配置的框架,配置比较繁重,影响开发效率,所以注解开发是一种趋势,注解代替 .xml
配置文件可以简化配置,提高开发效率。
注解 | 说明 |
---|---|
@Component("name") |
使用在类上用于实例化 Bean |
@Controller |
使用在 web 层类上用于实例化 Bean |
@Service |
使用在 service 层类上用于实例化 Bean |
@Repository |
使用在 dao 层类上用于实例化 Bean |
@Autowired |
使用在字段上用于根据【类型】依赖注入 |
@Qualifier |
结合@Autowired 一起使用用于根据【名称】进行依赖注入 |
@Resource |
相当于@Autowired + @Qualifier ,按照【名称】进行注入 |
@Value |
注入普通属性 |
@Scope |
标注Bean的作用范围 |
@PostConstruct |
使用在方法上标注该方法是Bean的初始化方法 |
@PreDestroy |
使用在方法上标注该方法是Bean的销毁方法 |
1. Bean
对象的管理
接下来,我们将一步一步的将基于配置文件的 bean
改为基于注解的 bean
。
1 | <bean id="userDao" class="dao.UserDaoImpl"/> |
1 | package dao; |
将其改为
1 | package dao; |
为了让 spring 容器知道这个叫 userDao
的组件(Component
),我们需要在配置文件中设置 <context:component-scan>
来“扫描”代码里的 @Component
。
1 |
|
我们能否使用纯注解开发,来代替上述的 .xml
中设置呢?可以。
- 首先我们使用一个带有
@Configuration
注解的类来代替.xml
中的骨架部分; - 然后在用
@ComponentScan("dao")
来代替“配置组件扫描”的语句; - 最后在测试中通过
new AnnotationConfigApplicationContext(SpringConfig.class)
,使ApplicationContext context
容器实例化
1 |
|
1 |
|
我们可以在任意方法上添加 @PostConsturt
和 @PreDestory
来指定初始化操作和销毁操作
1 | package dao; |
2. 依赖注入
自动装配
注入引用对象
在之前我们学习过,基于 .xml
配置文件是通过 [<bean ... autowire="byXxx"/>
](#自动装配 autowire
) 配置自动装配的。这里我们介绍注解方式的自动装配 @Autowired
。
-
我们在
SpringConfig
配置类里添加一个ComponentScan
的路径,可以用数组来储存。1
2
3
public class SpringConfig {} -
编写一个新的
UserServiceImpl
类,其依赖于UserDao
这个类,可以不提供setter
方法,spring 会通过暴力反射 强行设置。我们就可以使用@Autowired
配置自动装配。- 如果有多个相同类型的
bean
,我们使用@Qualifier("name")
来指定引用类型的名称
- 如果有多个相同类型的
1 |
|
注入基本数据类型
如果我们想将一个基本数据类型注入 bean
对象中,可以使用 @Value(val)
可以实现简单类型的注入。
1 |
|
1 |
|
如果我们想通过加载外部的配置文件,动态配置注入的基本数据类型。我们同样可以使用 ${}
占位符来读取加载的属性值。
配置文件 test.properties
1 | bookDao.name=gdai |
-
我们在
SpringConfig
配置类里添加一个@PropertySource("filename")
注解来加载配置文件,多文件时可以使用数组。1
2
3
4
public class SpringConfig {} -
bean
对象中使用${}
占位符来读取加载的属性值1
2
3
4
5
6
7
8
public class BookDaoImpl implements BookDao {
private String name;
public String toString() { return "[BookDao]: name="+name; }
}
3. 第三方 Bean
对象
和之前一样,我们依旧使用 c3p0
连接池做示范:
-
首先我们先导入
c3p0
的maven
坐标;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 -->
<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>
<!-- JDBC -->
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency> -
思路相同,我们需要使用
bean
对象来帮我们维护一个dataSource
对象用来连接数据库。但是这里需要我们自己配置这个bean
对象,并将这个对象返回,同时@Bean
注解定义。1
2
3
4
5
6
7
8
9
10
11public class DataSourceUtil {
public DataSource dataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("com.mysql.jdbc.Driver");
dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/moviesdata?serverTimezone=Asia/Shanghai");
dataSource.setUser("root");
dataSource.setPassword("root");
return dataSource; // 返回的这个对象作为 bean
}
} -
而在
SpringConfig
配置类中,我们这需要将其使用@Import
注解导入。1
2
3
4
5
public class SpringConfig {
} -
当然,这里也可以添加一个
@PropertySource("filename")
注解来加载配置文件。然后使用${}
占位符来使第三方bean
对象的参数从配置文件中加载获得。c3p0.properties
1
2
3
4jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/moviesdata?serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=root1
2
3
4
5
6
7
8
9
10
11
12
public class DataSourceUtil {
public DataSource getDataSource() throws PropertyVetoException {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setDriverClass("${jdbc.driver}");
dataSource.setJdbcUrl("${jdbc.url}");
dataSource.setUser("${jdbc.username}");
dataSource.setPassword("${jdbc.password}");
return dataSource; // 返回的这个对象作为 bean
}
}
第三方 Bean
依赖注入
在上面的例子中,第 4 步其实就是对于第三方 Bean
的基本类型注入。在此,我们可以给出更加一般(规范)的形式:
1 |
|
对于引用类型,可以直接将引用的对象通过【形参】传入 @Bean
这个对象中,spring 会通过类型匹配传入。
1 |
|
三、Spring 中的 AOP
课程回顾:代理模式
1 AOP
简介
AOP (Aspect Oriented Program)
,即面向切面编程。与 OOP
类似,AOP
也是一种编程思想,在不改动原始代码的基础上为其进行功能增强。使用动态代理机制实现。
1 | public interface Calculate { |
1 | public class CalculateImpl implements Calculate{ |
我们可以看到,"日志打印"和"业务代码"紧密的耦合在一起。那么我们是否能将"日志打印"和"业务代码"分离开呢?
我们试图找到一种方法,如下所示:使得"业务代码"作为一个类,"日志打印"单独作为一个特殊的类:
1
2
3
4
5
6
7
8
9 public class CalculateImpl implements Calculate{
public int add(int num1, int num2) { // 连接点: 所有方法
// 切入点: 需要"追加功能"的方法
// before, 从此处"切开", 插入日志打印
int result = num1 + num2; // 业务代码
// after, 再次从此处"切开", 插入日志打印
return result;
}
1
2
3
4
5
6 public class Aspect { // 通知类: Aspect
before: // 通知
System.out.println("Func_add : " + num1 + " + " + num2); // 日志打印
after: // 通知
System.out.println("Result : " + result); // 日志打印
}
而 AOP
要做的就是将日志代码全部抽象出去统一进行处理,计算器方法中只保留核心的业务代码。做到核心业务和非业务代码的解耦合。
术语解释
-
横切关注点(逻辑概念):
从每个方法中抽取出来的相同的非核心业务。在同一个项目中,我们可以使用多个横切关注点对方法进行增强。
-
通知(
Advice
):每个横切关注点要做的事情都需要写一个方法来实现,这样的方法就叫做通知方法。
-
前置通知:在被代理的目标方法前执行
-
后置通知:在被代理的目标方法***后 (
finally {}
块内)***执行 -
返回通知:在被代理的目标方法返回值之后执行
-
异常通知:在被代理的目标方法异常结束之后执行
-
环绕通知:在被代理的目标方法前、后执行
‼️ 后置通知 和 返回通知的区别 : 前者是
try {}
内部方法调用成功返回后 , 而后置通知是finally {}
里面的代码.
-
-
切面对象:封装通知方法的类。
-
目标对象:需要功能增强的对象。
-
代理对象:向目标对象应用通知后的代理对象,有
AOP
通过动态代理实现 -
连接点(
JointPoint
):抽取横切关注点的***位置(在 SpringAOP 中是一个方法)***。 -
切入点 (
Pointcut
):有些接入点不需要“切入”通知方法,那么这个连结点就不是切入点;反之,接入点需要“切入”通知方法,那么就是切入点。- 在 SpringAOP中,一个切入点可以描述一个具体方法,也可也匹配多个方法
- 。连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。
总结 :
AOP
重点编写切面类 ( 先抽取代码 ),通过切点表达式声明的方式(再套用到目标类上)告诉Spring 框架我要将我注解到的通知应用到那个类的那个连接点上。
2 AOP
入门案例
2.0 技术说明
- AspectJ : 本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态.。weaver 就是织入器,Spring 只是借用了 AspectJ 中的注解。
- JDK 动态代理:要求必须要有接口,最终生成的代理类在
com.sun.Proxy
包下,名为$Proxy2
,其和目标类实现的是相同接口。 - CGLIB 动态代理:最终生成的代理类会继承目标类,并且和目标类在相同的包下。
- 当被代理类没有接口的时候使用 CGLIB,有接口则使用 JDK
2.1 准备工作
- 导入
maven
坐标
1 | <dependency> |
- 制作连接点(接口及实现类)
1 | package fr.spring_aop.gdai.Interfaces; |
1 | package fr.spring_aop.gdai.Implements; |
-
配置 Spring 的参数(注解形式)
-
开启容器扫描
ComponenetScan
,以及确定扫描位置 -
开启基于注解的 AOP
EnableAspectJAutoProxy
-
1 |
|
2.2 实现切面类
@Aspect
注解:声明该类为切面类
@Pointcut 切入点表达式
@Pointcut()
注解:声明切入点表达式。当一个切入点表达式被多次使用时,我们可以定义一个公共的 Pointcut
,使得其他通知方法也可以使用这个切入点表达式而不用重复定义。
1 |
其中:
(1)可以使用通配符 *
来代替访问修饰符和返回值类型
(2)可以使用通配符 *
来代替当前包下的任意类
(3)可以使用通配符 *
来代替在某一类中的任意方法
(4)可以使用通配符 ..
来代替某一方法中的任意参数列表(可变参数)
1 |
|
JoinPoint 对象
JoinPoint
对象:可以在该通知方法中将 JoinPoint
对象作为参数传入方法,以获取(目标对象中的)连接点的信息。
其中:
(1)Signature joinPoint.getSignature()
:获取连接点对应方法的方法名
(2)Object[] joinPoint.getArgs()
:获取连接点对应方法的参数
@Before 前置通知
@Before()
注解:声明一个切入点的前置通知方法,其 value
值为切入点表达式,具体参数如下:
1 |
例:
1 |
|
1 |
|
注意:
如果我们这里直接使用
getBean(CalculateImpl.class)
时,Spring 会抛出No qualifying bean of type 'xxx.CalculateImpl' available
的异常,说明在有代理类的情况下,不能直接访问实现类。符合代理的初衷。
1 | Func_div : [1, 2] |
@After 后置通知
@After()
注解:声明一个切入点的后置通知方法,其在目标对象方法的 finally{}
子句中执行。
1 |
|
1 |
|
1 | Func_div : [2, 1] |
@AfterReturning 返回通知
@AfterReturning()
注解:声明一个切入点的返回通知方法,在被代理的目标方法返回值之后执行。
returning = "value"
属性:可以将目标方法的返回值以value
的形式作为参数传入返回通知方法中
1 |
|
1 |
|
1 | Func_div : [2, 1] |
@AfterThrowing 异常通知
@AfterReturning()
注解:声明一个切入点的异常通知方法,在被代理的目标方法抛出异常之后执行。
throwing = "exception"
属性:可以将目标方法的抛出的异常以exception
的形式作为参数传入返回通知方法中
1 |
|
1 |
|
1 | Func_div : [2, 0] |
@Around 环绕通知
@Around()
注解:声明一个切入点的环绕通知方法。顾名思义,即目标方法的前后执行的通知方法(以上四种通知方法的结合)。
ProceedingJoinPoint
对象:由于是对于一个切入点的环绕通知,那么我们就需要知道这个接入点JoinPoint
是在何时执行。即需要ProceedingJoinPoint
对象的proceed()
方法来表示接入点方法的执行位置。- 环绕通知方法的返回值必须与目标方法的返回值 一致
1 |
|
1 |
|
1 | @Around_前置通知 : Func_multi[2, 1] |
多个切面的优先级
只需要使用注解 @Order
在切面类的注解上即可,数值越小优先级越高。
- 默认值是
Integer.MAX_VALUE
四、声明式事务
1 JDBCTemplate
1.1 简介
Spring 对 JDBC 进行封装,使用 JDBCTemplate
方便实现数据库操作。
1.2 准备工作
(1) 导入 maven
坐标
1 | <!-- mysql 驱动--> |
(2) 编写实体类与创建数据库表
所有实体类我们都习惯性的将它们放在 dao
这个包下。
例:User.java
1 | package fr.gdai.spring.transaction.domain; |
创建数据库:
1 | mysql -h [数据库的主机名] -u [数据库用户名] -P [端口] -p |
1 | create database mybatis_test if not exists default charset utf8; |
(3) 配置 Spring
我们依旧使用纯注解的形式设置 Spring:
SpringConfig.java
1 | package fr.gdai.spring.transaction.configs; |
SpringJDBC.java
1 | package fr.gdai.spring.transaction.configs; |
DataSourceUtil.java
1 | package fr.gdai.spring.transaction.configs; |
jdbc.properties
:用来配置 JDBC 连接数据库的参数
1 | jdbc.driver=com.mysql.jdbc.Driver |
(3) 测试连接(Spring 整合 JUnit)
在这里,我们使用 Spring 整合 JUnit 测试。意味着我们可以注入的方式直接获取 IOC 容器中的 Bean
,而不再通过 ApplicationContext.getBean()
来获取 Bean
对象。
需要的配置如下:
@RunWith(SpringJUnit4ClassRunner.class)
:指定当前测试类在 Spring 整合 JUnit 的测试环境中执行。@ContextConfiguration(classes = {SpringConfig.class})
:然后导入 Spring 核心配置
1 | package fr.gdai.spring.transaction; |
2 声明式事务概念
2.1 编程式事务
在以往对于事务的实现中,事务功能的相关操作全部通过自己编写代码来实现:
1 | Connection con = new ...; |
我们可以看出,编程式事务有不少缺点:
- 细节没有被屏蔽 :所有细节都要程序员自己来实现,比较繁琐。
- 代码复用性不高 :没有有效抽取出来,每次实现功能都需要自己编写代码,代码没有得到复用。
2.2 声明式事务
我们为了解决编程式事务的缺点,提出了声明式事务。
声明式依托框架完成,框架将固定模式的事务代码抽取出来,进行相关的封装。封装完成后,只需要配置或注解来让框架完成事务的管理。我们可以把对事务的管理与 AOP 结合起来。
优点:
- 提高开发效率
- 消除冗余重复的代码
- 框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化。
3 基于注解的声明式事务
3.1 准备工作
(0) 继续开发
我们在第一节 JDBCTemplate
的基础上继续开发。
(1) 添加 domain
实体类与表
User.java
1 | package fr.gdai.spring.transaction.domain; |
修改 t_user
表,添加 balance
字段,并添加一条测试用的数据。
1 | alter table t_use add balance int unsigned default null comment '余额(无符号)'; |
Book.java
1 | package fr.gdai.spring.transaction.domain; |
新增 t_book
表,如下所示:
1 | -- auto-generated definition |
(2) 添加 dao
组件
创建 BookDao.java
1 | package fr.gdai.spring.transaction.dao; |
1 | package fr.gdai.spring.transaction.dao.impl; |
创建 UserDao.java
1 | package fr.gdai.spring.transaction.dao; |
1 | package fr.gdai.spring.transaction.dao.impl; |
(3) 添加 service
组件
创建 BookService.java
1 | package fr.gdai.spring.transaction.service; |
1 | package fr.gdai.spring.transaction.service.impl; |
(4) 添加 controller
组件
创建 BookContraller.java
1 | package fr.gdai.spring.transaction.controller; |
3.2 事务相关的测试
我们使用 Spring 结合 JUnit 的测试方法来测试当一个用户的余额 < 图书单价时,数据库的更新情况。
(1) 无事务功能的测试
1 | package fr.gdai.spring.transaction.service.impl; |
此时我们来检索数据库:
1 | select t_user.id user_id, t_user.balance, t_book.book_id, t_book.price, t_book.stock |
查询结果如下,我们可以看到,图书的库存 stock
数量 -1,但是用户的余额没有变化。
1 | +---------+---------+---------+-------+-------+ |
在当前没有手动设置事务时,默认的每条 SQL
语句都是一个事务,三个 SQL
语句各自独立。所以库存数量减少了,余额却没有变化。
(2) 实现事务的测试
-
配置事务管理器
TransactionManagerUtil
1
2
3
4
5
6
7
8
9
10package fr.gdai.spring.transaction.configs;
public class TransactionManagerUtil {
TransactionManager getTxManager(DataSource dataSource) {
TransactionManager txManager = new DataSourceTransactionManager(dataSource);
return txManager;
}
}在
SpringConfig.java
中添加以下注解,以开启事务的注解驱动。设置成功后可以使用@Transactional
注解声明该方法或类中所有方法使用事务进行管理1
或使用
.xml
注解的方式开启事务1
2
3
4
5
6
7
8<!-- 配置事务管理器 -->
<bean id="transactionManager"
class="org.springframework.transaction.annotation.EnableTransactionManagement">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 开启事务的注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/> -
在
Service
层设置开启事务。1
2
3
4
5
6
7
8
9
10
11package fr.gdai.spring.transaction.service.impl;
// @Transactional 说明类中所有方法都开始事务管理
public class BookServiceImpl implements BookService {
...
public void buyBook(Integer userId, Integer bookId) {...}
} -
再次执行测试
buyBook()
,并检索数据库。此时我们发现事务已经生效。1
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback;
1
2
3
4
5+---------+---------+---------+-------+-------+
| user_id | balance | book_id | price | stock |
+---------+---------+---------+-------+-------+
| 99 | 50 | 1 | 100 | 100 |
+---------+---------+---------+-------+-------+
3.3 设置事务的属性
只读 onlyRead
对一个查询操作来说,我们设置成只读 @readOnly
(默认 false
),就能明确告诉数据库这个操作不涉及写操作。这样数据库针对查询操作就能进行优化。
1 |
如果在非查询操作的事务中使用只读属性,会抛出以下错误
1 | nested exception is java.sql.SQLException: Connection is read-only. |
超时 timeout
事务的执行过程中,当程序阻塞住了,从而长时间占用数据库资源(大概率是 Java程序 或者 MySQL 数据库链接等等)。 这个时候设置超时(默认值为 -1
),可以在规定时间到达后立马回滚,将资源释放出来,并抛出 TransactionTimedOutException
:超时回滚,释放资源,抛出异常
1 |
超时后,抛出的异常如下
1 | org.springframework.transaction.TransactionTimedOutException: Transaction timed out |
回滚策略 rollback
声明式事务默认只针对运行时异常回滚,编译时异常不会滚。
roollbackForClassName = "异常对应的全类名"
:一般不使用,因为默认运行时异常回滚roollbackFor = 异常类.class
:一般不使用noRoollbackForClassName
:当出现发生异常时不回滚noRoollbackFor
:同上
事务隔离级别 isolation
🌟
课程回顾:并发事务的隔离性与隔离级别
总而言之,一个事务与另一个事务的隔离程度称为隔离级别,当隔离级别越高,数据的一致性就越好,但并发性越弱。
使用方式:
1 | // 数据库默认的隔离级别,其他可选项如下 |
READ UNCOMMITTED
:读未提交。允许 读取 未提交的修改。READ COMMITTED
:读已提交。 只能读取 提交的修改。REPEATABLE READ
:可重复读。 可以重复多次从一个字段中读取到相同的值。即在 执行期内禁止其他事务对该【字段】更新。SERIALIZABLE
:串行化。执行结果跟串行执行一样,即在 执行期内禁止其他事务对该【表】更新。
隔离级别 | 关键字 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交 | READ UNCOMMITTED |
❌ | ❌ | ❌ |
读已提交(oracal 默认隔离级别) |
READ COMMITTED |
✅ | ❌ | ❌ |
可重复读(mySQL 默认隔离级别) |
REPEATABLE READ |
✅ | ✅ | ❌ |
串行化 | SERIALIZABLE |
✅ | ✅ | ✅ |
传播行为 propegation
A 方法
调用 B 方法
,且两者都声明了事务,我们从B
的角度来思考:
B
使用自己的事务 : 如果B
回滚了,不会影响A
的回滚。propagation = Propagation.REQUIRED_NEW
B
使用A
的事务(默认):如果B
发生回滚了,则A
也会回滚。propagation = Propagation.REQUIRED
想象一下这样一个场景,如今我们的账户余额
balance = 150
,我们将如下两本书加入了购物车:A.price = 100
、B.price = 69
。在最后结账的时候,假设从购物车付款的逻辑为
1
2
3
4
5
6
public void checkOut(Integer userId, Integer[] bookIds) {
for(Integer bookId : bookIds) {
buyBook(userId, bookId); // 这个方法同样有 @Transactional
}
}因为余额
balance = 50
不足以支付B.price = 69
的价格,所以该次buyBook()
操作回滚。从而导致checkOut()
方法也回滚。