Spring框架----->(4) AOP面向切面编程

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:自定义变量,表示目标方法的返回值,其变量名必须和通知方法的形参名一样

后置通知的执行顺序

  1. Object res = doOther();
  2. 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("执行最终通知,总是会被执行的代码");
    //一般做资源清除工作的
}

猜你喜欢

转载自blog.csdn.net/hcz666/article/details/113621092