20201125 王维
学习总结
1 AOP的相关概念
1.1 概述
AOP:全称是 Aspect Oriented Programming 即:面向切面编程。通过预编译和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP(面向对象编程)的延续,是函数式编程的一种衍生泛型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发效率。
简单来说它就是吧我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上,对我们已有的方法进行增强。
1.2 静态代理与动态代理
Proxy代理模式是一种结构型设计模式,主要解决的问题是:在直接访问对象时带来的问题。其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
为了保持行为的一致性,代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代,所以在访问者看来两者没有丝毫的区别。通过代理类这中间一层,能有效控制对委托类对象的直接访问,也可以很好地隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。
当两个类需要通信时,引入第三方代理类,将两个类的关系解耦,让我们只了解代理类即可,而且代理的出现还可以让我们完成与另一个类之间的关系的统一管理,但是切记,代理类和委托类要实现相同的接口,因为代理真正调用的还是委托类的方法。
按照代理的创建时期,代理类可以分为两种:
静态:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的.class文件就已经存在了。
动态:在程序运行时运用反射机制动态创建而成。
静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
1.3 动态代理
1.3.1 动态代理常用的有两种方式
基于接口的动态代理 提供者:
- JDK 官方的 Proxy 类。
- 要求:被代理类最少实现一个接口。
基于子类的动态代理
- 提供者:第三方的 CGLib,如果报 asmxxxx 异常,需要导入 asm.jar。
- 要求:被代理类不能用 final 修饰的类(最终类)。
1.3.2 使用 JDK 官方的 Proxy 类创建代理对象
package com.itlaobing.spring.proxy.impl;
import com.itlaobing.spring.proxy.IActor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class Actor implements IActor {
@Override
public void basicAct(double money) {
System.out.println("拿到钱,开始其基本表演" + money);
}
@Override
public void dangerAct(double money) {
System.out.println("拿到钱, 开始危险表演" + money);
}
public static void main(String[] args) {
//找一个演员
Actor actor = new Actor();
//剧组直接联系演员
actor.basicAct(8000);
actor.dangerAct(50000);
//经纪公司代理演员
IActor proxyActor = (IActor) Proxy.newProxyInstance(
actor.getClass().getClassLoader(),
actor.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 执行被代理对象的任何方法,都会进过该方法。
* 此方法有拦截的功能
*
* @param proxy 代理对象的引用,不一定每次都用得到
* @param method 当前执行的方法对象
* @param args 执行方法所需的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String name = method.getName();
double money = (double) args[0];
Object rtValue = null;
if ("basicAct".equalsIgnoreCase(name)){
//基本演出,没有2000不演
if (money > 2000){
//经纪人抽成
//没有修改basicAct方法源码,但对方法进行了增强
rtValue = method.invoke(actor ,money/2);
}
}
if ("dangerAct".equals(name)){
if (money > 8000){
rtValue = method.invoke(actor , money/2);
}
}
return null;
}
});
//剧组联系经纪公司
proxyActor.basicAct(8000);
proxyActor.dangerAct(50000);
}
}
1.3.3 使用 CGLib 的 Enhancer 类创建代理对象
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.1_3</version>
</dependency>
package com.itlaobing.spring.proxy.impl;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class Actor2 {
public void basicAct(double money) {
System.out.println("拿到钱,开始其基本表演" + money);
}
public void dangerAct(double money) {
System.out.println("拿到钱, 开始危险表演" + money);
}
public static void main(String[] args) {
/**
* 基于子类的动态代理
* 要求:
* 被代理对对象不能是最终类
* 用到的类
* Enhancer
* create(Class , Callback)
* 方法的参数
* Class:被代理对象的字节码
* Callback:如何代理
*/
Actor2 actor2 = new Actor2();
Actor2 proxyActor = (Actor2) Enhancer.create(actor2.getClass()
, new MethodInterceptor() {
/**
* 执行被代理对象鹅任何方法,都会经过该方法
* 在此方法内部就可以对被代理对象的任何方法进行增强
*
* 参数
* @param obj
* @param method
* @param args
* @param proxy 当前执行方法的代理对象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
String name =method.getName();
double money = (double) args[0];
Object rtValue = null;
if ("basicAct".equalsIgnoreCase(name)){
if (money > 2000){
rtValue = method.invoke(actor2 , money/2);
}
}
if ("dangerAct".equalsIgnoreCase(name)){
if (money > 5000){
rtValue = method.invoke(actor2 , money/2);
}
}
return null;
}
});
proxyActor.basicAct(8000);
proxyActor.dangerAct(50000);
}
}
2 Spring 中的 AOP[掌握]
2.1 Spring 中 AOP 的细节
2.1.1 AOP 相关术语
Aspect(切面): 是切入点和通知(引介)的结合。
Joinpoint(连接点): 所谓连接点是指那些被拦截到的点。在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连 接点
Advice(通知/增强): 在特定的连接点处采取的操作就是通知。通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
Pointcut(切入点): 匹配连接点的谓词。建议与切入点表达式关联,并在与该切入点匹配的任何连接点处运行(例如,执行 具有特定名称的方法)。切入点表达式匹配的连接点的概念是AOP的核心,并且Spring默认使用 AspectJ切入点表达语言。
Introduction(引介): 引介是一种特殊的通知在不修改类代码的前提下, Introduction 可以在运行期为类动态地添加一些方法 或 Field。
Target(目标对象): 代理的目标对象。一个或多个方面通知的对象。由于Spring AOP是使用运行时代理实现的,因此该对象 将始终是代理对象。
Proxy(代理): 一个类被 AOP 织入增强后,就产生一个结果代理类。由AOP框架创建的一个对象,用于实现方法执行 等。在Spring框架中,AOP代理将是JDK动态代理或CGLIB代理。
Weaving(织入): 是指把增强应用到目标对象来创建新的代理对象的过程。
spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
2.1.2 学习 spring 中的 AOP 要明确的事
-
开发阶段(我们做的)
编写核心业务代码(开发主线):要求熟悉业务需求。
把公用代码抽取出来,制作成通知。(开发阶段最后再做)
在配置文件中,声明切入点与通知间的关系,即切面。
-
运行阶段(Spring 框架完成的)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目 标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的 代码逻辑运行。
2.1.3 关于代理的选择
在 spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式。
2.2 基于 XML 的 AOP 配置
2.2.1 使用步骤
导入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
抽取公共代码制作成通知
package com.itlaobing.spring.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
@Aspect //表明当前类是一个切面类
public class JdbcUtils {
@Resource
public ComboPooledDataSource ds;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
/**
* dao使用本方法获取连接
*/
public Connection getConnection() throws SQLException {
/**
* 如果有事务,放回当前事务的con
* 如果没有事务,通过连接池返回新的con
*/
//获取当前线程的事务连接
Connection con = tl.get();
if (con != null){
//有事务
return con;
}
return ds.getConnection();
}
/**
* 指定切入点表达式
*/
@Pointcut("execution(* com.itlaobing.spring.service.IAccountService.transfer(..))")
private void pt1(){}
/**
* 开启事务
*/
// @Before("pt1()")
public void beginTransaction() throws SQLException {
System.out.println("开启事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con != null){
throw new SQLException("已经开启了事务,不能重复开启!");
}
con = ds.getConnection();//给con赋值,表示开启了事务
con.setAutoCommit(false);//设置为手动提交
tl.set(con);//把当前事务连接放到tl中
}
/**
* 提交事务
*/
// @AfterReturning("pt1()")
public void commitTransaction() throws SQLException {
System.out.println("提交事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con == null) {
throw new SQLException("没有事务不能提交!");
}
con.commit();//提交事务
con.close();//关闭连接
con = null;//表示事务结束!
tl.remove();
}
/**
* 回滚事务
*/
// @AfterThrowing("pt1()")
public void rollbackTransaction() throws SQLException {
System.out.println("回滚事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con == null) {
throw new SQLException("没有事务不能回滚!");
}
con.rollback();
con.close();
con = null;
tl.remove();
}
/**
* 释放Connection
*/
// @After("pt1()")
public void releaseConnection() throws SQLException {
System.out.println("释放Connection");
}
/**
* 开启事务
* @param point 代理方法的参数
* @return
*/
@Around("pt1()")
public Object transactionAround(ProceedingJoinPoint point){
Object rtValue = null;
try {
Object[] args = point.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = point.proceed(args);
//后置通知:提交事务
commitTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
try {
rollbackTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}finally {
try {
releaseConnection();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return rtValue;
}
}
创建 spring 的配置文件并导入约束
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
把通知类用 bean 标签配置起来
<bean id="jdbcConfig" class="com.itlaobing.spring.util.JdbcConfig">
<property name="ds" ref="datasource"></property>
</bean>
使用 aop:config 声明 aop 配置
<aop:config>
<!-- 配置的代码都写在此处 -->
</aop:config>
aop:config
作用:用于声明开始 aop 的配置
使用 aop:pointcut 配置切入点表达式
<aop:pointcut id="pt1" expression="execution(* com.itlaobing.spring.service.IAccountService.transfer(..))"/>
aop:pointcut
作用:
用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
属性:
expression:用于定义切入点表达式。
id:用于给切入点表达式提供一个唯一标识
使用 aop:aspect 配置切面
<aop:aspect id="txAdvice" ref="jdbcUtils">
<!--配置通知的类型要写在此处-->
</aop:aspect>
使用 aop:xxx 配置对应的通知类型
<aop:before method="beginTransaction" pointcut-ref="pt1" />
<aop:after-returning method="commitTransaction" pointcut-ref="pt1" />
<aop:after-throwing method="rollbackTransaction" pointcut-ref="pt1" />
2.2.2 切入点表达式说明
execution: 匹配方法的执行(常用)
execution(表达式)
表达式语法:execution([修饰符] 返回值类型 包名.类名.方法名(参数))
写法说明:
全匹配方式:
public void com.itlaobing.service.impl.AccountServiceImpl.saveAccount(com.itlaobing.model.Accou nt)
访问修饰符可以省略
void com.itlaobing.service.impl.AccountServiceImpl.saveAccount(com.itlaobing.model.Accou nt)
返回值可以使用*号,表示任意返回值
* com.itlaobing.service.impl.AccountServiceImpl.saveAccount(com.itlaobing.model.Accou nt)
包名可以使用 * 号,表示任意包,但是有几级包,需要写几个*
*.*.*.*.AccountServiceImpl.saveAccount(com.itlaobing.model.Account)
使用..
来表示当前包,及其子包
com..AccountServiceImpl.saveAccount(com.itlaobing.model.Account)
类名可以使用*
号,表示任意类
com..*.saveAccount(com.itlaobing.model.Account)
方法名可以使用*
号,表示任意方法
com..*.*( com.itlaobing.model.Account)
参数列表可以使用*
,表示参数可以是任意数据类型,但是必须有参数
com..*.*(*)
参数列表可以使用..
表示有无参数均可,有参数可以是任意类型
com..*.*(..)
全通配方式: *..*.*(..)
注: 通常情况下,我们都是对业务层的方法进行增强,所以切入点表达式都是切到业务层实现类。 execution(* com.itlaobing.service.impl.*.*(..))
2.2.3 通知类型
aop:before
作用: 用于配置前置通知。指定增强的方法在切入点方法之前执行
属性:
method:用于指定通知类中的增强方法名称
ponitcut-ref:用于指定切入点的表达式的引用
poinitcut:用于指定切入点表达式
执行时间点: 切入点方法执行之前执行
aop:after-returning
作用: 用于配置后置通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点: 切入点方法正常执行之后。它和异常通知只能有一个执行
aop:after-throwing
作用: 用于配置异常通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点: 切入点方法执行产生异常后执行。它和后置通知只能执行一个
aop:after
作用: 用于配置最终通知
属性:
method:指定通知中方法的名称.
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
执行时间点: 无论切入点方法执行时是否有异常,它都会在其后面执行。
aop:around
作用: 用于配置环绕通知
属性:
method:指定通知中方法的名称。
pointct:定义切入点表达式
pointcut-ref:指定切入点表达式的引用
说明: 它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
注意: 通常情况下,环绕通知都是独立使用的
public Object transactionAround(ProceedingJoinPoint point){
Object rtValue = null;
try {
Object[] args = point.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = point.proceed(args);
//后置通知:提交事务
commitTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
try {
rollbackTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}finally {
try {
releaseConnection();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return rtValue;
}
}
<aop:around method="transactionAround" pointcut-ref="pt1" />
2.3 基于注解的 AOP 配置
2.3.1使用步骤
在 spring 配置文件中开启 spring 对注解 AOP 的支持
<!-- 开启 spring 对注解 AOP 的支持 -->
<aop:aspectj-autoproxy/>
把通知类也使用注解配置
@Component("jdbcConfig")
public class JdbcConfig {
@Resource
private DataSource ds = null;
}
在通知类上使用@Aspect 注解声明为切面
@Component("jdbcConfig")
@Aspect//表明当前类是一个切面类
public class JdbcConfig {
}
@Aspect 作用: 把当前类声明为切面类
在增强的方法上使用注解配置通知
package com.itlaobing.spring.util;
import com.mchange.v2.c3p0.ComboPooledDataSource;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@Component
@Aspect //表明当前类是一个切面类
public class JdbcUtils {
@Resource
public ComboPooledDataSource ds;
private static ThreadLocal<Connection> tl = new ThreadLocal<>();
/**
* dao使用本方法获取连接
*/
public Connection getConnection() throws SQLException {
/**
* 如果有事务,放回当前事务的con
* 如果没有事务,通过连接池返回新的con
*/
//获取当前线程的事务连接
Connection con = tl.get();
if (con != null){
//有事务
return con;
}
return ds.getConnection();
}
/**
* 指定切入点表达式
*/
@Pointcut("execution(* com.itlaobing.spring.service.IAccountService.transfer(..))")
private void pt1(){}
/**
* 开启事务
*/
// @Before("pt1()")
public void beginTransaction() throws SQLException {
System.out.println("开启事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con != null){
throw new SQLException("已经开启了事务,不能重复开启!");
}
con = ds.getConnection();//给con赋值,表示开启了事务
con.setAutoCommit(false);//设置为手动提交
tl.set(con);//把当前事务连接放到tl中
}
/**
* 提交事务
*/
// @AfterReturning("pt1()")
public void commitTransaction() throws SQLException {
System.out.println("提交事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con == null) {
throw new SQLException("没有事务不能提交!");
}
con.commit();//提交事务
con.close();//关闭连接
con = null;//表示事务结束!
tl.remove();
}
/**
* 回滚事务
*/
// @AfterThrowing("pt1()")
public void rollbackTransaction() throws SQLException {
System.out.println("回滚事务");
Connection con = tl.get();//获取当前线程的事务连接
if (con == null) {
throw new SQLException("没有事务不能回滚!");
}
con.rollback();
con.close();
con = null;
tl.remove();
}
/**
* 释放Connection
*/
// @After("pt1()")
public void releaseConnection() throws SQLException {
System.out.println("释放Connection");
}
/**
* 开启事务
* @param point 代理方法的参数
* @return
*/
@Around("pt1()")
public Object transactionAround(ProceedingJoinPoint point){
Object rtValue = null;
try {
Object[] args = point.getArgs();
//前置通知:开启事务
beginTransaction();
//执行方法
rtValue = point.proceed(args);
//后置通知:提交事务
commitTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
try {
rollbackTransaction();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}finally {
try {
releaseConnection();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return rtValue;
}
}
@AfterReturning
作用: 把当前方法看成是后置通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
@AfterThrowing
作用: 把当前方法看成是异常通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
@After
作用: 把当前方法看成是最终通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用
@Around
作用: 把当前方法看成是环绕通知。
属性: value:用于指定切入点表达式,还可以指定切入点表达式的引用。
切入点表达式注解
@Pointcut
作用: 指定切入点表达式
属性: value:指定表达式的内容
2.3.2 不使用 XML 的配置方式
@Configuration
@ComponentScan(basePackages="com.itlaobing")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
@EnableAspectJAutoProxy
作用: 开启对注解AOP的支持
心得体会
学习了面向切面编程AOP,这是一种编程思想,还学习了一种设计模式,代理模式,这种设计模式使代码复用性和易用性得到进一步提升,而这符合了面向对象的设计理念,其中的动态代理可以说是AOP思想的一种实现。Spring的AOP是将很多的步骤简化,我们只需要进行相应的配置就可以实现动态代理,大大提高了开发效率。
评论留言