AOP面向切面编程
(1)动态代理
【重点】概念:就是可以在程序执行过程中,创建代理对象
实现方式:jdk动态代理,使用jdk中的Proxy,Method,InvocaitonHanderl创建代理对象。jdk动态代理要求目标类必须实现接口。
【重点】动态代理的作用:
1)在目标类源代码不改变的情况下,增加功能。
2)减少代码的重复
3)专注业务逻辑代码
4)解耦合,让你的业务功能和日志,事务非业务功能分离。
1、没有使用动态代理实现功能增强的例子:
先定义好接口与一个实现类,该实现类中除了要实现接口中的方法外,还要再写两个非业务方法。非业务方法也称为交叉业务逻辑:
➢ doTransaction():用于事务处理
➢ doLog():用于日志处理
然后,再使接口方法调用它们。接口方法也称为主业务逻辑。
- 接口:
public interface SomeService {
void doSome();
void doOther();
}
- 接口实现类:
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
SerivceTools.doLog();
System.out.println("执行doSome方法");
SerivceTools.doTrans();
}
@Override
public void doOther() {
SerivceTools.doLog();
System.out.println("执行doOther方法");
SerivceTools.doTrans();
}
}
- 交叉业务逻辑代码工具类:
public class SerivceTools {
public static void doLog(){
System.out.println("非业务方法,方法执行时间:"+new Date());
}
public static void doTrans(){
System.out.println("非业务方法,方法执行完毕之后,提交事务");
}
}
存在弊端:
交叉业务与主业务深度耦合在一起。当交叉业务逻辑较多时,在主业务代码中会出现大量的交叉业务逻辑代码调用语句,大大影响了主业务逻辑的可读性,降低了代码的可维护性,同时也增加了开发难度。
2、使用jdk动态代理实现功能增强的例子:
jdk动态代理实现步骤:
1、创建目标类,SomeServiceImpl目标类,给它的doSome方法,doOther方法增加输出时间、事务
2、创建InvocationHandler接口实现类,在这个类实现给目标方法增加功能
3、使用jdk中类Proxy,创建代理对象,实现创建对象的能力
- 接口实现类:
public class SomeServiceImpl implements SomeService {
@Override
public void doSome() {
System.out.println("执行doSome方法");
}
@Override
public void doOther() {
System.out.println("执行doOther方法");
}
}
- 功能增强类:
public class MyIncationHandler implements InvocationHandler {
//目标对象
private Object target;//SomeServiceImpl类
public MyIncationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
//通过代理对象执行方法时,会调用执行这个目标类的invoke()方法
Object res = null;
if ("doSome".equals(methodName)) {
SerivceTools.doLog();//在目标方法之前,输出时间
//执行目标类的方法,通过Method类实现
res = method.invoke(target, args);//SomeServiceImpl.doSome()
SerivceTools.doTrans();//在目标方法之后,提交事务
}else{
res = method.invoke(target, args);//SomeServiceImpl.doOther()
}
//目标方法的执行结果
return res;
}
}
- 测试类:
public class TestMain {
public static void main(String[] args) {
//创建目标对象
SomeService target = new SomeServiceImpl();
//创建InvocationHandler对象---》传入目标对象
InvocationHandler handler = new MyIncationHandler(target);
//使用Proxy创建代理---》反射机制
SomeService service = (SomeService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),handler);
//通过代理执行方法,会调用handler 中的invoke()方法
service.doSome();
System.out.println("============================================");
service.doOther();
}
}
(2)cglib代理
概念:第三方的工具库,创建代理对象
实现方式:原理是继承,通过继承目标类来生成目标类的子类,而子类是增强过的,这个子类就是代理对象。 要求目标类不能是final的, 方法也不能是final的。
(3)AOP概述
- AOP(Aspect Orient Programming):
面向切面编程, 基于动态代理的,可以使用jdk,cglib两种代理方式。Aop就是动态代理的规范化, 把动态代理的实现步骤,方式都定义好了,让开发人员用一种统一的方式,使用动态代理。
- 面向切面编程:
就是将交叉业务逻辑封装成切面,利用 AOP 容器的功能将切面织入到
主业务逻辑中。所谓交叉业务逻辑是指,通用的、与主业务逻辑无关的代码,如安全检查、
事务、日志、缓存等。
- Aspect:
切面,给你的目标类增加的功能,就是切面。 像上面用的日志,事务都是切面。
切面的特点: 一般都是非业务方法,独立使用的。
- Orient:
面向, 对着。
- Programming:
编程
(4)面向对象编程和面向切面编程的区别
oop面向对象编程:
1)需要在分析项目功能时,找出对象
2)分析对象有哪些属性和方法
AOP面向切面编程:【重点】
1)需要在分析项目功能时,找出切面。
2)合理的安排切面的执行时间(在目标方法前, 还是目标方法后)
3)合理的安全切面执行的位置,在哪个类,哪个方法增加增强功能
(5)AOP术语
1)切面(Aspect)
切面泛指交叉业务逻辑,就是代码增强功能,完成某一个功能。非业务功能,常见的切面功能有日志,事务,统计信息,参数检查,权限验证。
2)连接点(JoinPoint)
连接点指的是连接业务方法和切面的位置,就某类中的业务方法
3)切入点(Pointcut)
切入点是指多个连接点方法的集合。被标记为 final 的方法是不能作为连接点与切入点的。因为最终的是不能被修改的,不能被增强的。
4)目标对象(Target)
目标对象指将要被增强的对象,也就是给哪个类的方法增加功能, 这个类就是目标对象
5)通知(Advice)
通知表示切面的执行时间,通知定义了增强代码切入到目标代码的时间点,是目标方法执行之前执行,还是之后执行等。通知类型不同,切入时间不同。
总结:
切入点定义切入的位置,通知定义切入的时间
切面实现的三个关键要素:
1)切面的功能代码,切面干什么
2)切面的执行位置,使用Pointcut表示切面执行的位置
3)切面的执行时间,使用Advice表示时间,在目标方法之前,还是目标方法之后
(6)AOP实现
AOP的技术实现框架:
1、spring:spring在内部实现了aop规范,能做aop的工作,spring主要在事务处理时使用。而我们在项目中很少用spring的aop实现,因为spring的aop比较笨重。
2、AspectJ:一个开源的专门做aop的框架。spring框架中集成了aspectj框架,通过spring就能使用aspectj的功能。
aspectJ框架实现aop有两种方式:
1.使用xml的配置文件 : 配置全局事务
2.使用注解,我们在项目中要做aop功能,一般都使用注解,aspectj有5个注解
(7)AspectJ的通知类型(切面执行时间)
AspectJ 中常用的通知有五种类型:
(1)前置通知 @Before
(2)后置通知 @AfterReturning
(3)环绕通知 @Around
(4)异常通知 @AfterThrowing
(5)最终通知 @After
(8)AspectJ的切入点表达式(切面执行位置)
- 切入表达式:
execution(访问权限 方法返回值 方法声明(参数) 异常类型)
切入点表达式要匹配的对象就是目标方法的方法名。所以,execution 表达式中明显就是方法的签名。注意,表达式中黄色文字表示不可省略部分,各部分间用空格分开。在其中可以使用以下符号:
- 举例:
execution(public * (…))
指定切入点为:任意公共方法。
execution( set*(…))
指定切入点为:任何一个以“set”开始的方法。
execution(* com.xyz.service..(…))
指定切入点为:定义在 service 包里的任意类的任意方法。
execution(* com.xyz.service….(…))
指定切入点为:定义在 service 包或者子包里的任意类的任意方法。“…”出现在类名中 时,后面必须跟“”,表示包、子包下的所有类。
execution( …service..*(…))
指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点
(9)AspectJ基于注解的AOP实现
1、@Before前置通知—JoinPoint参数
在目标方法之前执行,可以包含一个JoinPoint类型参数。通过该参数可获取切入表达式、方法签命、目标对象等。不光前置通知可以包含这个参数,所有通知方法均可包含该参数。
A、定义业务接口与实现类
public interface SomeService {
void doSome(String name,Integer age);
}
/**
* 目标类
*/
public class SomeServiceImpl implements SomeService {
@Override
public void doSome(String name, Integer age) {
//给doSome方法增加一个功能,在doSome方法执行之前,输出方法的实行时间
System.out.println("目标方法的执行doSome()");
}
}
B、定义切面类
类中定义了若干普通方法,将作为不同的通知方法,用来增强功能
/**
* 切面类
* @Aspect:是aspectj框架中的注解
* 作用:表示当前类是切面类
* 切面类:是用来给业务方法增加功能的类,在这个类中有切面的功能代码
* 位置:在类定义的上面
*/
@Aspect
public class MyAspect {
/**
* @Before:前置通知注解
* 属性:value,是切入点表达式,表示切面的功能执行位置
* 位置:在方法的上面
* 特点:
* 1、在目标方法之前执行的
* 2、不会改变目标方法执行的结果
* 3、不会影响目标方法的执行
*/
//1、时间 2、位置
@Before(value = "execution(* *..SomeServiceImpl.do*(..))")
public void myBefore(JoinPoint jp){
//获取方法的完整定义
System.out.println("方法的签名(定义)="+jp.getSignature());
System.out.println("方法的名称="+jp.getSignature().getName());
//获取方法的实参
Object[] args = jp.getArgs();
for (Object arg:args){
System.out.println("参数="+arg);
}
//就是你切面要执行的功能代码
System.out.println("前置通知,切面功能:在目标方法之前输出执行时间:"+ new Date());
}
}
C、声明目标对象和切面类对象
<!--把对象交给spring容器,由spring容器统一创建,管理对象-->
<!--声明目标对象-->
<bean id="someService" class="com.hcz.ba08.SomeServiceImpl"/>
<!--声明切面对象-->
<bean id="myAspect" class="com.hcz.ba08.MyAspect"/>
D、注册AspectJ的自动代理
在定义好切面 Aspect 后,需要通知 Spring 容器,让容器生成“目标类 + 切面”的代理对象。这个代理是由容器自动生成的。只需要在 Spring 配置文件中注册一个基于 aspectj 的自动代理生成器,其就会自动扫描到@Aspect 注解,并按通知类型与切入点,将其织入,并生成代理。
工作原理:
< aop:aspectj-autoproxy/ >通过扫描找到@Aspect 定义的切面类,再由切面类根据切入点找到目标类的目标方法,再由通知类型找到切入的时间点
<!--声明自动代理生成器-->
<aop:aspectj-autoproxy />
E、创建测试类
@Test
public void test01(){
String config="applicationContext.xml";
ApplicationContext ctx = new ClassPathXmlApplicationContext(config);
//从容器中获取目标对象
SomeService proxy = (SomeService)ctx.getBean("someService");
//jdk动态代理:proxy=com.sun.proxy.$Proxy8
System.out.println("proxy="+proxy.getClass().getName());
//通过代理的对象执行方法,实现目标方法执行时,增强了功能
proxy.doSome("张三",23);
}
2、@AfterReturning 后置通知— returning属性
在目标方法执行之后执行,该注解有一个returning属性,可以获取到目标方法的返回值,并修改这个返回值
A、接口增加方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name,Integer age);
}
B、实现方法
@Override
public String doOther(String name, Integer age) {
System.out.println("目标方法的执行doOther()");
return "abc";
}
C、定义切面方法
@AfterReturning(value = "execution(* *..SomeServiceImpl.doOther(..))",
returning = "res")
public void myAfterReturing(Object res){
//Object res:是目标方法执行后的返回值,根据返回值做你的切面的功能处理
System.out.println("后置通知:目标方法之后执行的,获取的返回值是="+res);
if (res.equals("abcd")){
}else {
}
if (res != null){
res = "hello";
}
}
解析:
returning:自定义变量,表示目标方法的返回值,其变量名必须和通知方法的形参名一样
后置通知的执行顺序:
- Object res = doOther();
- myAfterReturing(res);
3、@Around 环绕通知—ProceedingJoinPoint参数
在目标方法之前之后执行,包含一个ProceedingJoinPoint类型的参数,接口ProceedingJoinPoint有一个proceed( )方法,用于执行目标方法。如果目标方法有返回值,则该方法返回值就是目标方法的返回值。环绕通知经常用来做事务,在目标方法之前开启事务,执行目标方法,在目标方法之后提交事务。
- 特点:
1)它是功能最强的通知
2)在目标方法之前和之后都能增强功能
3)控制目标方法是否被调用执行
4)修改原来的目标方法的执行结果,影响最后的调用结果
A、接口增加方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name,Integer age);
}
B、实现方法
@Override
public String doFirst(String name, Integer age) {
System.out.println("业务方法doFirst");
return "doFirst";
}
C、定义切面方法
@Around(value = "execution(* *..SomeServiceImpl.doFirst(..))")
public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
//获取第一个参数值
String name = "";
Object[] args = pjp.getArgs();
if (args!=null&&args.length>1){
Object arg = args[0];
name = (String)arg;
}
//实现环绕通知
Object result = null;
System.out.println("环绕通知:在目标方法之前,输出时间:"+new Date());
//1、目标方法调用
if("lisi".equals(name)){
//符合条件,调用目标方法
result = pjp.proceed();//表示目标方法被调用 method.invoke(); Object result = doFirst();
}
System.out.println("环绕通知:在目标方法之后,提交事务");
//2、在目标方法之前或者之后加入功能
//修改目标方法的执行结果,影响方法最后的调用结果
if (result!=null){
result = "修改后结果";
}
//返回目标方法的执行结果
return result;
}
D、执行结果
环绕通知:在目标方法之前,输出时间:Wed Feb 03 20:59:06 CST 2021
业务方法doFirst
环绕通知:在目标方法之后,提交事务
str=修改后结果
4、@AfterThrowing异常通知—throwing属性
在目标方法抛出异常之后执行,包含一个参数throwing,表示发生的异常对象,变量名必须和方法名的参数一样。
- 执行原理:
try {
SomeServiceImpl.doSecond(..);
}catch (Exception e){
myAfterThrowing(e);
}
A、接口增加方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
}
B、实现方法
@Override
public void doSecond() {
System.out.println("业务方法doSecond"+(10/0));
}
C、定义切面方法
@AfterThrowing(value = "execution(* *..SomeServiceImpl.doSecond(..))",
throwing = "ex")
public void myAfterThrowing(Exception ex){
System.out.println("异常通知:方法发生异常时,执行:"+ex.getMessage());
//发送邮件等通知开发人员
}
5、@After最终通知
无论目标方法是否抛出异常,该增强方法都会执行。
- 执行原理:
try {
SomeServiceImpl.doThird(..);
}catch (Exception e){
myAfterThrowing(e);
}finally{
myAfter();
}
A、接口增加方法
public interface SomeService {
void doSome(String name, Integer age);
String doOther(String name, Integer age);
String doFirst(String name, Integer age);
void doSecond();
void doThird();
}
B、实现方法
@Override
public void doThird() {
System.out.println("业务方法doThird"+(10/0));
}
C、定义切面方法
@After(value = "execution(* *..SomeServiceImpl.doThird(..))")
public void myAfter(){
System.out.println("执行最终通知,总是会被执行的代码");
//一般做资源清除工作的
}
6、@Pointcut 定义切入点
当较多的通知增强方法使用相同的 execution 切入点表达式时,编写、维护均较为麻烦。
AspectJ 提供了@Pointcut 注解,用于定义 execution 切入点表达式。
其用法是,将@Pointcut 注解在一个方法之上,以后所有的 execution 的 value 属性值均可使用该方法名作为切入点。代表的就是@Pointcut 定义的切入点。这个使用@Pointcut 注解的方法一般使用 private 的标识方法,即没有实际作用的方法。
@Pointcut(value = "execution(* *..SomeServiceImpl.doThird(..))")
private void mypt(){
//无需代码
}
@Before(value = "mypt()")
public void myBefore(){
System.out.println("执行前置通知,在目标方法之前执行的");
}
@After(value = "mypt()")
public void myAfter(){
System.out.println("执行最终通知,总是会被执行的代码");
//一般做资源清除工作的
}