【Spring05】AOP面向切面编程

1 代理设计模式

1.1 代理设计模式的概念

代理设计模式是23种经典设计模式之一,在Spring的学习过程中我们也会去重点使用这种设计模式。

在Service层(业务层)的功能分为两大类:核心功能和辅助功能。

  • 核心功能:在本业务中会被使用,在其他业务中不会被使用的功能。

  • 辅助功能:每个业务基本上都需要使用,相关编码也基本上一致。

代理设计模式会将核心功能与辅助功能(事务、日志、性能监控代码)分离,达到核心业务更加纯粹的同时保证了辅助业务功能可以复用。

1.2 静态代理设计模式

静态代理是指通过代理类的对象,为原始业务类对象添加辅助功能。

对于房东对象而言,只需要做好签合同、收房租这类的“核心功能”即可,其他的辅助功能可以交给中介(代理)去做。

原始业务类:

public class FangDongServiceImpl implements FangDongService{
    @Override
    public void zufang() {
        //辅助功能、额外功能
        System.out.println("发布租房信息");
        System.out.println("带租客看房");
        
        //核心功能
        System.out.println("签合同");
        System.out.println("收房租");
        
    }
}

静态代理类:

public class FangDongProxy implements FangDongService{
    private FangDongService fangDongService = new FangDongServiceImpl();
    
    @Override
    public void zufang() {
        //辅助功能、额外功能
        System.out.println("发布租房信息");
        System.out.println("带租客看房");

        fangDongService.zufang();
    }
}
  • 在编写代理类的重要的原则:代理类的必须和原始业务类保持功能上的一致性,保证这一原则也很简单,只需要让代理类和原始业务类实现相同的接口即可;

  • 代理类=实现原始类相同的接口+添加辅助功能+调用原始类的业务方法。

原始业务类修改:

public class FangDongServiceImpl implements FangDongService{
    @Override
    public void zufang() {
        //核心功能
        System.out.println("签合同");
        System.out.println("收房租");

    }
}

静态代理实际上只是将辅助功能从原始业务类转移到代理类中,后续还是需要去维护代理类,还是会遇到代理类冗余、业务不纯净的问题。

1.3 动态代理设计模式

动态代理是指动态地创建代理类的对象,为原始类的对象添加辅助功能。

基于JDK原生动态代理(基于接口):

@Test
public void testJDK() {
    //目标
    FangDongService fangDongService = new FangDongServiceImpl();

    //额外功能
    InvocationHandler invocationHandler = new InvocationHandler() {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            //辅助功能
            System.out.println("发布租房信息1");
            System.out.println("带租客看房1");
            //核心
            fangDongService.zufang();
            return null;
        }
    };

    //动态生产代理类
    FangDongService fangDongProxy = (FangDongService)Proxy.newProxyInstance(DynamicProxyTest.class.getClassLoader(),
                                                                            fangDongService.getClass().getInterfaces(),
                                                                            invocationHandler);

    fangDongProxy.zufang();
}
  • InvocationHandler invocationHandler = new InvocationHandler():设置回调函数;

基于CGLIB动态代理(基于继承):

@Test
public void testCGLIB() {
    //目标
    FangDongService fangDongService = new FangDongServiceImpl();

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(FangDongServiceImpl.class);
    enhancer.setCallback(new org.springframework.cglib.proxy.InvocationHandler() {
        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            //辅助功能
            System.out.println("发布租房信息");
            System.out.println("带租客看房");
            //核心
            fangDongService.zufang();
            return null;
        }
    });

    FangDongServiceImpl  fangDongProxy = (FangDongServiceImpl) enhancer.create();
    fangDongProxy.zufang();
}
  • Enhancer enhancer = new Enhancer();:创建字节码增强对象;

  • enhancer.setSuperclass(FangDongServiceImpl.class);:设置父类,等价于实现原始类接口。

动态代理虽然可以解决项目中的额外功能问题,但使用起来过于繁琐,想要更加简洁地去解决这个问题,还是需要运用到Spring中的AOP相关技术。

2 AOP概念

5.2.1 概念

AOP(Aspect Oriented Programming),即面向切面编程,利用一种”横切“的技术,把一个对象的内部剖开,并将那些影响多个类的公共行为封装到一个可重用的模块,将其命名为”Aspect“,即切面。

何时使用AOP:想要给某些方法增加一些相同的功能,但源代码不能修改。给业务方法增加非业务功能也可以使用AOP

2.2 术语

  • Aspect:切面,给业务方法增加的功能;

    • 切面由切入点和通知组成;

  • JoinPoint:连接点,连接切面的业务方法,可被Spring拦截并切入内容,在这个业务方法执行时会同时增加切面的功能;

  • PointCut:切入点,是一个或多个连接点集合。表示这些方法执行时,都能增加切面的功能。(表示切面的执行位置);

  • Target:目标对象,给哪个对象增加切面的功能,那这个对象就是目标对象;

  • Advice:通知(增强),表示切面的执行时间。可以指定在目标方法执行之前执行切面,还是在目标方法之后执行切面;

  • Weaving:编织,把通知应用到具体的类,进而创建新的代理类的过程;

  • Proxy:代理,被AOP编织入通知后,产生的结果类。

其中最重要的的三个要素:Aspect(事件)、PointCut(地点)、Advice(时间):

  • 在Advice时间,在PointCut位置,执行Aspect。

2.3 作用

Spring的AOP编程是通过动态代理类为原始类的方法添加辅助功能。

3 AOP的基本使用

3.1 环境搭建

引入AOP相关依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>${spring.version}</version>
</dependency>

Spring配置文件引入AOP命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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.xsd">

3.2 定义原始类

原始类:

public class UserServiceImpl implements UserService{
    @Override
    public List<User> queryAllUser() {
        System.out.println("---queryAllUser---");
        return null;
    }

    @Override
    public Integer updateUser() {
        System.out.println("---updateUser---");
        return null;
    }

    @Override
    public Integer saveUser() {
        System.out.println("---saveUser---");
        return null;
    }

    @Override
    public Integer deleteUser() {
        System.out.println("---deleteUser---");
        return null;
    }
}

3.3 定义通知类

通知类:

public class MyBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("事务控制");
        System.out.println("日志打印");
    }
}
  • MethodBeforeAdvice:前置通知,

3.4 编织

首先需要将原始类和通知类都放到工厂中去交由Spring管理:

<!--目标-->
<bean id="userService" class="com.qianglj.test4.service.UserServiceImpl"/>
<!--通知-->
<bean id="myBeforeAdvice" class="com.qianglj.test4.advice.MyBeforeAdvice"/>

然后再进行编织,即设置原始类中的那些方法需要进行通知。

定义切入点、组装切面:

<aop:config>
    <!--定义切入点-->
    <aop:pointcut id="pc_1" expression="execution(* save*())"/>
    <!--组装-->
    <aop:advisor advice-ref="myBeforeAdvice" pointcut-ref="pc_1"/>
</aop:config>
  • expression="execution(* save*())":指定切入点表达式;

测试:

public void testSpringAOP(){
    ApplicationContext ac = new ClassPathXmlApplicationContext("beans011.xml");
    //通过目标的bean id 获取的是代理对象
    UserService userService = (UserService) ac.getBean("userService");
    userService.saveUser();
}

4 AOP通知详解

通知表示切面的执行时间。可以指定在目标方法执行之前执行切面,还是在目标方法之后执行切面。

通知的种类:

  • 前置通知:MethodBeforeAdvice

  • 后置通知:AfterAdvice

    • 后置无异常通知:AfterReturningAdvice

    • 后置异常通知:ThrowsAdvice

  • 环绕通知:MethodInterceptor

5 AOP切入点表达式详解

切入点表示切面的执行位置,是一个或多个连接点集合。表示这些方法执行时,都能增加切面的功能。

切入点的关键在于切入点表达式的编写,其语法格式如下:

execution(方法定义)
execution([访问修饰符(表达式)] 返回值类型(表达式) [包名类名(表达式)]方法名(表达式)(参数列表) [抛出异类型(表达式)])

其中的表达式:

  • *:0至多个任意字符;

  • ..

    • 用在方法参数中,表示任意多个参数;

    • 用在包名后,表示当前包及其子包路径;

  • +

    • 用在类名后,表示当前类及其子类;

    • 用在接口后,表示当前接口及其实现类。

举例:

  • execution(public * *(..)):任意公共方法;

  • execution(* set*(..)):任何一个以“set”开始的方法;

  • execution(* com.xyz.service.*.*(..)):定义在 service 包里的任意类的任意方法;

  • execution(* com.xyz.service..*.*(..)):定义在 service 包或者子包里的任意类的任意方法;

    • ..出现在类名中时,后面必须跟*,表示包、子包下的所有类;

  • execution(* *..service.*.*(..)):指定所有包下的 serivce 子包下所有类(接口)中所有方法为切入点;

  • execution(* *.service.*.*(..)):指定只有一级包下的 serivce 子包下所有类(接口)中所有方法为切入点;

  • execution(* *.ISomeService.*(..)):指定只有一级包下的 ISomeSerivce 接口中所有方法为切入点;

  • execution(* *..ISomeService.*(..)):指定所有包下的 ISomeSerivce 接口中所有方法为切入点

  • execution(* com.xyz.service.IAccountService.*(..)):IAccountService 接口中的任意方法。

  • execution(* com.xyz.service.IAccountService+.*(..)):IAccountService

    • 若为接口,则为接口中的任意方法及其所有实现类中的任意方法;

    • 若为类,则为该类及其子类中的任意方法;

  • execution(* joke(String,int))):所有的 joke(String,int)方法,且 joke()方法的第一个参数是String,第二个参数是 int;

    • 如果方法中的参数类型是 java.lang 包下的类,可以直接使用类名;

    • 否则必须使用全限定类名,如 joke( java.util.List, int);

  • execution(* joke(String,*))):所有的 joke()方法,该方法第一个参数为 String,第二个参数可以是任意类型;

    • 如 joke(String s1,String s2)和 joke(String s1,double d2)都是

    • 但 joke(String s1,double d2,String s3)不是

  • execution(* joke(String,..))):所有的 joke()方法,该方法第一个参数为 String,后面可以有任意个参数且参数类型不限;

    • 如 joke(String s1)、joke(String s1,String s2)和joke(String s1,double d2,String s3)都是

  • execution(* joke(Object)):所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型;

    • 如joke(Object ob)是;

    • 但joke(String s)与 joke(User u)均不是;

  • execution(* joke(Object+))):所有的 joke()方法,方法拥有一个参数,且参数是 Object 类型或该类的子类

    • 不仅 joke(Object ob)是,joke(String s)和 joke(User u)也是

6 AOP原理

6.1 JDK动态代理和CGlib动态代理的选择

Spring底层包含了JDK动态代理和CGlib动态代理梁总动态代理生成机制,选择使用哪一种机制的基本原则是:业务目标类如果有接口则用JDK代理,没有接口则用CGlib代理。

DefaultAopProxyFactory的核心方法:

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                                         "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    }
    else {
        return new JdkDynamicAopProxy(config);
    }
}
  • config.isProxyTargetClass():相当于在配置文件中做aop:config proxy-target-class="true">配置,不过默认是false

6.2 后处理器

在Spring中定义了很多后处理器,后处理器的作用是在bean创建完成之前都会有一个后处理过程,即再加工,对bean做出相关的改变和挑战。

spring-AOP中,就有一个专门的后处理器AspectJAwareAdvisorAutoProxyCreator,负责通过原始业务组件(Service),再加工得到一个代理组件。

  • 动态代理的生成时刻是在目标bean的后处理过程中额外生成的;

  • 更完整的bean生命周期:

    • 构造

    • 注入属性、满足依赖

    • 后处理器迁至过程

    • 初始化

    • 后处理器后主过程

    • 返回

    • 销毁

版权声明:
作者:jackqiang
链接:http://www.jackqiang.com/framework/spring/2008/aop/
来源:JackQiang's
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>
文章目录
关闭
目 录