Spring-AOP-2



引子

有一个需求
1. 写一个计算器
2. 计算器接口: Interface—->ArithmeticCalculator—->Calculator接口,里面有加减乘除四个方法。
3. 计算器接口实现类: 实现类—->ArithmeticCalculatorLogingImple—->实现了加减乘除四个方法
4. 日志: 在程序执行期间追踪正在发生的活动


接口代码实现

public interface ArithmeticCalculator {

    int add(int i,int j);
    int sub(int i,int j);

    int mul(int i,int j);
    int div(int i,int j);
}

包含日志功能的接口实现类代码

public class ArithmeticCalculatorLoggingImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        System.out.println("The method add begins with ["+i+","+j+"]");
        int result = i + j;
        System.out.println("The method add ends with "+ result);
        return result;
    }

    @Override
    public int sub(int i, int j) {
        System.out.println("The method sub begins with ["+i+","+j+"]");
        int result = i - j;
        System.out.println("The method sub ends with "+ result);
        return result;
    }

    @Override
    public int mul(int i, int j) {
        System.out.println("The method mul begins with ["+i+","+j+"]");
        int result = i * j;
        System.out.println("The method mul ends with "+ result);
        return result;
    }

    @Override
    public int div(int i, int j) {
        System.out.println("The method div begins with ["+i+","+j+"]");
        int result = i / j;
        System.out.println("The method div ends with "+ result);
        return result;
    }
}

测试代码

public class Main {

    public static void main(String[] args) {

        ArithmeticCalculator arithmeticCalculator= null;
        arithmeticCalculator=new ArithmeticCalculatorLoggingImpl();

        int result = arithmeticCalculator.add(1, 2);
        System.out.println("-->"+result);

        result = arithmeticCalculator.div(4, 2);
        System.out.println("-->"+result);
    }
}

从上面的接口实现类的代码可以看到几点问题。

代码混乱:
越来越多的非业务需求(日志和验证等)
加入后,原有的业务方法急剧膨胀.
每个方法在处理核心逻辑的同时还必须
兼顾其他多个关注点。

代码分散:
以日志需求为例,只是为了满足这个单一需求
就不得不在多个模块(方法)里多次重复相同的日志代码。
如果日志需求发生变化,必须修改所有模块。


动态代理

使用动态代理解决上述问题

代理设计模式的原理:
使用一个代理将对象包装起来,
然后用该代理对象取代原始对象。
任何对原始对象的调用都要通过代理。
代理对象决定是否以及何时将方法调用转到原始对象上。

spring-pic-1

接口代码

public interface ArithmeticCalculator {

    int add(int i,int j);
    int sub(int i,int j);

    int mul(int i,int j);
    int div(int i,int j);
}

接口实现类代码

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

代理代码

ArithmeticCalculator target —-> ArithmeticCalculator proxy

在proxy中对原先定义的方法进行一定的”包装”。

public class ArithmeticCalculatorLoggingProxy {

    //要代理的对象,代理类
    private ArithmeticCalculator target;

    //构造器
    public ArithmeticCalculatorLoggingProxy(ArithmeticCalculator target){
        this.target=target;
    }

    //代理
    public ArithmeticCalculator getLoggingProxy(){

        ArithmeticCalculator proxy=null;


        //new出的对象JVM有默认的类加载器
        //代理的对象要指定类加载器
        ClassLoader loader=target.getClass().getClassLoader();

        //代理对象的类型,即其中有哪些方法
        Class [] interfaces = new Class[]{ArithmeticCalculator.class};

        //当调用代理对象其中的方法时,该执行的代码
        InvocationHandler h=new InvocationHandler() {

            /**
             * proxy:正在返回的代理对象.一般情况下,在invoke方法中都不使用该对象。
             * method: 正在被调用的方法
             * args: 调用方法时传入的参数 
             */

            @Override
            public Object invoke(Object proxy, Method method, Object[] args)
                    throws Throwable {

                String methodName = method.getName();

                //日志
                System.out.println("The method " + methodName + " begins with" + Arrays.asList(args));

                //执行方法
                Object result=null; 

                try {
                    //前置通知
                    result=method.invoke(target, args);
                    //返回通知,可以访问到方法的返回值
                } catch (Exception e) {
                    e.printStackTrace();
                    //异常通知,可以访问到方法出现的异常
                }

                //后置通知,因为方法可能会出异常,所以访问不到方法的返回值.

                //日志
                System.out.println("The method " + methodName + " ends with" + result);
                return result;
            }
        };

        //代理实例
        proxy=(ArithmeticCalculator)Proxy.newProxyInstance(loader, interfaces, h);

        return proxy;
    }
}

测试代码

public class Main {

    public static void main(String[] args) {
        ArithmeticCalculator target = new ArithmeticCalculatorImpl();
        ArithmeticCalculator proxy = new ArithmeticCalculatorLoggingProxy(target).getLoggingProxy();

        int result = proxy.add(1, 2);
        System.out.println("-->"+result);

        result=proxy.div(4, 2);
        System.out.println("-->"+result);
    }
}

动态代理开发中可以直接用,但是不建议使用。
1.这样写有点麻烦
2.让一般的程序员搞动态代理要求有点高


AOP(面向切面编程)

一. AOP基础

AOP(Aspct-Oriented Programming,面向切面编程)
是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充。

AOP的主要编程对象是切面(aspect),而切面是横切关注点模块化的一个特殊的对象。

在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能在哪里,
以什么方式应用,并且不必修改受影响的类.
这样一来横切关注点就被模块化到特殊的对象(切面)里。

AOP的好处
每个事物逻辑位于一个位置,代码不分散,便于维护和升级
业务模块更简洁,只包含核心业务代码

AOP术语

切面(Aspect):横切关注点(跨越应用程序多个模块的功能)
被模块化的特殊对象

通知(Advice):切面必须要完成的工作。

目标(Target):被通知的对象

代理(Proxy):向目标对象应用通知之后创建的对象

连接点(Joinpoint):
程序执行的某个特定位置:如类某个方法调用前,调用后,方法抛出异常后等。

连接点由两个信息确定:方法表示的程序执行点相对点表示的方位。

例如ArithmethicCalculator#add()
方法执行前的连接点执行点为ArithmethicCalculator#add();
方位为该方法执行前的位置。

切点(pointcut):每个类都拥有多个连接点:例如ArithmethicCalculator的所有方法世纪上都是连接点。
即连接点是程序类中客观存在的事务。AOP通过切点定位到特定的连接点。
类比理解: 连接点相当于数据库中的记录,切点相当于查询条件。
切点和连接点不是一对一的关系,一个切点匹配多个连接点,
切点通过org.springframework.aop.Pointcut接口进行描述,
它使用类和方法作为连接点的查询条件。

spring-pic-2

三对概念的个人理解
目标和代理:比方目标是接口,代理是包装过的接口

切面和通知: 切面就是一个特殊的对象,里面的方法就是通知。它们是要对对象方法进行包装的方法。就等于说是共同功能。

连接点和切入点:
切入点就是用切入表达式来描述。来确定在目标接口方法当中那些方法上进行包装,这里把目标和切面结合起来了。

连接点是是个对象,JoinPoint,用它来拿到目标方法的方法名和参数。

这个过程可以理解成:
接口就是目标, 切面就是模糊理解成代理(还不完全是)。
接口里头有很多方法,切面里头有很多通知。
在切面当中用切入点表达式定位到方法,用连接点获得方法的细节。然后进行包装的工作。

二. AspectJ

AspectJ:Java社区里最完整最流行的AOP框架

Spring 2.0以上版本中,可以使用基于AspectJ注解基于XML配置的AOP。

Spring自己也有一个AOP框架,不如AspectJ

在Spring中启用AspectJ注解支持

要在Spring应用中使用AspectJ注解,必须在classpath下包含AspectJ类库:
aopallicance.jar
aspectj.weaver.jar
spring-aspects.jar

将aop Schema添加到根元素中。

要在SpringIOC容器中启用AspectJ注解支持,只要在Bean配置文件中定义一个空的XML元素

当Spring IOC容器侦测到Bean配置文件中的元素时候,会
自动为与AspectJ切面匹配的Bean创建代理。

三. 用AspectJ注解声明切面

要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为Bean实例。
当在Spring IOC容器中初始化AspectJ切面之后,
Spring IOC容器就会为那些与AspectJ切面相匹配的Bean创建代理。

AspectJ注解中,切面只是一个带有@Aspect注解的Java
通知是标注有某种注解的简单的Java方法

AspectJ支持5种类型的通知注解:
@Before:前置通知,在方法执行之前执行。
@After:后置通知,在方法执行之后执行。
@AfterRunning:返回通知,在方法返回结果之后执行。
@AfterThrowing:异常通知,在方法抛出异常之后
@Around:环绕通知,围绕着方法执行

一个切面可以包括一个或者多个通知。

前置通知:在方法执行之前执行的通知。
前置通知使用@Before注解,并将切入点表达式的值作为注解值

后置通知: 后置通知是在连接点完成之后执行的,即连接点返回结果抛出异常的时候,下面的后置通知记录了方法的终止。

在后置通知中还不能访问目标方法执行的结果。
目标方法执行的结果需要在返回通知当中访问。

返回通知: 目标方法正常执行,可以获取返回值的通知。

异常通知:目标方法发生了异常的时候,可以获取异常对象的通知。

引入通知(很少使用): 是一种特殊的通知类型,它通过为接口提供实现类,
允许动态地实现接口,就像对象已经在运行时扩展了实现类一样。

切入点表达式

利用方法签名编写AspectJ切入点表达式
最典型的切入点表达式是根据方法的签名来匹配各种方法的:

最典型的切入点表达式时根据方法的签名来匹配各种方法:

  • execution * com.mozart.spring.ArithmeticCalculator.*(..)
    匹配ArithmeticCalculator中声明的所有方法,
    第一个*代表任意修饰符及任意返回值。
    第二个*代表任意方法
    ..匹配任意数量的参数。
    若目标类与接口与该切面在同一个包中,可以省略包名。

  • execution public * ArithmeticCalculator.*(..)
    匹配ArithmeticCalculator接口的所有公共方法。

  • execution public double ArithmeticCalculator.*(..)
    匹配ArithmeticCalculator中返回double类型数值的方法

  • execution public double ArithmeticCalculator.*(double,..)
    匹配第一个参数为double类型的方法,
    ..匹配任意数量任意类型的参数

  • execution public double ArithmeticCalculator.*(double,double):
    匹配参数类型为double,double类型的方法。

四. AspectJ使用步骤

1) 加入jar包

com.springsource.org.aopalliance-1.0.0.jar
com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar
spring-aop-4.0.0.RELEASE.jar
spring-aspects-4.0.0.RELEASE.jar

commons-logging-1.1.1.jar
spring-beans-4.0.0.RELEASE.jar
spring-context-4.0.0.RELEASE.jar
spring-core-4.0.0.RELEASE.jar
spring-expression-4.0.0.RELEASE.jar

2) 在配置文件中加入aop的命名空间

3) 基于注解的方式

①.在配置文件中加入如下配置:

②.把横切关注点的代码抽象到切面的类中.

i.切面首先是一个IOC中的Bean,即加入@Component注解
2.切面还需要加入@Aspect 注解

③.在类中声明通知

i.声明一个方法
ii.在方法前加入@Before注解

④.可以在通知方法中声明一个类型为JoinPoint的参数
然后就能访问链接细节.如方法名称和参数值。

五. AspectJ代码示例

目标接口代码

public interface ArithmeticCalculator {

    int add(int i,int j);
    int sub(int i,int j);

    int mul(int i,int j);
    int div(int i,int j);
}

目标接口实现类代码

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {

    @Override
    public int add(int i, int j) {
        int result = i + j;
        return result;
    }

    @Override
    public int sub(int i, int j) {
        int result = i - j;
        return result;
    }

    @Override
    public int mul(int i, int j) {
        int result = i * j;
        return result;
    }

    @Override
    public int div(int i, int j) {
        int result = i / j;
        return result;
    }
}

切面代码

//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。

@Aspect
@Component
public class LoggingAspect {

    //声明该方法是一个前置通知:在目标方法开始之前执行
    @Before("execution(public int com.mozart.spring.aop.impl.ArithmeticCalculator.*(int, int))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName=joinPoint.getSignature().getName();
        List<Object> args=Arrays.asList(joinPoint.getArgs());
        System.out.println("The method " + methodName + " begins with " + args);
    }
}

配置文件内容

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan base-package="com.mozart.spring.aop.impl"></context:component-scan>

    <!-- 使AspectJ 注解起作用: 自动为匹配的类生成代理对象-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

测试代码

public class Main {

    public static void main(String[] args) {

        //1. 创建Spring的IOC容器
        ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");

        //2. 从IOC容器中获取bean的实例
        ArithmeticCalculator arithmeticCalculator=ctx.getBean(ArithmeticCalculator.class);

        //3. 使用bean
        int result = arithmeticCalculator.add(3, 6);
        System.out.println("result-->"+result);

        result = arithmeticCalculator.div(12, 6);
        System.out.println("result-->"+result);
    }
}

六. AspectJ切面通知代码示例

这是对上面切面代码的一个补充。

//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。

@Aspect
@Component
public class LoggingAspect {

    /**
     * 
     * 在com.mozart.spring.aop.ArithmeticCalculator接口的实现类的每一个方法开始之前执行一段代码
     */

    @Before("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName=joinPoint.getSignature().getName();
        Object [] args=joinPoint.getArgs();
        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }

    /**
     * 后置通知,在方法执行之后执行的代码,无论该方法是否出现异常。
     * @param joinPoint
     */

    @After("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }

    /**
     * 返回通知,在方法正常结束以后执行的代码,返回通知是可以访问到方法的返回值的。
     * @param joinPoint
     */

    @AfterReturning(value="execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))",
            returning="result")
    public void afterReturning(JoinPoint joinPoint,Object result){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends with "+result);
    }

    /**
     * 在目标方法出现异常时会执行代码
     * 可以访问到异常对象,且可以指定在出现特定异常时执行通知,Exception--> NullPointerException
     * @param joinPoint
     * @param ex
     */

    @AfterThrowing(value="execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))",
            throwing="ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " occurs exception "+ ex);
    }

    /**
     * 最强的,但是不是最常用的。
     * 环绕通知需要携带ProceedingJoinPoint类型的参数.
     * 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
     * 且环绕通知必须有返回值。返回值必为目标方法的返回值
     * @param pjd
     */

    /*
    @Around("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
    public Object aroundMethod(ProceedingJoinPoint pjd){
        Object result=null;
        String methodName=pjd.getSignature().getName();

        try {
            //前置通知
            System.out.println("The method "+methodName+" begins with "+Arrays.asList(pjd.getArgs()));

            //执行目标方法
            result=pjd.proceed();

            //返回通知
            System.out.println("The method "+methodName+" ends with "+ result);
        } catch (Throwable e) {

            //异常通知
            System.out.println("The method occurs exception: " + e);
            throw new RuntimeException(e);
        }

        //后置 通知

        System.out.println("The method "+methodName+" ends");
        return result;
    }
    */
}

七. AspectJ切面优先级

当目标有多个切面的时候,切面里头通知的包装顺序怎么确定呢?
可以使用@Order指定切面的通知的优先级。
值越小,优先级越高。

八. AspectJ切面中切入点表达式的复用

定义一个方法,用于声明切入点表达式,一般地,该方法中再不需要添入其他的代码
使用@Pointcut来声明切入点表达式
后面的其他通知直接使用方法名来引用当前的切入点表达式

九. 基于注解配置AOP最终代码展示

日志切面代码

//把这个类声明为一个切面;需要把该类放入到IOC容器中;再声明为一个切面。

@Order(2)
@Aspect
@Component
public class LoggingAspect {

    /**
     * 定义一个方法,用于声明切入点表达式,一般地,该方法中再不需要添入其他的代码
     * 使用@Pointcut来声明切入点表达式
     * 后面的其他通知直接使用方法名来引用当前的切入点表达式
     */

    @Pointcut("execution(* com.mozart.spring.aop.ArithmeticCalculator.*(..))")
    public void declareJointPointExpression(){}

    /**
     * 
     * 在com.mozart.spring.aop.ArithmeticCalculator接口的实现类的每一个方法开始之前执行一段代码
     */

    @Before("declareJointPointExpression()")
    public void beforeMethod(JoinPoint joinPoint){
        String methodName=joinPoint.getSignature().getName();
        Object [] args=joinPoint.getArgs();
        System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
    }

    /**
     * 后置通知,在方法执行之后执行的代码,无论该方法是否出现异常。
     * @param joinPoint
     */

    @After("declareJointPointExpression()")
    public void afterMethod(JoinPoint joinPoint){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends");
    }

    /**
     * 返回通知,在方法正常结束以后执行的代码,返回通知是可以访问到方法的返回值的。
     * @param joinPoint
     */

    @AfterReturning(value="declareJointPointExpression()",
            returning="result")
    public void afterReturning(JoinPoint joinPoint,Object result){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " ends with "+result);
    }

    /**
     * 在目标方法出现异常时会执行代码
     * 可以访问到异常对象,且可以指定在出现特定异常时执行通知,Exception--> NullPointerException
     * @param joinPoint
     * @param ex
     */

    @AfterThrowing(value="declareJointPointExpression()",
            throwing="ex")
    public void afterThrowing(JoinPoint joinPoint,Exception ex){
        String methodName=joinPoint.getSignature().getName();
        System.out.println("The method " + methodName + " occurs exception "+ ex);
    }

    /**
     * 最强的,但是不是最常用的。
     * 环绕通知需要携带ProceedingJoinPoint类型的参数.
     * 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
     * 且环绕通知必须有返回值。返回值必为目标方法的返回值
     * @param pjd
     */

    /*
    @Around("declareJointPointExpression()")
    public Object aroundMethod(ProceedingJoinPoint pjd){
        Object result=null;
        String methodName=pjd.getSignature().getName();

        try {
            //前置通知
            System.out.println("The method "+methodName+" begins with "+Arrays.asList(pjd.getArgs()));

            //执行目标方法
            result=pjd.proceed();

            //返回通知
            System.out.println("The method "+methodName+" ends with "+ result);
        } catch (Throwable e) {

            //异常通知
            System.out.println("The method occurs exception: " + e);
            throw new RuntimeException(e);
        }

        //后置 通知

        System.out.println("The method "+methodName+" ends");
        return result;
    }
    */
}

验证切面代码

@Order(1)
@Aspect
@Component
public class VlidationAspect {

    @Before("LoggingAspect.declareJointPointExpression()")
    public void vlidateArgs(JoinPoint joinPoint){
        System.out.println("-->validate: "+Arrays.asList(joinPoint.getArgs()));
    }
}

十. 基于xml配置AOP最终代码展示

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

    <!-- 配置bean -->
    <bean id="arithmeticCalculator"
        class="com.mozart.spring.aop.xml.ArithmeticCalculatorImpl"></bean>


    <!-- 配置切面的bean -->
    <bean id="loggingAspect"
        class="com.mozart.spring.aop.xml.LoggingAspect"></bean>

    <bean id="vlidationAspect"
        class="com.mozart.spring.aop.xml.VlidationAspect"></bean>

    <!-- 配置aop -->
    <aop:config>

        <!-- 配置切点表达式  -->

        <aop:pointcut expression="execution(* com.mozart.spring.aop.xml.ArithmeticCalculator.*(int,int))" 
        id="pointcut"/>

        <!-- 配置切面及通知 -->

        <aop:aspect ref="loggingAspect" order="2">
            <aop:before method="beforeMethod" pointcut-ref="pointcut"/>
            <aop:after method="afterMethod" pointcut-ref="pointcut"/>
            <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/>
            <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
            <!-- 
            <aop:around method="aroundMethod" pointcut-ref="pointcut"/>
             -->
        </aop:aspect>

        <aop:aspect ref="vlidationAspect" order="1">
            <aop:before method="vlidateArgs" pointcut-ref="pointcut"/>
        </aop:aspect>

    </aop:config>
</beans>

猜你喜欢

转载自blog.csdn.net/oneqinglong/article/details/73011647