代理模式
提出问题
现有缺陷
假设我们有一个计算类,里面有加减乘除四个方法,现在我们要为这四个方法添加日志,即在方法执行的前后分别输出一句话,这时我们会发现如下缺陷:
1.对核心业务有干扰。核心业务是加减乘除的实现,而我们现在还要额外编写日志的代码。
2.附加功能分散在各个业务功能方法中,不利于统一维护。
解决思路
解决这两个问题的核心就是解耦。我们需要把附加功能从业务功能代码中提取出来
困难
要抽取的代码在方法内部,无法像以前一样将子类中重复的代码抽取到父类来解决。因此要引入新的技术。
概念
代理模式:二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
我们之前使用计算类的过程为:
创建计算类对象,调用对象的方法,获取返回值
现在我们创建了一个代理类,使用的过程为:
创建代理类对象,调用代理类的加减乘除方法,代理类调用计算类的对应方法并获取返回值,代理类将返回值返回给我们
即代理类中有着目标类的相同方法,且核心业务的代码是调用目标类来实现的,这样就可以在核心代码的前后加上附加代码
代理模式类似于Servlet的过滤器:在访问对应业务前进行处理,访问完业务后再进行处理
相关术语:
代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
目标:被代理“套用”非核心逻辑代码的类、对象、方法。
静态代理
静态代理的实现:
1.创建代理类
2.目标类若实现了接口,代理类也要实现对应接口并重写方法
3.在代理类中创建目标类的成员变量
4.代理类在对应方法中调用目标类的方法实现核心功能
5.代理类在对应方法的前后添加附加功能
注:一个代理类对应一个目标类
静态代理的缺点:静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理 类来实现。这就需要使用动态代理技术了。
动态代理
我们可以创建一个代理类工厂,然后在工厂创建时获取目标对象,然后使用 newProxyInstance 方法创建代理对象并返回
newProxyInstance():创建一个代理对象,其中有三个参数:
1.classLoader:加载动态生成的代理类的类加载器
2.interfaces:目标对象实现的所有接口的class对象所组成的数组
3.invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
其中,classLoader一般是一个应用程序类加载器,我们自己编写的类一般都使用这个加载器,因此我们可以直接使用this.getClss().getClassLoader()获取
interfaces可以直接使用目标对象.getClss().getInterfaces()获取目标对象的所有接口
invocationHandler则是需要使用匿名内部类创建,其内部只有一个invoke方法
invoke():代理对象内部的方法的实现,有三个参数
proxy:代理对象
method:代理对象需要实现的方法,即其中需要重写的方法
args:method所对应方法的参数
注:由于代理对象和代理类都是自动生成的,我们不知道代理类的名称,但是我们知道代理类继承的接口,因此创建的代理对象我们都会使用接口类型的变量接,因此method的方法是接口中对应的方法,这个方式是可以使用目标类的对象调用的
在invoke内部核心业务的实现就是通过method.invoke(target,args)实现的
target为目标对象
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;public class ProxyFactory{private Object target;public ProxyFactory(Object target) {this.target = target;}public Object getProxy(){ClassLoader classLoader = this.getClass().getClassLoader();Class<?>[] interfaces = target.getClass().getInterfaces();InvocationHandler invocationHandler = new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object result = method.invoke(target, args);return result;}};return Proxy.newProxyInstance(classLoader,interfaces,invocationHandler);}}
public void test1(){ProxyFactory factory = new ProxyFactory(new CalculatorPureImpl());Calculator proxy = (Calculator) factory.getProxy();System.out.println(proxy.add(1, 1));}
动态代理有两种:
1.jdk动态代理,要求必须有接口,最终生成的代理类和目标类实现相同的接口,生成的代理类在com.sun.proxy包下,类名为$proxy+数字
2.cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下
AOP概念及相关术语
概念
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程(oop)的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
相关术语
横切关注点
从目标类中抽取出来的同一类的非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
通知
每一个横切关注点的功能都需要写一个方法来实现,这样的方法就叫通知方法
前置通知:在被代理的目标方法前执行
返回通知:在被代理的目标方法成功结束后执行
异常通知:在被代理的目标方法异常结束后执行
后置通知:在被代理的目标方法最终结束后执行
环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
切面
封装通知方法的类
目标
被代理的目标对象
代理
向目标对象应用通知之后创建的代理对象
连接点
横切关注点在目标类的位置,即我们要从哪里抽取非核心业务代码,那里就是连接点
注:连接点是一个纯逻辑概念,从哪抽取,哪就是连接点
切入点
定位连接点的方式
连接点只是一个逻辑概念,为了找到连接点的具体位置,需要用到切入点
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
基于注解的AOP
技术说明
动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因 为这个技术要求代理对象和目标对象实现同样的接口。
cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。
AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
使用步骤
- 在IOC所需依赖的基础上加入下述依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
- 创建切面类,将目标类和切面类交给IOC容器管理
- 使用@Aspect对切面类进行注解,在Spring中开启Aspectj的自动代理
<aop:aspectj-autoproxy />
- 配置切面类
-
- 在切面类内部创建通知方法,并编写内部代码
- 使用通知注解对通知方法进行标记,如@Before:前置通知
- 配置注解的value属性,指定要处理哪个类的哪个方法
@Before("execution(public int com.CalculatorPureImpl.add(int,int))")
- 在测试类中创建ApplicationContext对象,并获取目标类继承的接口类型的代理类对象,使用代理类对象调用方法
注:目标类在被代理过后,无法再通过IOC容器获取,因此获取代理类对象时可以直接使用根据类型获取bean的方式获取代理类对象,类型为接口的类型
各种通知
- 前置通知:使用@Before注解标识,在被代理的目标方法前执行
- 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行
- 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行
- 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行
- 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
各种通知的执行顺序:
- Spring版本5.3.x以前:
-
- 前置通知
- 目标操作
- 后置通知
- 返回通知或异常通知
- Spring版本5.3.x以后:
-
- 前置通知
- 目标操作
- 返回通知或异常通知
- 后置通知
切入点表达式语法
我们在使用注解将一个方法标记为通知后,还需要通过注解的value属性设置该通知的生效范围
其具体语法如下:
基础结构:
execution(权限修饰符 方法返回值 方法所在类型的全类名.方法名(参数列表))
@Before("execution(public int com.CalculatorPureImpl.add(int,int))")
其中,"execution()"是固定格式,参数列表只填参数的类型,使用逗号分隔
我们也可以使用通配符*代表任意,如
@Before("execution(* com.CalculatorPureImpl.*(..))")
上述代码代表权限修饰符和方法返回值任意,com包下的CalculatorPureImpl类,方法随意,方法的参数随意
具体细节:
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个“*”号只能代表包的层次结构中的一层,表示这一层是任意的。
-
- 例如:*.Hello匹配com.Hello,不匹配com.atguigu.Hello
- 在包名的部分,使用“*..”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
-
- 例如:*Service匹配所有名称以Service结尾的类或接口
- 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
-
- 例如:*Operation匹配所有方法名以Operation结尾的方法
- 在方法参数列表部分,使用(..)表示参数列表任意
- 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
-
- 切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
- 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
-
- 例如:execution(public int ..Service.*(.., int)) 正确
- 例如:execution(* int ..Service.*(.., int)) 错误
重用切入点表达式
我们希望设置一个切入点表达式,然后在任意切面类中都可以使用,降低代码的重复性
声明:
@Pointcut(切面表达式)
public void xxx(){}
@Pointcut("execution(* com.CalculatorPureImpl.*(..))")
public void pointCut(){}
在同一个切面的使用:
使用xxx()来代表这里的切面表达式,例:
@Before("pointCut()")
在不同切面使用:
需要使用全类名得到切面表达式所在的类,然后使用.xxx()使用切面表达式。例:
@Before("com.CommonPointCut.pointCut()")
获取通知的相关信息
获取连接点信息
获取连接点信息可以在通知方法的参数位置设置 JoinPoint 类型的形参
@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint){//获取连接点的签名信息String methodName = joinPoint.getSignature().getName();//获取目标方法得到的实参信息String args = Arrays.toString(joinPoint.getArgs());System.out.println("Logger-->前置通知,方法名:" + methodName + ",参数:" + args);
}
获取目标方法的返回值
@AfterReturning中的属性 returning ,可以设置通知方法的某个形参来接收目标方法的返回值
@AfterReturning(value = "pointCut()",returning = "result")
public void afterReturningMethod(JoinPoint joinPoint,Object result){String methodName = joinPoint.getSignature().getName();System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
}
获取目标方法的异常
@AfterThrowing中的属性throwing,可以设置通知方法的某个形参来接收目标方法的异常
@AfterThrowing(value = "pointCut()",throwing = "ex")
public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){String methodName = joinPoint.getSignature().getName();System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
}
注:通知方法中接收异常的形参的类型也可以是Exception
环绕通知
环绕通知相当于另外四种通知的总和
使用环绕通知时需要设置通知方法的形参为 ProceedingJoinPoint 类型
该类型的对象有一个方法:proceed(),代表目标方法的执行
注:当我们使用环绕通知时,通知方法需要返回 目标方法的返回值
@Around("pointCut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint){Object result = null;try {System.out.println("环绕通知-->目标对象方法执行之前");joinPoint.proceed();System.out.println("环绕通知-->目标对象方法返回值之后");} catch (Throwable throwable) {throwable.printStackTrace();System.out.println("环绕通知-->目标对象方法出现异常时");}finally {System.out.println("环绕通知-->目标对象方法执行完毕");}return result;
}
切面的优先级
当一个目标方法上同时存在多个切面时,优先级高的切面先执行
我们可以在切面上通过@Order注解的value属性设置切面的优先级
- value属性为一个int类型的数字,数字越小,优先级越高,默认值为Integer类型的最大值
基于xml的AOP
注:一般使用注解的方式
<context:component-scan base-package="com.atguigu.aop.xml"></context:componentscan><aop:config><!--配置切面类--><aop:aspect ref="loggerAspect"><!--配置公共的切入点表达式--><aop:pointcut id="pointCut" expression="execution(*com.atguigu.aop.xml.CalculatorImpl.*(..))"/><aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before><aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after><aop:after-returning method="afterReturningMethod" returning="result"pointcut-ref="pointCut"></aop:after-returning><aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing><aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around></aop:aspect><aop:aspect ref="validateAspect" order="1"><aop:before method="validateBeforeMethod" pointcut-ref="pointCut"></aop:before></aop:aspect></aop:config>
声明式事务
概念
编程式事务:事务功能的相关操作全部通过自己编写代码来实现
声明式事务:
事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
有以下两个概念:
编程式:自己写代码实现功能
声明式:通过配置让框架实现功能
基于注解的声明式事务
使用步骤
1.添加IOC容器的依赖和Spring持久化层支持jar包,以及mysql相关依赖
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-orm</artifactId><version>5.3.1</version>
</dependency><!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.1</version>
</dependency><!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
注:由于事务已经被封装为框架,不需要我们手动创建切面,因此可以不导入AOP的依赖
2.添加事务管理器
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"><property name="url" value="${jdbc.url}"/><property name="driverClassName" value="${jdbc.driver}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="druidDataSource"></property>
</bean>
添加事务管理器时还需要配置数据源
3.开启事务驱动
<!-- 开启事务的注解驱动-->
<!-- transaction-manager属性的默认值是transactionManager,
如果事务管理器bean的id正好就是这个默认值,则可以省略这个属性 -->
<tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
注:
1.开启事务的注解驱动时需要指定一个事务管理器的id,且默认值为 transactionManager
2.开启事务驱动时需要导入的名称空间为tx结尾的那个
4.添加事务注解
在需要被事务管理的方法上添加@Transactional注解
@Transactional注解的位置:
1.标识在需要被事务管理的方法上
2.标识在类上,则类中的所有方法都会被事务管理
事务属性
我们可以通过设置@Transactional注解的各个属性来更改事务属性
只读
对于一个查询操作来说,如果我们把它设置成只读,就能明确的告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化
@Transactional(readOnly = true)
readOnly的默认值为false
超时
事务在执行过程中,有可能会遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率时因为程序运行出现了问题。
此时这个很可能出现问题的程序应该被回滚,撤销它已做的操作,把资源让出来。
即超时回滚,释放资源
@Transactional(timeout = 3)
timeout的值代表超时多少秒就回滚并释放资源并抛出异常
回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚
可以通过以下属性设置回滚策略
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名
- noRollbackFor属性:需要设置一个Class类型的对象
- rollbackFor属性:需要设置一个字符串类型的全类名
注:前两个代表出现那些异常时回滚,后两个代表出现那些异常时不回滚
前两个很少使用,因为默认是出现任何运行时异常都进行回滚
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException"
上述代码代表的是出现数学运算异常(ArithmeticException)时不进行回滚 的两种形式
事务隔离级别
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
事务传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。
例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行
可以通过@Transactional中的propagation属性设置事务传播行为
注:应该修改被调用的事务方法的propagation属性
@Transactional(propagation = Propagation.REQUIRED),默认情况,表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。此时,只要大的事务中出现异常,即使调用的小的事务方法正常执行,也会被回滚。
@Transactional(propagation = Propagation.REQUIRES_NEW),表示不管当前线程上是否有已经开启 的事务,都要开启新事务。此时,如果大的事务中出现异常,但是调用的小的事务方法正常执行,那么小的事务方法的结果会被提交,而大的事务中其他操作正常回滚。
基于xml的声明式事务
使用步骤
1.相对于注解需要多加入下面的依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
2.添加事务管理器
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"><property name="url" value="${jdbc.url}"/><property name="driverClassName" value="${jdbc.driver}"/><property name="username" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/>
</bean>
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="druidDataSource"></property>
</bean>
添加事务管理器时还需要配置数据源
3.配置事务通知和切入点表达式
<!-- 配置切入点表达式 -->
<aop:config>
<aop:advisor advice-ref="txAdvice" pointcut="execution(*
com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
</aop:config><!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- tx:method标签:配置具体的事务方法 -->
<!-- name属性:指定方法名,可以使用星号代表多个字符 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<tx:method name="save*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
注:配置事务通知时,需要通过 tx:method 标签的 name 属性指定通知方法名来表明那些方法需要开启事务,可以使用 * 通配符,如果方法未被包含在内,那么将表示不开启事务