Spring的学习(三、Spring中的AOP)

Spring AOP简介

1. 什么是AOP

官方:在面向对象编程(oop)思想中,我们将事物纵向抽成一个个对象,而在面向切面编程的时候,我们将一个个的对象某些类似的方面横向抽成一个切面,对这个切面进行一些如权限控制,事务管理,记录日志等公用操作处理的过程就是面向切面编程的思想,aop底层是动态代理,如果是接口采用jdk动态代理,如果是类采用cglib方式实现动态代理。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

通俗:用刀切水果,切开的口子就是切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。

2. AOP术语

切面(Aspect):切入业务流程的一个独立模块。例如,前面案例的VerifyUser类,一个应用程序可以拥有任意数量的切面。

连接点(Join point):也就是业务流程在运行过程中需要插入切面的具体位置。

增强/通知(Advice):是切面的具体实现方法。可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)和环绕通知(Around)五种。实现方法具体属于哪类通知,是在配置文件和注解中指定的。

切点(Pointcut):用于定义通知应该切入到哪些连接点上,不同的通知通常需要切入到不同的连接点上。

目标对象(Target):被一个或者多个切面所通知的对象。

代理对象(Proxy):表示代理对象。将通知应用到目标对象之后被动态创建的对象。可以简单地理解为,代理对象为目标对象的业务逻辑功能加上被切入的切面所形成的对象。

织入(Weaving):将切面应用到目标对象从而创建一个新的代理对象的过程。这个过程可以发生在编译期、类装载期及运行期。

 

手动代理

1. JDK动态代理

jdk动态代理是通过jdk中的java.lang.reflect.Proxy类来实现的。

UserDao.java

public interface UserDao{
    public void save();
    public void update();
    public void delete();
    public void find();
}

UserDaoImpl.java

public class UserDaoImpl implements UserDao{
    public void save() {
        System.out.println("save");
    }
    public void update() {
        System.out.println("update");
    }
    public void delete() {
        System.out.println("delete");
    }
    public void find() {
        System.out.println("find");
    }
}

MyAspect.java

//切面类:可以存在多个通知Advice(增强的方法内容)
public class MyAspect{
    //这个类中主要是通知(Advice)的内容,代码中有两个增强方法,在实现动态代理的类中会调用这些方法
    public void myBefore(){
        System.out.println("方法执行前");
    }
    public void myAfter(){
        System.out.println("方法执行后");
    }
}

MyBeanFactory.java

 //该类通过Proxy实现动态代理
public class MyBeanFactory{
    //这里定义了一个静态的getBean()方法,模拟Spring框架IoC思想,通过调用getBean()方法创建实例
    public static UserDao getBean(){
        //1. 准备目标类(spring创建对象,IoC)
        final UserDao userDao = new UserDaoImpl();
        //2. 创建切面类实例
        final MyAspect myAspect = new MyAspect();
        //3. 使用代理类,对要创建的实例UserDaoImpl类中的方法进行增强。
        // 第一个参数:当前类的类加载器,第二个参数:创建实例的实现类接口,第二个参数:需要增强的方法
        return (UserDao) Proxy.newProxyInstance(MyBeanFactory.class.getClassLoader(), new Class[]{UserDao.class}, new InvocationHandler() {
            //在要执行的方法前后,分别执行切面类中的myBefore和myAfter方法。
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                //前增强
                myAspect.myBefore();
                //目标类的方式
                Object obj = method.invoke(userDao,args);
                //后增强
                myAspect.myAfter();
                return obj;
            }
        });
    }
}

测试类

public void test(){
    //从工厂获得指定的内容(相当于Spring获得,但此内容,是代理对象)
    UserDao userDao = MyBeanFactory.getBean();
    userDao.save();
    userDao.update();
    userDao.delete();
    userDao.find();
}

效果

//方法执行前
//save
//方法执行后
//方法执行前
//update
//方法执行后
//方法执行前
//delete
//方法执行后
//方法执行前
//find
//方法执行后

2. CGLIB代理

使用jkd的动态代理用起来简单,但是有局限性,使用动态代理的对象必须实现一个或者多个接口,如果想代理没有实现接口的类,可以使用CGLIB代理。

CGLIB是一个高性能开源的代码生成包,它的底层通过使用一个小而快的字节码处理框架ASM(java字节码操控框架)来转换字节码,为一个类创建子类,然后对子类进行增强,解决无接口代理问题。所以CGLIB要依赖于ASM的包。

BookDao.java

//普通类,没有实现任何接口
public class BookDao{
    public void save(){
        System.out.println("save添加图书");
    }
    public void update(){
        System.out.println("update修改图书");
    }
    public void delete(){
        System.out.println("delete删除图书");
    }
    public void find(){
        System.out.println("find图书");
    }
}

MyAspect.java

//切面类:可以存在多个通知Advice(增强的方法内容)
public class MyAspect{
    //这个类中主要是通知(Advice)的内容,代码中有两个增强方法,在实现动态代理的类中会调用这些方法
    public void myBefore(){
        System.out.println("方法执行前");
    }
    public void myAfter(){
        System.out.println("方法执行后");
    }
}

MyBeanFactory.java

public class MyBeanFactory{
    public static BookDao getBean(){
        //准备目标类(spring创建对象,IoC)
        final BookDao bookDao = new BookDao();
        //创建切面类实例
        final MyAspect myAspect = new MyAspect();
        //生成代理类,CGLIB在运行时,生成指定对象的子类,增强
        //ECGLIB的核心类Enhancer
        Enhancer enhancer = new Enhancer();
        //确定需要增强的类
        enhancer.setSuperclass(bookDao.getClass());
        //添加回调函数
        enhancer.setCallback(new MethodInterceptor() {
            //intercept相当于jdk invoke,三个参数与jdk invoke一致
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                //目标方法执行之前执行代码
                myAspect.myBefore();
                //目标方法执行
                Object obj = method.invoke(bookDao,args);
                //目标方法执行之后执行代码
                myAspect.myAfter();
                return obj;
            }
        });
        //创建代理类并返回
        BookDao bookDaoProxy = (BookDao)enhancer.create();
        return bookDaoProxy;
    }
}

测试类

public void test(){
    //从工厂获得指定的内容(相当于spring获得,但此内容,是代理对象)
    BookDao bookDao = MyBeanFactory.getBean();
    bookDao.save();
    bookDao.update();
    bookDao.delete();
    bookDao.find();
}

效果

//方法执行前
//save添加图书
//方法执行后
//方法执行前
//update修改图书
//方法执行后
//方法执行前
//delete删除图书
//方法执行后
//方法执行前
//find查询图书
//方法执行后

 

 

声明式工厂Bean

1. Spring通知类型

通知(Advice)就是对目标切入点增强的内容,AOP为通知定义了接口,Spring通知按照在目标类方法的连接点位置,可以分为5种类型,具体如下:

(1)前置通知:在目标方法执行前实施增强,可以应用于权限管理等功能。

(2)后置通知:在目标方法执行后实施增强,可以应用于关闭流、上传文件、删除临时文件等功能。

(3)环绕通知:在目标方法执行前后实施增强,可以应用于日志、事务管理等功能。

(4)异常抛出通知:在方法抛出异常后实施增强,可以应用于处理异常记录日志等功能。

(5)引介通知:在目标类中添加一些新的方法和属性,可以应用于修改老版本程序(增强类)。

2. 声明式Spring AOP

在Spring中创建一个AOP代理的基本方法是:使用org.springframeword.aop.framework.ProxyFactoryBean,这个类对应的切入点和通知提供了完整的控制能力,生成指定的内容。

ProxyFactoryBean类中的常用可配置属性:

(1)target:代理的目标对象。

(2)proxyInterfaces:代理要实现的接口,如果有多个接口,可以使用以下格式赋值<list><value></value>...</list>

(3)proxyTargetClass:是否对类代理而不是接口,设置为true时,使用CGLIB代理。

(4)interceptorNames:需要织入目标的Advice。

(5)singleton:删除index索引处的元素。

(6)optimize:当设置为true时,强制使用CGLIB。

环绕通知案例:

applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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">
    <!-- 目标类 -->
    <bean id="userDao" class="cn.tm.dao.UserDaoImpl"></bean>
    <!-- 通知advice -->
    <bean id="myAspect" class="cn.tm.MyAspect"></bean>
    <!-- 使用ProxyFactoryBean类生成代理对象 -->
    <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!--代理实现的接口 -->
        <property name="interfaces" value="cn.tm.dao.UserDao"></property>
        <!--代理的目标对象 -->
        <property name="target" ref="userDao"></property>
        <!--用通知增强目标(需要织入目标的通知) -->
        <property name="interceptorNames" value="myAspect"></property>
        <!--如何生成代理,true:使用cglib,false:使用jdk动态代理 -->
        <property name="proxyTargetClass" value="true"></property>
    </bean>
</beans>

UserDao.java

public interface UserDao{
    public void save();
    public void update();
    public void delete();
    public void find();
}

UserDaoImpl.java

public class UserDaoImpl implements UserDao{
    public void save() {
        System.out.println("save");
    }
    public void update() {
        System.out.println("update");
    }
    public void delete() {
        System.out.println("delete");
    }
    public void find() {
        System.out.println("find");
    }
}

MyAspect.java

//需要实现接口,确定哪个通知,及告诉spring应该执行哪个方法
public class MyAspect implements MethodInterceptor{
    //invoke方法用于确定目标方法mi,并且告诉spring要在目标方法前后执行哪些方法。
    public Object invoke(MethodInterceptor mi) throws Throwable{
        System.out.println("方法执行前");
        Object obj = mi.proceed();
        System.out.println("方法执行后");
        return obj;
    }
}

测试类

public class TestFactoryBean{
    public void test(){
        //这里需要配置applicationContext.xml的路径,简写了
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext(applicationContext.xml);
        //从spring容器获得内容
        UserDao userDao = (UserDao)applicationContext.getBean("userDaoProxy");
        userDao.save();
        userDao.update();
        userDao.delete();
        userDao.find();
    }
}

效果

//方法执行前
//save
//方法执行后
//方法执行前
//update
//方法执行后
//方法执行前
//delete
//方法执行后
//方法执行前
//find
//方法执行后

 

 

AspectJ开发

1. 基于XML的声明式AspectJ

基于XML的声明式AspectJ是指,通过在XML文件中进行配置,来定义切面、切入点及声明通知,所有的切面和通知都必须定义在<aop:config>元素内。

applicationContext.xml

<?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.xsd>
    <!-- 目标类 -->
    <bean id="userDao" class="cn.tm.dao.UserDaoImpl"></bean>
    <!-- 指定切面类 -->
    <bean id="myAspect" class="cn.tm.MyAspect"></bean>
    <!-- aop编程 -->
    <aop:config>
        <aop:aspect ref="myAspect">
            <!-- 配置切入点,通知最后增强哪些方法 expression里的内容意思是增强cn.tm.dao包下所有的方法-->
            <aop:pointcut expression="execution(* cn.tm.dao..* .* (..))" id="myPointCut"/>
            <!-- 关联通知Advice和切入点pointCut -->
            <!-- 1. 前置通知 method属性指定通知,pointcut-ref属性指定切入点,也就是要增强的方法(下面几种类型的method和pointcut-ref属性作用相同) -->
            <aop:before method="myBefore" pointcut-ref="myPointCut"/>
            <!-- 2. 后置通知,在方法返回之后执行,就可以获得返回值,returning属性:用于设置后置通知的第二个参数的名称,类型是Object -->
            <aop:after-returning method="myAfterReturning" pointcut-ref="myPointCut" returning="returnVal"/>
            <!-- 3. 环绕通知 -->
            <aop:around method="myAround" pointcut-ref="myPointCut"/>
            <!-- 4. 抛出通知,用于处理程序发生异常,就可以接收当前方法产生的异常 -->
            <!-- 注意:如果程序没有异常,将不会执行增强 throwing属性:用于设置通知第二个参数的名称 类型Throwable -->
            <aop:after-throwing method="myAfterThrowing" pointcut-ref="myPointCut" throwing="e"/>
            <!-- 5. 最终通知:无论程序发生任何事情,都将执行 -->
            <aop:after method="myAfter" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
</beans>

UserDao.java

public interface UserDao{
    public void save();
}

UserDaoImpl.java

public class UserDaoImpl implements UserDao{
    public void save() {
        System.out.println("save");
    }
}

MyAspect.java

//Joinpoint连接点作为参数传递,可以通过该参数获得目标对象的类名、目标方法名和目标方法参数等;
public class MyAspect{
    //前置通知
    public void myBefore(Joinpoint joinpoint){
        System.out.println("前置通知,目标:");
        System.out.println(joinpoint.getTarget()+",方法名称:");
        System.out.println(joinpoint.getSignature().getName());
    }
    //后置通知
    public void myAfterReturning(Joinpoint joinpoint){
        System.out.println("后置通知,方法名称:"+joinpoint.getSignature().getName());
    }
    //环绕通知
    //ProceedingJoinPoint是JoinPoint子接口,表示可以执行目标方法
    //1. 必须返回一个Object类型值
    //2. 必须接收一个参数,类型为ProceedingJoinPoint
    //3. 必须throws Throwable
    public Object myAroud(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        //开始
        System.out.println("环绕开始");
        //执行当前目标方法
        Object obj = proceedingJoinPoint.proceed();
        //结束
        System.out.println("环绕结束");
        return obj;
    }
    //异常通知(可以传入Throwable类型的参数)
    public void myAfterThrowing(Joinpoint joinpoint,Throwable e){
        System.out.println("异常通知出错了"+e.getMessage());
    }
    //最终通知
    public void myAfter(){
        System.out.println("最终通知");
    }
}

测试类

public void test(){
    //这里需要配置applicationContext.xml的路径,简写了
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext(applicationContext.xml);
    //从spring容器获得内容
    UserDao userDao = (UserDao)applicationContext.getBean("userDao");
    userDao.save();
}

效果1:(save方法没有错误的情况)

//前置通知,目标:cn.tm.dao.UserDaoImpl@429303,方法名称:save
//环绕开始
//save
//最终通知
//环绕结束
//后置通知,方法名称:save

效果2:(save方法里有错误,比如1/0)

//前置通知,目标:cn.tm.dao.UserDaoImpl@429303,方法名称:save
//环绕开始
//最终通知
//异常通知出错了/by zero

2. 基于Annotation的声明式AspectJ

XML的声明式AspectJ虽然方便,但是缺点是需要在Spring文件中配置大量的信息。为了解决这个问题,AspectJ框架为AOP提供了一套Annotation注解,用于取代xml中臃肿的代码。关于Annotation注解的介绍具体如下:

(1)AspectJ:用于定义一个切面。

(2)Before:用于定义前置通知,相当于BeforeAdvice。

(3)AfterReturning:用于定义后置通知,相当于AfterReturningAdvice。

(4)Around:用于定义环绕通知,相当于MethodInterceptor。

(5)AfterThrowing:用于定义抛出通知,相当于ThrowAdvice。

(6)After:用于定义最终final通知,不管是否异常,该通知都会执行。

(7)DeclareParents:用于定义引介通知,相当于IntroductionInterceptor(不要求掌握)。

applicationContex.xml

<?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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 扫描包:使注解生效 -->
    <context:component-scan base-package="cn.tm"></context:component-scan>
    <!-- 使切面开启自动代理 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

UserDao.java+UserDaoImpl.java

public interface UserDao{
    public void save();
}

@Repository()
public class UserDaoImpl implements UserDao{
    public void save() {
        System.out.println("save");
    }
}

MyAspect.java

//切面类,在此编写通知 基于注解实现aop编程
@Aspect//Aspect注解声明这是一个切面类
@Component//该类作为组件使用,所以加入Component注解
public class MyAspect{
    //用于取代<aop:pointcut expression="execution(* cn.tm.dao..* .* (..))" id="myPointCut"/> 要求:方法必须是private没有返回值,名称自定义,没有参数
    @PointCut("execution(* cn.tm.dao..* .* (..))")//PointCut注解用来配置切入点
    private void myPointCut(){ }
    //前置通知
    @Before("myPointCut()")
    public void myBefore(Joinpoint joinpoint){
        System.out.println("前置通知,目标:");
        System.out.println(joinpoint.getTarget()+",方法名称:");
        System.out.println(joinpoint.getSignature().getName());
    }
    //后置通知
    @AfterReturning(value="myPointCut()")
    public void myAfterReturning(Joinpoint joinpoint){
        System.out.println("后置通知,方法名称:"+joinpoint.getSignature().getName());
    }
    //环绕通知
    @Around("myPointCut()")
    public Object myAroud(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        //开始
        System.out.println("环绕开始");
        //执行当前目标方法
        Object obj = proceedingJoinPoint.proceed();
        //结束
        System.out.println("环绕结束");
        return obj;
    }
    //异常通知(可以传入Throwable类型的参数)
    @AfterThrowing(value="myPointCut()",throwing="e")
    public void myAfterThrowing(Joinpoint joinpoint,Throwable e){
        System.out.println("异常通知出错了"+e.getMessage());
    }
    //最终通知
    @After("myPointCut()")
    public void myAfter(){
        System.out.println("最终通知");
    }
}

测试类

@RunWith(SpringJUnit4ClassRunner.class)//RunWith注解表示这是一个JUnit4的测试程序
@ContextConfiguration("classpath:cn/tm/applicationContext.xml")//这个注解定义了Spring配置文件的路径
public class Test{
    @Autowired//这个注解将UserDao接口的实现类对象注入到该测试类中
    private UserDao userDao;
    @Test
    public void test(){
        userDao.save();
    }
}

效果1:(不报错)

//环绕开始
//前置通知,目标:cn.tm.dao.UserDaoImpl@429303,方法名称:save
//save
//环绕结束
//最终通知
//后置通知,方法名称:save

效果2:(报错)

//环绕开始
//前置通知,目标:cn.tm.dao.UserDaoImpl@429303,方法名称:save
//最终通知
//异常通知出错了/by zero

 

 

参考:

1. 《SSH框架整合实战教程》

持续更新!!!

posted @ 2020-04-06 10:37  夏夜凉凉  阅读(316)  评论(0编辑  收藏  举报