【原创】Spring AOP小结

    技术2022-05-13  2

    我自己编写的Word文档形式的总结,希望能帮助大家。下面开始讲第一节

    1.1    什么是 AOP

    AOP Aspect-Oriented Programming 的缩写,中文意思是面向切面编程 ,也有译作面向方面编程的,因为 Aspect 有“方面、见地”的意思。 AOP 实际上是一种编程思想 ,最初是由 Xerox PARC 研究中心的研究人员首先提出的,其目标是通过提供一些方法和技术,把各种问题分解成一系列的 functional component (功能组件)和一系列横跨多个 functional component aspect (方面),然后组合这些 component aspect ,获得系统的实现。

    在传统的面向对象( Object-Oriented Progr amming OOP )编程中,一直基于三层开发模式编写应用系统的我们总是在关注如何将业务代码在垂直切面下与表示层和数据访问层分离,“横切面(方面)”关注却很少。也就是说,我们利用 OOP 思想可以很好的处理业务流程,却不能把系统中的某些特定的重复性行为封装在某个模块中

    AOP 的编程思想,关注系统的“横切面(方面)”,允许我们在处理业务流程时不必再考虑日志、安全、事务等功能,而是把它交给一个特定的处理日志、安全或事务“拦截器”去完成,在适当的时候拦截程序的执行流程。这样,业务流程就完全的从其它无关的代码中解放出来,各模块之间的分工更加明确,程序维护也变得容易多了。

       

        以下是一些与 AOP 相关的名词解释:

    切面或方面( aspect :指“横切面”的关注点,如把“日志功能”看成是系统中的一个关注点或一个切面。

    连接点( join point :是程序执行中一个执行点,比如类中的一个方法,也就是触发了“通知( advice )”执行的那个方法。例如,连接点是个抽象的概念。

    切入点( pointcut : 切入点是连接点的集合,它通常和 advice (通知)联系在一起,是切面和程序流程的交叉点。比如说,定义一个 pointcut ,它将抛出异常 ClassNotFoundException 和某个 advice (通知)联系起来,那么在程序执行过程中,如果抛出了该异常,那么相应的通知就会被触发执行。

    通知( adcice ): 也可以叫做“装备”,指切面在程序运行到某个连接点所触发的动作。在这个动作中我们可以定义自己的处理逻辑。“通知”需要切入点和连接点联系起来才会被触发。目前 AOP 定义了五种通知:前置通知( Before advice )、后置通知( After returning advice )、环绕通知( Around Advice )、异常通知( After throwing advice )、最终通知( After advice )。

    目标对象( target object ): 被一个或者多个切面“通知”的对象。如已经配置在 Spring 中成为一个 Bean FwxxBizImpl ,就可以被认为是一个目标对象。

    1.2    Spring AOP

    AOP 并非是 OOP 的替代品,而是对 OOP 的一种补充,帮助 OOP 以更好的解决“横切面”问题。 AOP 一共有两种形式:

    l   静态 AOP :即第一代 AOP ,指在编译时期就职入 Aspect 代码,以最初的 AspectJ 为杰出代表。其特点是,相应的横切关注点以 Aspect 形式实现之后,会通过特定的编译器将实现后的 Aspect (方面)代码编译并职入到系统的静态类中。

    l   动态 AOP :又称为第二代 AOP ,该时代的 AOP 实现大都通过 Java 语句提供的各种动态特性如“动态代理”,实现 Aspect (方面)代码职入到当前系统的过程。利用 JDK 动态特效完成 AOP 的框架有: JBossAOP SpringAOP AOP 框架。

     

    静态 AOP 在编译期间完成职入过程,所以性能较好,但缺乏一定的灵活性 ;而动态 AOP 是通过 JDK 的“动态代理”机制完成的职入,所以灵活性较好,但损失了性能 。在选择使用哪种 AOP 时,需要根据项目中的具体清空来考虑。

     

    “拦截”的方式被 Spring 定义成 4 种:

    前置通知( Before :在一个连接点之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常),这里所说的连接点一般是指类中的“方法”。

     

    后置通知( After Returning :在连接点正常完成后执行的通知,例如,一个方法正常返回,没有抛出异常,则“横切面(方面)”代码执行。

     

    环绕通知( Around :包围一个连接点的通知,如方法调用。这是最强大的通知。 Around 通知在方法调用前后完成自定义的行为,它们负责选择继续执行连接点或通过返回它们自己的返回值或抛出汽车来中断连接点的执行。

     

    异常通知( Throws :在方法抛出异常时执行的通知。

     

    2.1 Spring 1.x 风格的 AOP

             我们以模拟的方式为一个网上书店系统的业务方法添加“业务日志”功能。要求在其业务方法调用前记录日志,记录被调用的方法名称、时间和参数。

     

     

             按照 AOP 的开发原则,我们先模拟两个业务方法 buy() (购买)和 comment() (评论)的实现,然后再专心利用 Spring AOP 实现日志方面的代码,最后在 Spring 配置文件中将日志 Pojo 职入到业务系统中。

     

    业务代码清单如下:

    package com.ssh.ct_13.biz;

     

    public interface IBookBiz {

     

        // 购买图书(顾客名,书名,价钱)

        public void buy(String uname,String bookName, double money);

        // 发表评论(顾客名,评论内容)

        public void comment(String uname,String comment);

    }

    package com.ssh.ct_13.biz.impl;

     

    import com.ssh.ct_13.biz.IBookBiz;

     

    public class BookBizImpl implements IBookBiz {

     

        public void buy(String uname, String bookName, double money) {

           System. out .println( "-- 业务 buy 方法开始执行 --" );

           System. out .println(uname+ " 购买图书: " +bookName);

           System. out .println(uname+ " 增加积分: " +( int )money/10);

           System. out .println( " 向物流系统发送货单 " );

           System. out .println( "-- 业务方法执行完毕 --" );

        }

     

        public void comment(String uname, String comment) {

           System. out .println( "-- 评论方法开始执行 --" );

           System. out .println(uname+ " 发表了评论: " +comment);

           System. out .println( "-- 评论方法执行完成 --" );

        }

     

    }

    1.        前置通知( Before

    不论业务方法是否执行成功,我们都应该将这次调用记录下来,所以使用前置通知来完成。

    前置通知代码如下:

    package com.ssh.ct_13.aop;

     

    import java.lang.reflect.Method;

    import java.text.SimpleDateFormat;

    import java.util.Arrays;

    import java.util.Date;

     

    import org.apache.commons.logging.LogFactory;

    import org.springframework.aop.MethodBeforeAdvice;

     

    public class LogAdvice implements MethodBeforeAdvice {

     

             public static final org.apache.commons.logging.Log log = LogFactory.getLog(LogAdvice.class);

            

             public void before(Method m, Object[] arg1, Object arg2)

                                throws Throwable {

                       // TODO Auto-generated method stub

                       SimpleDateFormat sf = new SimpleDateFormat("yyyy MM dd hh:mm:ss");

                       log.warn(" 【系统日志】: " + sf.format(new Date()) + " 调用 " + m.getName() + "(" + Arrays.toString(arg1) + ")");

             }

     

    }

    log4j.properties 文件配置如下:

    log4j.appender.stdout= org.apache.log4j.ConsoleAppender

    log4j.appender.stdout.Target= System.out

    log4j.appender.stdout.layout= org.apache.log4j.PatternLayout

    log4j.appender.stdout.layout.ConversionPattern= %d{yyyy-MM-dd HH : mm:ss} %m%n

    log4j.appender.file= org.apache.log4j.FileAppender

    log4j.appender.file.File= house.log

    log4j.appender.file.layout= org.apache.log4j.PatternLayout

    log4j.appender.file.layout.ConversionPattern= %d{yyyy-MM-dd HH : mm:ss} %1 %m%n

    log4j.rootLogger= warn,stdout,file

    前置通知需要实现 MethodBeforeAdvice 接口,参数中 m 为被通知的目标方法对象, args 为被通知目标方法的参数列表, target 是被调用方法所属的对象实例。

     

    Spring 采用“代理”的方式将通知职入到原 Bean 中。代理类 org.springframework.aop.framework.ProxyFactoryBean 将“业务 Bean ”和“方面代码”组装在一起。配置完成后,我们通过代理类访问原 Bean 时,代理类可以自动根据通知类型决定执行什么通知和原 Bean

    Spring 配置文件的配置( 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-2.5.xsd" >

     

        <!-- 业务组件 -->

        < bean id = "bookBizTarget " class = "com.ssh.ct_13.biz.impl.BookBizImpl" ></ bean >

        <!-- 日志组件 -->

        < bean id = "logAdvice " class = "com.ssh.ct_13.aop.LogAdvice" ></ bean >

        <!-- 代理 ProxyFactoryBean 为代理工厂类 -->

        < bean id = "bookBiz " class = "org.springframework.aop.framework.ProxyFactoryBean " >

           <!-- proxyInterfaces 是被代理的接口 -->

           < property name = "proxyInterfaces " >

               < value > com.ssh.ct_13.biz.IBookBiz </ value >

           </ property >

           <!-- interceptorNames 是通知的列表,可以有多个 -->

           < property name = "interceptorNames " >

               < list >

                  < value > logAdvice </ value >

               </ list >

           </ property >

           <!-- target 是被代理的实现类 -->

           < property name = "target" ref = "bookBizTarget" ></ property >

        </ bean >

    </ beans >

    测试代码如下:

    package com.ssh.ct_13.test;

     

    import org.springframework.context.ApplicationContext;

    import org.springframework.context.support.ClassPathXmlApplicationContext;

     

    import com.ssh.ct_13.biz.IBookBiz;

     

    public class AOPTest {

     

        /**

          * @param args

          */

        public static void main(String[] args) {

           // TODO Auto-generated method stub

           ApplicationContext context = new ClassPathXmlApplicationContext( "applicationContext.xml" );

           IBookBiz bookBiz = (IBookBiz)context.getBean ( "bookBiz" );

           bookBiz.buy( "Abe" , " 重构 - 改善既有代码的设计 " , 56.0);

           bookBiz.comment( " 佚名 " , " 软件工程领域的超级经典著作 " );

        }

     

    }

    运行程序,查看程序执行结果。

    2.        后置通知( After returning

    因商家要举行促销活动,在活动期间,买书和发表评论均额外增加 1 积分。

    业务方法已经完成,本机只需要专心编写“方面代码”即可,代码如下:

    package com.ssh.ct_13.aop;

     

    import java.lang.reflect.Method;

     

    import org.springframework.aop.AfterReturningAdvice;

     

    public class ActiveAfterAdvice implements AfterReturningAdvice {

     

             public void afterReturning (Object arg0, Method arg1, Object[] arg2,

                                Object arg3) throws Throwable {

                       System.out.println(" 后置通知:节日活动期间,买书和发表评论额外赠送 1 积分。 ");

             }

    }

    Spring 配置文件中只需要新建一个“通知” Bean ,再修改代理 Bean 的配置即可。修改后的 Spring 配置内容如下:

    <!-- 后置通知 -->

        < bean id = "activeAdvice" class = "com.ssh.ct_13.aop.ActiveAfterAdvice" ></ bean >

        <!-- 代理 ProxyFactoryBean 为代理工厂类 -->

        < bean id = "bookBiz" class = "org.springframework.aop.framework.ProxyFactoryBean" >

           <!-- proxyInterfaces 是被代理的接口 -->

           < property name = "proxyInterfaces" >

               < value > com.ssh.ct_13.biz.IBookBiz </ value >

           </ property >

           <!-- interceptorNames 是通知的列表,可以有多个 -->

           < property name = "interceptorNames" >

               < list >

                  < value > logAdvice </ value >

                  <!-- 仅仅是在这增加了一个 通知 ” -->

                  < value > activeAdvice </ value >

               </ list >

           </ property >

           <!-- target 是被代理的实现类 -->

           < property name = "target" ref = "bookBizTarget" ></ property >

        </ bean >

    测试代码不变,运行查看控制台输出结果。

    2.2   基于 <aop:config/> 元素的 AOP

    1.        异常通知

    在客户的“生产环境”,记录程序在项目实施后放生的异常,是一项重要的工作,它有助于在客户无法用语言描述清楚发生了什么程序 BUG 的情况下,找到解决问题的突破口。

     

    现在,我们使用 Spring2.x 风格的 <aop:config/> 来开发异常通知。我们重用已经开发完成的业务类,但因为要实现异常通知,所以还需要将业务类修改出一个异常。代码如下:

    package com.ssh.ct_13.biz.impl;

     

    import com.ssh.ct_13.biz.IBookBiz;

     

    public class BookBizImpl implements IBookBiz {

     

        public void buy(String uname, String bookName , double money) {

           System. out .println( "-- 业务 buy 方法开始执行 --" );

           System. out .println(uname+ " 购买图书: " +Integer.parseInt (bookName ) );

           System. out .println(uname+ " 增加积分: " +( int )money/10);

           System. out .println( " 向物流系统发送货单 " );

           System. out .println( "-- 业务方法执行完毕 --" );

        }

     

        public void comment(String uname, String comment) {

           System. out .println( "-- 评论方法开始执行 --" );

           System. out .println(uname+ " 发表了评论: " +comment);

           System. out .println( "-- 评论方法执行完成 --" );

        }

    }

    “异常通知”类的代码会发生一些变化。基于 Spring2.x 风格的 AOP 已经不需要我们实现指定的接口和方法,而仅仅是一个普通的 Pojo 。我们可以自定义类名和方法名,但方法参数一般须指定为 JoinPoint ,这个对象中存储被职入方法的参数值,方法名等信息,由 Spring 管理并传入。在本例中,由于是异常通知,所以还需多指定一个异常参数。代码如下:

    package com.ssh.ct_13.aop;

     

    import java.util.Arrays;

     

    import org.apache.commons.logging.LogFactory;

     

    public class LogThrowingAdvice {

     

        private static final org.apache.commons.logging.Log log = LogFactory.getLog (LogThrowingAdvice. class );

       

        public void afterThrowing(org.aspectj.lang.JoinPoint jp,Throwable throwable){

            log .error(jp.getSignature().getDeclaringType()+ "." +jp.getSignature().getName()+ "(" +Arrays.toString (jp.getArgs())+ ")" ,throwable);

           System. out .println( " 异常通知启动 " );

        }

    }

       虽然已经不需要实现任何指定的接口和方法,但仍需要告诉 Spring 我们的意愿,想在哪做 pointcut ?想为哪个类职入通知?等等这一切需要通过配置来完成。基于 Spring2.x 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-2.5.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd

        " >

        <!-- 异常通知日志组件 -->

        < bean id = "logThrowingAdvice" class = "com.ssh.ct_13.aop.LogThrowingAdvice" ></ bean >

        <!-- Spring2.x AOP 配置 -->

        < aop:config >

           < aop:pointcut id = "ex" expression = "execution(* com.ssh.ct_13.aop.*.*(..)) " />

           < aop:aspect ref = "logThrowingAdvice" >

               < aop:after-throwing pointcut-ref = "ex " method = "afterThrowing " throwing = "throwable " />

           </ aop:aspect >

        </ aop:config >

    </beans>

    注意:使用 Spring2.x AOP 配置需要在配置文件头部导入 AOP 命名空间。

    expression 属性是在定义切面为 execution(* com.ssh.ct_13.aop.*.*(..)) 。表示通知将职入到 com.ssh.ct_13.aop 包下的所有类的所有方法。

    Method 属性是在指定“异常通知”中的方法。 throwing 属性是在指定“异常通知”类的方法中能够传入异常的参数。

     

    最后,编写测试类如下:

    package com.ssh.ct_13.test;

     

    import org.springframework.context.ApplicationContext;

    import org.springframework.context.support.ClassPathXmlApplicationContext;

     

    import com.ssh.ct_13.biz.IBookBiz;

     

    public class AOPTest {

     

        /**

          * @param args

          */

        public static void main(String[] args) {

           // TODO Auto-generated method stub

           ApplicationContext context = new ClassPathXmlApplicationContext( "applicationContext.xml" );

           IBookBiz bookBiz = (IBookBiz)context.getBean( " bookBizTarget " );

           bookBiz.buy( "Abe" , " 重构 - 改善既有代码的设计 " , 56.0);

           bookBiz.comment( " 佚名 " , " 软件工程领域的超级经典著作 " );

        }

    }

    运行查看控制台结果和日志文件如下:

     


    最新回复(0)