技术的坑分享记录

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

看到一高赞回答有Bug,所以还是简单说几句吧。

先上结论:AOP不一定都像Spring AOP那样,是在运行时生成代理对象来织入的,还可以在编译期、类加载期织入,比如AspectJ。


下面再慢慢聊AOP。

什么时候要用到面向切面AOP呢?

举个例子,你想给你的网站加上鉴权,

对某些url,你认为不需要鉴权就可以访问,

对于某些url,你认为需要有特定权限的用户才能访问

如果你依然使用OOP,面向对象,

那你只能在那些url对应的Controller代码里面,一个一个写上鉴权的代码

而如果你使用了AOP呢?

那就像使用Spring Security进行安全管理一样简单(更新:Spring Security的拦截是基于Servlet的Filter的,不是aop,不过两者在使用方式上类似):

protected void configure(HttpSecurity http) throws Exception {
      http
        .authorizeRequests()
           .antMatchers("/static","/register").permitAll()
           .antMatchers("/user/**").hasRoles("USER", "ADMIN")

这样的做法,对原有代码毫无入侵性,这就是AOP的好处了,把和主业务无关的事情,放到代码外面去做。




所以当你下次发现某一行代码经常在你的Controller里出现,比如方法入口日志打印,那就要考虑使用AOP来精简你的代码了。


聊完了AOP是啥,现在再来聊聊实现原理。

AOP像OOP一样,只是一种编程范式,AOP并没有规定说,实现AOP协议的代码,要用什么方式去实现。

比如上面的鉴权的例子,假设我要给UserController的saveUser()方法加入鉴权,

第一种方式,我可以采用代理模式

什么是代理模式,就是我再生成一个代理类,去代理UserController的saveUser()方法,代码大概就长这样:

class UserControllerProxy {
    private UserController userController;

    public void saveUser() {
        checkAuth();
        userController.saveUser();
    }}

这样在实际调用saveUser()时,我调用的是代理对象的saveUser()方法,从而实现了鉴权。

代理分为静态代理动态代理,静态代理,顾名思义,就是你自己写代理对象,动态代理,则是在运行期,生成一个代理对象。

Spring AOP就是基于动态代理的,如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy,去创建代理对象,而对于没有实现接口的对象,就无法使用JDK Proxy去进行代理了(为啥?你写一个JDK Proxy的demo就知道了),这时候Spring AOP会使用Cglib,生成一个被代理对象的子类,来作为代理,放一张图出来就明白了:

好,上面讲的是AOP的第一种实现,运行时织入

但是不是所有AOP的实现都是在运行时进行织入的,因为这样效率太低了,而且只能针对方法进行AOP,无法针对构造函数、字段进行AOP。

我完全可以在编译成class时就织入啊,比如AspectJ,当然AspectJ还提供了后编译器织入和类加载期织入,这里我就不展开讨论了,我只是来澄清一下大家对AOP的误解,

有兴趣继续学习的,可以看一下这篇博客:

Comparing Spring AOP and AspectJ | Baeldungwww.baeldung.com图标

嗯,最后,个人公众号Bridge4You已经开通,欢迎关注!

有些话只能在那里跟你说 (〃'▽'〃)




最近在学这方面的内容,读到的这段话我感觉说的很清楚了:这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

面向切面编程(AOP是Aspect Oriented Program的首字母缩写) ,我们知道,面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。
     但是人们也发现,在分散代码的同时,也增加了代码的重复性。什么意思呢?比如说,我们在两个类中,可能都需要在每个方法中做日志。按面向对象的设计方法,我们就必须在两个类的方法中都加入日志的内容。也许他们是完全相同的,但就是因为面向对象的设计让类与类之间无法联系,而不能将这些重复的代码统一起来。
   也许有人会说,那好办啊,我们可以将这段代码写在一个独立的类独立的方法里,然后再在这两个类中调用。但是,这样一来,这两个类跟我们上面提到的独立的类就有耦合了,它的改变会影响这两个类。那么,有没有什么办法,能让我们在需要的时候,随意地加入代码呢?这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
     一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。
这样看来,AOP其实只是OOP的补充而已。OOP从横向上区分出一个个的类来,而AOP则从纵向上向对象中加入特定的代码。有了AOP,OOP变得立体了。如果加上时间维度,AOP使OOP由原来的二维变为三维了,由平面变成立体了。从技术上来说,AOP基本上是通过代理机制实现的。
    AOP在编程历史上可以说是里程碑式的,对OOP编程是一种十分有益的补充。

附上网址:JavaWeb过滤器.监听器.拦截器




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

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


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

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


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



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


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

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




这要从日志说起。

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


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


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

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


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

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

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


你们猜发生了什么事情?

所有的请求都被挂起了。


反正我是被吓到了。

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


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

怎么查看日志呢?


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

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


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



嗯。月光宝盒!

这就是日志的作用。


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

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



用来干嘛的呢?

就是用来记时的啊。


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

有没有遇到过?

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


emmmmmm,反正我是真没有。

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


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

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


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

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

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


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

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


在编程语言上,就是用:

Long start=System.currentTimeMillis();

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

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


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

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


完美~~~

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


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

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


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


那你每天切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应用最典型的场景。


咳咳,那个小姑娘是说对于切黄瓜还是没理解?好好好,我给你带几根黄瓜晚上示例一下。




===============我是结束的分割线========

IT修真院一直在尝试着将一些问题讲的简单透彻,并给出思考的角度。
最近这小半年已经贡献了不少这种答案,先从基础的一些概念入手,慢慢再引入相对高级的知识。

更是希望将一些好的思维方式,和坏的学习习惯总结出来。
很多人都在问我零基础怎么学习,其实学习的关键点,并不是只在于自己是否是零基础,更重要的是,你是否有一个高效的学习方法?

如果你喜欢听这样的分析,喜欢尝试这种学习方法。

欢迎关注~
虽然我是暗灭大人的小分身,可是很少用这个账号怼人的呦~

出处: https://www.zhihu.com/question/24863332

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

上下篇:

相关推荐