目录
概述
AOP(Aspect Oriented Programming)面向切面编程
作用:在不惊动原始设计的基础上为其进行功能增强。
设计理念:无侵入式
核心概念
1.连接点(JoinPoint):程序执行过程中的任意位置都可以指为连接点。
2.切入点(PointCut):匹配连接点的式子,指定哪个连接点进行功能增强。
3.通知类:封装通知的类。
4.通知(Advice):在切入点执行的操作,共性功能,也就是增强的方法。
5.切面(Aspect):用来描述切入点和通知之间的关系。例如:用切面来描述通知在原始方法前执行还是原始方法后执行。
看到这些概念,也许还是有点迷糊,举个例子:AOP的核心理念就是无侵入式的代码功能增强 ,现在,我们想给一个类中的所有方法都添加一个功能,就是在方法执行后打印当前时间,打印当前时间就相当于是AOP增强代码。在其中,连接点不用说,就是类中的任意位置,切入点就相当于是其中的所有方法,因为方法需要进行功能增强,通知相当于是其中的打印当前时间的方法,通知类中包含通知方法,切面用来描述切入点和通知之间的关系,也就是在原始方法”之后“执行通知方法。”之后”就是他们之间的关系。
AOP快速上手步骤
AOP测试项目结构
1.导入坐标aspectjweaver
2.定义原始方法进行测试(到时候使用AOP给此原始方法添加功能)
接口
public interface BookDAO {
void save();
void writer();
}
实现类
@Repository
public class BookDAOImpl implements BookDAO{
@Override
public void save() {
System.out.println(System.currentTimeMillis());
System.out.println("bookDAO...save...");
}
@Override
public void writer() {
System.out.println("bookDAO...writer...");
}
}
3.定义通知类和通知(共性功能)
@Component
@Aspect
public class Advice { //通知类
public Object getTime(ProceedingJoinPoint joinPoint) throws Throwable {
//定义通知
public void getTime(){
System.out.println(System.currentTimeMillis());
}
}
4.设置切入点 (设定哪个方法需要进行功能增强)
@Pointcut("execution(void com.mh.dao.BookDAO.writer())")
private void pt(){} //方法名任意,空参,空方法体
5.设置切面
@Before("pt()") //设置为before:在原始方法前添加此增强的功能
public void getTime(){
System.out.println(System.currentTimeMillis());
}
测试代码:
public class app {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDAO bean = context.getBean(BookDAO.class);
bean.writer();
}
测试结果:未使用AOP前结果
测试结果:使用AOP后结果
在其中值得注意的是:通知类需要加两个注释:@Component 和 @Aspect ,把通知类加入到IoC容器中,并且告诉IoC容器他是一个关于切面的bean。
还需要在Spring的配置类中添加注解:@EnableAspectJAutoProxy
就是开启切面的自动代理,用于bean加载时使用代理对象执行上增强的方法。
具体的AOP执行流程如下:
AOP执行流程
1.Spring容器启动。
2.读取所有切面配置的切入点(并不是所有切入点都读取)。
3.初始化bean,检查bean中的方法是否与读取到的切入点匹配上。
3.1:如果匹配上了,说明bean的方法需要进行代码增强(AOP),则创建原始对象(目标对象)的代理对象。
3.2:如果没有匹配上,说明bean中的方法没有配置切入点,则直接创建对象。
4.获取bean,执行方法。
AOP切入点表达式
说明
作用:用来描述哪个方法需要进行功能增强。
在上面的AOP使用步骤中,如果是第一次写AOP,应该会有点迷糊,因为切入点表达式的书写方式并不简单,甚至看着好像有点复杂。下面就来解读AOP切入点表达式的格式。
可以先用上面使用的切入点表达式来说明:
execution(void com.mh.dao.BookDAO.writer())
红色部分就是切入点表达式,
void代表返回值类型
com.mh.dao.BookDAO代表包路径和接口
writer()代表方法
这是简写的表达式,有一些省略掉了,并不是标准的格式。
标准格式的切入点表达式
execution(public Book com.mh.dao.BookDAO.writer(int))
解释说明:对应关系:
动作关键字(权限修饰符,返回值类型,包名.类/接口名.方法名(参数)异常名)
—值得注意的是:权限修饰符和异常名可以省略
使用通配符描述切入点(快速描述)
支持符号说明:
1. 符号:* 单个独立的任意符号,可以作为前缀或后缀的匹配符出现,表示任意多个字符。
举例:execution(public * com.mh.*.BookService.select*(*))
此表达式解释为:权限修饰符为public,任意返回值,com.mh下的所有包下的BookService接口下的select开头的方法,方法参数任意且一个。
2.符号:.. 表示多个连续的任意符号,常用于简化包名与参数的书写。
举例:execution(public Book com.mh..BookService.save(..))
此表达式解释为:权限修饰符为public,返回值类型为Book,con.mh包下的任意包下的BookService接口或类的save方法,方法参数为任意且可以一个或多个。
3.符号:+ 专用于匹配子类类型
举例:execution(* *..*Service+.*(..))
此表达式解释为:返回值类型任意,任意包下的以Service结尾的类或接口的子类。
总结
使用切入点表达式,按照自己的需求使用表达式进行描述,并没有什么硬性要求,只要能锁定到自己的目标连接点就行,如果没有头绪,可以参考如下的书写技巧。
书写技巧:
1.所有代码按照标准规范开发,否则以下技巧全部失效
2.描述切入点通常描述接口,而不描述实现类
3.访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)
4.返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述
5.包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
6.接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
7.方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
8.参数规则较为复杂,根据业务方法灵活调整
9.通常不使用异常作为匹配规则
AOP通知类型
说明
在上面的AOP快速上手中,我们用的通知类型为Before,也就是在原始方法前执行通知(增强的方法)。但是有时候我们的需要并不仅仅想要在“Before之前”,还有之后,中间等。这些东西在AOP中当然存在,下面进行介绍。
五种通知类型
1.@Before:在原始方法前执行通知方法。
2.@After:在原始方法后执行通知方法。
3.@Around:可以让通知方法在原始方法其中的指定位置执行。
4.@AfterReturning:通知方法在原始方法正常执行完毕后执行。
5.@AfterThrowing:通知方法在原始方法抛出异常后执行。
说明:在5种通知类型中,用法基本一致,常用的是@Around,下面将演示@Around的使用和说明Around使用时的注意事项。
//通知类
@Component
@Aspect
public class Advice {
//切入点
@Pointcut("execution(void com.mh.dao.BookDAO.writer())")
private void pt(){}
//切面和通知
@Around("pt()")
public Object getTime(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println(System.currentTimeMillis());
Object proceed = joinPoint.proceed();
System.out.println("after....");
return proceed;
}
}
可以看到,Around的使用方式和Before,After是有些区别的,需要使用Around描述的通知,要有声明ProceedingJoinPoint (程序连接点)类型的参数,用来表示原始方法的,在上面,我们使用joinPoint.proceed() 调用方法代表原始方法的位置。
值得注意的是:调用方法后需要获取返回值,并且将其返回出去。就像上面代码的proceed被返回。
使用的注意事项:
1.Around环绕通知必须依赖形参ProceedingJoinPoint才能实现原始方法的调用,进而可实现原始方法前后同时添加通知。
2.通知中如果未使用ProceedingJoinPoint对原始方法进行调用,将跳过原始方法的执行,也就是不执行原始方法,只执行增强的方法。
3.对原始方法的调用可以不接收返回值,将通知方法返回值设为void即可,有返回值只可以选择Object类型。
4.原始方法的返回值如果是void类型,通知方法的返回值类型可以设成void,也可以设为Object,不必保持一致。
5.由于无法预知原始方法执行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象。
通知方法获取原始方法执行时数据
1.获取执行签名信息
我们知道可以使用ProceedingJoinPoint来调用原始方法,但是如果方法过多且类型不明确等因素,我们就可以这样调用获取方法名和执行类型:
@Around("pt()")
public Object getTime(ProceedingJoinPoint joinPoint) throws Throwable {
//获取签名信息
Signature signature = joinPoint.getSignature();
//通过签名获取执行类型(接口名)
String declaringTypeName = signature.getDeclaringTypeName();
//通过签名获取执行操作名(方法名)
String name = signature.getName();
System.out.println("执行类型名:" + declaringTypeName);
System.out.println("执行操作名:" + name);
}
执行结果
2.获取原始方法调用参数
通过通知方法参数ProceedingJoinPoint调用:
Object[] args = joinPoint.getArgs();
返回的是数组。
3.获取返回值
其实获取返回值很简单,就是调用原始方法时获取返回值就行。例如:
Object proceed = joinPoint.proceed(); 调用了原始方法,获取返回值proceed,此变量就是原始方法的返回值。
但是,还有一种方式,是在@AfterReturning中获取返回值,在切面中声明返回值将之获取。
例如:
@AfterReturning(value = "pt()",returning = "retv")
public void getReturnValue(String retv){
System.out.println("原始方法返回值:" + retv);
}
4.获取异常
获取异常的方法和获取返回值很相似,都是在切面中声明后调用即可。例如:
@AfterThrowing(value = "pt()",throwing = "t")
public void getReturnValue(Throwable t) {
System.out.println("原始方法抛出的异常:" + t);
}
除了在@AfterThrowing中获取异常,我们也可以在其他通知类型中获取异常,方式如下:
(调用原始方法时使用try-cat捕获异常)
@Around("pt()")
public Object getTime(ProceedingJoinPoint joinPoint) throws Throwable {
try {
Object proceed = joinPoint.proceed();
}catch (Throwable t){
t.printStackTrace();
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/154550.html