Java- Spring框架
Spring框架
1. Spring介绍
Spring框架是一个开放源代码的J2EE应用程序框架,由Rod Johnson发起,是针对
bean的生命周期进行管理的轻量级容器(lightweight container)。 Spring解决了开发者在J2EE开发中遇到的许多常见的问题,提供了功能强大I0C、AOP及Web MVC等功能。Spring可以单独应用于构筑应用程序,也可以和Struts、Webwork、Tapestry等众多Web框架组合使用,并且可以与 Swing等桌面应用程序AP组合。因此,Spring不仅仅能应用于J2EE应用程序之中,也可以应用于桌面应用程序以及小应用程序之中。Spring框架主要由七部分组成,分别是SpringCore、SpringAOP、SpringORM、Spring DAO、SpringContext、 Spring Web和 SpringWebMVC.
总结:
- bean生命周期管理:提供了java对象的创建、销毁、使用等生命周期管理功能。
- 轻量级: 使用简单,配置容易
- 容器: Spring可以管理对象,创建好对象,放入Spring容器中,容器负责管理这些对象生命周期

自底向上的内部结构:
- Spring的集成测试
- Spring的核心容器
- Spring的AOP技术
- Spring的数据访问技术(和mybatis整合后,不再学习,但是事务管理必须由Spring提供)
- Spring的Web开发技术(也就是SpringMVC)
2.搭建环境
只是为了测试Spring的简单功能,并没有实际的业务逻辑代码。等后续全部整合以后再进行业务逻辑的编写。
2.1 创建普通的Maven项目

2.2 引入依赖,Spring组件有很多,这里只为了演示核心容器,所以只导入核心容器相关的依赖
由上面的结构图可以看到,核心容器主要有core,beans,context,expression四个模块。所以我们只需要导入这四个依赖即可。但是通过图片可以看到,spring-context依赖中还关联了spring-aop和spring-beans等依赖,所以我们不再需要单独导入其他依赖。

<dependencies>
<!-- 只需要导入spring-context依赖即可,其他依赖maven会自动导入-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.33</version>
</dependency>
</dependencies>
2.3 spring配置文件
spring的功能大多数都依赖配置文件来完成。文件名称无限制,通常命名为:spring.xml,spring-context.xml等。最常用的是
applicationContext.xml。
在resources目录下创建applicationContext.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">
<!-- 后续再配置bean -->
</beans>
tip:主要关注头文件,一般创建xml方式会有Spring配置的选项,如果没有可以直接ctrl+c/v的,只需要有个印象就可以,一定不能写错。
2.4 测试使用
这里只是测试Spring的核心容器,所以只需要测试最简单的bean的创建和注入即可。
2.4.1 首先,创建一个简单的测试类
package org.demo;
public class MyClass {
public void test(){
System.out.println("测试spring运行环境....");
}
}
2.4.2 使用spring容器管理bean(即使用spring来创建对象,通过编写applicationContext.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标签,就是spring管理的一个类-->
<!-- id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!-- class 类的全限定名称-->
<bean id="myClass" class="org.demo.MyClass"/>
</beans>
2.4.3 测试类中创建工厂Bean,由工厂加载配置文件,通过配置文件创建容器,从容器中获得对象
package org.demo;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MyClassTest {
@Test
public void testClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
// MyClass myClass = (MyClass) context.getBean("myClass");
//这种方法也可以,不需要强转
MyClass myClass = context.getBean("myClass", MyClass.class);
//调用对象中的测试方法
myClass.test();
}
}
3. IOC(inversion of control,控制反转) 和 DI (Dependency injection,依赖注入)
Spring的核心功能之一就是IOC和DI。IOC是一种设计思想,DI是IOC的一种具体实现。
- IOC(控制反转):
创建对象的控制权从程序员转移到Spring容器。也就是说,由Spring容器来创建对象,而不是程序员自己创建对象。(就像上面,是通过容器获取Bean对象)
简单来说,就是由我们自己去new对象,变为由Spring容器去创建对象。
- DI(依赖注入):
依赖关系由Spring容器注入到对象中。也就是说,Spring容器在创建对象时,会自动将依赖的对象注入到对象中。
接下来,通过简单编写代码来理解IOC和DI。
3.1 IOC
3.1.1 编写dao接口和实现类
因为仅作演示,所以不操作数据库,用输出代表方法的执行。
package org.demo.dao;
public interface BookDao {
void getAllBook();
}
实现类
package org.demo.dao.impl;
import org.demo.dao.BookDao;
public class BookDaoImpl implements BookDao {
@Override
public void getAllBook() {
System.out.println("访问数据访问层");
}
}
3.1.2 编写service接口和实现类
package org.demo.service;
public interface BookService {
void getAllBook();
}
实现类
package org.demo.service.impl;
import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;
public class BookServiceImpl implements BookService {
// 以前我们需要在这里创建dao对象
private BookDao bookDao = new BookDaoImpl();
@Override
public void getAllBook() {
System.out.println("访问业务层");
bookDao.getAllBook();
}
}
3.1.3 编写配置文件,注册业务对象和dao对象
<bean id="bookService" class="org.demo.service.impl.BookServiceImpl"/>
3.1.4 测试类
@Test
public void testServiceClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
BookService service = context.getBean("bookService", BookService.class);
//调用对象中的测试方法
service.getAllBook();
}
输出结果:
访问业务层
访问数据访问层
可以看到,业务层调用了dao层的对象,并且业务层对象并不是我们自己创建的,而是由Spring容器创建的,这就是IOC的作用。
3.1.5 修改业务层实现类和注册dao对象,让dao由spring容器创建
那现在还有个新的问题,我们使用的dao对象,是我们自己手动
new创建的,并不是从容器中拿到的,那我们对业务层实现类就行修改,我想让dao也能由spring容器去创建。
package org.demo.service.impl;
import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;
public class BookServiceImpl implements BookService {
// 以前我们需要在这里创建dao对象
// private BookDao bookDao = new BookDaoImpl();
// 现在我们不需要去手动创建对象,只需要声明属性,spring会自动创建对象并赋值
private BookDao bookDao;
@Override
public void getAllBook() {
System.out.println("访问业务层");
bookDao.getAllBook();
}
}
<?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标签,就是spring管理的一个类-->
<!-- id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!-- class 类的全限定名称-->
<bean id="myClass" class="org.demo.MyClass"/>
<!-- 注册业务层对象-->
<bean id="bookService" class="org.demo.service.impl.BookServiceImpl"/>
<!-- 注册dao层对象-->
<bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>
接下来我们再测试执行一下,看看dao对象有没有由容器管理
测试输出
java.lang.NullPointerException: Cannot invoke "org.demo.dao.BookDao.getAllBook()" because "this.bookDao" is null
3.1.6 注入dao对象
看到报错了,说我们的dao对象是空的,这是为什么,不是说spring会帮助我们创建对象,并进行赋值吗?难道说我们的dao对象并没有注册到容器中吗?
那我们接下来修改一下测试类,测试一下dao对象是否注册到容器中了。
@Test
public void testServiceClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
// //通过容器获得Bean对象(通过配置文件中bean标签的id)
// BookService service = context.getBean("bookService", BookService.class);
// //调用对象中的测试方法
// service.getAllBook();
BookDao bookDao = context.getBean("bookDao", BookDao.class);
bookDao.getAllBook();
}
输出
访问数据访问层
正常输出了呀,那为什么使用业务层再去调用,会出现dao对象为空的情况呢?那我们接下来去官方文档查看一下,看看有没有什么其他的配置。
其实,到这个时候,我们就已经到了下一步,DI(依赖注入)的配置了。我们的对象已经由容器创建了,就差
DI对dao属性自动赋值
要实现依赖注入很简单,只需要给出属性的setter方法,就可以实现赋值,现在再修改一次业务层,为dao属性提供setter方法。
package org.demo.service.impl;
import org.demo.dao.BookDao;
import org.demo.dao.impl.BookDaoImpl;
import org.demo.service.BookService;
public class BookServiceImpl implements BookService {
// 以前我们需要在这里创建dao对象
// private BookDao bookDao = new BookDaoImpl();
// 现在我们不需要去手动创建对象,只需要声明属性,spring会自动创建对象并赋值
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
@Override
public void getAllBook() {
System.out.println("访问业务层");
bookDao.getAllBook();
}
}
设置完成setter方法后,注意,还没有结束。
我们查看官网文档后发现,给出的例子中,业务层对象,在
bean中声明了property标签,并设置了ref属性,来指定(引用)dao对象。
那我们直接照猫画虎,也为我们的业务层对象,添加property标签,并使用ref来引用注入我们的dao对象。
<?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标签,就是spring管理的一个类-->
<!-- id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!-- class 类的全限定名称-->
<bean id="myClass" class="org.demo.MyClass"/>
<!-- 注册业务层对象-->
<bean id="bookService" class="org.demo.service.impl.BookServiceImpl">
<!-- 注入数据层对象-->
<!-- name 是注入的属性名-->
<!-- ref 给引用类型值,也就是注入的bean的id-->
<!-- value 给基本类型值 如 <property name="age" value="20"/>-->
<property name="bookDao" ref="bookDao"/>
</bean>
<!-- 注册dao层对象-->
<bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>
接下来,我们再去测试类执行代码
@Test
public void testServiceClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
BookService service = context.getBean("bookService", BookService.class);
//调用对象中的测试方法
service.getAllBook();
}
输出
访问业务层
访问数据访问层
可以看到,业务层调用了dao层的对象,并且业务层对象并不是我们自己创建的,而是由Spring容器创建的,这就是IOC的作用。并且,我们还实现了DI(依赖注入)的配置,dao对象也由Spring容器创建并注入到业务层对象中。
3.2 DI
DI(依赖注入)是IOC的一种具体实现。依赖注入是指当一个对象需要另一个对象来协助完成其工作时,通过容器(Spring容器)来提供这个依赖对象。也就是属性赋值。
上面,我们简单演示了一下DI,通过setter方法来注入dao对象。现在我们详细说一下DI的方法。
- setters方法注入
- 构造方法注入
- 自动赋值注入
3.2.1 setters方法注入
最常用的注入方式,通过setter方法注入。接下来我们使用setter方法演示一下注入不同类型。
- 创建测试实体类,并设置getter/setter方法,方便注入。
package org.demo.entity;
import java.util.*;
public class TestDI {
private Integer age;
private String name;
private Date birthday;
private String[] hobby;
private List<String> bookList;
private Set<String> phones;
private Map<String, String> map;
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
public String[] getHobby() {
return hobby;
}
public void setHobby(String[] hobby) {
this.hobby = hobby;
}
public List<String> getBookList() {
return bookList;
}
public void setBookList(List<String> bookList) {
this.bookList = bookList;
}
public Set<String> getPhones() {
return phones;
}
public void setPhones(Set<String> phones) {
this.phones = phones;
}
public Map<String, String> getMap() {
return map;
}
public void setMap(Map<String, String> map) {
this.map = map;
}
@Override
public String toString() {
return "TestDI{" +
"age=" + age +
", name='" + name + '\'' +
", birthday=" + birthday +
", hobby=" + Arrays.toString(hobby) +
", bookList=" + bookList +
", phones=" + phones +
", map=" + map +
'}';
}
}
- 修改配置文件,注册对象并添加属性
<bean id="test" class="org.demo.entity.TestDI">
<!-- 基本数据类型-->
<property name="age" value="20"/>
<property name="name" value="张三"/>
<property name="birthday" value="2004/5/6"/>
<!-- 引用数据类型-->
<!-- 数组类型-->
<property name="hobby">
<array>
<value>篮球</value>
<value>读书</value>
<value>听音乐</value>
</array>
</property>
<!-- List集合类型-->
<property name="bookList">
<list>
<value>理想国</value>
<value>墨菲定律</value>
</list>
</property>
<!-- Map集合类型-->
<property name="map">
<map>
<entry key="山西省" value="太原市"/>
</map>
</property>
<property name="phones">
<set>
<value>13288888888</value>
<value>13288866666</value>
</set>
</property>
</bean>
- 测试类测试输出
@Test
public void testDI(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
TestDI testDI = context.getBean("test", TestDI.class);
//调用对象中的测试方法
System.out.println(testDI.toString());
}
输出
TestDI{age=20, name='张三', birthday=Thu May 06 00:00:00 HKT 2004, hobby=[篮球, 读书, 听音乐], bookList=[理想国, 墨菲定律], phones=[13288888888, 13288866666], map={山西省=太原市}}
可以看到,我们创建了不同类型的属性,设置了setter方法,在配置文件中进行了赋值,成功的注入到了对象中。
总结:
- 属性必须要有setter方法
- 注入时配置文件写
标签 - name写属性名
- 基本数据类型/包装类/String使用value赋值,其他用ref赋值
3.2.2 构造方法注入
- 创建测试类,并添加构造方法
package org.demo.entity;
public class TestDI2 {
private String name;
private Integer age;
public TestDI2() {
}
public TestDI2(String name, Integer age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "TestDI2{" +
"name='" + name + '\'' +
", age='" + age + '\'' +
'}';
}
}
- 修改配置文件,注册对象并添加属性
<bean id="test2" class="org.demo.entity.TestDI2">
<!-- 构造注入,需要使用constructor-arg标签 -->
<!-- type 参数类型-->
<!-- index 参数下标-->
<!-- name 参数名 -->
<!-- 上面三个属性只能做参数辅助,不能同时使用-->
<constructor-arg name="age" value="24"/>
<constructor-arg name="name" value="李四"/>
</bean>
- 测试类测试输出
@Test
public void testDI2(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
TestDI2 testDI2 = context.getBean("test2", TestDI2.class);
//调用对象中的测试方法
System.out.println(testDI2.toString());
}
输出
TestDI2{name='李四', age='24'}
可以看到我们通过构造方法,在配置文件中使用
tip:如果我们想用构造方法注入一个属性,类那边需要有对应的构造方法,否则会报错。
3.2.3 自动注入
自动注入是指Spring容器在创建对象时,会自动判断对象所需的依赖对象,并自动注入。主要是给引用类型赋值。
上面我们注入引用类型的时候,都是手动使用ref的方式去引用,现在我们可以修改一下配置文件,让其实现自动注入。
<?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标签,就是spring管理的一个类-->
<!-- id bean的唯一标识,也就是创建该类后的对象名,后续通过id就可以从容器中获得该对象-->
<!-- 注册业务层对象-->
<!-- autowire 自动装配,两种方案-->
<!-- 1. byType:通过类型赋值-->
<!-- 解释: spring容器创建BookServiceImpl时,会解析到该对象需要BookDao这些的属性spring容器中刚好有一个bookDao对象,-->
<!-- 类型刚好也是BookDao,所以可以自动注入-->
<!-- 2. byName:通过名字注入-->
<!-- 解释: 当需要注入属性时,如果容器中如果同类型有多个,那就会不知道注入此时就可以通过对象名来注入-->
<bean id="bookService" class="org.demo.service.impl.BookServiceImpl" autowire="byName"/>
<!-- 注册dao层对象-->
<bean id="bookDao" class="org.demo.dao.impl.BookDaoImpl"/>
</beans>
测试类直接测试
@Test
public void testServiceClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
BookService service = context.getBean("bookService", BookService.class);
//调用对象中的测试方法
service.getAllBook();
}
输出
访问业务层
访问数据访问层
可以看到,我们在bean标签中添加了属性autowire自动装配,让其按照byName名称来匹配实现自动注入(不再演示byType的了,只要改一下就行了,大差不差)。
tip:使用自动装配,必须有setter方法,只是简化了ref手动引用。还有需要注意的是,name是指需要注入的属性名和bean的id要一致,所以写代码尽量规范命名。
3.3 Bean的细节
IOC创建变量的时候,可以控制对象的创建方式,比如单例模式、多例模式等。
- 单例模式:单实例,即为单个对象,也就是说这个类有且仅有一个对象
- 多例模式:多示例,即为多个对象,也就是说这个类可以有多个对象
接下来演示一下如何查看对象的创建模式,调用对象的hasCode()方法即可
@Test
public void testClass(){
//配置文件路径
String path= "applicationContext.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
// MyClass myClass = (MyClass) context.getBean("myClass");
MyClass myClass = context.getBean("myClass", MyClass.class);
System.out.println(myClass.hashCode());
MyClass myClass1 = context.getBean("myClass", MyClass.class);
System.out.println(myClass1.hashCode());
}
输出
249034932
249034932
我们从容器中获取了两次对象,并输出了对应的哈希值,发现哈希值相同,说明我们取到的是同一个对象,这就说明IOC默认是单例的。
也就是说,在配置文件中,bean默认是
<!-- singleton 默认 单例 -->
<bean id="myClass" class="org.demo.MyClass" scope="singleton"/>
这个时候我们可以改成多例
<bean id="myClass" class="org.demo.MyClass" scope="prototype"/>
这个时候再测试输出一下查看对象哈希值
输出
1278254413
940584193
这个时候就可以看到,哈希值不同,说明取到的并不是同一个对象。
tip:默认是单例模式就可以,因为要实现对象的共享。
3.4 注解开发IOC(最常使用)
注解开发,省略大部分配置,提高开发效率。
3.4.1 IOC 相关注解
| 注解声明 | 作用解释 |
|---|---|
| @Component | 通用注解,可以注解类、方法、属性 |
| @Repository | 注解数据访问层(DAO层)类,作用同@Component |
| @Service | 注解业务层类,作用同@Component |
| @Controller | 注解控制层类,作用同@Component |
| @Scope | 注解作用域,可以指定Bean的作用域,如单例、多例等 |
tip:以上注解可以取代配置文件中的<bean>标签
3.4.2 DI 相关注解
| 注解声明 | 作用解释 |
|---|---|
| @Autowired | 注解自动装配,主要给引用类型注入(默认按照类型注入) |
| @Resource | 注解自动装配,作用同@Autowired,适用于同类型有多个的情况(默认按照名字注入) |
| @Value | 注解注入基本类型数据 |
tip:以上注解可以取代配置文件中的<property>标签
3.4.3 开始注解开发
- 创建dao层接口及其实现类并添加注解
UserDao.java
package org.demo.dao;
public interface UserDao {
void getUserInfo();
}
UserDaoImpl.java
package org.demo.dao.impl;
import org.demo.dao.UserDao;
import org.springframework.stereotype.Repository;
/**
* @Repository 注解标识当前类为持久层组件 相当于<bean id="userDaoImpl" class="org.demo.dao.impl.UserDaoImpl"/>
*/
@Repository
public class UserDaoImpl implements UserDao {
@Override
public void getUserInfo() {
System.out.println("数据访问层UserDaoImpl=====>获得用户信息");
}
}
- 创建业务类及实现类
UserService.java
package org.demo.service;
public interface UserService {
void getUserInfo();
}
UserServiceImpl.java
package org.demo.service.impl;
import org.demo.dao.UserDao;
import org.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @Service 注解 将UserServiceImpl类交给Spring容器管理,相当于配置文件中的<bean id="userServiceImpl" class="org.demo.service.impl.UserServiceImpl"/>
*/
@Service
public class UserServiceImpl implements UserService {
/**
* @Autowired 注解, 自动装配,autowird="byType" 根据类型查找
* */
@Autowired
private UserDao userDao;
@Override
public void getUserInfo() {
System.out.println("业务层UserServiceImpl======>获取用户信息");
userDao.getUserInfo();
}
}
- 编写配置文件
applicationContextAnno.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"
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">
<!-- 配置组件扫描 指明哪些包下有注解的类需要被Spring容器管理 -->
<context:component-scan base-package="org.demo"/>
</beans>
- 测试类
@Test
public void testAnno(){
//配置文件路径
String path= "applicationContextAnno.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
UserService userService = context.getBean("userService", UserService.class);
//调用对象中的测试方法
userService.getUserInfo();
}
- 输出
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'userService' available
...
...
看到输出,报错了,说No bean named 'userService' available,没有找到userService,这是因为我们使用了@Service这种注解,它会默认命名为对象名的首字母小写,我们是在UserServiceImpl类上加的注解,所以这里我们应该写userServiceImpl。
- 修改代码,在进行测试输出
@Test
public void testAnno(){
//配置文件路径
String path= "applicationContextAnno.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
//name 是 对象名的首字母小写注解在UserServiceImpl类。所以名称为userServiceImpl
UserService userService = context.getBean("userServiceImpl", UserService.class);
//调用对象中的测试方法
userService.getUserInfo();
}
此时再进行测试输出
业务层UserServiceImpl======>获取用户信息
数据访问层UserDaoImpl=====>获得用户信息
可以看到,此时我们就使用注解,自动配置注入了相应的对象,成功访问了业务层和数据访问层,大大省略了配置文件的编写,所写的也仅仅只有一句<context:component-scan base-package="org.demo"/>用来指明哪里有注解需要扫描配置而已。
- 扩展基本类型注解赋值
创建User实体类,使用@Component注解将User类交给Spring容器管理,并使用@Value注解给基本类型属性赋值。
User.java
package org.demo.entity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.sql.Date;
@Component
public class User {
@Value("1")
private Integer id;
@Value("大海")
private String name;
@Value("666666@163.com")
private String email;
@Value("山西省太原市迎泽区")
private String address;
@Value("13666666666")
private String phone;
@Value("2024/10/30")
private Date createTime;
@Value("2024/10/30")
private Date updateTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getUpdateTime() {
return updateTime;
}
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", name='" + name + '\'' +
", email='" + email + '\'' +
", address='" + address + '\'' +
", phone='" + phone + '\'' +
", createTime=" + createTime +
", updateTime=" + updateTime +
'}';
}
}
编写测试类
@Test
public void testUserAnno(){
//配置文件路径
String path= "applicationContextAnno.xml";
//通过配置文件,创建容器
ApplicationContext context = new ClassPathXmlApplicationContext(path);
//通过容器获得Bean对象(通过配置文件中bean标签的id)
//因为没有指定名称,所以默认为类名首字母小写
User user = context.getBean("user", User.class);
//调用对象中的测试方法
System.out.println(user.toString());
}
测试输出
User{id=1, name='大海', email='666666@163.com', address='山西省太原市迎泽区', phone='13666666666', createTime=Wed Oct 30 00:00:00 HKT 2024, updateTime=Wed Oct 30 00:00:00 HKT 2024}
可以看到,输出的User对象已经自动注入了相应的属性值,而且基本类型属性也被赋值成功。
tip:Date对象引入的包为java.util.Date,不要引入成java.sql.Date,会报错。
3.4.4 注解使用细节
- IOC创建对象的注解,四个功能一样,都可以创建对象,但是尽量还是按照名称在规定的位置使用,符合规范
- 加上创建对象的注解后,默认对象是
类名首字母小写,即需要通过类名的首字母小写来从容器中获得对象 - 如果我们不想使用默认的对象名,可以手动指定对象名,例如:@Service("userService"),这样就不会按照默认的规则来创建对象了。
- 自动注入@AutoWired,默认是
按照类型注入的,但是如果有多个相同类型的不同实现类对象,就会注入报错,这时候就需要我们使用@Resource注解,它可以根据名字注入。例如:@Resource(name="userDaoImpl2"),这样就不会报错了。
4. 代理设计模式
代理的设计理念是限制对象的直接访问,即不能通过 new 的方式得到想要的对象,而是访问该对象的代理类。这样的话,我们就保护了内部对象,如果有一天内部对象因为 某个原因换了个名或者换了个方法字段等等,那对访问者来说 一点不影响,因为他拿到的只是代理类而已,从而使该访问对 象具有高扩展性。
代理类可以实现拦截方法,修改原方法的参数和返回值,满足了代理自身需求和目的也就是代理的方法增强性。
按照代理的创建时期,代理可分为:静态代理和动态代理。
静态代理由开发者手动创建,在程序运行前,已经存在;
动态代理不需要手动创建,它是在程序运行时动态的创建代理类。
总结:代理模式--给某个对象提供一个代理,以改变对该对象的访 问方式
4.1 静态代理
我们可以创建FangDong房东类,创建chuzu()出租方法,使用FangDongProxy中介类来实现静态代理的演示。
- 创建FangDong()类 及其实现类
package org.demo.proxy;
public interface FangDong {
void chuzu();
}
package org.demo.proxy;
public class FangDongImpl implements FangDong{
@Override
public void chuzu() {
System.out.println("房东出租房子");
}
}
- 创建代理类
package org.demo.proxy;
public class FangDongProxy {
private FangDong fangDong;
public FangDongProxy(FangDong fangDong){
this.fangDong = fangDong;
}
public void chuzu() {
// 前置增强
System.out.println("中介发布出租信息");
fangDong.chuzu();
//后置增强
System.out.println("签订合同,中介负责后期维护");
}
}
- 编写测试类
package org.demo.proxy;
public class TestProxy {
public static void main(String[] args) {
FangDongProxy fangDong = new FangDongProxy(new FangDongImpl());
fangDong.chuzu();
}
}
- 测试输出
中介发布出租信息
房东出租房子
签订合同,中介负责后期维护
这就是静态代理,FangdongProxy在程序运行之前,是提前创建好的,从而对我们的FangDong类实现了前置和后置增强。
4.2 动态代理
其实我们发现,实现代理,需要新写代理类,这是租房需要一个代理类,那我们如果需要买车, 买酒等等,那就得再去写代理买车,买酒等的代理类去实现一些前后增强,随着我们要做的事情越多,代理类也就越多,多个代理类的功能代码也就显得更加冗余。这时候,有没有办法出现一个代理类,能够根据我们的目的,动态产生代理,从而减少代码冗余呢?这就到了我们的这一小节,动态代理。
动态代理:也就是在程序运行过程中,动态的为目标类产生代理类。
实现动态代理,有两种方案:
- JDK动态代理(JDK自带)
- JDK动态代理,只能代理接口,即目标类必须有接口
- cglib动态代理(第三方技术,需要导入jar包)
- 目标类可以是接口也可以是实现类
4.2.1 JDK动态代理
- 创建代理类MyInvocationHandler
package org.demo.proxy.dynamic;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandle implements InvocationHandler {
private Object target;
public MyInvocationHandle(Object target)
{
this.target = target;
}
/**
*
* @param proxy 代理对象
* @param method 目标方法
* @param args 目标方法执行所需要的参数
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// method.invoke(代理对象,参数);
System.out.println("中介前期宣传(方法前日志)");
Object ret = method.invoke(target, args);
System.out.println("中介后期维护(方法后日志)");
return ret;
}
}
- 创建目标类及其实现类
将上面静态代理的类及其实现类复制过来即可,不再给出
- 编写测试类
package org.demo.proxy.dynamic;
import java.lang.reflect.Proxy;
public class TestDynamic {
public static void main(String[] args) {
FangDong fangDong = new FangDongImpl();
/**
* ClassLoader loader, 类加载器
* Class<?>[] interfaces, 目标对象实现的接口
* InvocationHandler h 类拦截器
*
*/
FangDong ret = (FangDong) Proxy.newProxyInstance(fangDong.getClass().getClassLoader(), fangDong.getClass().getInterfaces(),new MyInvocationHandle(fangDong));
ret.chuzu();
}
}
- 测试输出
中介前期宣传(方法前日志)
房东出租房子
中介后期维护(方法后日志)
可以看到,我们使用了JDK提供的动态代理,编写了代理接口,这样无论有多少个目标类需要去代理,我们都不用再去重复的编写新的代理类,仅需要这一个代理类即可完成所有代理操作。
tip:注意,使用JKD的代理,目标类必须实现接口!
4.2.2 cglib 动态代理
cglib动态代理,虽然是第三方技术,需要引入第三方jar包,但是spring框架已经整合了cglib技术,所以在导入了spring依赖后,就已经导入了cglib包(在spring-core下就可以看到),所以不需要另外导入。cglib虽然写法同jdk代理不同,但是思想是极其相似的。
- 创建代理类MyProxyInterceptor.java
package org.demo.proxy.cglib;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class MyProxyInterceptor implements MethodInterceptor {
// cglib增强器
private Enhancer enhancer = new Enhancer();
public MyProxyInterceptor(Class tragetClass){
enhancer.setSuperclass(tragetClass);
enhancer.setCallback(this);
}
// 获得代理对象
public Object getProxy() {
return enhancer.create();
}
/**
*
* @param o 目标对象
* @param method 目标方法
* @param objects 方法参数
* @param methodProxy 代理方法
*/
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("cglib在方法执行前,添加日志");
Object ret = methodProxy.invokeSuper(o, objects);
System.out.println("cglib在方法执行后,添加日志");
return ret;
}
}
- 创建目标类及其实现类
同上,不再给出
- 创建测试类
package org.demo.proxy.dynamic;
import org.demo.proxy.cglib.MyProxyInterceptor;
public class TestDynamic {
public static void main(String[] args) {
FangDong fangDong = (FangDong) new MyProxyInterceptor(FangDongImpl.class).getProxy();
fangDong.chuzu();
}
}
- 测试输出
cglib在方法执行前,添加日志
房东出租房子
cglib在方法执行后,添加日志
可以看到,使用cglib 动态代理,也可以很方便的动态创建代理,减少我们代码的编写量和可维护性。
tip:cglib动态代理于jdk动态代理的最大区别就是,cglib要求目标实现类,可以没有接口,即为FangDongImpl 可以不实现自FangDong,但是JDK代理不行,必须要求有接口有实现类。
5. AOP(面向切面编程)
5.1 AOP概述
spring中的另外一个重要功能,
AOP(Aspect-Oriented Programming,面向切面编程),它可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可维护性。
AOP面向切面编程,利用 一种称为横切的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为抽取出封装到一个可重用模块,并将其命名为Aspect,即切面。所谓切面,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
就比如下图中,业务层的一些方法,都需要在方法前开启事务或者进行权限校验,方法调用后又需要打印日志记录,成功提交事务,失败了回滚事务,这些都是与业务无关的操作,如果每个方法都要写这些代码,代码量会很大,而且如果有多个方法都需要这样的操作,那么代码的重复率会很高,这时候就可以使用AOP来进行封装。

这个时候,我们将重复的代码进行抽取出来,封装成一个切面,然后在业务层的每个方法调用之前/后,都要先调用这个切面,然后再调用业务层的方法,这样就可以将这些与业务无关的操作封装起来,从而提高代码的复用率。如下图

面向切面编程的作用:就是将项目中与核心逻辑无关的代码横向抽取成切面类,通过织入作用到目标方法,以使目标方法执行前后达到增强的效果
原理: AOP底层使用的就是动态代理,给AOP指定哪些类型(目标类)需要增强,就会产生对应的代理对象,代理对象执行方法前后会先执行增强的方法
好处:减少系统的重复代码,降低模块之间的耦合度,便于维护,可以只关注核心业务
5.2 AOP术语
- 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容即
每个方法在切入之前,都是连接点 - 切入点(Pointcut):被Spring切入连接点。即
真正会增强的目标方法 - 通知、增强(Advice):可以
为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知等。 - 目标对象(Target):代理的目标对象
- 引介(introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。
- 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
- 代理(Proxy):被AOP织入通知后,产生的结果类。
- 切面(Aspect):由
切点和通知组成,将横切逻辑织入切面所指定的连接点中。
5.3 应用场景
- 事务管理
- 权限校验
- 日志记录
- 性能检测
- 等等
5.4 AOP入门编程实现
需求UserService及其实现类,在其方法执行前/后执行增强的方法。
编程的过程:
- 创建项目
- 导入依赖
- 核心依赖
- 切面依赖
- 目标类
- 切面类
- 织入(配置文件设置)
- 测试
-
创建项目,创建普通maven项目即可
-
导入依赖,导入spring核心依赖和aop依赖即可
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.33</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.33</version>
</dependency>
- 编写业务层及其实现类(目标类)
package org.demo.service;
public interface UserService {
void findUserById();
void deleteUserById();
}
package org.demo.service.impl;
import org.demo.service.UserService;
public class UserServiceImpl implements UserService {
@Override
public void findUserById() {
System.out.println("业务层=====》查询用户");
}
@Override
public void deleteUserById() {
System.out.println("业务层=====》删除用户");
}
}
- 切面类(做环绕演示,环绕包括前置和后置)
package org.demo.aspect;
import org.aspectj.lang.ProceedingJoinPoint;
public class MyAspect {
/**
* 定义通知的方法,前置通知 和 后置通知
*/
/**
* 环绕通知,包括前置通知和后置通知
* @param point 连接点/切入点,即目标方法
*/
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕通知====>方法前 开启事务/日志记录/权限校验");
Object proceed = point.proceed();
System.out.println("环绕通知====>方法后 提交事务/日志记录");
return proceed;
}
}
- 织入(配置文件设置)
<?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:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="userService" class="org.demo.service.impl.UserServiceImpl"/>
<bean id="myAspect" class="org.demo.aspect.MyAspect"/>
<aop:config>
<!-- 确定切面-->
<aop:aspect ref="myAspect">
<!-- 1.确定通知/增强类型-->
<!-- 2.确定切面中增强的方法-->
<!-- 3.确定目标类以及目标方法
execution(返回值类型 包路径.类名.方法名(参数列表))
返回值类型任意 就写 *
类名任意 就写 *
方法名就写具体方法名,如果该类所有方法都需要通知/增强,也写 *
参数列表任意 就写 (..)
-->
<aop:around method="around" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
</aop:aspect>
</aop:config>
</beans>
- 测试
package org.demo;
import org.demo.service.UserService;
import org.demo.service.impl.UserServiceImpl;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestAspect {
@Test
public void testAround() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserService userService = context.getBean("userService",UserService.class);
userService.findUserById();
System.out.println("===============================");
userService.deleteUserById();
}
}
输出
环绕通知====>方法前 开启事务/日志记录/权限校验
业务层=====》查询用户
环绕通知====>方法后 提交事务/日志记录
===============================
环绕通知====>方法前 开启事务/日志记录/权限校验
业务层=====》删除用户
环绕通知====>方法后 提交事务/日志记录
可以看到正常的业务逻辑执行了,并且在执行前后都执行了增强的方法。因为我们在织入的时候,选择的是UserServiceImpl中的所有方法,所以findUserById和deleteUserById都被织入了。
5.5 其他增强
- 环绕通知(目标方法执行前后,都有增强的方法,上面已经演示过)
- 常用于事务管理(事务管理有开启和关闭)
- 前置通知
- 常用于权限校验
- 后置通知
- 常用于日志记录或者释放资源
- 后置返回通知
- 常用于获得目标方法的返回值,处理返回值
- 异常通知
- 常用于代码有异常后去执行一些功能
5.5.1 前置通知
- 编写前置方法
/**
* 前置通知 JoinPoint 目标对象
*/
public void before(JoinPoint point) {
Object target = point.getTarget();
System.out.println("目标对象:" + target);
Signature signature = point.getSignature();
System.out.println("调用方法签名:" + signature);
System.out.println("实现前置增强====》权限校验功能");
}
- 编写配置文件
<aop:before method="before" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
- 测试
@Test
public void testBefore() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserService userService = context.getBean("userService",UserService.class);
userService.findUserById();
}
4.输出
目标对象:org.demo.service.impl.UserServiceImpl@2254127a
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能
业务层=====》查询用
前置通知和环绕通知的配置相似,前置通知传入的是目标对象,可以看到目标对象是UserServiceImpl,并且调用了findUserById和deleteUserById方法,通常在前置通知中,进行权限校验,如果校验不通过,则不执行目标方法,我们可以手动抛出异常,或者返回false,让目标方法不执行。
5. 修改通知方法,模拟权限校验不通过不执行目标方法
public void before(JoinPoint point) {
Object target = point.getTarget();
System.out.println("目标对象:" + target);
Signature signature = point.getSignature();
System.out.println("调用方法签名:" + signature);
System.out.println("实现前置增强====》权限校验功能");
throw new RuntimeException("校验未通过");
}
- 再次输出测试
目标对象:org.demo.service.impl.UserServiceImpl@2254127a
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能
java.lang.RuntimeException: 校验未通过`
at org.demo.aspect.MyAspect.before(MyAspect.java:21)
...
...
tip:这里的权限校验仅作参考,实际并不是这样粗糙的编写异常让目标方法不执行,而是应该根据实际情况编写具体的权限校验逻辑。
5.5.2 后置通知
后置通知同前置通知基本相似,只不过是在目标方法执行后执行,配置文件也由<aop:before>修改为<aop:after>,在修改方法名即可。
- 编写后置方法`
/**
* 后置通知
*/
public void after() {
System.out.println("后置增强=====>记录日志,释放资源");
}
- 编写配置文件
<aop:after method="after" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
- 测试
public void testafter() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserService userService = context.getBean("userService",UserService.class);
userService.findUserById();
System.out.println("===============================");
userService.deleteUserById();
}
- 输出
业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源
tip:后置通知方法可以写JoinPoint point也可以不写,这个主要是为了获取对象,但是后置通知一般都是用于记录日志,释放资源,获取对象也不会进行什么操作。
5.5.3 后置返回通知
后置返回通知是在目标方法执行后,获取目标方法的返回值,并进行一些处理。
- 编写后置返回方法
/**
* 后置返回通知
* @param result 用于接收目标方法的返回值
* @return 处理后返回
*/
public Object afterReturn(Object result) {
System.out.println("后置返回通知=====>获取到的返回结果:" + result);
// 模拟业务处理
if(result instanceof Integer value){
return value * 5;
}
return result;
}
- 编写配置文件
<!-- 多了一个returning'属性,里面填写后置返回通知方法的返回值参数名,这里是result -->
<aop:after-returning method="afterReturn" returning="result" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
- 测试
@Test
public void testAfterReturn() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserService userService = context.getBean("userService",UserService.class);
userService.findUserById();
System.out.println("===============================");
int ret = userService.deleteUserById();
System.out.println("处理后的返回值:" + ret);
}
- 输出
业务层=====》查询用户
后置返回通知=====>获取到的返回结果:null
===============================
业务层=====》删除用户
后置返回通知=====>获取到的返回结果:100
处理后的返回值:100
这时候 我们发现了一个问题,为什么,我们在后置返回方法中判断如果返回值是Integer类型,就强制转换后,乘5,然后返回,但是为什么返回值还是100呢?
这是因为后置返回通知无法改变目标方法的返回值,如果我们需要改变返回值,可以考虑使用环绕通知。
5.5.4 异常通知
异常通知主要是为了捕获目标方法的异常,并进行一些处理。
- 编写异常通知方法
/**
* 异常通知
* @param ex 目标方法抛出的异常
*/
public void afterThrowing(Throwable ex) {
System.out.println("异常通知=====>" + ex.getMessage());
}
- 编写配置文件
<!-- 注意这里参数使用的是throwing属性,里面填写异常通知方法的异常参数名,这里是ex -->
<aop:after-throwing method="afterThrowing" throwing="ex" pointcut="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
- 为了测试,我们在业务层中抛出一个异常
@Override
public Integer deleteUserById() {
System.out.println("业务层=====》删除用户");
return 100/0;
}
- 测试
@Test
public void testAfterThrowing() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserService userService = context.getBean("userService",UserService.class);
userService.findUserById();
System.out.println("===============================");
int ret = userService.deleteUserById();
}
输出
业务层=====》查询用户
===============================
业务层=====》删除用户
异常通知=====>/ by zero
java.lang.ArithmeticException: / by zero
at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
...
...
可以看到,我们在业务层删除方法返回值里故意抛出了一个异常,然后在配置文件中配置了异常通知,可以看到后置异常报告打印了异常信息,并没有影响到业务逻辑的执行。
tip:通知可以一起使用,比如前置通知和后置通知,前置通知可以进行权限校验,后置通知可以记录日志,异常通知可以进行异常处理等。
5.6 AOP底层动态代理
AOP的
底层原理实际上就是使用的动态代理,使用的技术是JDK和cglib混合。
为什么我会知道用的是jdk和cglib动态代理呢?很简单,我们可以模拟一下试一下。
首先,为了便于演示,我们使用后置通知,并且手动抛出异常。直接在测试类中运行
业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源
java.lang.ArithmeticException: / by zero
at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:569)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:241)
at jdk.proxy2/jdk.proxy2.$Proxy10.deleteUserById(Unknown Source)
可以看到,报错信息中,出现了JdkDynamicAopProxy这个关键词,这个关键词的意思就是JDK动态代理
接下来改动一下业务层,不再实现接口,改为普通方法
package org.demo.service.impl;
public class UserServiceImpl {
// @Override
public void findUserById() {
System.out.println("业务层=====》查询用户");
}
// @Override
public Integer deleteUserById() {
System.out.println("业务层=====》删除用户");
return 100/0;
}
}
再改动一下测试类,因为我们改动了业务层不再实现接口,所以测试类从容器中获取对象转换那里需要改动
@Test
public void testAround() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-context.xml");
UserServiceImpl userService = context.getBean("userService",UserServiceImpl.class);
userService.findUserById();
System.out.println("===============================");
int ret = userService.deleteUserById();
}
再次运行,可以看到
业务层=====》查询用户
后置增强=====>记录日志,释放资源
===============================
业务层=====》删除用户
后置增强=====>记录日志,释放资源
java.lang.ArithmeticException: / by zero
at org.demo.service.impl.UserServiceImpl.deleteUserById(UserServiceImpl.java:14)
at org.demo.service.impl.UserServiceImpl$$FastClassBySpringCGLIB$$fed48ef6.invoke(<generated>)
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:762)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:707)
at org.demo.service.impl.UserServiceImpl$$EnhancerBySpringCGLIB$$16cf8a7e.deleteUserById(<generated>)
报错信息中,频繁出现了CglibAopProxy,也就是cglib动态代理。
由此我们可以得知:
- 当目标类有接口的时候,使用的就是JDK动态代理。
- 当目标类没有接口的时候,使用的就是cglib动态代理。
tip:当你要启动多个通知方法配置时,且作用范围相同,可以将方法提取出来放置在<aop:pointcut>中,然后在<aop:before>、<aop:after>、<aop:after-returning>、<aop:after-throwing>等中引用。
例如:
<aop:config>
<aop:aspect ref="myAspect">
<aop:pointcut id="methodScope" expression="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
<aop:before method="before" pointcut-ref="methodScope"/>
<aop:after method="after" pointcut-ref="methodScope"/>
</aop:aspect>
</aop:config>
6. 注解开发
使用注解开发可以简化配置,提高开发效率。本节,则使用注解开发IOC、DI、AOP。
AOP相关的注解有:
| 注解声明 | 作用解释|
| --- | --- |
| @Aspect | 定义切面类,标注切面类,可以被Spring AOP框架识别|
| @Pointcut | 定义切点,标注切点,可以被切面类引用(也就是AOP生效的范围)|
| @Before | 前置通知,在目标方法执行前执行|
| @After | 后置通知,在目标方法执行后执行|
| @AfterReturning | 后置返回通知,在目标方法执行后,获取目标方法的返回值|
| @AfterThrowing | 异常通知,在目标方法抛出异常后执行|
| @Around | 环绕通知,在目标方法执行前后,执行自定义逻辑|
接下来通过改写上面的案例,来详细理解一下每个注解的使用。
6.1 IOC注解,只需要在UserServiceImpl实现类添加注解@Service即可
因为仅作演示,没用到注入,所以不需要使用注入注解。
package org.demo.service.impl;
import org.demo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService {
@Override
public void findUserById() {
System.out.println("业务层=====》查询用户");
}
@Override
public Integer deleteUserById() {
System.out.println("业务层=====》删除用户");
return 100;
}
}
6.2 编写切面类,使用注解完成配置
@Component注解用于将切面类注册到Spring容器中,@Aspect注解用于定义切面类,@Pointcut注解用于定义切点,@Before、@After、@AfterReturning、@AfterThrowing、@Around注解用于定义通知方法。
package org.demo.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAnnoAspect {
@Pointcut("execution(* org.demo.service.impl.UserServiceImpl.*(..))")
void myPoint(){}
@Before("myPoint()")
public void before(JoinPoint point) {
Object target = point.getTarget();
System.out.println("目标对象:" + target);
Signature signature = point.getSignature();
System.out.println("调用方法签名:" + signature);
System.out.println("实现前置增强====》权限校验功能");
// throw new RuntimeException("校验未通过");
}
@After("myPoint()")
public void after() {
System.out.println("后置增强=====>记录日志,释放资源");
}
@AfterReturning(value = "myPoint()",returning = "result")
public Object afterReturn(Object result) {
System.out.println("后置返回通知=====>获取到的返回结果:" + result);
// 模拟业务处理
if(result instanceof Integer value){
return value * 5;
}
return result;
}
@AfterThrowing(value = "myPoint()",throwing = "ex")
public void afterThrowing(Throwable ex) {
System.out.println("异常通知=====>" + ex.getMessage());
}
@Around("myPoint()")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕增强====>方法前 开启事务/日志记录/权限校验");
Object proceed = point.proceed();
System.out.println("方法返回值:" + proceed);
System.out.println("环绕增强====>方法后 提交事务/日志记录");
if(proceed instanceof Integer){
return (Integer) proceed * 5;
}
return proceed;
}
}
6.3 编写配置文件,配置扫描注解位置,启用AOP代理
<?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:aop="http://www.springframework.org/schema/aop"
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/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- 扫描IOC、DI注解-->
<context:component-scan base-package="org.demo"/>
<!-- AOP开启自动代理,注解生效-->
<aop:aspectj-autoproxy/>
</beans>
6.4 测试
@Test
public void testAnno() {
ApplicationContext context = new ClassPathXmlApplicationContext("application-contextAnno.xml");
UserService userService = context.getBean("userServiceImpl",UserService.class);
userService.findUserById();
System.out.println("===============================");
int ret = userService.deleteUserById();
}
输出
环绕增强====>方法前 开启事务/日志记录/权限校验
目标对象:org.demo.service.impl.UserServiceImpl@d771cc9
调用方法签名:void org.demo.service.UserService.findUserById()
实现前置增强====》权限校验功能
业务层=====》查询用户
后置返回通知=====>获取到的返回结果:null
后置增强=====>记录日志,释放资源
方法返回值:null
环绕增强====>方法后 提交事务/日志记录
===============================
环绕增强====>方法前 开启事务/日志记录/权限校验
目标对象:org.demo.service.impl.UserServiceImpl@d771cc9
调用方法签名:Integer org.demo.service.UserService.deleteUserById()
实现前置增强====》权限校验功能
业务层=====》删除用户
后置返回通知=====>获取到的返回结果:100
后置增强=====>记录日志,释放资源
方法返回值:100
环绕增强====>方法后 提交事务/日志记录
因为所有通知方法都被调用,所以输出了很多信息。接下来将注解详细对应一下配置文件中的标签。
- @Service注解对应
<bean>标签
<bean id="userService" class="org.demo.service.impl.UserServiceImpl"/>
- @Component注解对应
<bean>标签,
<bean id="MyAspectAnno" class="org.demo.aspect.MyAspectAnno"/>
- @Aspect注解对应
<aop:config>
<aop:aspect ref="MyAspectAnno">
</aop:aspect>
</aop:config>`
- @Pointcut注解对应
<aop:pointcut id="methodScope" expression="execution(* org.demo.service.impl.UserServiceImpl.*(..))"/>
- @Before("myPoint()")注解对应
<aop:before method="before" pointcut-ref="methodScope"/>
- @After("myPoint()")注解对应
<aop:after method="after" pointcut-ref="methodScope"/>
- @AfterReturning(value = "myPoint()",returning = "result")注解对应
<aop:after-returning method="afterReturn" pointcut-ref="methodScope" returning="result"/>
- @AfterThrowing(value = "myPoint()",throwing = "ex")注解对应
<aop:after-throwing method="afterThrowing" pointcut-ref="methodScope" throwing="ex"/>
- @Around("myPoint()")注解对应
<aop:around method="around" pointcut-ref="methodScope"/>
最后,只需要在配置文件中配置好扫描注解的位置,让spring可以扫描到IOC、DI的相关注解,再配置好AOP的自动代理,就可以生效AOP相关的注解,至此,我们就可以通过注解开发,我们可以很方便的完成IOC\DI\AOP开发配置,提高开发效率。
tip:如果按照配置完成,启动测试报错,相关错误是org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'userService' available这样子的,那你一定是忘记了在IOC小节中,使用注解后,默认名字为类名首字母小写。
7. Spring + Mybatis整合
在Spring小节中,主要学习了IOC和AOP的使用,本节将学习如何利用IOC和AOP来完成Spring与Mybatis的整合。
- IOC:控制反转,用来创建对象
- XxxService
- 通过数据源获取数据库连接
- 创建SqlSessionFactory
- 创建SqlSession
- 获取XxxMapper代理对象
- AOP:面向切面编程
- 事务管理
具体的整合思路:
- 创建项目
- 导入依赖
- spring核心依赖
- aop依赖
- mybatis依赖
- 数据库驱动依赖
- 单元测试依赖
- 日志依赖
- 连接池依赖
专业用与spring和mybatis整合的依赖spring-jdbc依赖,用于DAO层支持
- 配置文件
spring配置文件- mybatis配置文件
- db配置文件
- log4配置文件
- 具体业务代码
- 实体类 pojo/entity
- XxxMapper、XxxMapper.xml dao/mapper
- XxxService、 XxxServiceImpl service
- 测试
7.1 创建项目
直接创建Maven项目即可,此处不再赘述
7.2 导入依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>springAndMybatis</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- spring核心依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.33</version>
</dependency>
<!-- mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.33</version>
</dependency>
<!-- mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.14</version>
</dependency>
<!-- 单元测试依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- 日志依赖-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<!-- mybatis分页插件依赖-->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.3.3</version>
</dependency>
<!-- alibaba数据池依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
<!-- 整合依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.1</version>
</dependency>
<!-- spring-jdbc依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.33</version>
</dependency>
</dependencies>
<build>
<!-- 在IDEA的MAVEN项目中,默认源代码(java)目录下的xml等资源文件并不在编译的时候y一块打包进classes文件夹,-->
<!-- 而是直接舍弃掉,为了解决这个问题,有两种方法-->
<!-- 方法1 将xml或者properties等配置文件放在resources下,并修改配置文件的代码,比如说注册xml文件的位置-->
<!-- 方法2 在maven中添加过滤,将java下的xml进行过 滤-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<!-- 默认 新添加自定义则失效-->
<include>*.xml</include>
<!-- 新添加 */代表一级目录 **/代表多级目录-->
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
</build>
</project>
7.3 配置文件
因为之前写过mybatis、db、log4的配置,所以这里直接给出,主要是spring的配置。
log4.properties
log4j.rootLogger=ERROR, stdout
log4j.logger.org.demo.mapper=DEBUG
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/book?useSSL=false
jdbc.username=root
jdbc.password=
jdbc.initialSize=5
jdbc.maxActive=20
jdbc.minIdle=3
jdbc.maxWait=0
jdbc.timeBetweenEvictionRunsMillis=0
jdbc.minEvictableIdleTimeMillis=30000
mybatis-config.xml,由于和spring整合,所以大部分配置都在spring中配置。(我觉得在spring中配置插件太麻烦,所以没挪过去,会给出对应配置)
<?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>
<settings>
<setting name="logImpl" value="LOG4J"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="cacheEnabled" value="true"/>
</settings>
<plugins>
<!-- 插件拦截器的路径-->
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
</configuration>
applicationContext.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"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
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 http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 扫描注解-->
<context:component-scan base-package="org.demo"/>
<!-- 启用aop自动代理-->
<!-- <aop:aspectj-autoproxy/>-->
<!-- 1. 加载数据源配置文件-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2. 创建数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
<property name="initialSize" value="${jdbc.initialSize}"/>
<property name="maxActive" value="${jdbc.maxActive}"/>
<property name="minIdle" value="${jdbc.minIdle}"/>
<property name="maxWait" value="${jdbc.maxWait}"/>
<property name="timeBetweenEvictionRunsMillis" value="${jdbc.timeBetweenEvictionRunsMillis}"/>
<property name="minEvictableIdleTimeMillis" value="${jdbc.minEvictableIdleTimeMillis}"/>
</bean>
<!-- 3. 创建SqlSessionFactory-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- 这些配置,如果mybaitis-config.xml写了,就可以不用再写了-->
<property name="typeAliasesPackage" value="org.demo.entity"/>
<!-- 可以直接引用-->
<property name="configLocation" value="mybatis-config.xml"/>
<!-- 配置mybatis的插件-->
<!-- <property name="plugins">-->
<!-- <set>-->
<!-- <bean class="com.github.pagehelper.PageInterceptor">-->
<!-- <property name="properties">-->
<!-- <props>-->
<!-- <prop key="helperDialect">mysql</prop>-->
<!-- </props>-->
<!-- </property>-->
<!-- </bean>-->
<!-- </set>-->
<!-- </property>-->
</bean>
<!-- 4. 扫描mapper,使mapper插入容器,获得mapper代理-->
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 此处是上面的工厂id-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 扫描mapper,产生代理对象,加入spring容器-->
<property name="basePackage" value="org.demo.mapper"/>
</bean>
</beans>
7.4 实现具体业务代码
- 实体类
org.demo.entity下创建 Book.java
package org.demo.entity;
import java.io.Serializable;
public class Book implements Serializable {
private Integer id;
private String name;
private String author;
private String desc;
private Double price;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", name='" + name + '\'' +
", author='" + author + '\'' +
", desc='" + desc + '\'' +
", price=" + price +
'}';
}
}
- 创建mapper接口及其对应的xml映射文件
org.demo.mapper下创建 BookMapper.java、BookMapper.xml
BookMapper.java
package org.demo.mapper;
import org.demo.entity.Book;
public interface BookMapper {
Book selectById(Integer id);
}
BookMapper.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">
<mapper namespace="org.demo.mapper.BookMapper">
<select id="selectById" resultType="Book">
select * from bookinfo where id = #{id}
</select>
</mapper>
- 创建service接口及其实现类
org.demo.service下创建 BookService.java、BookServiceImpl.java
BookService.java
package org.demo.service;
import org.demo.entity.Book;
public interface BookService {
Book selectById(Integer id);
}
BookServiceImpl.java
package org.demo.service.impl;
import org.demo.entity.Book;
import org.demo.mapper.BookMapper;
import org.demo.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookMapper bookMapper;
public Book selectById(Integer id) {
return bookMapper.selectById(id);
}
}
7.5 测试
直接创建测试类,测试service的selectById方法
@Test
public void testSelectById(){
String url ="applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(url);
BookService bookServiceImpl = context.getBean("bookServiceImpl", BookService.class);
Book book = bookServiceImpl.selectById(1);
System.out.println(book);
}
输出
DEBUG [main] - ==> Preparing: select * from bookinfo where id = ?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
Book{id=1, name='道听途说', author='何金银', desc='清空二级缓存测试', price=29.7}
8. spring实现事务管理
本来在之前学习mybatis时,提供了jdbc事务管理的功能,但是在spring和mybatis整合之后,规定事务管理必须由springtx来管理。
Spring管理事务,有两种方案:
- 编程式事务:也就是在具体业务的执行区域,手动编写代码控制事务
- 需要手动给每个方法编写事务代码,
- 代码大量冗余,业务代码不灵活
声明式事务:通过AOP的方式,在配置文件中声明事务的属性- 在applicationContext.xml中配置AOP,声明哪些方法需要事务管理
- 业务代码不需要任何改动,就可以实现事务管理
在学习声明式事务之前,先来回忆一下,AOP是如何配置的。
AOP配置过程:
- 目标方法
- 定义切面类,定义增强的方法
- 配置文件中织入
那声明式事务也是通过AOP方式实现的,应该如何配置呢?
事务配置过程:
- 目标方法
- 使用spring提供的事务管理器,即增强的方法(不需要我们自己去定义了)
- 配置文件中织入
8.1 声明式事务配置使用
接下来,我们使用一个例子,来学习spring事务管理。
需求:数据库中有account表,我们对其进行操作,实现转账功能。
- 编写实体类Account.java
package org.demo.entity;
import java.io.Serializable;
public class Account implements Serializable {
private Integer id;
private String name;
private Double balance;
@Override
public String toString() {
return "Account{" +
"id=" + id +
", name='" + name + '\'' +
", balance=" + balance +
'}';
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
}
- 编写mapper接口及其对应的xml映射文件AccountMapper.java、AccountMapper.xml
AccountMapper.java
package org.demo.mapper;
import org.apache.ibatis.annotations.Param;
public interface AccountMapper {
int addMoney(@Param("toId") int toId, @Param("balance")int balance);
int delMoney(@Param("fromId") int fromId,@Param("balance") int balance);
}
AccountMapper.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">
<mapper namespace="org.demo.mapper.AccountMapper">
<update id="addMoney">
update account set balance = balance + #{balance} where id = #{toId}
</update>
<update id="delMoney">
update account set balance = balance - #{balance} where id = #{fromId}
</update>
</mapper>
- 编写service接口及其实现类AccountService.java、AccountServiceImpl.java
AccountService.java
package org.demo.service;
public interface AccountService {
boolean transferMoney(Integer fromId,Integer toId,Integer balance);
}
AccountServiceImpl.java
package org.demo.service.impl;
import org.demo.mapper.AccountMapper;
import org.demo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Override
public boolean transferMoney(Integer fromId, Integer toId, Integer balance) {
System.out.println("转账开始");
//减少
int i = accountMapper.delMoney(fromId, balance);
i = 100/0;
//增加
int j = accountMapper.addMoney(toId, balance);
return i == 1 && j == 1;
}
}
- 配置spring事务管理器
<!-- 5. 配置事务管理 相当于切面-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 需要注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 6.配置事务管理的增强方法(配置事务特性)-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<!-- 配置目标方法-->
<tx:attributes>
<!-- 目标方法名-->
<tx:method name="transferMoney"/>
<!-- 除此之外还可以模糊匹配写 匹配transferXxx()之类的方法-->
<!-- <tx:method name="transfer*"/>-->
<!-- 所有方法都需要事务-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- 7. 织入AOP(将增强的方法作用到目标方法上)-->
<aop:config>
<aop:pointcut id="myAspect" expression="execution(* org.demo.service.impl.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="myAspect"/>
</aop:config>
- 测试
为了验证我们的事务管理是否配置成功,我已经在业务层故意写了一个异常,接下来我们可以编写一个测试类,测试转账功能,查看如果出现异常,是否会进行回滚。
@Test
public void testTransfer(){
String url ="applicationContext.xml";
ApplicationContext context = new ClassPathXmlApplicationContext(url);
AccountService accountServiceImpl = context.getBean("accountServiceImpl", AccountService.class);
System.out.println(accountServiceImpl.transferMoney(1, 2, 300));
}
首先先将刚才配置的事务管理注释掉,运行测试类,输出结果为:
转账开始
DEBUG [main] - ==> Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <== Updates: 1
java.lang.ArithmeticException: / by zero
...
数据库查看到信息,余额变为700,说明在我们没有配置事务管理器前,是没有事务的,出现了异常,余额仍然出现了变动,这显然是不对的。
接下来取消注释掉我们的事务配置,再次运行测试类,输出结果为:
转账开始
DEBUG [main] - ==> Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <== Updates: 1
java.lang.ArithmeticException: / by zero
数据库查看信息,可以发现,数据库中数据并没有发生变化,说明业务发生了异常以后,事务自动进行了回滚,确保了数据的一致性。
8.2 事务属性配置
-
propagation:事务的传播行为,有以下几种取值:
- REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值(适合增删改高频率方法)。
- SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行(适合查询等方法)。
- MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- REQUIRES_NEW:创建一个新的事务,无论当前是否存在事务。
- NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
-
isolation:事务的隔离级别,有以下几种取值:
- DEFAULT:使用数据库默认的隔离级别。
- READ_UNCOMMITTED:最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读。
- READ_COMMITTED:允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生(ORACLE数据库默认级别)。
- REPEATABLE_READ:对同一字段的多次读取结果都是一致的,除非数据被本身事务外的事务改变,可以阻止脏读和不可重复读,但幻读仍有可能发生(MYSQL数据库默认级别)。
- SERIALIZABLE:最高的隔离级别,完全串行化的读写,并发控制,防止脏读、幻读和不可重复读。
-
timeout:事务的超时时间,单位为秒,默认为-1,表示没有超时时间限制。
-
read-only:是否为只读事务,true表示只读(只能查询),false表示可读写。
-
rollback-for:出现哪些异常,事务回滚。
-
no-rollback-for:出现哪些异常,不回滚事务。
8.3 注解开发事务
使用注解开发事务,可以大大减少配置的复杂度,提高开发效率。例如配置文件中的
<tx:advice>和<aop:config>标签,都可以用注解来替代。
注解开发事务很简单,只需要在需要事务的方法上添加注解即可,注解有以下几种:
@Transactional:最常用的注解,可以应用到方法、类、接口上,表示该方法的事务属性。@EnableTransactionManagement:开启事务管理,相当于配置文件中的<tx:annotation-driven>标签。@Transactional(propagation=Propagation.REQUIRED, isolation=Isolation.DEFAULT, timeout=30, rollbackFor=Exception.class):自定义事务属性。
接下来对上面进行改动,使用注解开发:
- 修改业务层实现类,使用注解
package org.demo.service.impl;
import org.demo.mapper.AccountMapper;
import org.demo.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountMapper accountMapper;
@Transactional
@Override
public boolean transferMoney(Integer fromId, Integer toId, Integer balance) {
System.out.println("转账开始");
//减少
int i = accountMapper.delMoney(fromId, balance);
i = 100/0;
//增加
int j = accountMapper.addMoney(toId, balance);
return i == 1 && j == 1;
}
}
- 修改配置文件,启用注解
<!-- 5. 配置事务管理 相当于切面-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 需要注入数据源-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 6.配置事务管理的增强方法(配置事务特性)-->
<!-- <tx:advice id="txAdvice" transaction-manager="transactionManager">-->
<!--<!– 配置目标方法–>-->
<!-- <tx:attributes>-->
<!--<!– 目标方法名–>-->
<!-- <tx:method name="transferMoney"/>-->
<!--<!– 除此之外还可以模糊匹配写 匹配transferXxx()之类的方法–>-->
<!--<!– <tx:method name="transfer*"/>–>-->
<!--<!– 所有方法都需要事务–>-->
<!-- <tx:method name="*"/>-->
<!-- </tx:attributes>-->
<!-- </tx:advice>-->
<!--<!– 7. 织入AOP(将增强的方法作用到目标方法上)–>-->
<!-- <aop:config>-->
<!-- <aop:pointcut id="myAspect" expression="execution(* org.demo.service.impl.*.*(..))"/>-->
<!-- <aop:advisor advice-ref="txAdvice" pointcut-ref="myAspect"/>-->
<!-- </aop:config>-->
<!-- 使用注解开发事务,将6/7替换为8-->
<tx:annotation-driven transaction-manager="transactionManager"/>
- 测试输出
转账开始
DEBUG [main] - ==> Preparing: update account set balance = balance - ? where id = ?
DEBUG [main] - ==> Parameters: 300(Integer), 1(Integer)
DEBUG [main] - <== Updates: 1
java.lang.ArithmeticException: / by zero
可以看到,使用注解大大减少了配置文件的编写,并且也成功实现了业务异常后事务的回滚。