文章目录
- 4.详讲AOP
- 4.1什么是AOP?
- 4.2为什么叫做面向切面编程?
- 4.3AOP解决的问题
- 4.4AOP应用场景
- 4.5AOP实现方式
- 4.5.1动态代理的实现
- (1)JDK代理
- (2)CGLB代理
- (3)基于接口的动态代理
- (4)基于子类的动态代理
- (5)使用动态代理对spring进行方法增强
- 4.5.2基于xml的配置
- (1)步骤说明
- (2)切入点表达式写法
- (3)通知类型说明
- (4)代码实现
- 4.5.3基于注解的配置(重要)
- (1)步骤说明
- (2)代码实现
- 5.Spring事务
- 5.1事务概念
- 5.1.1定义
- 5.1.2事务的四个特性
- 5.2Spring的事务管理
- 5.2.1声明式事务管理
- (1)核心思想
- (2)举例
- (3)优点
- (4)缺点
- 5.3基于XML的声明式事务配置
- 5.3.1使用步骤
- 5.3.2配置事务的属性
- (1)isolation——隔离级别(五种)
- (2)propagation——传播类型(七种)
- 5.3.3代码
- (1)xml配置
- (2)持久层
- (3)业务层
- (4)测试类
- 5.4基于注解的声明式事务配置(重要)
- 5.4.1使用步骤
- 5.4.2代码
- (1)JdbcConfig.xml
- (2)JdbcConfig类
- (3)SpringConfiguration类
- (4)TransactionConfig类——配置事务管理器
- (5)业务层
- (6)持久层
- (7)测试类
- 6.Spring的设计模式
- 6.1回顾控制反转(IOC)和依赖注入(DI)
- 6.2工厂设计模式(简单工厂和工厂方法模式)
- 6.3单例设计模式
- 6.3.1 单例bean
- 6.3.2拓展——ConcurrentHashMap
- 6.3.3问题:单例Bean存在线程安全问题吗?
- 6.4代理设计模式
- 6.4.1代理模式在AOP中的应用
- 6.4.2Spring AOP和AspectJ AOP有什么区别?
- 6.5模板方法
- 6.6观察者模式
- 6.6.1Spring事件驱动模型中的三种角色
- (1)事件角色
- (2)事件监听者模式
- (3)事件发布者角色
- 6.6.2Spring的事件流程总结
- 6.7适配器模式
- 6.7.1Spring AOP中的适配器模式
- 6.7.2Spring MVC中的适配器模式
- 6.8装饰者模式
- 6.9总结(共7种设计模式)
4.详讲AOP
4.1什么是AOP?
AOP(Aspect Oriented Programming)即面向切面编程,AOP 是 OOP(面向对象编程)的一种延续,二者互补,并不对立。
- AOP 的目的是将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从核心业务逻辑中分离出来,通过动态代理、字节码操作等技术,实现代码的复用和解耦,提高代码的可维护性和可扩展性。
- OOP 的目的是将业务逻辑按照对象的属性和行为进行封装,通过类、对象、继承、多态等概念,实现代码的模块化和层次化(也能实现代码的复用),提高代码的可读性和可维护性。
4.2为什么叫做面向切面编程?
AOP 之所以叫面向切面编程,是因为它的核心思想就是将横切关注点从核心业务逻辑中分离出来,形成一个个的切面(Aspect)。
这里顺带总结一下 AOP 关键术语(不理解也没关系,可以继续往下看):
- 横切关注点(cross-cutting concerns) :多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等)。
- 切面(Aspect):对横切关注点进行封装的类,一个切面是一个类。切面可以定义多个通知,用来实现具体的功能。
- 连接点(JoinPoint):连接点是方法调用或者方法执行时的某个特定时刻(如方法调用、异常抛出等)。
- 通知(Advice):通知就是切面在某个连接点要执行的操作。通知有五种类型,分别是前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)和环绕通知(Around)。前四种通知都是在目标方法的前后执行,而环绕通知可以控制目标方法的执行过程。
- 切点(Pointcut):一个切点是一个表达式,它用来匹配哪些连接点需要被切面所增强。切点可以通过注解、正则表达式、逻辑运算等方式来定义。比如
execution(* com.xyz.service..*(..))
匹配com.xyz.service
包及其子包下的类或接口。 - 织入(Weaving):织入是将切面和目标对象连接起来的过程,也就是将通知应用到切点匹配的连接点上。常见的织入时机有两种,分别是编译期织入(AspectJ)和运行期织入(AspectJ)。
4.3AOP解决的问题
OOP 不能很好地处理一些分散在多个类或对象中的公共行为(如日志记录、事务管理、权限控制、接口限流、接口幂等等),这些行为通常被称为 横切关注点(cross-cutting concerns) 。如果我们在每个类或对象中都重复实现这些行为,那么会导致代码的冗余、复杂和难以维护。
AOP 可以将横切关注点(如日志记录、事务管理、权限控制、接口限流、接口幂等等)从 核心业务逻辑(core concerns,核心关注点) 中分离出来,实现关注点的分离。
以日志记录为例进行介绍,假如我们需要对某些方法进行统一格式的日志记录,没有使用 AOP 技术之前,我们需要挨个写日志记录的逻辑代码,全是重复的的逻辑。
public CommonResponse<Object> method1() {
// 业务逻辑
xxService.method1();
// 省略具体的业务处理逻辑
// 日志记录
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...
return CommonResponse.success();
}
public CommonResponse<Object> method2() {
// 业务逻辑
xxService.method2();
// 省略具体的业务处理逻辑
// 日志记录
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 省略记录日志的具体逻辑 如:获取各种信息,写入数据库等操作...
return CommonResponse.success();
}
// ...
使用 AOP 技术之后,我们可以将日志记录的逻辑封装成一个切面,然后通过切入点和通知来指定在哪些方法需要执行日志记录的操作。
// 日志注解
@Target({ElementType.PARAMETER,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 描述
*/
String description() default "";
/**
* 方法类型 INSERT DELETE UPDATE OTHER
*/
MethodType methodType() default MethodType.OTHER;
}
// 日志切面
@Component
@Aspect
public class LogAspect {
// 切入点,所有被 Log 注解标注的方法
@Pointcut("@annotation(cn.javaguide.annotation.Log)")
public void webLog() {
}
/**
* 环绕通知
*/
@Around("webLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 省略具体的处理逻辑
}
// 省略其他代码
}
这样的话,我们一行注解即可实现日志记录:
@Log(description = "method1",methodType = MethodType.INSERT)
public CommonResponse<Object> method1() {
// 业务逻辑
xxService.method1();
// 省略具体的业务处理逻辑
return CommonResponse.success();
}
4.4AOP应用场景
- 日志记录:自定义日志记录注解,利用 AOP,一行代码即可实现日志记录。
- 性能统计:利用 AOP 在目标方法的执行前后统计方法的执行时间,方便优化和分析。
- 事务管理:
@Transactional
注解可以让 Spring 为我们进行事务管理比如回滚异常操作,免去了重复的事务管理逻辑。@Transactional
注解就是基于 AOP 实现的。 - 权限控制:利用 AOP 在目标方法执行前判断用户是否具备所需要的权限,如果具备,就执行目标方法,否则就不执行。例如,SpringSecurity 利用
@PreAuthorize
注解一行代码即可自定义权限校验。 - 接口限流:利用 AOP 在目标方法执行前通过具体的限流算法和实现对请求进行限流处理。
- 缓存管理:利用 AOP 在目标方法执行前后进行缓存的读取和更新。
4.5AOP实现方式
-
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。
-
静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
-
静态代理
- AspectJ是静态代理的增强,即AOP框架会在编译阶段生成AOP代理类
- 因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
-
动态代理
- AOP框架不会去修改字节码,
- 每次运行时在内存中临时为方法生成一个AOP对象
- 这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
-
4.5.1动态代理的实现
Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:
(1)JDK代理
- JDK动态代理只提供接口的代理,不支持类的代理。
- 核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
- InvocationHandler 的 invoke(Object proxy,Method method,Object[] args):proxy是最终生成的代理实例; proxy 是代理对象的引用;method 是被代理目标实例的某个具体方法; args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。
(2)CGLB代理
- 如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。
- CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
(3)基于接口的动态代理
public class Test1 {
@Test
public void test1(){
//被代理类对象要声明为最终的
final Producer producer=new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于接口的动态代理:
* 涉及的类:Proxy
* 提供者:JDK官方
* 如何创建代理对象:
* 使用Proxy类中的newProxyInstance方法
* 创建代理对象的要求:
* 被代理类最少实现一个接口,如果没有则不能使用
* newProxyInstance方法的参数:
* ClassLoader:类加载器
* 它是用于加载代理对象字节码的。和被代理对象使用相同的类加载器。固定写法。
* Class[]:字节码数组
* 它是用于让代理对象和被代理对象相同方法。固定写法。
* InvocationHandler:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
*/
//代理对象和被代理类对象要实现同一个接口
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:执行被代理对象的任何接口方法都会经过该方法
* 方法参数的含义
* @param proxy 代理对象的引用
* 1. 可以使用反射获取代理对象的信息(也就是proxy.getClass().getName()。
* 2. 可以将代理对象返回以进行连续调用,这就是proxy存在的目的,因为this并不是代理对象。
* @param method 当前执行的方法
* @param args 当前执行方法所需的参数
* @return 和被代理对象方法相同的返回值
* @throws Throwable
*/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value=null;
//获取方法执行的参数
//判断当前方法是不是销售
if ("saleProduct".equals(method.getName())){
Float money= (Float) args[0];
//两个参数:被代理类对象,方法增强的参数
value=method.invoke(producer,money*0.8f);
}
return value;
}
});
proxyProducer.saleProduct(10000f);
}
}
- IProducer 生产者的接口
public interface IProducer {
/**
* 销售
* @param money
*/
public void saleProduct(float money);
/**
* 售后
* @param money
*/
public void afterService(float money);
}
- Producer 生产者实现类:
public class Producer implements IProducer{
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
(4)基于子类的动态代理
public class Test4 {
@Test
public void test() {
final Producer producer = new Producer();
/**
* 动态代理:
* 特点:字节码随用随创建,随用随加载
* 作用:不修改源码的基础上对方法增强
* 分类:
* 基于接口的动态代理
* 基于子类的动态代理
* 基于子类的动态代理:
* 涉及的类:Enhancer
* 提供者:第方cglib库
* 如何创建代理对象:
* 使用Enhancer类中的create方法
* 创建代理对象的要求:
* 被代理类是最终类
* create方法的参数:
* Class:字节码
* 它是用于指定被代理对象的字节码。
*
* intercept:用于提供增强的代码
* 它是让我们写如何代理。我们一般都是些一个该接口的实现类,通常情况下都是匿名内部类,但不是必须的。
* 此接口的实现类都是谁用谁写。
* 我们一般写的都是该接口的子接口实现类:MethodInterceptor
*/
Producer cglibProducer = (Producer) Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 执行被代理对象的任何方法都会经过该方法
* @param proxy
* @param method
* @param args
* 以上个参数和基于接口的动态代理中invoke方法的参数是一样的
* @param methodProxy :当前执行方法的代理对象
* @return
* @throws Throwable
*/
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增强的代码
Object returnValue = null;
//1.获取方法执行的参数
Float money = (Float) args[0];
//2.判断当前方法是不是销售
if ("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money * 0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
- 生产者类
public class Producer {
/**
* 销售
* @param money
*/
public void saleProduct(float money){
System.out.println("销售产品,并拿到钱:"+money);
}
/**
* 售后
* @param money
*/
public void afterService(float money){
System.out.println("提供售后服务,并拿到钱:"+money);
}
}
(5)使用动态代理对spring进行方法增强
- 实现类
public class java1 implements MyInterface{
public int add(int a,int b){
return a+b;
}
public int del(int a,int b){
return a-b;
}
public int che(int a,int b){
return a*b;
}
public int div(int a,int b){
return a/b;
}
}
- BeanFactory 类
public class BeanFactory {
private java1 java;
public void setJava(java1 java) {
this.java = java;
}
public MyInterface getBean(){
MyInterface proxyJava = (MyInterface) Proxy.newProxyInstance(
java.getClass().getClassLoader(),
java.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object value = null;
System.out.println("方法执行前....");
value = method.invoke(java, args);
System.out.println("方法执行之后....");
return value;
}
}
);
return proxyJava;
}
}
- 配置文件
<bean id="factory" class="com.atguigu.factory.BeanFactory">
<property name="java" ref="java"></property>
</bean>
<bean id="java" class="com.atguigu.java1.java1"></bean>
<bean id="proxyJava" factory-bean="factory" factory-method="getBean"></bean>
- 测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class Test1 {
@Autowired
@Qualifier("proxyJava")
private MyInterface myInterface;
@Test
public void test1(){
System.out.println(myInterface.add(1, 2));
}
@Test
public void test2(){
System.out.println(myInterface.del(1, 2));
}
@Test
public void test3(){
System.out.println(myInterface.che(1, 2));
}
@Test
public void test4(){
System.out.println(myInterface.div(1, 2));
}
}
4.5.2基于xml的配置
(1)步骤说明
-
把通知Bean也交给spring来管理
-
使用 aop:config标签表明开始AOP的配置
-
使用 aop:aspect标签表明配置切面
-
id属性:是给切面提供一个唯一标识
-
ref属性:是指定通知类bean的Id。
-
-
在aop:aspect标签的内部使用对应标签来配置通知的类型。
示例:让printLog方法在切入点方法执行之前:所以是前置通知,aop:before:表示配置前置通知-
method属性:用于指定Logger类中哪个方法是前置通知
-
pointcut属性:用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强
-
举例
<aop:before method="printLog" pointcut="execution(* com.itheima.service.impl.*.*(..))"></aop:before>
-
(2)切入点表达式写法
-
关键字:execution(表达式)
-
表达式:访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
-
标准的表达式写法:
public void com.itheima.service.impl.AccountServiceImpl.saveAccount()
-
访问修饰符可以省略
void com.itheima.service.impl.AccountServiceImpl.saveAccount()
-
返回值可以使用通配符,表示任意返回值
* com.itheima.service.impl.AccountServiceImpl.saveAccount()
-
包名可以使用通配符,表示任意包。但是几级包,就需要写几个*.
* *.*.*.*.AccountServiceImpl.saveAccount())
-
包名可以使用…表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
-
类名和方法名都可以使用*来实现通配
* *..*.*()
- 参数列表
- 基本类型直接写名称 int
- 引用类型写包名.类名的方式 java.lang.String
- 可以使用通配符表示任意类型,但是必须参数
- 可以使用…表示无参数均可,参数可以是任意类型
-
全通配写法:
* *..*.*(..)
-
实际开发中切入点表达式的通常写法:
切到业务层实现类下的所方法:* com.itheima.service.impl.*.*(..)
(3)通知类型说明
- 前置通知(Before advice):在某连接点(join point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常)。
- 返回后通知(After returning advice):在某连接点(join point)正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
- 抛出异常后通知(After throwing advice):在方法抛出异常退出时执行的通知。
- 后通知(After (finally) advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出)。
- 环绕通知(Around Advice):包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。 环绕通知是最常用的一种通知类型。大部分基于拦截的AOP框架,例如Nanning和JBoss4,都只提供环绕通知。
(4)代码实现
- 配置文件:
<?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
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置srping的Ioc,把service对象配置进来-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger类 -->
<bean id="logger" class="com.itheima.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!-- 配置切入点表达式 id属性用于指定表达式的唯一标识。expression属性用于指定表达式内容
此标签写在aop:aspect标签内部只能当前切面使用。
它还可以写在aop:aspect外面,此时就变成了所有切面可用
-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知:在切入点方法执行之前执行-->
<aop:before method="beforePrintLog" pointcut-ref="pt1" ></aop:before>
<!-- 配置后置通知:在切入点方法正常执行之后值。它和异常通知永远只能执行一个-->
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<!-- 配置异常通知:在切入点方法执行产生异常之后执行。它和后置通知永远只能执行一个-->
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行-->
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
</beans>
- 实现类
/**
* 账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService{
public void saveAccount() {
System.out.println("执行了保存");
// int i=1/0;
}
public void updateAccount(int i) {
System.out.println("执行了更新"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
- 日志类
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
}
- 测试类
public class AOPTest {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3.执行方法
as.saveAccount();
}
}
4.5.3基于注解的配置(重要)
(1)步骤说明
- 首先在配置文件里开启声明式aop注解支持
<!--开启声明式事务注解,对aop支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 在logging类上声明其为一个切面类
@Aspect //表示当前类是一个切面类
- 在类中声明一个方法作为切入点表达式
@Pointcut("execution(* com.itheima.service.impl.*.*(..))") //表示切入点
private void pt1(){
}
- 在各个方法上添加注解
@Component("logger") //表示把当前类放入spring容器中
@Before("pt1()") //表示前置通知,在方法执行之前执行
@AfterReturning("pt1()") //表示后置通知,在方法执行之后执行
@AfterRunning:返回通知,在方法返回结果之后执行
@AfterThrowing("pt1()") //表示异常通知,在方法抛出异常之后执行
@After("pt1()") //表示最终通知
@Around("pt1()") //表示环绕通知,围绕着方法执行
@Service("accountService") //表示当前类是一个业务层
- 设置切面优先级
@Order(2)//通过@Order(2)注解指定切面优先级,value值越小,优先级越高,默认是int最大值。
注解:(context名称空间和约束)
(2)代码实现
- 配置文件:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.itheima"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 日志类
/**
* 用于记录日志的工具类,它里面提供了公共的代码
*/
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.itheima.service.impl.*.*(..))")
private void pt1(){}
/**
* 前置通知
*/
// @Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog方法开始记录日志了。。。");
}
/**
* 后置通知
*/
// @AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog方法开始记录日志了。。。");
}
/**
* 异常通知
*/
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog方法开始记录日志了。。。");
}
/**
* 最终通知
*/
// @After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog方法开始记录日志了。。。");
}
@Around("pt1()")
public Object aroundPringLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try{
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPringLog方法开始记录日志了。。。最终");
}
}
}
- 实现类
/**
* 账户的业务层实现类
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService{
public void saveAccount() {
System.out.println("执行了保存");
int i=1/0;
}
public void updateAccount(int i) {
System.out.println("执行了更新"+i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
- 测试类
/**
* 测试AOP的配置
*/
public class AOPTest {
public static void main(String[] args) {
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as = (IAccountService)ac.getBean("accountService");
//3.执行方法
as.saveAccount();
}
}
5.Spring事务
5.1事务概念
5.1.1定义
- 事务:把一组业务当成一个业务来做;要么都成功,要么都失败。
- 事务在项目开发中,十分的重要,涉及到数据的一致性问题,不能马虎。
5.1.2事务的四个特性
- 原子性:事务应被视为单个操作单元,这意味着整个操作序列要么成功,要么不成功。
- 一致性:这代表了数据库的引用完整性、表中唯一主键等的一致性。
- 隔离性:一个事务的执行不能其它事务干扰。
- 持续性:一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。
5.2Spring的事务管理
- Spring支持编程式事务管理和声明式事务管理两种方式:
- 编程式事务管理:通过
TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用 - 声明式事务管理:建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务
- 编程式事务管理:通过
- 声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式,使业务代码不受污染,只要加上注解就可以获得完全的事务支持。
5.2.1声明式事务管理
(1)核心思想
通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中
(2)举例
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
(3)优点
- 不需要在业务逻辑代码中掺杂事务管理的代码
- 只需在配置文件中做相关的事务规则声明或通过 @Transactional注解的方式,便可以将事务规则应用到业务逻辑中。
(4)缺点
唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。
5.3基于XML的声明式事务配置
5.3.1使用步骤
-
配置事务管理器
-
配置事务的通知:此时我们需要 导入事务的约束 tx名称空间和约束,同时也需要aop的。
使用tx:advice标签配置事务通知。
属性:
-
id:给事务通知起一个唯一标识
-
transaction-manager:给事务通知提供一个事务管理器引用。
举例:
<tx:advice id = "txAdvice" transaction-manager = "transactionManager"> <tx:attributes> <tx:method name = "create"/> </tx:attributes> </tx:advice>
-
-
配置AOP中的通用切入点表达式
-
建立事务通知和切入点表达式的对应关系
-
配置事务的属性:在事务的通知tx:advice标签的内部。
5.3.2配置事务的属性
- isolation:用于指定事务的隔离级别。默认值是DEFAULT,表示使用数据库的默认隔离级别。
- propagation:用于指定事务的传播行为。默认值是REQUIRED,表示一定会有事务,增删改的选择。查询方法可以选择SUPPORTS。
- rollback-for:用于指定一个异常,当产生该异常时,事务回滚,产生其他异常时,事务不回滚。没有默认值。表示任何异常都回滚。
- read-only:用于指定事务是否只读。只查询方法才能设置为true。默认值是false,表示读写。
- timeout:用于指定事务的超时时间,默认值是-1,表示永不超时。如果指定了数值,以秒为单位。
- no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时事务回滚。没有默认值。表示任何异常都回滚。
(1)isolation——隔离级别(五种)
- 首先回顾下脏读、不可重复读、以及幻读的概念
- 脏读:一个事务读取到了另一个事务还未提交的数据,但另一个事务后面又取消该数据的提交
- 不可重复读:一个事务先后两次读取同一个记 录,但两次读取的数据不同
- 幻读:一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在了
该属性的可能取值有以下几种:
- ISOLATION_DEFAULT:默认的隔离级别(采用数据库默认的隔离级别,即可重复读)
- ISOLATION_READ_UNCOMMITTED:
- 允许另外一个事务可以看到这个事务未提交的数据
- 可能出现脏读、不可重复读和幻读
- ISOLATION_READ_COMMITTED:
- 保证一个事务修改的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新
- 可能出现不可重复读、幻读
- ISOLATION_REPEATABLE_READ:
- 保证一个事务修改的数据提交后才能被另一事务读取,但是 不能看到该事务对已有记录的更新
- 可能出现幻读
- ISOLATION_SERIALIZABLE:
- 一个事务在执行的过程中完全看不到其他事务对数据库所做的更新。
- 序列化,不会出现脏读、不可重复读和幻读
(2)propagation——传播类型(七种)
- REQUIRED:支持当前事务,如果当前没有事务,就新建一个事务。 这是最常见的选择。默认
- ==SUPPORTS:==支持当前事务,如果当前没有事务,就以非务方式执行。
- MANDATORY:支持当前事务,如果当前没有事务,就抛出异常。
- REQUIRES_ NEW:新建事务,如果当前存在事务,把当前事务挂起。
- NOT_ _SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- NEVER:以啡事務方式执行,如果当前存在務,则抛出异常。
- NESTED:支持当前事务,如果当前事务存在,则执行一个嵌套事务,如果当前没有事务,就新建一个事务。
5.3.3代码
(1)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"
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/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">
<!-- 配置业务层-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<!-- 配置账户的持久层-->
<bean id="accountDao" class="com.itheima.dao.impl.AccountDaoImpl">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置数据源-->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
</bean>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 配置事务的通知-->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="*" propagation="REQUIRED" read-only="false"/>
<tx:method name="find*" propagation="SUPPORTS" read-only="true"></tx:method>
</tx:attributes>
</tx:advice>
<!-- 配置aop-->
<aop:config>
<!-- 配置切入点表达式-->
<aop:pointcut id="pt1" expression="execution(* com.itheima.service.impl.*.*(..))"></aop:pointcut>
<!--建立切入点表达式和事务通知的对应关系 -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="pt1"></aop:advisor>
</aop:config>
</beans>
(2)持久层
/**
* 账户的持久层实现类
*/
public class AccountDaoImpl extends JdbcDaoSupport implements IAccountDao {
public Account findAccountById(Integer accountId) {
List<Account> accounts = super.getJdbcTemplate().query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
return accounts.isEmpty()?null:accounts.get(0);
}
public Account findAccountByName(String accountName) {
List<Account> accounts = super.getJdbcTemplate().query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
if(accounts.isEmpty()){
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
public void updateAccount(Account account) {
super.getJdbcTemplate().update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}
}
(3)业务层
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
public class AccountServiceImpl implements IAccountService{
private IAccountDao accountDao;
public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
accountDao.updateAccount(source);
// int i=1/0;
//2.6更新转入账户
accountDao.updateAccount(target);
}
}
(4)测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class Test1 {
@Autowired
private IAccountService service;
@Test
public void test1(){
service.transfer("aaa","bbb",1000f);
}
5.4基于注解的声明式事务配置(重要)
5.4.1使用步骤
-
配置事务管理器
-
开启spring对注解事务的支持
-
在需要事务支持的地方使用 @Transactional注解(业务层的实现)
-
作用范围:
- 方法:推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类:如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口:不推荐在接口上使用。
-
属性:
- propagation:事务的传播行为,默认值为 REQUIRED
- isolation:事务的隔离级别,默认值采用 DEFAULT
- readOnly:指定事务是否为只读事务,默认值为 false。(对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中)
- rollbackFor:用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
- timeout:事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
-
举例:
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Exception.class}, isolation = Isolation.DEFAULT, readOnly = false)
-
5.4.2代码
(1)JdbcConfig.xml
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/eesy
jdbc.username=root
jdbc.password=123456
(2)JdbcConfig类
/**
* 和链接数据库相关的配置类
*/
public class JdbcConfig {
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
/**
* 创建JdbcTemplate
* @param dataSource
* @return
*/
@Bean(name = "jdbcTemplate")
public JdbcTemplate createJdbcTemplate(DataSource dataSource){
return new JdbcTemplate(dataSource);
}
/**
* 创建数据库对象
* @return
*/
@Bean(name = "dataSource")
public DataSource createDataSource(){
DriverManagerDataSource ds = new DriverManagerDataSource();
ds.setDriverClassName(driver);
ds.setUrl(url);
ds.setUsername(username);
ds.setPassword(password);
return ds;
}
}
(3)SpringConfiguration类
/**
* spring的配置类,相当于bean.xml
*/
@Configuration
@ComponentScan("com.itheima")
@Import({JdbcConfig.class,TransactionConfig.class})
@PropertySource("jdbcConfig.properties")
@EnableTransactionManagement//开启spring对注解事务的支持
public class SpringConfiguration {
}
(4)TransactionConfig类——配置事务管理器
/**
* 和事务相关的配置类
*/
public class TransactionConfig {
/**
* 用于创建事务管理器对象
* @param dataSource
* @return
*/
@Bean(name = "transactionManager")
public PlatformTransactionManager createTransactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
(5)业务层
/**
* 账户的业务层实现类
*
* 事务控制应该都是在业务层
*/
@Service("accountService")
@Transactional(propagation = Propagation.SUPPORTS,readOnly = true) //只读型事物的配置
public class AccountServiceImpl implements IAccountService{
@Autowired
private IAccountDao accountDao;
public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}
//需要的是读写型事务配置
@Transactional(propagation = Propagation.REQUIRED,readOnly = false)
public void transfer(String sourceName, String targetName, Float money) {
System.out.println("transfer....");
//2.1根据名称查询转出账户
Account source = accountDao.findAccountByName(sourceName);
//2.2根据名称查询转入账户
Account target = accountDao.findAccountByName(targetName);
//2.3转出账户减钱
source.setMoney(source.getMoney()-money);
//2.4转入账户加钱
target.setMoney(target.getMoney()+money);
//2.5更新转出账户
accountDao.updateAccount(source);
// int i=1/0;
//2.6更新转入账户
accountDao.updateAccount(target);
}
}
(6)持久层
/**
* 账户的持久层实现类
*/
@Repository("accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private JdbcTemplate jdbcTemplate;
public Account findAccountById(Integer accountId) {
List<Account> accounts = jdbcTemplate.query("select * from account where id = ?",new BeanPropertyRowMapper<Account>(Account.class),accountId);
return accounts.isEmpty()?null:accounts.get(0);
}
public Account findAccountByName(String accountName) {
List<Account> accounts = jdbcTemplate.query("select * from account where name = ?",new BeanPropertyRowMapper<Account>(Account.class),accountName);
if(accounts.isEmpty()){
return null;
}
if(accounts.size()>1){
throw new RuntimeException("结果集不唯一");
}
return accounts.get(0);
}
public void updateAccount(Account account) {
jdbcTemplate.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}
}
(7)测试类
/**
* 使用Junit单元测试:测试我们的配置
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {
@Autowired
private IAccountService as;
@Test
public void testTransfer(){
as.transfer("aaa","bbb",100f);
}
}
6.Spring的设计模式
6.1回顾控制反转(IOC)和依赖注入(DI)
-
IOC是一种解耦的设计思想。
-
IOC 的主要目的是借助于“第三方”(Spring 中的 IoC 容器) 实现具有依赖关系的对象之间的解耦(IOC 容器管理对象,你只管使用即可),从而降低代码之间的耦合度
-
Spring IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的
- 控制:对象的创建与管理(包括对象之间的依赖关系)
- 反转:交给IOC容器管理
6.2工厂设计模式(简单工厂和工厂方法模式)
-
Spring使用工厂方法模式可以通过==BeanFactory或ApplicationContext==创建bean对象。
-
对比:
-
BeanFactory :延迟注入(使用到某个 bean 的时候才会注入),相比于BeanFactory来说会占用更少的内存,程序启动速度更快。
-
ApplicationContext :容器启动的时候,不管你用没用到,一次性创建所有 bean 。BeanFactory 仅提供了最基本的依赖注入支持,ApplicationContext 扩展了 BeanFactory ,除了有BeanFactory的功能还有额外更多功能,所以一般开发人员使用ApplicationContext会更多。
-
6.3单例设计模式
6.3.1 单例bean
Spring 中 bean 的默认作用域就是 singleton(单例) 的。 除了 singleton 作用域,Spring 中 bean 还有下面5中作用域:
- prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续
getBean()
两次,得到的是不同的 Bean 实例。 - request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),,该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
Spring中实现单例的两个方式:
xml格式:<bean id="userService" class="top.snailclimb.UserService" scope="singleton"/>
注解:@Scope(value = "singleton")
6.3.2拓展——ConcurrentHashMap
Spring 通过 ConcurrentHashMap
实现单例注册表的特殊方式实现单例模式。
Spring 实现单例的核心代码如下:
// 通过 ConcurrentHashMap(线程安全) 实现单例注册表
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(64);
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "'beanName' must not be null");
synchronized (this.singletonObjects) {
// 检查缓存中是否存在实例
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
//...省略了很多代码
try {
singletonObject = singletonFactory.getObject();
}
//...省略了很多代码
// 如果实例对象在不存在,我们注册到单例注册表中。
addSingleton(beanName, singletonObject);
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}
//将对象添加到单例注册表
protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
this.singletonObjects.put(beanName, (singletonObject != null ? singletonObject : NULL_OBJECT));
}
}
}
6.3.3问题:单例Bean存在线程安全问题吗?
- 单例Bean可能存在线程安全问题(类里面有可变的成员变量)
- 解决方法:
- 在 Bean 中尽量避免定义可变的成员变量。
- 在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐的一种方式)。
- 不过,大部 分 Bean 实际都是无状态(没有实例变量) 的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
6.4代理设计模式
6.4.1代理模式在AOP中的应用
- AOP( Aspect-Oriented Programming,面向切面编程) 能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。
- Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 JDK Proxy 去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 Cglib 生成一个被代理对象的子类来作为代理,如下图所示:
当然也可以使用 AspectJ ,Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。
6.4.2Spring AOP和AspectJ AOP有什么区别?
Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。 Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。
Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,
如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。
6.5模板方法
模板方法模式是一种行为设计模式,它定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。 模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤的实现方式。
public abstract class Template {
//这是我们的模板方法
public final void TemplateMethod(){
PrimitiveOperation1();
PrimitiveOperation2();
PrimitiveOperation3();
}
protected void PrimitiveOperation1(){
//当前类实现
}
//被子类实现的方法
protected abstract void PrimitiveOperation2();
protected abstract void PrimitiveOperation3();
}
public class TemplateImpl extends Template {
@Override
public void PrimitiveOperation2() {
//当前类实现
}
@Override
public void PrimitiveOperation3() {
//当前类实现
}
}
Spring 中 JdbcTemplate
、HibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。一般情况下,我们都是使用继承的方式来实现模板模式,但是 Spring 并没有使用这种方式,而是使用 Callback 模式与模板方法模式配合,既达到了代码复用的效果,同时增加了灵活性。
6.6观察者模式
- 观察者模式是一种对象行为型模式。
- 它表示的是一种对象与对象之间具有依赖关系,当一个对象发生改变的时候,依赖这个对象的所有对象也会做出反应。
- Spring 事件驱动模型就是观察者模式很经典的一个应用。Spring 事件驱动模型非常有用,在很多场景都可以解耦我们的代码。比如我们每次添加商品的时候都需要重新更新商品索引,这个时候就可以利用观察者模式来解决这个问题。
6.6.1Spring事件驱动模型中的三种角色
(1)事件角色
ApplicationEvent
(org.springframework.context
包下)充当事件的角色,这是一个抽象类,它继承了java.util.EventObject
并实现了 java.io.Serializable
接口。
Spring 中默认存在以下事件,他们都是对 ApplicationContextEvent
的实现(继承自ApplicationContextEvent
):
ContextStartedEvent
:ApplicationContext
启动后触发的事件;ContextStoppedEvent
:ApplicationContext
停止后触发的事件;ContextRefreshedEvent
:ApplicationContext
初始化或刷新完成后触发的事件;ContextClosedEvent
:ApplicationContext
关闭后触发的事件。
(2)事件监听者模式
ApplicationListener
充当了事件监听者角色,它是一个接口,里面只定义了一个 onApplicationEvent()
方法来处理ApplicationEvent
。ApplicationListener
接口类源码如下,可以看出接口定义看出接口中的事件只要实现了 ApplicationEvent
就可以了。所以,在 Spring 中我们只要实现 ApplicationListener
接口的 onApplicationEvent()
方法即可完成监听事件
package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
void onApplicationEvent(E var1);
}
(3)事件发布者角色
ApplicationEventPublisher
充当了事件的发布者,它也是一个接口。
@FunctionalInterface
public interface ApplicationEventPublisher {
default void publishEvent(ApplicationEvent event) {
this.publishEvent((Object)event);
}
void publishEvent(Object var1);
}
ApplicationEventPublisher
接口的publishEvent()
这个方法在AbstractApplicationContext
类中被实现,阅读这个方法的实现,你会发现实际上事件真正是通过ApplicationEventMulticaster
来广播出去
6.6.2Spring的事件流程总结
- 定义一个事件: 实现一个继承自
ApplicationEvent
,并且写相应的构造函数; - 定义一个事件监听者:实现
ApplicationListener
接口,重写onApplicationEvent()
方法; - 使用事件发布者发布消息: 可以通过
ApplicationEventPublisher
的publishEvent()
方法发布消息。
// 定义一个事件,继承自ApplicationEvent并且写相应的构造函数
public class DemoEvent extends ApplicationEvent{
private static final long serialVersionUID = 1L;
private String message;
public DemoEvent(Object source,String message){
super(source);
this.message = message;
}
public String getMessage() {
return message;
}
// 定义一个事件监听者,实现ApplicationListener接口,重写 onApplicationEvent() 方法;
@Component
public class DemoListener implements ApplicationListener<DemoEvent>{
//使用onApplicationEvent接收消息
@Override
public void onApplicationEvent(DemoEvent event) {
String msg = event.getMessage();
System.out.println("接收到的信息是:"+msg);
}
}
// 发布事件,可以通过ApplicationEventPublisher 的 publishEvent() 方法发布消息。
@Component
public class DemoPublisher {
@Autowired
ApplicationContext applicationContext;
public void publish(String message){
//发布事件
applicationContext.publishEvent(new DemoEvent(this, message));
}
}
当调用 DemoPublisher
的 publish()
方法的时候,比如 demoPublisher.publish("你好")
,控制台就会打印出:接收到的信息是:你好
。
6.7适配器模式
适配器模式(Adapter Pattern) 将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作。
6.7.1Spring AOP中的适配器模式
Spring AOP 的实现是基于代理模式,但是 Spring AOP 的增强或通知(Advice)使用到了适配器模式,与之相关的接口是AdvisorAdapter
。
- Advice 常用的类型有:
BeforeAdvice
(目标方法调用前,前置通知)、AfterAdvice
(目标方法调用后,后置通知)、AfterReturningAdvice
(目标方法执行结束后,return 之前)等等。每个类型 Advice(通知)都有对应的拦截器:MethodBeforeAdviceInterceptor
、AfterReturningAdviceInterceptor
、ThrowsAdviceInterceptor
等等。 - Spring 预定义的通知要通过对应的适配器,适配成
MethodInterceptor
接口(方法拦截器)类型的对象(如:MethodBeforeAdviceAdapter
通过调用getInterceptor
方法,将MethodBeforeAdvice
适配成MethodBeforeAdviceInterceptor
)。
6.7.2Spring MVC中的适配器模式
在 Spring MVC 中,DispatcherServlet
根据请求信息调用 HandlerMapping
,解析请求对应的 Handler
。解析到对应的 Handler
(也就是我们平常说的 Controller
控制器)后,开始由HandlerAdapter
适配器处理。HandlerAdapter
作为期望接口,具体的适配器实现类用于对目标类进行适配,Controller
作为需要适配的类。
- 使用的原因:
Spring MVC 中的 Controller
种类众多,不同类型的 Controller
通过不同的方法来对请求进行处理。如果不利用适配器模式的话,DispatcherServlet
直接获取对应类型的 Controller
,需要的自行来判断,像下面这段代码一样:
if(mappedHandler.getHandler() instanceof MultiActionController){
((MultiActionController)mappedHandler.getHandler()).xxx
}else if(mappedHandler.getHandler() instanceof XXX){
...
}else if(...){
...
}
假如我们再增加一个 Controller
类型就要在上面代码中再加入一行 判断语句,这种形式就使得程序难以维护,也违反了设计模式中的开闭原则 – 对扩展开放,对修改关闭。
6.8装饰者模式
装饰者模式可以动态地给对象添加一些额外的属性或行为。相比于使用继承,装饰者模式更加灵活。简单点儿说就是当我们需要修改原有的功能,但我们又不愿直接去修改原有的代码时,设计一个 Decorator 套在原有代码外面。其实在 JDK 中就有很多地方用到了装饰者模式,比如 InputStream
家族,InputStream
类下有 FileInputStream
(读取文件)、BufferedInputStream
(增加缓存,使读取文件速度大大提升)等子类都在不修改InputStream
代码的情况下扩展了它的功能。
Spring 中配置 DataSource 的时候,DataSource 可能是不同的数据库和数据源。我们能否根据客户的需求在少修改原有类的代码下动态切换不同的数据源?这个时候就要用到装饰者模式(这一点我自己还没太理解具体原理)。Spring 中用到的包装器模式在类名上含有 Wrapper
或者 Decorator
。这些类基本上都是动态地给一个对象添加一些额外的职责
6.9总结(共7种设计模式)
- 工厂设计模式 : Spring 使用工厂模式通过
BeanFactory
、ApplicationContext
创建 bean 对象。 - 单例设计模式 : Spring 中的 Bean 默认都是单例的。
- 代理设计模式 : Spring AOP 功能的实现。
- 模板方法模式 : Spring 中
jdbcTemplate
、hibernateTemplate
等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。 - 观察者模式: Spring 事件驱动模型就是观察者模式很经典的一个应用。
- 适配器模式 :Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配
Controller
。
- 装饰器设计模式 : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。