首先感谢动力节点和杜老师的教学分享!Respect!
学习来源:B站
https://www.bilibili.com/video/BV1Ft4y1g7Fb/?spm_id_from=333.337.search-card.all.click&vd_source=07c8a1a7d89af39fe20c3a6894f5ff6a
资料来源:百度网盘
链接:https://pan.baidu.com/s/1yXl8Kg80utfqazkuaE_Q8g?pwd=dljd
提取码:dljd
文章目录
十四、GoF之代理模式
14.1 对代理模式的理解
生活场景1:牛村的牛二看上了隔壁村小花,牛二不好意思直接找小花,于是牛二找来了媒婆王妈妈。这里面就有一个非常典型的代理模式。牛二不能和小花直接对接,只能找一个中间人。其中王妈妈是代理类,牛二是目标类。王妈妈代替牛二和小花先见个面。(现实生活中的婚介所)【在程序中,对象A和对象B无法直接交互时。】
生活场景2:你刚到北京,要租房子,可以自己找,也可以找链家帮你找。其中链家是代理类,你是目标类。你们两个都有共同的行为:找房子。不过链家除了满足你找房子,另外会收取一些费用的。(现实生活中的房产中介)【在程序中,功能需要增强时。】
西游记场景:八戒和高小姐的故事。八戒要强抢民女高翠兰。悟空得知此事之后怎么做的?悟空幻化成高小姐的模样。代替高小姐与八戒会面。其中八戒是客户端程序。悟空是代理类。高小姐是目标类。那天夜里,在八戒眼里,眼前的就是高小姐,对于八戒来说,他是不知道眼前的高小姐是悟空幻化的,在他内心里这就是高小姐。所以悟空代替高小姐和八戒亲了嘴儿。这是非常典型的代理模式实现的保护机制。代理模式中有一个非常重要的特点:对于客户端程序来说,使用代理对象时就像在使用目标对象一样。【在程序中,目标需要被保护时】
业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】
代理模式是GoF23种设计模式之一。属于结构型设计模式。
代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。
代理模式中的角色:
- 代理类(代理主题)
- 目标类(真实主题)
- 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。
代理模式的类图:
代理模式在代码实现上,包括两种形式:
- 静态代理
- 动态代理
14.2 静态代理
现在有这样一个接口和实现类:
- com.powernode.mall.service.OrderService
package com.powernode.mall.service;
/**
* 订单接口
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.OrderService
* @date 2022/11/22
* @since 1.0
*/
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
- OrderService接口的实现类: com.powernode.mall.service.impl.OrderServiceImpl
package com.powernode.mall.service.impl;
import com.powernode.mall.service.OrderService;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.impl.OrderServiceImpl
* @date 2022/11/22
* @since 1.0
*/
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
Thread.sleep(2413);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
其中Thread.sleep()方法的调用是为了模拟操作耗时。
项目已上线,并且运行正常,只是客户反馈系统有一些地方运行较慢,要求项目组对系统进行优化。于是项目负责人就下达了这个需求。首先需要搞清楚是哪些业务方法耗时较长,于是让我们统计每个业务方法所耗费的时长。如果是你,你该怎么做呢?
第一种方案:直接修改Java源代码,在每个业务方法中添加统计逻辑,如下:
- com.powernode.mall.service.impl.OrderServiceImpl
package com.powernode.mall.service.impl;
import com.powernode.mall.service.OrderService;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.impl.OrderServiceImpl
* @date 2022/11/22
* @since 1.0
*/
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(2413);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
}
- 测试程序
package com.powernode.spring6.test;
import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.OrderServiceProxy;
import com.powernode.mall.service.impl.OrderServiceImpl;
import com.powernode.mall.service.impl.OrderServiceImplSub;
import org.junit.Test;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.ProxyModeTest
* @date 2022/11/22
* @since 1.0
*/
public class ProxyModeTest {
@Test
public void testImpl(){
OrderService target = new OrderServiceImpl();
target.generate();
target.detail();
target.modify();
}
}
- 测试结果
需求可以满足,但显然是违背了OCP开闭原则。这种方案不可取。
第二种方案:编写一个子类继承OrderServiceImpl,在子类中重写每个方法,代码如下:
- OrderServiceImpl的子类: com.powernode.mall.service.impl.OrderServiceImplSub
package com.powernode.mall.service.impl;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.impl.OrderServiceImplSub
* @date 2022/11/22
* @since 1.0
*/
public class OrderServiceImplSub extends OrderServiceImpl{
@Override
public void generate() {
long begin = System.currentTimeMillis();
super.generate();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
super.detail();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
super.modify();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
}
- 测试程序
@Test
public void testImplSub(){
OrderService target = new OrderServiceImplSub();
target.generate();
target.detail();
target.modify();
}
- 测试结果
这种方式可以解决,但是存在两个问题:
- 第一个问题:假设系统中有100个这样的业务类,需要提供100个子类,并且之前写好的创建Service对象的代码,都要修改为创建子类对象。
- 第二个问题:由于采用了继承的方式,导致代码之间的耦合度较高。
这种方案也不可取。
第三种方案:使用代理模式(这里采用静态代理)
可以为OrderService接口提供一个代理类。
- com.powernode.mall.service.OrderServiceProxy
package com.powernode.mall.service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.OrderServiceProxy
* @date 2022/11/22
* @since 1.0
*/
public class OrderServiceProxy implements OrderService{
// 目标对象
private OrderService orderService;
// 通过构造方法将目标对象传递给代理对象
public OrderServiceProxy(OrderService orderService) {
this.orderService = orderService;
}
@Override
public void generate() {
long begin = System.currentTimeMillis();
orderService.generate();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void detail() {
long begin = System.currentTimeMillis();
orderService.detail();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
@Override
public void modify() {
long begin = System.currentTimeMillis();
orderService.modify();
long end = System.currentTimeMillis();
System.out.println("耗费时长为" + (end - begin) + "ms");
}
}
这种方式的优点:符合OCP开闭原则,同时采用的是关联关系,所以程序的耦合度较低。所以这种方案是被推荐的。
- 测试程序
@Test
public void testProxy(){
// 创建目标对象
OrderService target = new OrderServiceImpl();
// 创建代理对象
OrderService proxy = new OrderServiceProxy(target);
// 调用代理对象的代理方法
proxy.generate();
proxy.detail();
proxy.modify();
}
- 测试结果
以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。
大家思考一下:如果系统中业务接口很多,一个接口对应一个代理类,显然也是不合理的,会导致类爆炸。怎么解决这个问题?动态代理可以解决。因为在动态代理中可以在内存中动态的为我们生成代理类的字节码。代理类不需要我们写了。类爆炸解决了,而且代码只需要写一次,代码也会得到复用。
14.3 动态代理
在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。
在内存当中动态生成类的技术常见的包括:
- JDK动态代理技术:只能代理接口。
- CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。)
- Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。
14.3.1 JDK动态代理
我们还是使用静态代理中的例子:一个接口和一个实现类。
- com.powernode.mall.service.OrderService
package com.powernode.mall.service;
/**
* 订单接口
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.OrderService
* @date 2022/11/22
* @since 1.0
*/
public interface OrderService {
/**
* 生成订单
*/
void generate();
/**
* 查看订单详情
*/
void detail();
/**
* 修改订单
*/
void modify();
}
- OrderService接口的实现类: com.powernode.mall.service.impl.OrderServiceImpl
package com.powernode.mall.service.impl;
import com.powernode.mall.service.OrderService;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.impl.OrderServiceImpl
* @date 2022/11/22
* @since 1.0
*/
public class OrderServiceImpl implements OrderService {
@Override
public void generate() {
try {
Thread.sleep(1234);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已生成");
}
@Override
public void detail() {
try {
Thread.sleep(2413);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单信息如下:******");
}
@Override
public void modify() {
try {
Thread.sleep(1010);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("订单已修改");
}
}
其中Thread.sleep()方法的调用是为了模拟操作耗时。
我们在静态代理的时候,除了以上一个接口和一个实现类之外,是不是要写一个代理类UserServiceProxy呀!在动态代理中UserServiceProxy代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可
- 客户端程序
package com.powernode.mall;
import com.powernode.mall.service.OrderService;
import com.powernode.mall.service.impl.OrderServiceImpl;
import java.lang.reflect.Proxy;
/**
* @author 动力节点
* @version 1.0
* @className Client
* @since 1.0
**/
public class Client {
public static void main(String[] args) {
// 第一步:创建目标对象
OrderService target = new OrderServiceImpl();
// 第二步:创建代理对象
OrderService orderServiceProxy = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), 调用处理器对象);
// 第三步:调用代理对象的代理方法
orderServiceProxy.detail();
orderServiceProxy.modify();
orderServiceProxy.generate();
}
}
以上第二步创建代理对象是需要大家理解的:
OrderService orderServiceProxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
调用处理器对象);
这行代码做了两件事:
- 第一件事:在内存中生成了代理类的字节码
- 第二件事:创建代理对象
Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。
其中newProxyInstance()方法有三个参数:
- 第一个参数:类加载器。在内存中生成了字节码,要想执行这个字节码,也是需要先把这个字节码加载到内存当中的。所以要指定使用哪个类加载器加载。
- 第二个参数:接口类型。代理类和目标类实现相同的接口,所以要通过这个参数告诉JDK动态代理生成的类要实现哪些接口。
- 第三个参数:调用处理器。这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。
所以接下来我们要写一下java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:
- TimerInvocationHandler
package com.powernode.mall.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author 动力节点
* @version 1.0
* @className TimerInvocationHandler
* @since 1.0
**/
public class TimerInvocationHandler implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return null;
}
}
InvocationHandler接口中有一个方法invoke,这个invoke方法上有三个参数:
- 第一个参数:Object proxy。代理对象。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。
- 第二个参数:Method method。目标方法。
- 第三个参数:Object[] args。目标方法调用时要传的参数。
我们将来肯定是要调用“目标方法”的,但要调用目标方法的话,需要“目标对象”的存在,“目标对象”从哪儿来呢?我们可以给TimerInvocationHandler提供一个构造方法,可以通过这个构造方法传过来“目标对象”,代码如下:
- com.powernode.mall.service.TimerInvocationHandler
package com.powernode.mall.service;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.TimerInvocationHandler
* @date 2022/11/22
* @since 1.0
*/
public class TimerInvocationHandler implements InvocationHandler {
// 目标对象
private Object target;
// 通过构造方法来传目标对象
public TimerInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 目标执行之前增强
long begin = System.currentTimeMillis();
// 调用目标对象的目标方法
Object reValue = method.invoke(target, args);
// 目标执行之后增强
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "ms");
// 一定要记得返回!
return reValue;
}
}
- 测试程序
@Test
public void testDynamicProxy(){
// 第一步:创建目标对象
OrderService target = new OrderServiceImpl();
// 第二步:创建代理对象
OrderService orderServiceProxy = (OrderService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));
// 第三步:调用代理对象的代理方法
orderServiceProxy.generate();
orderServiceProxy.detail();
orderServiceProxy.modify();
}
大家可能会比较好奇:那个InvocationHandler接口中的invoke()方法没看见在哪里调用呀?
注意:当你调用代理对象的代理方法的时候,注册在InvocationHandler接口中的invoke()方法会被调用。也就是上面代码第10 11 12行,这三行代码中任意一行代码执行,注册在InvocationHandler接口中的invoke()方法都会被调用。
- 测试结果
学到这里可能会感觉有点懵,折腾半天,到最后这不是还得写一个接口的实现类吗?没省劲儿呀?
你要这样想就错了!!!
我们可以看到,不管你有多少个Service接口,多少个业务类,这个TimerInvocationHandler接口是不是只需要写一次就行了,代码是不是得到复用了!!!!
而且最重要的是,以后程序员只需要关注核心业务的编写了,像这种统计时间的代码根本不需要关注。因为这种统计时间的代码只需要在调用处理器中编写一次即可。
到这里,JDK动态代理的原理就结束了。
不过我们看以下这个代码确实有点繁琐,对于客户端来说,用起来不方便:
我们可以提供一个工具类:ProxyUtil,封装一个方法:
- com.powernode.mall.util.ProxyUtil
package com.powernode.mall.util;
import com.powernode.mall.service.TimerInvocationHandler;
import java.lang.reflect.Proxy;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.util.ProxyUtil
* @date 2022/11/22
* @since 1.0
*/
public class ProxyUtil {
/**
* 返回方法执行计时器代理对象
* @param target
* @return
*/
public static Object newProxyInstance(Object target){
return Proxy.newProxyInstance(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TimerInvocationHandler(target));
}
}
- 测试程序
@Test
public void testDynamicProxy(){
// 第一步:创建目标对象
OrderService target = new OrderServiceImpl();
// 第二步:创建代理对象
OrderService orderServiceProxy = (OrderService) ProxyUtil.newProxyInstance(target);
// 第三步:调用代理对象的代理方法
orderServiceProxy.generate();
orderServiceProxy.detail();
orderServiceProxy.modify();
}
- 测试结果
14.3.2 CGLIB动态代理
CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
使用CGLIB,需要引入它的依赖:
- pom.xml
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
我们准备一个没有实现接口的类,如下:
- com.powernode.mall.service.UserService
package com.powernode.mall.service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.UserService
* @date 2022/11/22
* @since 1.0
*/
public class UserService {
public void login(){
System.out.println("用户正在登录系统......");
}
public void logout(){
System.out.println("用户正在退出系统......");
}
}
使用CGLIB在内存中为UserService类生成代理类,并创建对象:
- 测试程序
@Test
public void testCGLIBDynamicProxy(){
// 创建字节码增强器
Enhancer enhancer = new Enhancer();
// 告诉cglib要继承哪个类
enhancer.setSuperclass(UserService.class);
// 设置回调接口
enhancer.setCallback(方法拦截器对象);
// 生成源码,编译class,加载到JVM,并创建代理对象
UserService userServiceProxy = (UserService) enhancer.create();
userServiceProxy.login();
userServiceProxy.logout();
}
和JDK动态代理原理差不多,在CGLIB中需要提供的不是InvocationHandler,而是:net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:
- com.powernode.mall.service.TimerMethodInterceptor
package com.powernode.mall.service;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.TimerMethodInterceptor
* @date 2022/11/22
* @since 1.0
*/
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return null;
}
}
MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:
第一个参数:目标对象
第二个参数:目标方法
第三个参数:目标方法调用时的实参
第四个参数:代理方法
在MethodInterceptor的intercept()方法中调用目标以及添加增强:
- com.powernode.mall.service.TimerMethodInterceptor
package com.powernode.mall.service;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.mall.service.TimerMethodInterceptor
* @date 2022/11/22
* @since 1.0
*/
public class TimerMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 前增强
long begin = System.currentTimeMillis();
// 调用目标
Object reValue = methodProxy.invokeSuper(target, objects);
// 后增强
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "ms");
// 一定要返回!
return reValue;
}
}
回调已经写完了,可以修改客户端程序了:
- 测试程序
@Test
public void testCGLIBDynamicProxy(){
// 创建字节码增强器
Enhancer enhancer = new Enhancer();
// 告诉cglib要继承哪个类
enhancer.setSuperclass(UserService.class);
// 设置回调接口
enhancer.setCallback(new TimerMethodInterceptor());
// 生成源码,编译class,加载到JVM,并创建代理对象
UserService userServiceProxy = (UserService) enhancer.create();
userServiceProxy.login();
userServiceProxy.logout();
}
对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:
- –add-opens java.base/java.lang=ALL-UNNAMED
- –add-opens java.base/sun.net.util=ALL-UNNAMED
本人使用idea 2022.2.3,直接点测试方法左边的执行按钮是会报错的,配置界面如下,仅修改Junit的VM option参数为上述第一个参数,apply后点击右上的run按钮执行可以成功运行不报错
- 测试结果
十五、面向切面编程AOP
IoC使软件组件松耦合。AOP让你能够捕捉系统中经常使用的功能,把它转化成组件。
AOP(Aspect Oriented Programming):面向切面编程,面向方面编程。(AOP是一种编程技术)
AOP是对OOP的补充延伸。
AOP底层使用的就是动态代理来实现的。
Spring的AOP使用的动态代理是:JDK动态代理 + CGLIB动态代理技术。Spring在这两种动态代理中灵活切换:
- 如果是代理接口,会默认使用JDK动态代理,
- 如果要代理某个类,这个类没有实现接口,就会切换使用CGLIB。
- 当然,你也可以强制通过一些配置让Spring只使用CGLIB。
15.1 AOP介绍
一般一个系统当中都会有一些系统服务,例如:日志、事务管理、安全等。这些系统服务被称为:交叉业务
这些交叉业务几乎是通用的,不管你是做银行账户转账,还是删除用户数据。日志、事务管理、安全,这些都是需要做的。
如果在每一个业务处理过程当中,都掺杂这些交叉业务代码进去的话,存在两方面问题:
- 第一:交叉业务代码在多个业务流程中反复出现,显然这个交叉业务代码没有得到复用。并且修改这些交叉业务代码的话,需要修改多处。
- 第二:程序员无法专注核心业务代码的编写,在编写核心业务代码的同时还需要处理这些交叉业务。
使用AOP可以很轻松的解决以上问题。
请看下图,可以帮助你快速理解AOP的思想:
用一句话总结AOP:将与核心业务无关的代码独立的抽取出来,形成一个独立的组件,然后以横向交叉的方式应用到业务流程当中的过程被称为AOP。
AOP的优点:
- 第一:代码复用性增强。
- 第二:代码易维护。
- 第三:使开发者更关注业务逻辑。
15.2 AOP的七大术语
- com.powernode.spring6.service.UserService
package com.powernode.spring6.service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.UserService
* @date 2022/11/23
* @since 1.0
*/
public class UserService {
public void do1(){
System.out.println("do 1");
}
public void do2(){
System.out.println("do 2");
}
public void do3(){
System.out.println("do 3");
}
public void do4(){
System.out.println("do 4");
}
public void do5(){
System.out.println("do 5");
}
public void service(){
do1();
do2();
do3();
do4();
do5();
}
}
-
连接点 Joinpoint
- 在程序的整个执行流程中,可以织入切面的位置。方法的执行前后,异常抛出之后等位置。
-
切点 Pointcut
- 在程序执行流程中,真正织入切面的方法。(一个切点对应多个连接点)
-
通知 Advice
-
通知又叫增强,就是具体你要织入的代码。
-
通知包括:
- 前置通知
- 后置通知
- 环绕通知
- 异常通知
- 最终通知
-
-
切面 Aspect
- 切点 + 通知就是切面。
-
织入 Weaving
- 把通知应用到目标对象上的过程。
-
代理对象 Proxy
- 一个目标对象被织入通知后产生的新对象。
-
目标对象 Target
- 被织入通知的对象。
通过下图,大家可以很好的理解AOP的相关术语:
15.3 切点表达式
切点表达式用来定义通知(Advice)往哪些方法上切入。
切入点表达式语法格式:
execution([访问控制权限修饰符] 返回值类型 [全限定类名]方法名(形式参数列表) [异常])
访问控制权限修饰符:
- 可选项。
- 没写,就是4个权限都包括。
- 写public就表示只包括公开的方法。
返回值类型:
- 必填项。
- * 表示返回值类型任意。
全限定类名:
- 可选项。
- 两个点“..”代表当前包以及子包下的所有类。
- 省略时表示所有的类。
方法名:
- 必填项。
- *表示所有方法。
- set*表示所有的set方法。
形式参数列表:
- 必填项
- () 表示没有参数的方法
- (..) 参数类型和个数随意的方法
- (*) 只有一个参数的方法
- (*, String) 第一个参数类型随意,第二个参数是String的。
异常:
- 可选项。
- 省略时表示任意异常类型。
理解以下的切点表达式:
- service包下所有的类中以delete开始的所有方法
execution(public * com.powernode.mall.service.*.delete*(..))
- mall包下所有的类的所有的方法
execution(* com.powernode.mall..*(..))
- 所有类的所有方法
execution(* *(..))
15.4 使用Spring的AOP
Spring对AOP的实现包括以下3种方式:
- 第一种方式:Spring框架结合AspectJ框架实现的AOP,基于注解方式。
- 第二种方式:Spring框架结合AspectJ框架实现的AOP,基于XML方式。
- 第三种方式:Spring框架自己实现的AOP,基于XML配置方式。
实际开发中,都是Spring+AspectJ来实现AOP。所以我们重点学习第一种和第二种方式。
什么是AspectJ?(Eclipse组织的一个支持AOP的框架。AspectJ框架是独立于Spring框架之外的一个框架,Spring框架用了AspectJ)
AspectJ项目起源于帕洛阿尔托(Palo Alto)研究中心(缩写为PARC)。该中心由Xerox集团资助,Gregor Kiczales领导,从1997年开始致力于AspectJ的开发,1998年第一次发布给外部用户,2001年发布1.0 release。为了推动AspectJ技术和社团的发展,PARC在2003年3月正式将AspectJ项目移交给了Eclipse组织,因为AspectJ的发展和受关注程度大大超出了PARC的预期,他们已经无力继续维持它的发展。
15.4.1 准备工作
使用Spring+AspectJ的AOP需要引入的依赖如下:
- pom.xml
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aop依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring aspects依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
Spring配置文件中添加context命名空间和aop命名空间
- spring.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 http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
</beans>
15.4.2 基于AspectJ的AOP注解式开发
实现步骤
第一步:定义目标类以及目标方法
- 目标类:com.powernode.spring6.service.OrderService
package com.powernode.spring6.service;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.OrderService
* @date 2022/11/23
* @since 1.0
*/
@Service
// 目标类
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
}
}
第二步:定义切面类
- 切面类:com.powernode.spring6.service.MyAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
public class MyAspect {
}
第三步:目标类和切面类都纳入spring bean管理
在目标类OrderService上添加@Component注解。
在切面类MyAspect类上添加@Component注解。
第四步:在spring配置文件中添加组建扫描
- spring.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 http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
</beans>
第五步:在切面类中添加通知
- com.powernode.spring6.service.MyAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
public class MyAspect {
// 这就是需要增强的代码(Advice,通知)
public void advice(){
System.out.println("我是一个通知!");
}
}
第六步:在通知上添加切点表达式
- 通知+切点=切面:
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
public class MyAspect {
// 这就是需要增强的代码(Advice,通知)
// 切点表达式
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void advice(){
System.out.println("我是一个通知!");
}
}
注解@Before表示前置通知。
第七步:在spring配置文件中启用自动代理
- spring.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 http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--开启组件扫描-->
<context:component-scan base-package="com.powernode.spring6.service"/>
<!--开启自动代理-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
<aop:aspectj-autoproxy proxy-target-class="true"/>
开启自动代理之后,凡事带有@Aspect注解的bean都会生成代理对象。
proxy-target-class="true"
表示采用cglib动态代理。
proxy-target-class="false"
表示采用jdk动态代理。默认值是false。即使写成false,当没有接口的时候,也会自动选择cglib生成代理类。
- 测试程序:
package com.powernode.spring6.test;
import com.powernode.spring6.service.OrderService;
import com.powernode.spring6.service.Spring6Configuration;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.AOPTest
* @date 2022/11/23
* @since 1.0
*/
public class AOPTest {
@Test
public void testAOP(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
}
- 执行结果
通知类型
通知类型包括:
- 前置通知:@Before 目标方法执行之前的通知
- 后置通知:@AfterReturning 目标方法执行之后的通知
- 环绕通知:@Around 目标方法之前添加通知,同时目标方法执行之后添加通知。
- 异常通知:@AfterThrowing 发生异常之后执行的通知
- 最终通知:@After 放在finally语句块中的通知
接下来,编写程序来测试这几个通知的执行顺序:
- com.powernode.spring6.service.MyAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
public class MyAspect {
// 这就是需要增强的代码(Advice,通知)
// 切点表达式
/*@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void advice(){
System.out.println("我是一个通知!");
}*/
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
- 测试结果
通过上面的执行结果就可以判断他们的执行顺序了,这里不再赘述。
结果中没有异常通知,这是因为目标程序执行过程中没有发生异常。我们尝试让目标方法发生异常:
- 让目标方法执行过程中发生异常: com.powernode.spring6.service.OrderService
package com.powernode.spring6.service;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.OrderService
* @date 2022/11/23
* @since 1.0
*/
@Service
// 目标类
public class OrderService {
// 目标方法
public void generate(){
System.out.println("订单已生成!");
if (1 == 1) {
throw new RuntimeException("模拟异常发生!");
}
}
}
- 测试结果
通过测试得知,当发生异常之后,最终通知也会执行,因为最终通知@After会出现在finally语句块中。
出现异常之后,后置通知和环绕通知的结束部分不会执行。
切面的先后顺序
我们知道,业务流程当中不一定只有一个切面,可能有的切面控制事务,有的记录日志,有的进行安全控制,如果多个切面的话,顺序如何控制:可以使用@Order注解来标识切面类,为@Order注解的value指定一个整数型的数字,数字越小,优先级越高。
再定义一个切面类,如下:
- 另一个切面类,并设置优先级:com.powernode.spring6.service.YourAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.YourAspect
* @date 2022/11/23
* @since 1.0
*/
//@Aspect
@Component
// 设置优先级
@Order(1)
public class YourAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("YourAspect环绕通知开始");
// 执行目标方法
proceedingJoinPoint.proceed();
System.out.println("YourAspect环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("YourAspect前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturingAdvice(){
System.out.println("YourAspect后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("YourAspect异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("YourAspect最终通知");
}
}
- 设置切面类MyAspect的优先级:
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
// 设置优先级
@Order(2)
public class MyAspect {
// 这就是需要增强的代码(Advice,通知)
// 切点表达式
/*@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void advice(){
System.out.println("我是一个通知!");
}*/
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
- 测试结果
通过修改@Order注解的整数值来切换顺序,执行测试程序:
优化使用切点表达式
观看以下代码中的切点表达式:
- com.powernode.spring6.service.MyAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
@Aspect
@Order(2)
public class MyAspect {
@Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void beforeAdvice(){
System.out.println("前置通知");
}
@AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
@AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
@After("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void afterAdvice(){
System.out.println("最终通知");
}
}
缺点是:
- 第一:切点表达式重复写了多次,没有得到复用。
- 第二:如果要修改切点表达式,需要修改多处,难维护。
可以这样做:将切点表达式单独的定义出来,在需要的位置引入即可。如下:
- com.powernode.spring6.service.MyAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.MyAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
// 切面类
@Aspect
// 设置优先级
@Order(1)
public class MyAspect {
// 这就是需要增强的代码(Advice,通知)
// 切点表达式
/*@Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void advice(){
System.out.println("我是一个通知!");
}*/
@Pointcut("execution(* com.powernode.spring6.service.OrderService.*(..))")
public void pointcut(){}
// @Around("execution(* com.powernode.spring6.service.OrderService.*(..))")
@Around("pointcut()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("环绕通知开始");
// 执行目标方法
proceedingJoinPoint.proceed();
System.out.println("环绕通知结束");
}
// @Before("execution(* com.powernode.spring6.service.OrderService.*(..))")
@Before("pointcut()")
public void beforeAdvice(){
System.out.println("前置通知");
}
// @AfterReturning("execution(* com.powernode.spring6.service.OrderService.*(..))")
@AfterReturning("pointcut()")
public void afterReturningAdvice(){
System.out.println("后置通知");
}
// @AfterThrowing("execution(* com.powernode.spring6.service.OrderService.*(..))")
@AfterThrowing("pointcut()")
public void afterThrowingAdvice(){
System.out.println("异常通知");
}
// @After("execution(* com.powernode.spring6.service.OrderService.*(..))")
@After("pointcut()")
public void afterAdvice(){
System.out.println("最终通知");
}
}
使用@Pointcut注解来定义独立的切点表达式。
注意这个@Pointcut注解标注的方法随意,只是起到一个能够让@Pointcut注解编写的位置。
- 测试结果
全注解式开发AOP
就是编写一个类,在这个类上面使用大量注解来代替spring的配置文件,spring配置文件消失了,如下:
- com.powernode.spring6.service.Spring6Configuration
package com.powernode.spring6.service;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.Spring6Configuration
* @date 2022/11/23
* @since 1.0
*/
@Configuration
@ComponentScan("com.powernode.spring6.service")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class Spring6Configuration {
}
- 测试程序也变化了:
@Test
public void testAOPWithAllAnnotation(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
orderService.generate();
}
15.4.3 基于XML配置方式的AOP(了解)
第一步:编写目标类
- 不添加@Component注解:com.powernode.spring6.service.VipService
package com.powernode.spring6.service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.VipService
* @date 2022/11/23
* @since 1.0
*/
// 目标类
public class VipService {
public void add(){
System.out.println("保存vip信息......");
}
}
第二步:编写切面类,并且编写通知
- 不添加@Component注解: com.powernode.spring6.service.TimeAspect
package com.powernode.spring6.service;
import org.aspectj.lang.ProceedingJoinPoint;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.service.TimeAspect
* @date 2022/11/23
* @since 1.0
*/
// 负责计时的切面类
public class TimeAspect {
public void time(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
long begin = System.currentTimeMillis();
// 执行目标
proceedingJoinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("耗时" + (end - begin) + "ms");
}
}
第三步:编写spring配置文件
- spring-aop-xml.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: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">
<!--纳入spring Bean管理-->
<bean id="vipService" class="com.powernode.spring6.service.VipService"/>
<bean id="timeAspect" class="com.powernode.spring6.service.TimeAspect"/>
<!--aop配置-->
<aop:config>
<!--切点表达式-->
<aop:pointcut id="p" expression="execution(* com.powernode.spring6.service.VipService.*(..))"/>
<!--切面-->
<aop:aspect ref="timeAspect">
<!--切面 = 通知 + 切点-->
<aop:around method="time" pointcut-ref="p"/>
</aop:aspect>
</aop:config>
</beans>
- 测试程序
@Test
public void testAOPXml(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-aop-xml.xml");
VipService vipService = applicationContext.getBean("vipService", VipService.class);
vipService.add();
}
- 测试结果
15.5 AOP的实际案例:事务处理
项目中的事务控制是在所难免的。在一个业务流程当中,可能需要多条DML语句共同完成,为了保证数据的安全,这多条DML语句要么同时成功,要么同时失败。这就需要添加事务控制的代码。例如以下伪代码:
- 伪代码
class 业务类1{
public void 业务方法1(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法2(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法3(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
}
class 业务类2{
public void 业务方法1(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法2(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
public void 业务方法3(){
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
step1();
step2();
step3();
....
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
}
}
//......
可以看到,这些业务类中的每一个业务方法都是需要控制事务的,而控制事务的代码又是固定的格式,都是:
try{
// 开启事务
startTransaction();
// 执行核心业务逻辑
//......
// 提交事务
commitTransaction();
}catch(Exception e){
// 回滚事务
rollbackTransaction();
}
这个控制事务的代码就是和业务逻辑没有关系的“交叉业务”。以上伪代码当中可以看到这些交叉业务的代码没有得到复用,并且如果这些交叉业务代码需要修改,那必然需要修改多处,难维护,怎么解决?可以采用AOP思想解决。可以把以上控制事务的代码作为环绕通知,切入到目标类的方法当中。接下来我们做一下这件事,有两个业务类,如下:
- 订单业务类: com.powernode.spring6.biz.OrderService
package com.powernode.spring6.biz;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.OrderService
* @date 2022/11/23
* @since 1.0
*/
@Service
// 业务类
public class OrderService {
// 生成订单
public void generate(){
System.out.println("正在生成订单......");
}
// 取消订单
public void cancel(){
System.out.println("正在取消订单......");
}
}
- 银行账户的业务类: com.powernode.spring6.biz.AccountService
package com.powernode.spring6.biz;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.AccountService
* @date 2022/11/23
* @since 1.0
*/
@Service
public class AccountService {
// 转账业务方法
public void transfer(){
System.out.println("正在进行银行账户转账");
}
// 取款业务方法
public void withdraw(){
System.out.println("正在进行取款操作");
}
}
注意,以上两个业务类已经纳入spring bean的管理,因为都添加了@Component注解。
接下来我们给以上两个业务类的4个方法添加事务控制代码,使用AOP来完成:
- 事务切面类: com.powernode.spring6.biz.TransactionAspect
package com.powernode.spring6.biz;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.TransactionAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
@Aspect
public class TransactionAspect {
@Around("execution(* com.powernode.spring6.biz..*(..))")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
try {
System.out.println("开启事务");
// 执行目标
proceedingJoinPoint.proceed();
System.out.println("提交事务");
} catch (Throwable e) {
System.out.println("回滚事务");
e.printStackTrace();
}
}
}
你看,这个事务控制代码是不是只需要写一次就行了,并且修改起来也没有成本。
- spring-transaction.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 http://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="com.powernode.spring6.biz"/>
<aop:aspectj-autoproxy proxy-target-class="true"/>
</beans>
- 编写测试程序:
package com.powernode.spring6.test;
import com.powernode.spring6.biz.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.powernode.spring6.biz.OrderService;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.AOPTest2
* @date 2022/11/23
* @since 1.0
*/
public class AOPTest2 {
@Test
public void testTransaction(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-transaction.xml");
OrderService orderService = applicationContext.getBean("orderService", OrderService.class);
// 生成订单
orderService.generate();
// 取消订单
orderService.cancel();
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
// 转账
accountService.transfer();
// 取款
accountService.withdraw();
}
}
- 测试结果
通过测试可以看到,所有的业务方法都添加了事务控制的代码。
15.6 AOP的实际案例:安全日志
需求是这样的:项目开发结束了,已经上线了。运行正常。客户提出了新的需求:凡事在系统中进行修改操作的,删除操作的,新增操作的,都要把这个人记录下来。因为这几个操作是属于危险行为。例如有业务类和业务方法:
- 用户业务类:com.powernode.spring6.biz.UserService
package com.powernode.spring6.biz;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.UserService
* @date 2022/11/23
* @since 1.0
*/
@Service
// 用户业务
public class UserService {
public void getUser(){
System.out.println("获取用户信息");
}
public void saveUser(){
System.out.println("保存用户");
}
public void deleteUser(){
System.out.println("删除用户");
}
public void modifyUser(){
System.out.println("修改用户");
}
}
- 商品业务类:com.powernode.spring6.biz.ProductService
package com.powernode.spring6.biz;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.ProductService
* @date 2022/11/23
* @since 1.0
*/
@Service
// 商品业务
public class ProductService {
public void getProduct(){
System.out.println("获取商品信息");
}
public void saveProduct(){
System.out.println("保存商品");
}
public void deleteProduct(){
System.out.println("删除商品");
}
public void modifyProduct(){
System.out.println("修改商品");
}
}
注意:已经添加了@Component注解。
接下来我们使用aop来解决上面的需求:编写一个负责安全的切面类
- com.powernode.spring6.biz.SecurityAspect
package com.powernode.spring6.biz;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.biz.SecurityAspect
* @date 2022/11/23
* @since 1.0
*/
@Component
@Aspect
public class SecurityAspect {
@Pointcut("execution(* com.powernode.spring6.biz..save*(..))")
public void savePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..delete*(..))")
public void deletePointcut(){}
@Pointcut("execution(* com.powernode.spring6.biz..modify*(..))")
public void modifyPointcut(){}
@Before("savePointcut() || deletePointcut() || modifyPointcut()")
public void beforeAdvice(JoinPoint joinPoint){
System.out.println("xxx操作员正在操作" + joinPoint.getSignature().getName() + "方法");
}
}
- 测试程序(记得把上一个切面类注释掉)
@Test
public void testSecurity(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-transaction.xml");
UserService userService = applicationContext.getBean("userService", UserService.class);
userService.getUser();
userService.saveUser();
userService.deleteUser();
userService.modifyUser();
ProductService productService = applicationContext.getBean("productService", ProductService.class);
productService.getProduct();
productService.saveProduct();
productService.deleteProduct();
productService.modifyProduct();
}
- 测试结果
十六、Spring对事务的支持
16.1 事务概述
-
什么是事务
-
- 在一个业务流程当中,通常需要多条DML(insert delete update)语句共同联合才能完成,这多条DML语句必须同时成功,或者同时失败,这样才能保证数据的安全。
- 多条DML要么同时成功,要么同时失败,这叫做事务。
- 事务:Transaction(tx)
-
事务的四个处理过程:
-
- 第一步:开启事务 (start transaction)
- 第二步:执行核心业务代码
- 第三步:提交事务(如果核心业务处理过程中没有出现异常)(commit transaction)
- 第四步:回滚事务(如果核心业务处理过程中出现异常)(rollback transaction)
-
事务的四个特性:
-
- A 原子性:事务是最小的工作单元,不可再分。
- C 一致性:事务要求要么同时成功,要么同时失败。事务前和事务后的总量不变。
- I 隔离性:事务和事务之间因为有隔离性,才可以保证互不干扰。
- D 持久性:持久性是事务结束的标志。
16.2 引入事务场景
以银行账户转账为例学习事务。两个账户act-001和act-002。act-001账户向act-002账户转账10000,必须同时成功,或者同时失败。(一个减成功,一个加成功, 这两条update语句必须同时成功,或同时失败。)
连接数据库的技术采用Spring框架的JdbcTemplate。
采用三层架构搭建:
模块名:spring6-014-tx-bank(依赖如下)
- pom.xml
<?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>com.powernode</groupId>
<artifactId>spring6-014-tx-bank</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<!--依赖-->
<dependencies>
<!--spring context-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring jdbc-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<!--德鲁伊连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<!--@Resource注解-->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>2.1.1</version>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
第一步:准备数据库表
表结构:
表数据:
第二步:创建包结构
com.powernode.bank.pojo
com.powernode.bank.service
com.powernode.bank.service.impl
com.powernode.bank.dao
com.powernode.bank.dao.impl
第三步:准备POJO类
- com.powernode.bank.pojo.Account
package com.powernode.bank.pojo;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.pojo.Account
* @date 2022/11/23
* @since 1.0
*/
public class Account {
private String actno;
private Double balance;
@Override
public String toString() {
return "Account{" +
"actno='" + actno + '\'' +
", balance=" + balance +
'}';
}
public String getActno() {
return actno;
}
public void setActno(String actno) {
this.actno = actno;
}
public Double getBalance() {
return balance;
}
public void setBalance(Double balance) {
this.balance = balance;
}
public Account(String actno, Double balance) {
this.actno = actno;
this.balance = balance;
}
public Account() {
}
}
第四步:编写持久层
- com.powernode.bank.dao.AccountDao
package com.powernode.bank.dao;
import com.powernode.bank.pojo.Account;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.dao.AccountDao
* @date 2022/11/23
* @since 1.0
*/
public interface AccountDao {
/**
* 根据账号查询账户余额
* @param actno
* @return
*/
Account selectByActno(String actno);
/**
* 更新账户信息
* @param act
* @return
*/
int update(Account act);
}
- com.powernode.bank.dao.impl.AccountDaoImpl
package com.powernode.bank.dao.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.dao.impl.AccountDaoImpl
* @date 2022/11/23
* @since 1.0
*/
@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {
@Resource(name = "jdbcTemplate")
private JdbcTemplate jdbcTemplate;
@Override
public Account selectByActno(String actno) {
String sql = "select * from t_act where actno=?";
Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno);
return account;
}
@Override
public int update(Account act) {
String sql = "update t_act set balance=? where actno=?";
int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno());
return count;
}
}
第五步:编写业务层
- com.powernode.bank.service.AccountService
package com.powernode.bank.service;
import com.powernode.bank.pojo.Account;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.AccountService
* @date 2022/11/23
* @since 1.0
*/
public interface AccountService {
/**
* 转账
* @param fromActno
* @param toActno
* @param money
*/
void transfer(String fromActno, String toActno, double money);
}
- com.powernode.bank.service.impl.AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.AccountServiceImpl
* @date 2022/11/23
* @since 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足,无法转账!");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,未知原因!");
}
}
}
第六步:编写Spring配置文件
- spring.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">
<context:component-scan base-package="com.powernode.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
</beans>
第七步:编写表示层(测试程序)
- 测试程序
package com.powernode.spring6.test;
import com.powernode.bank.service.AccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.BanKTest
* @date 2022/11/23
* @since 1.0
*/
public class BanKTest {
@Test
public void testTransfer(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try{
accountService.transfer("act-002", "act-001", 10000);
System.out.println("转账成功!");
}catch (Exception e){
e.printStackTrace();
}
}
}
- 测试结果
模拟异常
- com.powernode.bank.service.impl.AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.AccountServiceImpl
* @date 2022/11/23
* @since 1.0
*/
@Service("accountService")
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足,无法转账!");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,未知原因!");
}
}
}
- 测试结果
丢了1万。
16.3 Spring对事务的支持
Spring实现事务的两种方式
-
编程式事务
-
- 通过编写代码的方式来实现事务的管理。
-
声明式事务
-
- 基于注解方式
- 基于XML配置方式
Spring事务管理API
Spring对事务的管理底层实现方式是基于AOP实现的。采用AOP的方式进行了封装。所以Spring专门针对事务开发了一套API,API的核心接口如下:
PlatformTransactionManager接口:spring事务管理器的核心接口。在Spring6中它有两个实现:
- DataSourceTransactionManager:支持JdbcTemplate、MyBatis、Hibernate等事务管理。
- JtaTransactionManager:支持分布式事务管理。
如果要在Spring6中使用JdbcTemplate,就要使用DataSourceTransactionManager来管理事务。(Spring内置写好了,可以直接用。)
声明式事务之注解实现方式
-
第一步:在spring配置文件中配置事务管理器。
- spring.xml
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>
-
第二步:在spring配置文件中引入tx命名空间。
- spring.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:tx="http://www.springframework.org/schema/tx"
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
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
-
第三步:在spring配置文件中配置“事务注解驱动器”,开始注解的方式控制事务。
- spring.xml
<tx:annotation-driven transaction-manager="transactionManager"/>
-
第四步:在service类上或方法上添加@Transactional注解
在类上添加该注解,该类中所有的方法都有事务。在某个方法上添加该注解,表示只有这个方法使用事务。
- com.powernode.bank.service.impl.AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.AccountServiceImpl
* @date 2022/11/23
* @since 1.0
*/
@Service("accountService")
@Transactional
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足,无法转账!");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,未知原因!");
}
}
}
当前数据库表中的数据:
- 测试结果
虽然出现异常了,再次查看数据库表中数据:
通过测试,发现数据没有变化,事务起作用了。
事务属性
事务属性包括哪些
事务中的重点属性:
- 事务传播行为
- 事务隔离级别
- 事务超时
- 只读事务
- 设置出现哪些异常回滚事务
- 设置出现哪些异常不回滚事务
事务传播行为
什么是事务的传播行为?
在service类中有a()方法和b()方法,a()方法上有事务,b()方法上也有事务,当a()方法执行过程中调用了b()方法,事务是如何传递的?合并到一个事务里?还是开启一个新的事务?这就是事务传播行为。
事务传播行为在spring框架中被定义为枚举类型:
一共有七种传播行为:
- REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
- SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
- MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
- REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
- NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
- NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
- NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
在代码中设置事务的传播行为:
@Transactional(propagation = Propagation.REQUIRED)
可以编写程序测试一下传播行为:
-
com.powernode.bank.dao.AccountDao
package com.powernode.bank.dao; import com.powernode.bank.pojo.Account; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.dao.AccountDao * @date 2022/11/23 * @since 1.0 */ public interface AccountDao { /** * 根据账号查询账户余额 * @param actno * @return */ Account selectByActno(String actno); /** * 更新账户信息 * @param act * @return */ int update(Account act); /** * 新增用户信息 * @param act * @return */ int insert(Account act); }
-
com.powernode.bank.dao.impl.AccountDaoImpl
package com.powernode.bank.dao.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import jakarta.annotation.Resource; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.dao.impl.AccountDaoImpl * @date 2022/11/23 * @since 1.0 */ @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource(name = "jdbcTemplate") private JdbcTemplate jdbcTemplate; @Override public Account selectByActno(String actno) { String sql = "select * from t_act where actno=?"; Account account = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Account.class), actno); return account; } @Override public int update(Account act) { String sql = "update t_act set balance=? where actno=?"; int count = jdbcTemplate.update(sql, act.getBalance(), act.getActno()); return count; } @Override public int insert(Account act) { String sql = "insert into t_act values(?, ?)"; int count = jdbcTemplate.update(sql, act.getActno(), act.getBalance()); return count; } }
-
com.powernode.bank.service.AccountService
package com.powernode.bank.service; import com.powernode.bank.pojo.Account; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.service.AccountService * @date 2022/11/23 * @since 1.0 */ public interface AccountService { /** * 转账 * @param fromActno * @param toActno * @param money */ void transfer(String fromActno, String toActno, double money); /** * 保存账户信息 * @param act */ void save(Account act); }
-
com.powernode.bank.service.impl.AccountServiceImpl
package com.powernode.bank.service.impl; import com.powernode.bank.dao.AccountDao; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.service.impl.AccountServiceImpl * @date 2022/11/23 * @since 1.0 */ @Service("accountService") //@Transactional public class AccountServiceImpl implements AccountService { @Resource(name = "accountDao") private AccountDao accountDao; @Override public void transfer(String fromActno, String toActno, double money) { // 查询账户余额是否充足 Account fromAct = accountDao.selectByActno(fromActno); if (fromAct.getBalance() < money) { throw new RuntimeException("账户余额不足,无法转账!"); } // 余额充足,开始转账 Account toAct = accountDao.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountDao.update(fromAct); // 模拟异常 String s = null; s.toString(); count += accountDao.update(toAct); if (count != 2) { throw new RuntimeException("转账失败,未知原因!"); } } @Resource(name = "accountService2") private AccountService accountService; @Override @Transactional(propagation = Propagation.REQUIRED) public void save(Account act) { // 调用dao的insert方法 // 保存act-003账户 accountDao.insert(act); // 创建账户对象 Account act2 = new Account("act-004", 1000.0); try{ accountService.save(act2); }catch (Exception e){ e.printStackTrace(); } } }
-
com.powernode.bank.service.impl.AccountServiceImpl2
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.AccountServiceImpl2
* @date 2022/11/23
* @since 1.0
*/
@Service("accountService2")
public class AccountServiceImpl2 implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
}
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
accountDao.insert(act);
}
}
- 测试程序
@Test
public void testPropagation(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 获取1号service对象
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
Account act = new Account("act-003", 1000.0);
accountService.save(act);
}
一定要集成Log4j2日志框架,在日志信息中可以看到更加详细的信息。
- 测试结果
删除新增数据,继续,在AccountServiceImpl2中模拟异常,查看是否可以新增成功
- com.powernode.bank.service.impl.AccountServiceImpl2
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
accountDao.insert(act);
// 模拟异常
String s = null;
s.toString();
// 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}
- 测试结果
可以看到发生异常后没有新增数据,那如果在前一个方法中捕获异常能否解决问题呢?
- com.powernode.bank.service.impl.AccountServiceImpl
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
// 调用dao的insert方法
// 保存act-003账户
accountDao.insert(act);
// 创建账户对象
Account act2 = new Account("act-004", 1000.0);
try{
accountService.save(act2);
}catch (Exception e){
e.printStackTrace();
}
}
- 测试结果
还是不行,那换成其他的传播行为可以解决异常问题吗?
将AccountServiceImpl2的传播行为修改为REQUIRES_NEW,先不模拟异常
- com.powernode.bank.service.impl.AccountServiceImpl2
@Override
// @Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
accountDao.insert(act);
// 模拟异常
// String s = null;
// s.toString();
// 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}
- 执行结果
新增数据成功,删除新增数据,那么我们再模拟异常,并在上一方法中捕捉
- com.powernode.bank.service.impl.AccountServiceImpl2
@Override
// @Transactional(propagation = Propagation.REQUIRED)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void save(Account act) {
accountDao.insert(act);
// 模拟异常
String s = null;
s.toString();
// 事儿没有处理完,这个大括号当中的后续也许还有其他的DML语句。
}
- com.powernode.bank.service.impl.AccountServiceImpl
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
// 调用dao的insert方法
// 保存act-003账户
accountDao.insert(act);
// 创建账户对象
Account act2 = new Account("act-004", 1000.0);
try{
accountService.save(act2);
}catch (Exception e){
e.printStackTrace();
}
// 继续往后进行我当前1号事务自己的事儿。
}
- 测试结果
异常仍然存在,从下图我们可以看到,两个不同的事务分别采用不同的连接对象,并都修改为手动提交
而在最后,仅有未发生异常的连接对象进行了提交
而数据库内的数据也仅有未发生异常的事务数据完成了新增,这就是REQUIRES_NEW传播行为的作用,其他传播行为等需要时再进行进一步学习
事务隔离级别
事务隔离级别类似于教室A和教室B之间的那道墙,隔离级别越高表示墙体越厚。隔音效果越好。
数据库中读取数据存在的三大问题:(三大读问题)
- 脏读:读取到没有提交到数据库的数据,叫做脏读。
- 不可重复读:在同一个事务当中,第一次和第二次读取的数据不一样。
- 幻读:读到的数据是假的。
事务隔离级别包括四个级别:
-
读未提交:READ_UNCOMMITTED
-
- 这种隔离级别,存在脏读问题,所谓的脏读(dirty read)表示能够读取到其它事务未提交的数据。
-
读提交:READ_COMMITTED
-
- 解决了脏读问题,其它事务提交之后才能读到,但存在不可重复读问题。
-
可重复读:REPEATABLE_READ
-
- 解决了不可重复读,可以达到可重复读效果,只要当前事务不结束,读取到的数据一直都是一样的。但存在幻读问题。
-
序列化:SERIALIZABLE
-
- 解决了幻读问题,事务排队执行。不支持并发。
大家可以通过一个表格来记忆:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交 | 有 | 有 | 有 |
读提交 | 无 | 有 | 有 |
可重复读 | 无 | 无 | 有 |
序列化 | 无 | 无 | 无 |
在Spring代码中如何设置隔离级别?
隔离级别在spring中以枚举类型存在:
@Transactional(isolation = Isolation.READ_COMMITTED)
测试事务隔离级别:READ_UNCOMMITTED 和 READ_COMMITTED
怎么测试:一个service负责插入,一个service负责查询。负责插入的service要模拟延迟。
READ_UNCOMMITTED :能够读取到其它事务未提交的数据。
- com.powernode.bank.service.impl.IsolationService1
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.IsolationService1
* @date 2022/11/23
* @since 1.0
*/
@Service("i1")
public class IsolationService1 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 1号
// 负责查询
// 当前事务可以读取到别的事务没有提交的数据
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public void getByActno(String actno){
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
- com.powernode.bank.service.impl.IsolationService2
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.IsolationService2
* @date 2022/11/23
* @since 1.0
*/
@Service("i2")
public class IsolationService2 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 2号
// 负责insert
@Transactional
public void save(Account act){
accountDao.insert(act);
// 睡眠一会儿
try {
Thread.sleep(1000 * 20);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 测试程序
@Test
public void testIsolation1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService1 i1 = applicationContext.getBean("i1", IsolationService1.class);
i1.getByActno("act-004");
}
@Test
public void testIsolation2(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
IsolationService2 i2 = applicationContext.getBean("i2", IsolationService2.class);
Account act = new Account("act-004", 1000.0);
i2.save(act);
}
- 测试结果
在事务提交之前,数据库还没有新增数据,但在i1中已经可以读到数据
READ_COMMITTED:其他事务提交之后才能读到
- com.powernode.bank.service.impl.IsolationService1
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.IsolationService1
* @date 2022/11/23
* @since 1.0
*/
@Service("i1")
public class IsolationService1 {
@Resource(name = "accountDao")
private AccountDao accountDao;
// 1号
// 负责查询
// 当前事务可以读取到别的事务没有提交的数据
// @Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 对方事务提交之后的数据我才能读到
@Transactional(isolation = Isolation.READ_COMMITTED)
public void getByActno(String actno){
Account account = accountDao.selectByActno(actno);
System.out.println("查询到的账户信息:" + account);
}
}
- 测试结果
在事务提交之前,数据库内没有新增数据,i1中也无法读到
通过执行结果可以清晰的看出隔离级别不同,执行效果不同。
事务超时
代码如下:
@Transactional(timeout = 10)
以上代码表示设置事务的超时时间为10秒。
表示超过10秒如果该事务中所有的DML语句还没有执行完毕的话,最终结果会选择回滚。
默认值-1,表示没有时间限制。
这里有个坑,事务的超时时间指的是哪段时间?
在当前事务当中,最后一条DML语句执行之前的时间。如果最后一条DML语句后面很有很多业务逻辑,这些业务代码执行的时间不被计入超时时间。
- 以下代码的超时不会被计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
accountDao.insert(act);
// 睡眠一会
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 以下代码超时时间会被计入超时时间
@Transactional(timeout = 10) // 设置事务超时时间为10秒。
public void save(Account act) {
// 睡眠一会
try {
Thread.sleep(1000 * 15);
} catch (InterruptedException e) {
e.printStackTrace();
}
accountDao.insert(act);
}
当然,如果想让整个方法的所有代码都计入超时时间的话,可以在方法最后一行添加一行无关紧要的DML语句。
只读事务
代码如下:
@Transactional(readOnly = true)
将当前事务设置为只读事务,在该事务执行过程中只允许select语句执行,delete insert update均不可执行。
该特性的作用是:启动spring的优化策略。提高select语句执行效率。
如果该事务中确实没有增删改操作,建议设置为只读事务。
设置哪些异常回滚事务
代码如下:
@Transactional(rollbackFor = RuntimeException.class)
表示只有发生RuntimeException异常或该异常的子类异常才回滚。
设置哪些异常不回滚事务
代码如下:
@Transactional(noRollbackFor = NullPointerException.class)
表示发生NullPointerException或该异常的子类异常不回滚,其他异常则回滚。
事务的全注解式开发
编写一个类来代替配置文件,代码如下:
- com.powernode.bank.Spring6Configuration
package com.powernode.bank;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.Spring6Configuration
* @date 2022/11/23
* @since 1.0
*/
@Configuration
@ComponentScan("com.powernode.bank")
@EnableTransactionManagement
public class Spring6Configuration {
@Bean
public DataSource getDataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/spring6");
dataSource.setUsername("root");
dataSource.setPassword("123");
return dataSource;
}
@Bean(name = "jdbcTemplate")
public JdbcTemplate getJdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
@Bean
public DataSourceTransactionManager getDataSourceTransactionManager(DataSource dataSource){
DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
dataSourceTransactionManager.setDataSource(dataSource);
return dataSourceTransactionManager;
}
}
- 测试程序
@Test
public void testNoXml(){
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(Spring6Configuration.class);
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try{
accountService.transfer("act-001", "act-002", 10000);
System.out.println("转账成功!");
}catch (Exception e){
e.printStackTrace();
}
}
- 测试结果
数据库表中数据:
说明事务配置成功
声明式事务之XML实现方式
配置步骤:
- 第一步:配置事务管理器
- 第二步:配置通知
- 第三步:配置切面
记得添加aspectj的依赖:
- pom.xml
<!--aspectj依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.0-M2</version>
</dependency>
Spring配置文件如下:
记得添加aop的命名空间。
- spirng-transaction.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:tx="http://www.springframework.org/schema/tx"
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 http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<context:component-scan base-package="com.powernode.bank"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/spring6"/>
<property name="username" value="root"/>
<property name="password" value="123"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="del*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="update*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
<tx:method name="transfer*" propagation="REQUIRED" rollback-for="java.lang.Throwable"/>
</tx:attributes>
</tx:advice>
<!--配置切面-->
<aop:config>
<aop:pointcut id="txPointcut" expression="execution(* com.powernode.bank.service..*(..))"/>
<!--切面 = 通知 + 切点-->
<aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>
</beans>
将AccountServiceImpl类上的@Transactional注解删除。
- com.powernode.bank.service.impl.AccountServiceImpl
package com.powernode.bank.service.impl;
import com.powernode.bank.dao.AccountDao;
import com.powernode.bank.pojo.Account;
import com.powernode.bank.service.AccountService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.bank.service.impl.AccountServiceImpl
* @date 2022/11/23
* @since 1.0
*/
@Service("accountService")
//@Transactional
public class AccountServiceImpl implements AccountService {
@Resource(name = "accountDao")
private AccountDao accountDao;
@Override
public void transfer(String fromActno, String toActno, double money) {
// 查询账户余额是否充足
Account fromAct = accountDao.selectByActno(fromActno);
if (fromAct.getBalance() < money) {
throw new RuntimeException("账户余额不足,无法转账!");
}
// 余额充足,开始转账
Account toAct = accountDao.selectByActno(toActno);
fromAct.setBalance(fromAct.getBalance() - money);
toAct.setBalance(toAct.getBalance() + money);
int count = accountDao.update(fromAct);
// 模拟异常
String s = null;
s.toString();
count += accountDao.update(toAct);
if (count != 2) {
throw new RuntimeException("转账失败,未知原因!");
}
}
@Resource(name = "accountService2")
private AccountService accountService;
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void save(Account act) {
// 调用dao的insert方法
// 保存act-003账户
accountDao.insert(act);
// 创建账户对象
Account act2 = new Account("act-004", 1000.0);
try{
accountService.save(act2);
}catch (Exception e){
e.printStackTrace();
}
}
}
- 测试程序
@Test
public void testTransferXml(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-transaction.xml");
AccountService accountService = applicationContext.getBean("accountService", AccountService.class);
try{
accountService.transfer("act-001", "act-002", 10000);
System.out.println("转账成功!");
}catch (Exception e){
e.printStackTrace();
}
}
- 测试结果
数据库表中记录:
通过测试可以看到配置XML已经起作用了。
十七、Spring6整合JUnit5
17.1 Spring对JUnit4的支持
准备工作:
- pom.xml
<?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>com.powernode</groupId>
<artifactId>spring6-015-junit</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit4依赖-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
- 声明Bean:com.powernode.spring6.bean.User
package com.powernode.spring6.bean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.bean.User
* @date 2022/11/23
* @since 1.0
*/
@Component
public class User {
@Value("张三")
private String name;
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User(String name) {
this.name = name;
}
public User() {
}
}
- spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<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:component-scan base-package="com.powernode.spring6.bean"/>
</beans>
单元测试
- 测试程序
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.SpringJUnit4Test
* @date 2022/11/23
* @since 1.0
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit4Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
- 测试结果
Spring提供的方便主要是这几个注解:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(“classpath:spring.xml”)
在单元测试类上使用这两个注解之后,在单元测试类中的属性上可以使用@Autowired。比较方便。
17.2 Spring对JUnit5的支持
引入JUnit5的依赖,Spring对JUnit支持的依赖还是:spring-test,如下:、
- pom.xml
<?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>com.powernode</groupId>
<artifactId>spring6-015-junit</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!--仓库-->
<repositories>
<!--spring里程碑版本的仓库-->
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<!--spring context依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--spring对junit的支持相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>6.0.0-M2</version>
</dependency>
<!--junit5依赖-->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
</project>
单元测试
- 测试程序
package com.powernode.spring6.test;
import com.powernode.spring6.bean.User;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* @author shanglinsong
* @version 1.0
* @className com.powernode.spring6.test.SpringJUnit5Test
* @date 2022/11/23
* @since 1.0
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration("classpath:spring.xml")
public class SpringJUnit5Test {
@Autowired
private User user;
@Test
public void testUser(){
System.out.println(user.getName());
}
}
- 测试结果
在JUnit5当中,可以使用Spring提供的以下两个注解,标注到单元测试类上,这样在类当中就可以使用@Autowired注解了。
@ExtendWith(SpringExtension.class)
@ContextConfiguration(“classpath:spring.xml”)
十八、Spring6集成MyBatis3.5
18.1 实现步骤
-
第一步:准备数据库表
- 使用t_act表(账户表)
-
第二步:IDEA中创建一个模块,并引入依赖
- spring-context
- spring-jdbc
- mysql驱动
- mybatis
- mybatis-spring:mybatis提供的与spring框架集成的依赖
- 德鲁伊连接池
- junit
-
第三步:基于三层架构实现,所以提前创建好所有的包
- com.powernode.bank.mapper
- com.powernode.bank.service
- com.powernode.bank.service.impl
- com.powernode.bank.pojo
-
第四步:编写pojo
- Account,属性私有化,提供公开的setter getter和toString。
-
第五步:编写mapper接口
- AccountMapper接口,定义方法
-
第六步:编写mapper配置文件
- 在配置文件中配置命名空间,以及每一个方法对应的sql。
-
第七步:编写service接口和service接口实现类
- AccountService
- AccountServiceImpl
-
第八步:编写jdbc.properties配置文件
- 数据库连接池相关信息
-
第九步:编写mybatis-config.xml配置文件
- 该文件可以没有,大部分的配置可以转移到spring配置文件中。
- 如果遇到mybatis相关的系统级配置,还是需要这个文件。
-
第十步:编写spring.xml配置文件
-
组件扫描
-
引入外部的属性文件
-
数据源
-
SqlSessionFactoryBean配置
- 注入mybatis核心配置文件路径
- 指定别名包
- 注入数据源
-
Mapper扫描配置器
- 指定扫描的包
-
事务管理器DataSourceTransactionManager
- 注入数据源
-
启用事务注解
- 注入事务管理器
-
-
第十一步:编写测试程序,并添加事务,进行测试
18.2 具体实现
- 第一步:准备数据库表
连接数据库的工具有很多,除了之前我们使用的navicat for mysql之外,也可以使用IDEA工具自带的DataBase插件。可以根据下图提示自行配置:
- 第二步:IDEA中创建一个模块,并引入依赖
<?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>com.powernode</groupId>
<artifactId>spring6-016-sm</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<repositories>
<repository>
<id>repository.spring.milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>6.0.0-M2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
- 第三步:基于三层架构实现,所以提前创建好所有的包
-
第四步:编写pojo
- com.powernode.bank.pojo.Account
package com.powernode.bank.pojo; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.pojo.Account * @date 2022/11/23 * @since 1.0 */ public class Account { private String actno; private Double balance; @Override public String toString() { return "Account{" + "actno='" + actno + '\'' + ", balance=" + balance + '}'; } public String getActno() { return actno; } public void setActno(String actno) { this.actno = actno; } public Double getBalance() { return balance; } public void setBalance(Double balance) { this.balance = balance; } public Account(String actno, Double balance) { this.actno = actno; this.balance = balance; } public Account() { } }
-
第五步:编写mapper接口
- com.powernode.bank.mapper.AccountMapper
package com.powernode.bank.mapper; import com.powernode.bank.pojo.Account; import java.util.List; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.mapper.AccountMapper * @date 2022/11/23 * @since 1.0 */ public interface AccountMapper { /** * 新增账户 * @param act * @return */ int insert(Account act); /** * 根据账户名删除用户 * @param actno * @return */ int deleteByActno(String actno); /** * 更新账户 * @param act * @return */ int update(Account act); /** * 根据账户名查询账户信息 * @param actno * @return */ Account selectByActno(String actno); /** * 查询所有账户信息 * @return */ List<Account> selectAll(); }
-
第六步:编写mapper配置文件
一定要注意,按照下图提示创建这个目录。注意是斜杠不是点儿。在resources目录下新建。并且要和Mapper接口包对应上。
如果接口叫做AccountMapper,配置文件必须是AccountMapper.xml
- 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="com.powernode.bank.mapper.AccountMapper"> <insert id="insert"> insert into t_act values (#{actno}, #{balance}) </insert> <delete id="deleteByActno"> delete from t_act where actno=#{actno} </delete> <update id="update"> update t_act set balance=#{balance} where actno=#{actno} </update> <select id="selectByActno" resultType="Account"> select * from t_act where actno=#{actno} </select> <select id="selectAll" resultType="Account"> select * from t_act </select> </mapper>
-
第七步:编写service接口和service接口实现类
注意编写的service实现类纳入IoC容器管理:
- com.powernode.bank.service.AccountService
package com.powernode.bank.service; import com.powernode.bank.pojo.Account; import java.util.List; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.service.AccountService * @date 2022/11/23 * @since 1.0 */ public interface AccountService { /** * 开户 * @param act */ void save(Account act); /** * 销户 * @param actno */ void remove(String actno); /** * 修改信息 * @param act */ void modify(Account act); /** * 查询余额 * @param actno * @return */ Account queryByActno(String actno); /** * 查询所有用户信息 * @return */ List<Account> queryAll(); /** * 转账 * @param fromActno 转出账户 * @param toActno 转入账户 * @param money 转账金额 */ void transfer(String fromActno, String toActno, double money); }
- com.powernode.bank.service.impl.AccountServiceImpl,注入AccountMapper
package com.powernode.bank.service.impl; import com.powernode.bank.mapper.AccountMapper; import com.powernode.bank.pojo.Account; import com.powernode.bank.service.AccountService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * @author shanglinsong * @version 1.0 * @className com.powernode.bank.service.impl.AccountServiceImpl * @date 2022/11/23 * @since 1.0 */ @Service("accountService") @Transactional public class AccountServiceImpl implements AccountService { @Autowired private AccountMapper accountMapper; @Override public void save(Account act) { accountMapper.insert(act); } @Override public void remove(String actno) { accountMapper.deleteByActno(actno); } @Override public void modify(Account act) { accountMapper.update(act); } @Override public Account queryByActno(String actno) { return accountMapper.selectByActno(actno); } @Override public List<Account> queryAll() { return accountMapper.selectAll(); } @Override public void transfer(String fromActno, String toActno, double money) { Account fromAct = accountMapper.selectByActno(fromActno); if (money > fromAct.getBalance()) { throw new RuntimeException("余额不足,无法转账!"); } Account toAct = accountMapper.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountMapper.update(fromAct); count += accountMapper.update(toAct); if (2 != count) { throw new RuntimeException("转账失败,未知错误!"); } } }
-
第八步:编写jdbc.properties配置文件
放在类的根路径下
- jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring6?serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123
jdbc.url=jdbc:mysql://localhost:3306/spring6?serverTimezone=Asia/Shanghai
这样写是为了解决数据库时区异常报错
The server time zone value ‘�й���ʱ��’ is unrecognized or represents more than one time zone.
- 我在第一次使用后就不报错了,后续去掉
?serverTimezone=Asia/Shanghai
也不会报错了
-
第九步:编写mybatis-config.xml配置文件
放在类的根路径下,只开启日志,其他配置到spring.xml中。
- mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> </configuration>
-
第十步:编写spring.xml配置文件
注意:当你在spring.xml文件中直接写标签内容时,IDEA会自动给你添加命名空间
- spring.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:tx="http://www.springframework.org/schema/tx" 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/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <!--组件扫描--> <context:component-scan base-package="com.powernode.bank"/> <!--外部属性配置文件--> <context:property-placeholder location="jdbc.properties"/> <!--数据源--> <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}"/> </bean> <!--SqlSessionFactoryBean--> <bean class="org.mybatis.spring.SqlSessionFactoryBean"> <!--mybatis核心配置文件路径--> <property name="configLocation" value="mybatis-config.xml"/> <!--注入数据源--> <property name="dataSource" ref="dataSource"/> <!--起别名--> <property name="typeAliasesPackage" value="com.powernode.bank.pojo"/> </bean> <!--Mapper扫描器--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.powernode.bank.mapper"/> </bean> <!--事务管理器--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <!--开启事务注解--> <tx:annotation-driven transaction-manager="txManager"/> </beans
-
第十一步:编写测试程序,并添加事务,进行测试
package com.powernode.spring6.test; import com.powernode.bank.service.AccountService; import org.junit.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author shanglinsong * @version 1.0 * @className com.powernode.spring6.test.SMTest * @date 2022/11/23 * @since 1.0 */ public class SMTest { @Test public void testSM(){ ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml"); AccountService accountService = applicationContext.getBean("accountService", AccountService.class); try{ accountService.transfer("act-001", "act-002", 10000.0); System.out.println("转账成功!"); }catch (Exception e){ System.out.println("转账失败!"); e.printStackTrace(); } } }
-
测试结果
最后大家别忘了测试事务!!!!
-
模拟异常
- com.powernode.bank.service.impl.AccountServiceImpl
@Override public void transfer(String fromActno, String toActno, double money) { Account fromAct = accountMapper.selectByActno(fromActno); if (money > fromAct.getBalance()) { throw new RuntimeException("余额不足,无法转账!"); } Account toAct = accountMapper.selectByActno(toActno); fromAct.setBalance(fromAct.getBalance() - money); toAct.setBalance(toAct.getBalance() + money); int count = accountMapper.update(fromAct); // 模拟异常 String s = null; s.toString(); count += accountMapper.update(toAct); if (2 != count) { throw new RuntimeException("转账失败,未知错误!"); } }
- 测试结果
事务没问题!
18.3 spring配置文件的import
spring配置文件有多个,并且可以在spring的核心配置文件中使用import进行引入,我们可以将组件扫描单独定义到一个配置文件中,如下:
- common.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">
<!--组件扫描-->
<context:component-scan base-package="com.powernode.bank"/>
</beans>
然后在核心配置文件中引入:
- spring.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:tx="http://www.springframework.org/schema/tx"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--引入其他的spring配置文件-->
<import resource="common.xml"/>
</beans>
注意:在实际开发中,service单独配置到一个文件中,dao单独配置到一个文件中,然后在核心配置文件中引入,养成好习惯。
十九、Spring中的八大模式
19.1 简单工厂模式
BeanFactory的getBean()方法,通过唯一标识来获取Bean对象。是典型的简单工厂模式(静态工厂模式);
19.2 工厂方法模式
FactoryBean是典型的工厂方法模式。在配置文件中通过factory-method属性来指定工厂方法,该方法是一个实例方法。
19.3 单例模式
Spring用的是双重判断加锁的单例模式。请看下面代码,我们之前讲解Bean的循环依赖的时候见过:
19.4 代理模式
Spring的AOP就是使用了动态代理实现的。
19.5 装饰器模式
JavaSE中的IO流是非常典型的装饰器模式。
Spring 中配置 DataSource 的时候,这些dataSource可能是各种不同类型的,比如不同的数据库:Oracle、SQL Server、MySQL等,也可能是不同的数据源:比如apache 提供的org.apache.commons.dbcp.BasicDataSource、spring提供的org.springframework.jndi.JndiObjectFactoryBean等。
这时,能否在尽可能少修改原有类代码下的情况下,做到动态切换不同的数据源?此时就可以用到装饰者模式。
Spring根据每次请求的不同,将dataSource属性设置成不同的数据源,以到达切换数据源的目的。
Spring中类名中带有:Decorator和Wrapper单词的类,都是装饰器模式。
19.6 观察者模式
定义对象间的一对多的关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新。Spring中观察者模式一般用在listener的实现。
Spring中的事件编程模型就是观察者模式的实现。在Spring中定义了一个ApplicationListener接口,用来监听Application的事件,Application其实就是ApplicationContext,ApplicationContext内置了几个事件,其中比较容易理解的是:ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent
19.7 策略模式
策略模式是行为性模式,调用不同的方法,适应行为的变化 ,强调父类的调用子类的特性 。
getHandler是HandlerMapping接口中的唯一方法,用于根据请求找到匹配的处理器。
比如我们自己写了AccountDao接口,然后这个接口下有不同的实现类:AccountDaoForMySQL,AccountDaoForOracle。对于service来说不需要关心底层具体的实现,只需要面向AccountDao接口调用,底层可以灵活切换实现,这就是策略模式。
19.8 模板方法模式
Spring中的JdbcTemplate类就是一个模板类。它就是一个模板方法设计模式的体现。在模板类的模板方法execute中编写核心算法,具体的实现步骤在子类中完成。