面试必问之AOP(面向切面编程)
  楠木大叔   4/5/21 11:05:15 PM
将相同的事情统一拦截到一处进行处理,这就是AOP的核心思想。

导航

  • 前言
  • 地铁安检门
  • 看图说话
  • OOP与AOP
    • OOP
    • AOP
    • 二者区别
  • .NET Core中的AOP
  • 实战
    • 创建库表和实体
    • 仓储层
    • 新建一个中间件
    • 在Startup.cs的Configure()方法中注册中间件
    • Controller 测试
  • 结语
  • 参考

感谢您的阅读,预计阅读时长5min。 智客工坊出品必属精品。

前言

将相同的事情统一拦截到一处进行处理,这就是AOP的核心思想。——阿里巴巴淘系技术

  如果使用过.NET MVC,Spring等框架,那么您一定对过滤器(Filter),拦截器(Interceptor),中间件(MiddleWare)等有所耳闻。而这些实现的核心思想就是AOP(Aspect Oriented Programming)——面向切面编程。AOP在项目中的主要的应用是事务控制,日志记录,安全控制,异常处理,缓存,埋点,性能统计等。

地铁安检门

  您一定有坐地铁或飞机的经历。当我们进入地铁站乘坐地铁的时候,首先会经过两道检查工序。

  • 安检门,检查是否有携带违禁物品,如果携带违禁物品,物品会被拦截下来。
  • 验票闸机,这里会验证是否购票,如果没有购票,人会被拦截下来。

  当两道检查通过之后,我们就可以放心大胆地去坐地铁了。

  如果只有一条地铁线,这个就很好理解。但是实际上,一个城市的地铁线路实际上是很多的,成网状个结构(如图,仅是示意图,不针对特定城市)。



  那么,问题来了,如果您是城市地铁的规划者,你会如何设计地铁的安检程序(流程)呢?此刻,请忘掉您的乘坐地铁的安检经历,以免先入为主。

  答案一定是多样的。地铁交通线路是一个网状,由不同颜色的线路交织而成,每种颜色代表一条线路。在这张地铁网中,会有一些换乘站,顾客如果要切换不同线路,需要在换乘站换乘。

  方案一,从线路独立运营的角度,类似不同的业务线,每条线都会设置安检门,顾客乘坐每条线路,都必须通过该线路的安检门。比如,顾客A,乘坐1号线,就需要通过一号线的安检门,换乘2号线,就必须通过2号线的安检门。这是非常好理解的,因为每条线路都要保证自己的安全。

  方案二,不妨在站得高一点,把城市地铁看成一个整体运营系统。 方案一,从理论上来讲没有毛病,各条地铁线也能保证业务独立运行。但是,这一定程度上会给乘客造成麻烦(用户体验将会大大折扣),乘客只是想进城逛个商场,换乘三条线路,就需要过三次安检和验票,如果有行李,更是不方便。站在决策者的角度,你一定会思考,我们能不能让乘客进出地铁,不管中间换乘,只做一次安检呢? 这个思路正好和软件设计中的AOP思想不谋而合——将相同的事情统一拦截到一处进行处理

看图说话

艺术来源于生活,地铁的安检设计正好印证了这一点。(而程序设计本身就是艺术活儿,请阅读《项目架构设计》)。如果您觉得地铁的描述还是不够准确,那么您可以看看下面的示例。

  关于AOP,很多书籍都给了比较长的篇幅来解释与AOP相关的几个术语,如通知(advice)、切点(pointcut)、连接点(join point)、切点(PoinCut)、切面(Aspect)等。而这些术语比较晦涩,不易理解。

  这里我们通过一个例子来认识,什么是切面(Aspect)。

  如果您有过web编程的经验,那您一定听过管道的概念。

  在Web程序中,用户的每次请求线性的,都会对应一个请求管道(request pipeline)。请求管道可以将多个相互独立的业务逻辑模块串联起来,然后服务于用户请求。



  在Web(或web api)开发中,我们访问每个模块(比如商品管理),程序处理的大致流程:客户端(如网页,小程序,app)-->服务端业务处理-->数据库处理。



  当然,一个系统肯定不止一个业务模块(按照OOP的设计模式,我们一般会将不同业务独立封装成不同的程序模块),写的多了就变成下面这个样子:



  当用户发起http请求时,请求管道可以将多个相互独立的业务逻辑模块串联起来,然后服务于用户请求。我们发现,每个业务模块都会有一些公共的处理逻辑,比如访问控制权限,用户必须登录才能访问。

  我们希望用户在访问各个模块时,优先检查是否有访问权限,如果没有就跳转到通知页面或者给一个友好的提示。

  这个时候你该怎么办?



  最容易想到办法就是在各个业务模块加上访问控制权限代码,这似乎也是每个程序员在成长过程中都会干的事情。

   但是如果您知道AOP,那您一定会选择下面这种方式,这种处理方式更加优雅。



  在实际项目中,我们会将很多与业务没有直接关系的逻辑抽取出来,织入业务流程中的某个环节,织入点就是切点,影响面(范围)就是切面。所以,到了这里,您应该对面向切面有了个比较形象的认识了。

Notes: AOP是一种思想,不同语言的实现略有不同。在Spring,.NET Core中都有类似的实现,比如过滤器,拦截器,中间件等。程序的设计真是一门艺术活儿,有兴趣的同学可以评鉴一下《项目架构设计》

OOP与AOP

OOP

Java一切都是对象。

  Java中有一个极其重要思想——OOP。如果没有这种思维你就很难理解别人的代码。面向对象程序设计(Object Oriented Programming),其本质是以建立模型体现出来的抽象思维过程和面向对象的方法。他的基本特征是封装,继承,多态,抽象。

  • ①封装:把描述一个对象的属性和行为的代码封装在一个模块中,也就是一个类中,属性用变量定义,行为用方法定义,方法可以直接访问同一个对象的属性
  • ②抽象:把现实生活中的对象抽象为类。分为过程抽象和数据抽象。
  • ③继承:子类继承父类的特征和行为。子类可以有父类的方法,属性(非private)。子类也可以对父类进行扩展,也可以重写父类的方法。缺点是提高了代码的耦合性。
  • ④多态:指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定。而是在程序运行期间才确定(比如:向上转型,只有运行才能确定其对象属性)。方法重写和重载体现了多态性。

  目前在实际工作中,我们编写业务代码几乎都是基于OOP的思想。

Note: 基于OOP,也衍生了很多编程最佳实践—————设计模式。对于希望进阶的同学来说,值得去研究和实践一下。

AOP

AOP 是 OOP (面对对象) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

  在软件开发中,散布于应用中多处的功能被称为横切关注点(cross-cutting concern)。通常来讲,这些横切关注点从概念上是与应用业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是AOP所要解决的问题。

  AOP的具体思想是:定义一个切面,在切面的纵向定义处理方法,处理完成之后,回到横向业务流。



二者的区别

  OOP(面向对象编程)针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。

  AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。这两种设计思想在目标上有着本质的差异。

  • ①面向目标不同:OOP是面向名词领域,AOP面向动词领域。
  • ②思想结构不同:OOP是纵向结构,AOP 是横向结构。
  • ③注重方面不同:OOP注重业务逻辑单元的划分,AOP偏重业务处理过程中的某个步骤或阶段。

  通常在项目开发中,我们主要使用OOP的继承和组合来消除重复,而那些零散存在于业务方法中的功能代码,我们称之为横切面关注点。横切面关注点不属于业务范围,应该从业务代码中剥离出来。

.NET Core中的AOP

  在.NET Core中有大量AOP的思想和实现。比如,中间件(Middleware)。ASP.NET Core有很多内置中间件。在实际开发中,我们也可以自己定义中间件。

Notes: 从.NET Framwork.NET Core的同学注意了,在.NET Framwork中,是管道模型是基于HTTP 处理程序和模块(IHttpHandler,IHttpModule)。.NET Core后又完全重新设计了框架的底层,似的管道模型更加灵活便捷,可做到热插拔,通过管道可以随意注册自己想要的服务或者第三方服务插件。更深入的了解可以查看将 HTTP 处理程序和模块迁移到 ASP.NET Core 中间件

  ASP.NET Core 请求管道包含一系列请求委托,依次调用。 下图演示了这一概念。 沿黑色箭头执行。



  IHttpModule 和IHttpHandler 已经不复存在了,取而代之的是一个个中间件(Middleware)。Server将接收到的请求直接向后传递,依次经过每一个中间件进行处理,然后由最后一个中间件处理并生成响应内容后回传,再反向以此经过每个中间件,直到由Server发送出去。中间件就像一层一层的“滤网”,过滤所有的请求和响应。这一设计非常适用于“请求-响应”这样的场景–消息从管道头流入最后反向流出。

实战

talk is cheap, show me the code.

  在日常工作中,我们往往会做一些日志统计。比如,在生产环境,我们使用了阿里云的kong网关,它可以记录接口访问的日志,包括输入,输出参数和耗时。



  下面我们就开始编写一个aop来记录接口耗时。

创建库表和实体

  sql

CREATE TABLE `recordlog` (
  `Id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `CostTime` smallint(6) NOT NULL COMMENT '花费时间',
  `ReqRoute` varchar(500) DEFAULT NULL COMMENT '请求路由',
  `CreateTime` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`Id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

  对应的实体

public class RecordLog
    {
        /// <summary>
        /// 主键ID
        /// </summary>
        //[Description("主键ID")]
        //[KEY]
        public LONG Id { get; SET; }

        [Description("花费时间(必须)")]
        public LONG CostTime { get; SET; }

        [Description("请求路由")]
        public STRING ReqRoute { get; SET; }

        [Description("创建时间")]
        public DATETIME CreateTime { get; SET; }
    }

仓储层

public interface IRecordLogRepository: IRepository
    {
        Task<bool> AddAsync(RecordLog recordLog);
        bool Add(RecordLog recordLog);
    }

新建一个中间件

 public class CalculateExecutionTimeMiddleware
    {
        private readonly RequestDelegate _next;//下一个中间件
        private readonly ILogger<CalculateExecutionTimeMiddleware> _logger;
        private readonly IRecordLogRepository _recordLogRepository;

        Stopwatch stopwatch;
        public CalculateExecutionTimeMiddleware(
            RequestDelegate next,
            ILogger<CalculateExecutionTimeMiddleware> logger,
            IRecordLogRepository recordLogRepository)
        {
            _recordLogRepository = recordLogRepository;
           

            if (next == null)
            {
                throw new ArgumentNullException(nameof(next));
            }
            this._next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            stopwatch = new Stopwatch();
            stopwatch.Start();//在下一个中间价处理前,启动计时器
            await _next.Invoke(context);

            stopwatch.Stop();//所有的中间件处理完后,停止秒表。
            string msg = $@"接口{context.Request.Path}耗时{stopwatch.ElapsedMilliseconds}ms";
            //记录日志
            this._logger.LogInformation(msg);
            var userAgent = context.Request.Headers["User-Agent"].FirstOrDefault();
            await _recordLogRepository.AddAsync(new ZhiKeCore.Models.RecordLog
            {
                CostTime = stopwatch.ElapsedMilliseconds,
                Content = msg,
                UserAgent = userAgent,
                ReqRoute = context.Request.Path
            });
        }
    }

    public static class CalculateExecutionTimeMiddlewareExtensions
    {
        public static IApplicationBuilder UseCalculateExecutionTime(
            this IApplicationBuilder app, 
            IRecordLogRepository recordLogRepository)
        {

            if (app == null)
            {
                throw new ArgumentNullException(nameof(app));
            }
            return app.UseMiddleware<CalculateExecutionTimeMiddleware>(recordLogRepository);
        }
    }

在Startup.cs的Configure()方法中注册中间件

 public void Configure(IApplicationBuilder app, 
            IHostingEnvironment env, 
            IRecordLogRepository record)
        {
            //添加记录接口时常的中间件
            app.UseCalculateExecutionTime(record);
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            ...

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                name: "Admin",
                template: "{area:exists}/{controller=default}/{action=Index}/{id?}");
                routes.MapRoute(
                name: "default",
                template: "{controller=default}/{action=Index}/{id?}");
            });
        }

Controller 测试

  启动程序,看看结果:



  这样我能便可以统计出接口耗时。

Notes: AOP是一种思想,和语言无关。在Spring也有相关的实现。参见《Spring Boot 实战纪实-AOP》

结语

  学习Springboot,AOP是一个核心。将相同的事情统一拦截到一处进行处理,这就是AOP的核心思想。在业务迭代的过程中,遇到雷同的需求,不妨斟酌一下,试试AOP的方式。

参考:

版权声明: 本文为智客工坊「楠木大叔」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。