java-13: 面向切面编程AOP

本文对知乎上的面向切面编程AOP做了一个两篇文章的合集,包括理解和实例。

第一部分:AOP的简单理解。来自知乎。

第二部分:AOP的深入理解。来自知乎。

第三部分:AOP的实例。来自博客园。地址:https://www.cnblogs.com/xrq730/p/4919025.html

知乎连接:https://www.zhihu.com/question/24863332,问题:什么是面向切面编程AOP?

-------------------------------------

第一部分:AOP的简单理解

作者:林湾村龙猫
链接:https://www.zhihu.com/question/24863332/answer/350855397
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

要理解切面编程,就需要先理解什么是切面。用刀把一个西瓜分成两瓣,切开的切口就是切面;炒菜,锅与炉子共同来完成炒菜,锅与炉子就是切面。web层级设计中,web层->网关层->服务层->数据层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面

 

具象化理解

我们一般做活动的时候,一般对每一个接口都会做活动的有效性校验(是否开始、是否结束等等)、以及这个接口是不是需要用户登录。

按照正常的逻辑,我们可以这么做。

java-13: 面向切面编程AOP_第1张图片

 

第1版

这有个问题就是,有多少接口,就要多少次代码copy。对于一个“懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个接口。这里有点切面的味道了。

 

 

java-13: 面向切面编程AOP_第2张图片

 

第2版

同样有个问题,我虽然不用每次都copy代码了,但是,每个接口总得要调用这个方法吧。于是就有了切面的概念,我将方法注入到接口调用的某个地方(切点)。

java-13: 面向切面编程AOP_第3张图片

 

第3版

这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。

红框处,就是面向切面编程。

 

-------------------------------------------------------------------------------------------------------------

第二部分:面向切面编程AOP,详细理解

作者:技能树IT修真院
链接:https://www.zhihu.com/question/24863332/answer/478673303
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

刚开始看到面向切面编程的时候,就觉得好神奇。

切面?立体几何吗,有没有面向挂面编程?

 

脑袋里面很难有这种概念,想像不出来什么叫做切面。

这种类似的东西挺多,包括Pipe,Port,Stream之类的。

 

所以倒底应该怎么样正确理解他们呢?

 

 

“想要学会,先要忘记” -by 暗灭大人

 

先忘记这是什么概念,我们先看看看存在什么问题。

是的,按照修真院一直推荐的【上帝视角】,我们先不管AOP是什么,先关注于要解决的问题是什么。

 

 

 

这要从日志说起。

对于后端而言,解决问题的方法有三种。

 

1 断点调试。
2 查看日志。
3 重启。

 

前端用断点比较多,可以不夸张的说,大部分前端都可以通过回放操作的方式来完成系统的调试。

而对于后端的工程师兄弟而言,这个难度就要大很多了。

 

因为后端是要部署到远程服务器的,在服务器上,往往同时要处理很多很问题。

所以是绝对不能断点调试的,其实有远程断点功能。

在我年少无知的时候,很Happy的试了一下。。。

 

你们猜发生了什么事情?

所有的请求都被挂起了。

 

反正我是被吓到了。

在线远程调试?算了,就是测试环境我也不会想,自己的本地环境还是可以考虑一下的。

 

所以,后端有一个很重要的解决问题的方式,就是查看日志。

怎么查看日志呢?

 

从收到用户的请求开始,调用了什么方法,数据发生了哪些变化,经历了什么分支,全部写的清清楚楚(在线上的话会做很多简化,毕竟日志是很耗性能的)。

所以每一个后端工程师,在某种程度上去有福尔摩斯的潜质。

 

要通过蛛丝马迹,呸,日志打那么清楚了还蛛丝马迹,要通过神迹,把事情发生的顺序,一点一点的在脑袋里回放。

 

java-13: 面向切面编程AOP_第4张图片

 

嗯。月光宝盒!

这就是日志的作用。

 

但除此之外内,日志还有一个很重要的作用,就是用来记录响应时间。

你们去饭店,一定见过一个沙漏吧?

java-13: 面向切面编程AOP_第5张图片

 

用来干嘛的呢?

就是用来记时的啊。

如果沙漏流完了,在规定的时间之内,菜品还没上齐,就要免单。

有没有遇到过?

有没有偷偷把沙漏反过来?

emmmmmm,反正我是真没有。

沙漏就是一个端到端的计时器,对于服务端来说,就是一个端到端的响应时间,打开浏览器,打开F12,查看响应时间,就是同样的沙漏。

那么,我们看到的是这么一个沙漏,在饭店的后厨,是不是也应该有这种同样的沙漏,来确认每一个环节不出问题呢?

这就是关于性能的追求产生的需求,对程序员的专用术语来讲,就是我们要弄清楚,倒底可以分解成哪些阶段,每个阶段各自花费的时间是多少。

怎么统一一个方法的执行时间呢?

很简单,比如说切菜师傅,切菜师傅手里有一只笔,在接到一个单子之后,立刻看一下厨房里的时钟,在纸上记录下当前时间。

等菜切完之后,再记录一下结束时间。

 

结束时间减去开始时间,这就是他切菜用的时间,对不对?

以此类推,洗菜,切菜,炒菜,装盘,上桌等等几个环节都可以用同样的方式来处理时间的问题。

 

在编程语言上,就是用:

Long start=System.currentTimeMillis();

//process start
........
........
........
........
// process end
Long end =System.currentTimeMillis();

log.info("process use time is "+(end-start))

 

这就是想当于把后厨做饭的每一个环节都先标记时间,然后再记录结束时间。

最终我们知道了所有的环节处理时间。

 

完美~~~

可是后来发现有一个问题。

 

就是记录的时间太多了,而我们的最初要记录这些时间的目标是什么呢?

是为了找出响应缓慢的时间节点啊。那些正常的响应时间我不需要知道。

 

意思就是找异常。比如说,正常来讲,切一个黄瓜丝,3分钟,结果你用了15分钟,想把黄瓜切出花来,在每一根丝上都留下自己的名字。

java-13: 面向切面编程AOP_第6张图片

 

那你每天切100根黄瓜(喂,那位漂亮的黄发女生不要捂脸害羞的笑啊,你想到哪里去了)

其中99根都是在3分钟之内切完的,我就不需要知道了啊。

 

只有一根你花了15分钟,我就需要花时间去调研一下了问题出在哪里了,是不是对这根黄瓜产生感情了。。。?

好了好了,不要多想了,我们就是想说明,我们的需求是这样的。

 

对方法的响应时间做一个判断,超过200MS,我们就打出来日志。没超过200MS,我就不打日志了。

这代表什么含义呢?

切菜的师父(假设就是少楠在切菜)仍然记录时间,切完之后再记录时间,然后判断一下这个时间是否超过了200MS(emmmm切菜肯定超过),如果超过了。就在纸上写一下,这根黄瓜用了多长时间,如果没超过,就不写了。

这样后厨主管半导来检查的时候,就可以直接看这些异常的时间就好了。

那换成代码会怎么写?

Long start=System.currentTimeMillis();


//process start
........
........
........
........
// process end
Long end =System.currentTimeMillis();

if(end-start>200){
   log.info("process use time is "+(end-start))
}


现在看起来也不错?但是你有没有注意到,这样的代码很丑陋,想像一下,如果我们有六个环节。

那么代码应该就是这个样子。

 

// step 1
Long start=System.currentTimeMillis();



//process start
........
........
........
........
// process end
Long end =System.currentTimeMillis();

if(end-start>200){
   log.info("process use time is "+(end-start))
}



// step 2
Long start2=System.currentTimeMillis();



//process start
........
........
........
........
// process end
Long end2 =System.currentTimeMillis();

if(end2-start2>200){
   log.info("process use time is "+(end2-start2))
}



// step 3
Long start3=System.currentTimeMillis();



//process start
........
........
........
........
// process end
Long end3 =System.currentTimeMillis();

if(end3-start3>200){
   log.info("process use time is "+(end3-start3))
}



 

这种代码能忍么?哪有什么业务逻辑?如果你注意到我们之前讲过的Spring的IOC,其实就会想到,道理是一样的,可不可以不相关的业务逻辑踢出去,只保留我们正常要处理的业务逻辑?

 

这是代码的简洁之道,当然,并不仅仅是为了好看,还是为了统一的管理。比如说,半导说了,把切黄瓜时间大于200MS的过程都记录下来不合适,因为人是不可能在200MS之内切完黄瓜的,所以我们应该改成3分钟。

 

那么写代码的时候是不是要把所有的方法都改一遍?

你可以说我们用常量,但是假设我们有了更复杂的业务逻辑呢?比如说我想判断一下,一次切了几个黄瓜?

 

这就是我们要解决的问题,我们不用黄瓜和切菜来比喻,抽像一下,问题是这样的:

 

在系统中,大量的穿插着同样的操作,可能是在操作前,也可能是在操作后,我们并不关心具体的操作是什么,所以,有没有什么办法,对所有的操作都做统一的处理?

 

正确的提问,就是解决问题的90%.

其实很好办啊,怎么做?所有的工序,都不让每一个师傅自己去记录时间啦。

切菜的少楠师兄,洗菜的瑶瑶师姐,炒菜的沁修女神,上菜的然然师妹,都不用自己去记录时间啦。

 

谁来记?安排一个人后勤总管,比如说楠楠大总管 ,就坐在后厨里,每一道工序在执行之前,先到楠楠大总管这里登记。

 

楠楠大总管戴着墨镜,穿着西服和光滑的皮鞋,坐在办公桌面前,一份黄瓜要被洗,楠楠大总管就先记录一下当前的时间,然后扔给瑶瑶师姐,瑶瑶师姐洗完了,楠楠大总管再记录一下结束时间,再记录一下当前时间,再扔给少楠师兄。

 

就这样,所有的日志记录工作,都是由楠楠大总管一个人来完成,是不是很酷?

无论有多少道工序,只要是做饭,楠楠大总管都一直在努力的记录时间,可以统一的处理各种问题。

而少楠师兄,瑶瑶师姐们只需要关注自己的黄瓜,根本不需要记录时间。

 

这种方式是不是挺好的?但是想要实现这个功能,就必须要做到一点。

就是知道一个方法被调用 。然后在被调用之前,执行自己想要的方法,在被调用之后,执行自己想要的方法。

 

这种编程的方式,就叫做面向切面编程。

所以,再来看一下,什么叫做切面呢?

 

就是洗菜,切菜,炒菜,装盘,上菜这些环节之间,都切切切切切进去一张张卡片,在原来正常的业务流程中,加了很多埋点。

这就是切面。

 

在Java里,是通过静态代理,或者是动态代理的方式实现的。

这是另一个话题。

 

而理解AOP的关键点就在于两点。

第一点,我们为什么需要这种AOP

第二点,我们不是所有的编程都用AOP的方式来做

 

好了。不知道这个切黄瓜的讲解有没有讲清楚AOP的事情。

总之,当年我理解AOP的时候,满头雾水,直到我自己写了一个所有调用RMI服务响应时间的Util类的时候,才恍然大悟,原来这就是AOP啊。同样的,在Java中最常见的,还有拦截器,也是AOP应用最典型的场景。

-------------------------------------------------------------------------------------------------------------------

第三部分:AOP的实例,地址:https://www.cnblogs.com/xrq730/p/4919025.html

AOP

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

使用"横切"技术,AOP把软件系统分为两个部分:核心关注点横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

 

AOP核心概念

1、横切关注点

对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点

2、切面(aspect)

类是对物体特征的抽象,切面就是对横切关注点的抽象

3、连接点(joinpoint)

被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器

4、切入点(pointcut)

对连接点进行拦截的定义

5、通知(advice)

所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类

6、目标对象

代理的目标对象

7、织入(weave)

将切面应用到目标对象并导致代理对象创建的过程

8、引入(introduction)

在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

 

Spring对AOP的支持

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

1、默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了

2、当需要代理的类不是代理接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB

AOP编程其实是很简单的事情,纵观AOP编程,程序员只需要参与三个部分:

1、定义普通业务组件

2、定义切入点,一个切入点可能横切多个业务组件

3、定义增强处理,增强处理就是在AOP框架为普通业务组件织入的处理动作

所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

下面给出一个Spring AOP的.xml文件模板,名字叫做aop.xml,之后的内容都在aop.xml上进行扩展:

复制代码



            

复制代码

 

基于Spring的AOP简单实现

注意一下,在讲解之前,说明一点:使用Spring AOP,要成功运行起代码,只用Spring提供给开发者的jar包是不够的,请额外上网下载两个jar包:

1、aopalliance.jar

2、aspectjweaver.jar

开始讲解用Spring AOP的XML实现方式,先定义一个接口:

public interface HelloWorld
{
    void printHelloWorld();
    void doPrint();
}

定义两个接口实现类:

复制代码

public class HelloWorldImpl1 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl1.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl1.doPrint()");
        return ;
    }
}

复制代码

复制代码

public class HelloWorldImpl2 implements HelloWorld
{
    public void printHelloWorld()
    {
        System.out.println("Enter HelloWorldImpl2.printHelloWorld()");
    }
    
    public void doPrint()
    {
        System.out.println("Enter HelloWorldImpl2.doPrint()");
        return ;
    }
}

复制代码

横切关注点,这里是打印时间:

复制代码

public class TimeHandler
{
    public void printTime()
    {
        System.out.println("CurrentTime = " + System.currentTimeMillis());
    }
}

复制代码

有这三个类就可以实现一个简单的Spring AOP了,看一下aop.xml的配置:

复制代码



        
        
        
        
        
        
            
                
                
                
            
        

复制代码

写一个main函数调用一下:

复制代码

public static void main(String[] args)
{
    ApplicationContext ctx = 
            new ClassPathXmlApplicationContext("aop.xml");
        
    HelloWorld hw1 = (HelloWorld)ctx.getBean("helloWorldImpl1");
    HelloWorld hw2 = (HelloWorld)ctx.getBean("helloWorldImpl2");
    hw1.printHelloWorld();
    System.out.println();
    hw1.doPrint();
    
    System.out.println();
    hw2.printHelloWorld();
    System.out.println();
    hw2.doPrint();
}

复制代码

运行结果为:

复制代码

CurrentTime = 1446129611993
Enter HelloWorldImpl1.printHelloWorld()
CurrentTime = 1446129611993

CurrentTime = 1446129611994
Enter HelloWorldImpl1.doPrint()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.printHelloWorld()
CurrentTime = 1446129611994

CurrentTime = 1446129611994
Enter HelloWorldImpl2.doPrint()
CurrentTime = 1446129611994

复制代码

看到给HelloWorld接口的两个实现类的所有方法都加上了代理,代理内容就是打印时间

 

基于Spring的AOP使用其他细节

1、增加一个横切关注点,打印日志,Java类为:

复制代码

public class LogHandler
{
    public void LogBefore()
    {
        System.out.println("Log before method");
    }
    
    public void LogAfter()
    {
        System.out.println("Log after method");
    }
}

复制代码

aop.xml配置为:

复制代码



        
        
        
        
        
        
        
            
                
                
                
            
            
                
                
                
            
        

复制代码

测试类不变,打印结果为:

复制代码

CurrentTime = 1446130273734
Log before method
Enter HelloWorldImpl1.printHelloWorld()
Log after method
CurrentTime = 1446130273735

CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl1.doPrint()
Log after method
CurrentTime = 1446130273736

CurrentTime = 1446130273736
Log before method
Enter HelloWorldImpl2.printHelloWorld()
Log after method
CurrentTime = 1446130273736

CurrentTime = 1446130273737
Log before method
Enter HelloWorldImpl2.doPrint()
Log after method
CurrentTime = 1446130273737

复制代码

要想让logHandler在timeHandler前使用有两个办法:

(1)aspect里面有一个order属性,order属性的数字就是横切关注点的顺序

(2)把logHandler定义在timeHandler前面,Spring默认以aspect的定义顺序作为织入顺序

2、我只想织入接口中的某些方法

修改一下pointcut的expression就好了:

复制代码



        
        
        
        
        
        
        
            
                
                
                
            
            
                
                
                
            
        

复制代码

表示timeHandler只会织入HelloWorld接口print开头的方法,logHandler只会织入HelloWorld接口do开头的方法

3、强制使用CGLIB生成代理

前面说过Spring使用动态代理或是CGLIB生成代理是有规则的,高版本的Spring会自动选择是使用动态代理还是CGLIB生成代理内容,当然我们也可以强制使用CGLIB生成代理,那就是里面有一个"proxy-target-class"属性,这个属性值如果被设置为true,那么基于类的代理将起作用,如果proxy-target-class被设置为false或者这个属性被省略,那么基于接口的代理将起作用。

================================================================================== 

我不能保证写的每个地方都是对的,但是至少能保证不复制、不黏贴,保证每一句话、每一行代码都经过了认真的推敲、仔细的斟酌。每一篇文章的背后,希望都能看到自己对于技术、对于生活的态度。

我相信乔布斯说的,只有那些疯狂到认为自己可以改变世界的人才能真正地改变世界。面对压力,我可以挑灯夜战、不眠不休;面对困难,我愿意迎难而上、永不退缩。

其实我想说的是,我只是一个程序员,这就是我现在纯粹人生的全部。

 

 

 

 

 

 

你可能感兴趣的:(Java,java,面向切面编程)