Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >基于AOP的代码注入测试

基于AOP的代码注入测试

作者头像
bettermanlu
发布于 2025-04-07 06:27:57
发布于 2025-04-07 06:27:57
79010
代码可运行
举报
运行总次数:10
代码可运行

16.3 代码注入测试

在当今的互联网时代,“小步快跑,快速迭代”几乎成为每个互联网产品的研发策略。如何保证产品质量的情况下,同时又能够支持产品的快速上线,软件测试人员目前面临了更大的挑战。传统的黑盒测试存在测试效率低、发现问题能力有限等局限性,而白盒测试存在人力投入成本大、耗时较长等问题,行业更多的采用了折中的灰盒测试方案,但灰盒测试同样也面临着一定的挑战。本节将讲述一种基于AOP技术注入测试代码到被测对象中的技术方案,通过采集程序的异常行为、构造各类异常等手段,提升灰盒测试的能力,发现更多潜在的产品缺陷。

1. 灰盒测试面临的挑战

在软件测试领域,从是否感知软件的内部工作结构(源码)的角度,可以大致分为黑盒测试、白盒测试以及灰盒测试。

黑盒测试也称为功能测试,测试人员不需要了解源码,主要从用户交互界面进行测试。黑盒测试实施难度低,也比较贴近用户。虽然黑盒测试在发现软件的潜在问题方面能力有限,但在项目时间紧张或是软件不具备实施白盒测试的条件下,仍然是很多项目的首选方案。

白盒测试是基于代码的测试,包含代码评审、单元测试、代码静态扫描、代码覆盖率分析等手段。白盒测试能够有效发现软件的潜在问题,但耗时比较长,在落地时也面临一定的挑战,例如:软件架构设计上是否支持可测性等问题,会对单元测试的实施带来影响,所以在实际项目中更多的是采用了折中方案:灰盒测试。

灰盒测试则是介于黑盒测试和白盒测试之间的一种测试方法,通过了解一定的软件的内部工作结构来指导测试场景的设计,比如业内最常见的代码覆盖率分析:采集和分析测试过程中的未覆盖的代码,通过增加测试用例来提升代码覆盖率,在一定程度上提升了测试的覆盖度。

但灰盒测试仍然面临一定的挑战:

(1)如何采集和监控程序更多的异常行为?典型的异常行为有:异常处理是否合理、线程间的关系是否合理、消息间的时序是什么样的等。

(2)如何快捷的构造程序的异常行为?比如:如何抛出代码里特定的异常对象?

针对上面的挑战,一种常见的思路是在业务代码里增加一定的测试代码。但这里又引入了新的问题:

(1)注入的测试代码如何管理和维护?

(2)如何保证测试代码不污染产品代码?如何避免发布时夹带测试代码的风险?

如果我们能够将测试代码和开发代码做到完全分离,编译出不同的版本(有测试代码的插桩版和无测试代码的非插桩版),是不是就可以解决这个问题了?我们先把视线暂时从测试领域转移到开发领域,看看是否有相似的问题和解决方案。

2. OOP的困境及AOP的解决思路

大家熟知的OOP面向对象编程(Object-Oriented Programming),做到了组件的可重用性和模块化等特性,降低了软件的复杂度和维护成本,但对于某类需求,OOP却无法很好的解决。

我们来看一个例子,图1中org.apache.tomcat的源码的模块分布情况,红色柱状图是XML parsing模块的实现和调用情况,它被很好的封装在一个模块里,和其他的柱状模块基本没有交互,管理和维护成本比较低。

图1:tomcat源码中XML parsing模块的分布情况

图2中显示的是logging功能在org.apache.tomcat各个模块的分布情况,logging模块本身可以做到很好的封装,但调用它的地方却分散到各个模块中,logging代码和非logging模块代码纠缠在一起。那这有什么问题呢?设想一下改动logging代码这个需求,比如:logging模块的接口发生变更,要求将接口的入参由char*,变为string类型,或是需要由2个参数增加为3个,那么所有调用logging的地方都要发生变更。如果要梳理出所有模块对logging使用情况,也就需要把所有的调用者相关代码梳理一遍。

这种纠缠代码(tangled code)注定带来了复杂性和维护成本:

(1) 冗余代码:多处同样的代码块。

(2) 难于理解:代码散落到各处,没有一个集中的地方。

(3) 难于变更:需要找到所有的相关代码,变更一处时要考虑到是否会影响其他的地方。

图2:tomcat源码中logging模块的分布情况

像logging这类需求,称作“横切需求(crosscutting concern)”,也就是实现时横跨了多个模块的需求。AOP(Aspect Oriented Programming),即“面向切面编程”,很好的解决了这类问题。简单说,AOP将需求分为两类:主需求(core concern)和横切需求,AOP提出将横切需求与主需求在代码层面上分离,横切需求的代码单独维护,避免出现代码交织现象。AOP是如何做到的呢?

我们还是以日志功能为例进行讲解。图3是一个简单的交易系统示例,有四个模块:账户模块、转账模块、数据库模块和日志模块。其中账户模块、转账模块和数据库模块需要调用日志模块的功能进行日志记录。传统的方式是将这些日志模块的API调用嵌入到各个调用方的模块中,从而存在代码纠缠问题。

AOP的实现方式如图4所示,它新增了一个Logging Aspect(方面)模块,Aspect类似于OOP的类。各个主需求模块不再直接调用日志模块的API,而是将对日志的调用统一放到了Logging Aspect这个模块中进行实现,然后在编译或是运行时将Logging Aspect的实现自动织入到各个主需求模块中,从而解决了代码纠缠问题。

图3 传统方式实现日志功能,存在代码纠缠问题

图4 AOP方式实现日志功能,代码分离

我们再以AspectJ(AOP在JAVA上的一种实现)为例,看一看AOP是如何做到自动织入的。图5是AspectJ的一种编译时织入方式(compile-time weaving),我们的主需求使用JAVA语言实现,使用javac或是AspectJ的编译器ajc来编译;aspect的编写则使用AspectJ语言实现或是使用JAVA的annotation方式实现,然后使用ajc进行编译; 最后一步使用ajc将上面两步各自生成的class文件织入(weaving)到一起,生成最终的业务对象。

图5 AspectJ编译织入过程

AspectJ支持三种织入方式:

(1)编译时织入(compile-time weaving):AspectJ同时编译业务源码和Aspect源码,编译过程中完成织入,也就是上面提到的织入方式。

(2)编译后织入(post-compile weaving):也叫二进制织入(binary-weaving),它通常用于对已经编译好的java class文件或jar包进行织入。

(3)加载时织入(load-time weaving):这也是一种二进制织入方式,和编译后织入的不同在于,它是在类被JVM加载时完成织入。

具体选择哪种织入方式,可以根据实际项目的需要来决定,比如没有业务源码的情况下,可以选择编译后织入或加载时织入。

那么AOP是如何完成这些横切需求的呢?下面我们了解下AOP的一些基本概念,以及它带给我们的一些启示。

3. AOP基本概念及其启示

3.1 AOP基本术语

AOP有四个核心的概念,分别如下:

(1)连接点(join point):简单的来说join point就是程序执行流程中的一个个可识别的执行点,比如,对象的创建、函数的调用、if/else/while等等。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。

(2)切入点(point cut):是一组一个或多个连接点,可以在其中执行通知(advice)。

(3)通知(advice):是point cut的执行代码,是执行“方面”(aspect)的具体逻辑。

(4)方面(aspect):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类。

我们再用一个DB的类比来更好的理解下AOP的这几个概念。

(1)连接点 vs DB行数据:每个连接点类似于DB的每行数据。

(2)切入点 vs SQL语句:切入点类似于DB的SQL语言,通过编写SQL语言过滤出来我们感兴趣的数据。

(3)通知 vs Trigger:通知则类似于DB的trigger,当有满足条件的DB数据被修改时,会触发预先存储到DB里的SQL脚本代码。

下面我们结合着AspectJ来具体看下AOP的这些概念。

3.2 AspectJ简介

AspectJ是AOP在JAVA上的一种实现,支持丰富的切入点注入,易学易用,其官网地址是:https://www.eclipse.org/aspectj/。

在JAVA领域,除了AspectJ外,还有一些其他的AOP实现工具,比如:和Spring框架集成的 Spring AOP,来自阿里巴巴开源的JVM Sandbox等,读者可以查询它们的相关资料,选择适合自己项目的一款实现工具。

另外,在C/C++领域,典型的AOP实现工具是:AspectC++。读者可以参考https://en.wikipedia.org/wiki/Aspect-oriented_programming了解更多其他语言的AOP实现的相关信息。

总的来说,目前JAVA领域的AOP实现工具是最成熟的,在工程上可以比较好的落地,这也是本节选择AspectJ讲解的一个原因。

3.2.1 Join point和Pointcut

上文提到Join point是程序的一个个的执行点,对于AspectJ来说,它取了join point的一个子集,而不是全部的join point,只有这些暴露的join point才是插桩点,在AspectJ中用pointcut来定义这些join point.

AspectJ主要支持下面几类join point:

- 对象方法和构造函数的调用(call)

- 对象方法和构造函数的本身的执行(exectution)

- 对象属性的访问操作(get & set)

- 异常handler的执行(handler)

- 类的静态方法的初始化(initialization)

3.2.2 Pointcut语法

AspectJ提供了很灵活的pointcut语法,既支持精准匹配,如某个package的某个函数,又支持通匹配符,如../*/+等,如Activity+表示Activity类及其子类,用来过滤出感兴趣的join points插桩点,这里不详细介绍,仅举一些例子作为说明。

表1 Pointcut签名举例

Pointcut签名

说明

public int android.net.NetworkInfo.getType()

精确匹配该getType方法

* Activity+.onCreate(..)

匹配Activity类及其子类的名称为onCreate的所有方法,入参及返回值可以为任意类型,访问类型可以是public, private, protected类型。

* *.*(..) 或* *(..)

匹配所有的函数调用

!get(* *.*) && !set(* *.*)

所有非对象属性的读写操作

* *(..) throws IOException

匹配所有抛出IOException的函数

表1是Pointcut的一些签名举例,签名主要是用来定义我们感兴趣的join points。

pointcut的定义格式是:pointcut类型(pointcut签名)。

常见的pointcut类型有:

(1)execution(pointcut签名):由pointcut签名指定的函数自身在执行。

(2)call(pointcut签名):由pointcut签名指定的函数被调用。

(3)handler(异常类型):某个异常类型的exception handler被执行。

(4)this(SomeType):当前执行的对象(即this指针)是SomeType类型的。

(5)target(SomeType):触发的目标对象是SomeType类型的,如通过call(pointcut签名)触发的目标对象。

(6)within(SomeClass):当前执行的代码属于SomeClass。

(7)args(某个变量对象):用于将传入到joinpoint的入参变量对象保存下来,传递给advice。

更多的详细介绍可参考官方文档:https://www.eclipse.org/aspectj/doc/next/progguide/starting-aspectj.html。

3.2.3 Advice

Advice就是在我们选择出来的pointcut点上要执行的代码块,advice分为三类before/after/around,分别用于控制advice在pointcut的周围何时执行。

顾名思义,before在pointcut插桩点执行前先执行,比如调用某个函数,在该函数执行前获取下当前系统时间。

after在pointcut插桩点执行后执行,比如此时再获取下当前系统时间,这样和before做下对比下,就能算出该函数的执行时间了。after可以细分为两类:after returning和after throwing,前者是指函数正常返回,后者是指函数抛出异常返回;after则是两者的并集。

around是最灵活的一种,可以用自己的代码替代原pointcut插桩点的执行代码,可以决定是否要原pointcut代码继续执行,如果需要继续执行则调用proceed函数即可。

3.2.4 上下文之thisJoinPoint

Advice的执行上下文和插桩对象是在同一个进程空间的,确切的说,advice代码实际上在编译阶段,是直接插入到插桩点。那么advice代码中理所当然的应该能够像被插桩的joinpoint一样访问资源,比如类内部的方法、属性等,这些是通过thisJoinPoint 对象来获取。通过thisJoinPoint可以获取当前joinpoint的基本信息,如代码行号、joinpoint的名称信息,如函数名称等,同时thisJoinPoint的getThis()是最强大的函数,它返回当前advice所在对象的this指针,有了this指针,自然可以调用this对象的方法/属性等信息了。

3.2.5 实例

下面我们举一个基于AspectJ的简单的例子来介绍下AOP。关于开发环境搭建,AspectJ在多个IDE(如Eclipse/Netbeans/IntelliJ IDEA等)上都有插件,也支持命令行/maven/ant等编译方式,读者可以查阅相关指引自行搭建。

代码语言:txt
AI代码解释
复制
package helloworld;            
 
public class Hello {            
 
 public void sayHello(String name)            
 {            
  System.out.println("hello, " +name);            
 }            
 
 public static void main(String[] args) {            
  Hello h = new Hello();            
  h.sayHello("tom");            
 }            
}            

代码段1 : 业务代码Hello示例

代码语言:txt
AI代码解释
复制
package helloworld;            
 
import org.aspectj.lang.JoinPoint;            
import org.aspectj.lang.reflect.CodeSignature;            
 
public aspect Tracing {            
 pointcut tracedCalls():call(* Hello.*(..))             
 && !within(Tracing) && !within(Around);            
  
 before():tracedCalls(){            
  System.out.println("[Aspect Tracing][before] Entering: "+thisJoinPoint);            
  printParameters(thisJoinPoint);            
 }            
  
 after():tracedCalls(){            
  System.out.println("[Aspect Tracing][after] Leaving: "+thisJoinPoint);            
 }            
 
 //打印函数的基本入参信息            
 static private void printParameters(JoinPoint jp) {            
  System.out.println("Arguments: " );            
  Object[] args = jp.getArgs();            
  String[] names = ((CodeSignature)jp.getSignature()).getParameterNames();            
  Class[] types = ((CodeSignature)jp.getSignature()).getParameterTypes();            
  for (int i = 0; i < args.length; i++) {            
    System.out.println(" " + i + ". " + names[i] +            
  " : " + types[i].getName() +            
  " = " + args[i]);            
  }            
  }            
}             

代码段2 : tracing aspect代码示例

代码语言:txt
AI代码解释
复制
package helloworld;            
 
public aspect Around {            
  
 pointcut hello(String name):execution(* Hello.sayHello(..)) && args(name);            
 
  
 void around(String name):hello(name){            
 String new_name = "jerry";            
  System.out.println("[Aspect around]:" +            
 "before executing sayHello, change name from "            
 + name + " to " + new_name);//篡改入参            
  proceed(new_name); //使用新的入参继续执行原函数            
  return;            
 }            
}            

代码段3 : around aspect代码示例

代码段1是一个简单的hello程序,包含了一个函数sayHello(),sayHello()有一个string入参。

代码段2是完成Tracing的功能的aspectj代码,在函数被调用前和调用后输出日志,同时打印入参信息。

代码段3是完成篡改sayhello()入参的aspectj代码,它是通过aspectj的around机制实现的。

插桩前后的hello程序的输出,如代码段4所示。

代码语言:txt
AI代码解释
复制
插桩前的hello程序输出:            
hello, tom            
 
插桩后的hello程序输出:            
[Aspect Tracing][before] Entering: call(void helloworld.Hello.sayHello(String))            
Arguments:             
 0. name : java.lang.String = tom            
[Aspect around]: before executing sayHello, change name from tom to jerry            
hello, jerry            
[Aspect Tracing][after] Leaving: call(void helloworld.Hello.sayHello(String))            

代码段4 : 插桩前后的hello程序的输出

3.3 AOP的启示

AOP作为一种编程范式,将开发代码和测试代码完全隔离,完美的解决了横切需求带来的代码纠缠困扰。同时,在编译时,通过编译脚本控制是否进行测试代码的注入,可以同时生成两个版本,一个是无测试代码注入的发布版本,一个是有测试代码的插桩版本,这样就保证了测试代码不污染产品代码,也避免发布时夹带测试代码的风险。

另外,AspectJ的join point语法可以支持灵活的插桩点,然后执行任意的advice代码。这些能力将有效的帮助我们采集程序异常行为以及构造程序的异常行为。我们将在本节的的第4小节结合实战案例进行讲解。

4. 基于AOP的测试实战案例

以下案例基于AspectJ的实现方案来进行讲解。

4.1 程序异常行为采集

常规的黑盒或是灰盒测试中,虽然通过前端UI能够感知一定的程序功能,但对于一些程序的异常行为却感知较少,特别是一些异常可能被程序捕获,并通过降级服务来补偿,但这类异常可能是非预期的异常,从而被忽略。下面将讲解如何通过AOP来捕获一些常见的可能会被忽略的程序异常行为,进而发现更多潜在的程序bug。

4.1.1 发现那些消失的异常

通过AOP可以捕获到代码中被try…catch代码catch住的异常:这类异常比较隐蔽,因为被catch住了,所以一般不会导致程序崩溃,如果没有被应用表现出来,就很容易被忽略。当然,有些被捕获的异常可能是符合预期的,所以需要做进一步的分析判断。

如何捕获这类异常呢?代码段5是AspectJ的实现代码,pointcut中的handler(*)将会在所有的catch处注入我们的advice代码,从而我们可以获取异常对象的一些运行时的信息,并记录下来。

代码语言:txt
AI代码解释
复制
//捕获所有被catch住的异常            
pointcut exceptionHandlerPointcut(Throwable ex, Object exHandlerObject):             
 handler(*) && args(ex)             
 && this(exHandlerObject);            
 
before(Throwable ex, Object exHandlerObject):            
 exceptionHandlerPointcut(ex, exHandlerObject)            
{            
 String str = "Exception caught by :" + exHandlerObject + "\n";            
 str += "Signature: " + thisJoinPoint.getStaticPart().getSignature() + "\n";            
 str += "Source Line: " + thisJoinPoint.getStaticPart().getSourceLocation()             
 + "\n";            
 str += StackTraceUtil.getStackTrace(ex);//获取异常对象的堆栈信息            
 logger.info(str); //输出异常对象信息到日志中            
}  

代码段5:Exception catcher: 发现那些消失的异常

这里举一个具体非预期的异常实例。在某个App执行过程中,通过上面的Exception Catcher捕获到了下面的一个异常:android.database.sqlite.SQLiteException: no such column: Ol_7132 (code 1): , while compiling: UPDATE onlinetable SET time=?,xmlContent=?,key=? WHERE key=Ol_7132

但从App前端交互及功能上没有发现任何问题,这个异常是怎么发生的,又是如何被处理的呢?通过代码分析,这个bug对应的是App的一个性能优化的辅助功能,App会缓存一些网络页面到数据库中。如果有缓存,则拉取本地缓存数据,否则从服务器重新拉取数据。这个SQLiteException导致数据库缓存操作时失败,故主程序会从服务器重新拉取数据,主流程仍然能够跑通,但该缓存功能彻底失效。最后发现这个异常的根本原因是查询SQL书写格式有问题导致的。

通过这个案例,我们看到AOP技术可以非常方便的帮助我们采集到业务代码里的一些看不到的程序行为,有了这些行为日志后,我们接着做人工分析,进而将一些从前端交互上无法发现的异常挖掘出来。

4.1.2 发现看不见的函数执行

如果我们想知道某个功能背后有哪些函数被执行过,最直接的方式就是在代码里对每个函数的入口处打印一行日志,代码量较少的情况下,手动做这些事情还可以接受,但随着代码量的增加,手工维护这类日志代码就是个很大的负担了。这个需求很明显是属于我们前面提到的横切需求,下面让我们看下AOP的代码是如何解决这个问题的。

代码语言:txt
AI代码解释
复制
//不需要记录aspectJ插桩代码里的函数执行            
pointcut excludedAJ():!within (com.scream.aop..*) && !cflow(adviceexecution());            
 
//定义要监控的函数执行,排除一些java基类Object的访问函数以及对类的成员属性的访问函数            
pointcut funcExecutionPointcut():execution(* *.*(..))            
 
    && !execution(* *.access$*(..));            
 
before():funcExecutionPointcut() && excludedAJ()            
{            
 Signature sig = thisJoinPoint.getStaticPart().getSignature();            
 SourceLocation sl = thisJoinPoint.getStaticPart().getSourceLocation();            
 int line = sl.getLine();            
 String file = sl.getFileName();            
 String className = "";            
 if (thisJoinPoint.getThis() != null)            
  className = thisJoinPoint.getThis().getClass().getName();            
 mylogger.log(Level.INFO,"Entering [" + className + "." + sig.getName()             
 + "] @" + line + "@" +file);            
 NDC.push(prefix);//NDC对象来自于log4j,用于控制log行的缩进            
}            
  
after():funcExecutionPointcut() && excludedAJ()            
{            
 NDC.pop();            
}            

代码段6:Function Tracing: 记录函数执行顺序

代码段6实现了对被测对象的所有函数入口执行时进行记录的功能,其中pointcut excludedAJ()是用于将部分package和自身advice的执行排除在外,对它们的调用不需要记录。NDC对象来自于log4j,用于控制log行的缩进。

我们看看一个具体的日志输出实例:

图6 函数调用层次输出示例

从图6,我们可以看到函数调用层次关系清晰明了。我们该如何利用这些信息呢?

(1)精准测试

前端进行UI操作时,将函数调用关系记录下来,这样可以将前端操作用例和代码对应起来,建立起二者的正向映射关系。当代码有变更的时候,我们就可以反向推测出哪些相关的用例需要执行,达到精准测试的目的。关于精准测试,读者可以参考16.2精准测试章节。

(2)发现潜在的性能问题

举一个实际的案例,在某次功能测试时(启动app,然后按home键,将程序切换到后台执行),但短短几分钟内,Function Tracing:一直往sd卡写log数据, 1M,2M,3M,… 10M…30M… 同时看到Eclispe的logcat窗口里满屏的log输出,是不是业务代码有问题?我们可以写个函数执行次数的统计脚本,统计每个函数的执行次数、都被谁调用过。

图7 函数调用次数统计示例

图7统计数据显示getFirstVisiblePosition()函数会在短短的几分钟内执行了2004次,结合着函数调用树往上找,我们找到了问题的根源。这个函数是被ImageListManager的mLoadThread线程调用,在应用app不可见的时候,仍然一直被调用执行。这个线程没有做任何的启停控制,启动后就一直以200ms的频率sleep-waitup的循环方式执行,没有任何机制来暂停这个线程。至此我们找到了优化点,当app切换到后台后,我们会暂停这个线程的执行。

当然这些bug的发现也依赖于用例的设计,我们可以事先分析下可能会有问题的场景,然后去验证自己的设想。比如:在没有任何UI操作的时候,是否有看不见的线程/函数在空跑?在缓存完成后,缓存线程是否自动结束?当打开歌词activity后关闭该activity时,歌词线程是否会结束?等等。

通过统计函数的调用次数,重点分析top的函数调用,我们在实际的项目中发现很多这类函数空转的问题,函数空转带来的影响是应用性能问题,对于手机app来说会有手机电量的损耗,而这个问题也是手机app需要特别关注的。

4.1.3 发现异常的网络请求

针对网络异常包的监控,传统的方式通常是:PC端可以通过工具比如Fiddler或Wireshark等抓包工具进行网络抓包;手机App则可以通过手机设置代理,接入PC热点的方式进行抓包;然后再对这些采集到的网络请求包进行过滤分析,从中找到一些可疑的数据。这种方式主要的缺点是当采集的网络请求数据量比较大时,容易出现分析遗漏,同时在抓包环境设置上也比较繁琐。

以HTTP网络消息为例,我们看看AOP是如何监控异常的HTTP请求和响应数据的。

代码语言:txt
AI代码解释
复制
//要监控的HTTP函数            
pointcut callConnect(java.net.HttpURLConnection callerObj):            
   call(* java.net.URLConnection.connect()) && target(callerObj);            
 
before(java.net.HttpURLConnection callerObj):callConnect(callerObj)            
{            
 TLog.i(TAG,"======HTTP Request headers==========");            
 dumpHttReqHeaders(callerObj);           
}            
 
after(java.net.HttpURLConnection callerObj) returning : callConnect(callerObj)            
{            
 TLog.i(TAG,"======HTTP Response headers==========");            
 dumpHttpResHeaders(callerObj);            
}            
 
//打印HTTP请求体的基本信息            
private static void dumpHttReqHeaders(HttpURLConnection httpCon)            
{            
 String output = "";            
 output += "requestUrl =" + httpCon.getURL().toString() + "\n";            
 for (String header : httpCon.getRequestProperties().keySet()) {            
  if (header != null) {            
    for (String value : httpCon.getRequestProperties().get(header)) {            
      output += header + ":" + value + "\n";            
   }            
  }            
 }            
 TLog.i(TAG,output);    
}            
 
//打印HTTP响应体的基本信息            
private static void dumpHttpResHeaders(HttpURLConnection httpCon)            
{            
 String output = "";            
 output += "requestUrl =" + httpCon.getURL().toString() + "\n";            
 Map> hdrs = httpCon.getHeaderFields();       
 if (hdrs == null)       
 return;       
 SethdrKeys = hdrs.keySet();   
 for (String k : hdrKeys)        
  output += k + ":" + hdrs.get(k) + "\n";        
 try {        
   if (httpCon.getResponseCode() >= 300) { //只记录返回码>=300的可疑响应        
   TLog.e(TAG,output);    
   }        
   else {        
   TLog.i(TAG,output);        
   }        
     
  } catch (IOException e) {    
   TLog.e(TAG, e);        
  }        
}        
  

代码段7:Dump HTTP Headers : 记录HTTP的请求和响应的异常数据

代码段7展示了如何编写AOP的pointcut规则来过滤出HTTP相关的Reqest和Reponse请求,同时对Reponse Code >=300的可疑响应会记录在日志文件里,作为后续的重点排查对象。

下面看一个我们的实际案例,看看这个监控能力是如何帮助我们发现产品bug的。

比如,某音乐App有个功能叫CDN竞速,在播放在线歌曲时,先连接几个CDN节点竞速,选择较快的一个CDN节点。但因为代码问题,导致竞速请求失败返回404错误,App的兜底策略最终走了默认节点,但前端功能正常,如果不通过网络包分析,这类问题是很难发现的。但通过AOP记录的异常数据包,我们快速发现并准确定位到问题,原来是HTTP header中的某个Cookie字段设置有误导致的。

4.1.4 发现异常线程

对于多线程程序,我们可以通过AOP来采集线程的生命期信息,包括线程的父子关系,线程创建和销毁的时间点等基本信息,从中发现一些可能的信息,比如:线程的创建是否合理?线程间的父子关系是否合理?那么如何采集线程的生命期信息呢?

代码语言:txt
AI代码解释
复制
//线程自身执行的入口函数            
pointcut threadRun():execution(public void java.lang.Thread+.run())             
 || execution(public void java.lang.Runnable+.run());            
 
//线程被启动时的函数            
pointcut threadStart(Thread startedThread):            
 call(public void java.lang.Thread+.start())            
 && target(startedThread);            
 
//记录线程何时被创建的,以及线程间的父子关系            
before(Thread startedThread) : threadStart(startedThread)            
{            
 String parentThreadName = Thread.currentThread().getName();//获取父线程名称            
 long parentThreadId = Thread.currentThread().getId();//获取父线程Id            
 String targetThreadName = startedThread.getName();//获取子线程名称            
 long targetThreadId = startedThread.getId();//获取子线程Id            
  
 //获取joinpoint的基本信息            
 Signature sig = thisJoinPoint.getStaticPart().getSignature();            
 SourceLocation sl = thisJoinPoint.getStaticPart().getSourceLocation();            
 int line = sl.getLine();            
 String file = sl.getFileName();            
 String className = "";            
 if (thisJoinPoint.getThis() != null)            
 className = thisJoinPoint.getThis().getClass().getName();            
  
 TLog.i(TAG, "Thread ["+ parentThreadName + "(" + parentThreadId             
 + ")] has started a new Thread [" + targetThreadName             
 + "(" + targetThreadId +")]. [(" + className + ")"             
 + sig.toShortString() + "] @" + line + "@" +file);            
}            
 
void around():threadRun() //线程执行前随机sleep xx 秒            
{            
 Random random = new Random();            
 int randTime = 0;            
 int randConfig = readSleepRandomFromConfig(); //读取配置文件中的随机sleep时间            
 if (randConfig > 0 )            
   randTime = random.nextInt(randConfig);             
 String threadName = Thread.currentThread().getName();            
 long threadId = Thread.currentThread().getId();            
 Signature sig = thisJoinPoint.getStaticPart().getSignature();            
 SourceLocation sl = thisJoinPoint.getStaticPart().getSourceLocation();            
 int line = sl.getLine();            
 String file = sl.getFileName();            
 String className = "";            
 if (thisJoinPoint.getThis() != null)            
 className = thisJoinPoint.getThis().getClass().getName();            
  
 TLog.i(TAG, "Thread ["+ threadName +"(" + threadId + ")] is running. [("             
 + className + ")" + sig.toShortString() + "] @" + line + "@" +file);            
 try {            
 if (randTime > 0)//随机sleep xx 秒            
 {            
 TLog.i(TAG, "Trying to put thread[" + threadName +            
 "(" + threadId + ")] sleep " + randTime + " sec");            
 Thread.sleep(randTime*1000);            
 }            
 } catch (InterruptedException e) {            
 TLog.e(TAG, "Failed to put thread[" + threadName +"("             
 + threadId + ")] sleep " + randTime + " sec");            
 TLog.e(TAG, e);            
 }            
 proceed(); //随机sleep后,让线程继续执行            
  
 //线程结束执行时,记录下该事件.            
 TLog.i(TAG, "Thread ["+ threadName +"(" + threadId + ")] is terminated. [("             
 + className + ")" + sig.toShortString() + "] @" + line + "@" +file);            
}            

代码段8:ThreadMonkeyRunner : 记录线程的生命期信息及构建线程随机sleep异常

代码段8展示了通过threadRun()和threadStart()这两个pointcut可以完成对线程的生命期信息的收集以及如何构建线程随机sleep异常。关于线程随机sleep异常,我们将在4.2.3章节讲解。

有了线程的生命期信息,我们可以基于此做人工分析,发现一些潜在的bug。比如:关闭某音乐App的连接智能音箱功能,重启App后,置于后台一段时间,然后观察一下线程执行情况,通过日志发现该功能的连接音箱线程仍被启动了,但实际上是不需要启动的。

我们还可以基于日志,绘制出线程间的父子关系,线程的创建和消亡时间,来帮助我们更好的理解业务的代码逻辑,据此来指导我们构造更多线程异常。这部分我们将放在4.2节进行讲解。

4.1.5 自动记录UI操作流

为什么要做自动记录UI操作流呢?因为在日常测试中经常遇到一些非预期的bug,但又记不清了之前做了哪些UI操作。如果能够记录下用户的各个操作流,那么就会更方便定位问题。如果对Android的控件开发比较熟悉的话,控件是基于事件响应的,即实现各种onXXX函数。比如:onClick(View),onKeyDown(int keyCode, KeyEvent event), onItemClick等等。利用AOP可以过滤出这些pointcuts,然后插入相应的log记录代码即可,而不需要在App的各个UI界面编写每个控件的操作日志,从而极大的简化了代码。

我们看下AspectJ的代码实现例子,因篇幅有限,代码段9中我们仅举几个控件操作的例子。

代码语言:txt
AI代码解释
复制
//click事件            
pointcut onClick(View v): execution(public void onClick(View)) && args(v);            
 
//keyDown事件            
pointcut onKeyDown(int keyCode, KeyEvent event):             
 execution(public boolean onKeyDown(int, KeyEve)             
 && args(keyCode, event);            
 
//菜单项点击事件            
pointcut onMenuItemClick(MenuItem menuItem):             
 execution(public void onMenuItemClick(MenuItem)) && args(menuItem);            
 
before(View view):onClick(view)             
{            
 TLog.i(TAG, "ClassName = " + thisJoinPoint.getThis().getClass().getName());            
 UIUtil.getTextFromUIElement(TAG,view);//输出点击view的文本信息            
}            
  
before(int keyCode, KeyEvent event):onKeyDown(keyCode, event)            
{            
 TLog.i(TAG, "ClassName = " + thisJoinPoint.getThis().getClass().getName());            
 TLog.i(TAG, "onKeyDown : \n\tkeyCode = " + keyCode             
 + "\n\tKeyEvent = " + event);   
}            
 
before(MenuItem menuItem):onMenuItemClick(menuItem)            
{            
 TLog.i(TAG, "onMenuItemClick : \n\tmenuItem = " + menuItem.getTitle());            
}            

代码段9:UI Action Tracing: 记录UI操作流

代码段9中的UIUtil.getTextFromUIElement()函数是自定义的函数用于遍历View的子对象(该View对象可能是个Layout容器)找到一个有TextView对象获取其text属性,若是其他非TextView对象,则返回IDName作为该控件的文本标识。

下面我们举个实际的例子来看看这种方式记录下来的操作流。

图8 UI操作流举例

从图8,我们可以很清楚的了解到在最后一个SocketException异常前我们做了哪些操作以及相关的控件信息。有了这些信息,可以很好的帮助我们理解bug出现的上下文。

以上就是一些通过AOP来采集程序异常行为的场景举例。当然不仅仅是这些,只要我们能够清楚的描述出要过滤的pointcuts,然后编写相应的监控advice代码,就可以收集相应的程序行为数据了,比如:自动开启android的strictmode来发现ANR及资源泄露问题、自动记录app crash事件并生成内存转储文件等等。

4.2 程序异常行为构造

AOP除了可以监控程序的异常行为外,还可以帮助我们构造一些特定的异常,覆盖手工测试难以模拟的场景。下面我们将讲解几个典型的场景,帮助大家理解AOP在这方面的能力。

4.2.1 特定异常注入

基于功能的黑盒测试方案,如果要覆盖一些特殊的异常场景,存在一定的难度及测试时间成本,比如机器内存不足,需要开启大量的程序将系统的内存耗尽;磁盘空间不足,则需要通过拷贝文件等方式将本地磁盘空间占满;同时对于一些程序内部的异常分支,如某个函数异常返回,黑盒测试更加难以模拟。

AOP通过按照指定的规则对代码中的函数触发点(包括系统函数和应用自定义函数)进行截获并插入一定的测试桩代码,按照一定的规则修改程序的运行行为,如修改函数的指定返回值,从而达到覆盖各类场景的目的。比如:机器内存不足的场景,当应用调用new函数时,对该系统函数进行截获,不是继续系统函数的调用,而是直接抛出Out of Memory异常,从而模拟到测试内存不足的场景;同样,对于其他的测试场景也可以按照这种方式进行模拟。

下面我们看个例子:

代码语言:txt
AI代码解释
复制
package helloworld;            
 
public class Hello {            
 
 //访问数组,可能会存在数组下标越界异常            
 private void accessArray() throws ArrayIndexOutOfBoundsException            
 {            
  int a[] = new int[2];            
  a[0] = 0;            
  a[1] = 1;            
  System.out.println(a[0]);            
  System.out.println(a[1]);            
  System.out.println("within access Array");            
 }            
 
 //调用accessArray()函数,捕获可能的数组下标越界异常            
 public void helloException()            
 {            
   try{            
  accessArray();            
  }catch(ArrayIndexOutOfBoundsException e){            
  System.out.println("Caught Exception :" + e);            
  }            
 }            
 
 public static void main(String[] args) {            
  Hello h = new Hello();            
  h.helloException();            
 }            
}            
 

代码段10:hello exception的业务代码
package helloworld;            
 
public aspect MyException {            
 
 pointcut hiException():execution(* Hello.accessArray(..));            
 
 //在accessArray被执行时,抛出数组下标越界异常            
 void around():hiException()            
 {            
  System.out.println("[Aspect MyException]: before executing accessArray,"            
 + " throw an exception.");            
  
  throw new ArrayIndexOutOfBoundsException();            
 }            
}            

代码段11:抛出exception的aspectj代码

代码段10里我们有一个accessArray函数,它可能会抛出访问数组越界的异常(ArrayIndexOutOfBoundsException),这个异常会被调用函数helloException()捕获,在测试时,想覆盖这个异常场景,但因为当前的代码通过黑盒的方式很难模拟出这个异常。

代码段11展示了如何通过aspectj的around机制完成异常的模拟。

通过代码段12,我们可以看到这个抛出的异常被helloException()捕获到,这样我们就轻松的验证到了当这个异常发生时,helloException()是否正确处理了这个异常。

代码语言:txt
AI代码解释
复制
[aspect MyException]: before executing accessArray,throw an exception.            
 
Caught Exception :java.lang.ArrayIndexOutOfBoundsException.            

代码段12:抛出exception的程序输出

4.2.2 网络类型欺骗

关于通过修改函数返回值来篡改程序行为,我们看个网络类型欺骗的例子。

在与网络相关的手机终端测试中,需要覆盖各类不同的网络类型,如:wifi/5G/4G/3G等,现有的测试方案基本上都是基于实际的物理手机卡在真实的物理环境下进行测试。当前基于物理手机卡来覆盖各类网络类型的测试方案存在一定的缺陷,如:一些网络场景很难自由的切换和覆盖到,如从4G模式变更到3G模式,需要寻找到4G信号较弱的场所;同时各种不同的运营商的网络类型,需要更多的实体卡和终端手机,带来一定的测试成本开销。

在Android平台,查询网络类型的API主要有android.net.NetworkInfo.getType()、android.net.NetworkInfo.getSubType()和android.net.NetworkInfo.getExtraInfo()等,AOP可以通过截获被测程序对网络类型系统API函数的调用,按照指定的规则篡改系统API的返回值,返回指定的网络类型,而不是当前手机的真实网络类型,从而达到网络类型欺骗的目的。

代码语言:txt
AI代码解释
复制
pointcut getNetworkType():call(int android.net.NetworkInfo.getType());            
  
pointcut getSubtype():call(int android.net.NetworkInfo.getSubtype());            
  
pointcut getExtraInfo():call(String android.net.NetworkInfo.getExtraInfo());            
 
int around():getNetworkType()            
{            
 //read Cheated NetType from a config file.            
 int typeFromFile = readNetTypeFromConfig();            
 TLog.d(TAG,"netType = " + typeFromFile );            
 if (typeFromFile == -1)            
  return proceed();            
 return typeFromFile;   
}            
  
int around():getSubtype()            
{            
 //read cheated Subtype from a config file.            
 int subTypeFromFile = readSubtypeFromConfig();            
 TLog.d(TAG,"netSubtype = " + subTypeFromFile );            
 if (subTypeFromFile == -1)            
  return proceed();            
 return subTypeFromFile;   
}            
  
String around():getExtraInfo()            
{            
 //read cheated extraInfo from a config file.            
 String extraInfo = readExtraInfoFromConfig();            
 TLog.d(TAG,"extraInfo = " + extraInfo );            
 if (extraInfo == "")            
  return proceed();            
 return extraInfo;            
}            

代码段13:NetworkTypeCheater:模拟不同网络类型

4.2.3 线程时序异常构造

在4.1.4发现异常线程章节里,我们讲解了如何通过截获线程相关的函数调用来获取线程的生命期信息,同时我们也可以对线程执行的关键函数进行一定的篡改,从而达到扰乱线程时序的效果,进一步验证线程间是否存在一定的时序关系。

首先我们可以先根据日志绘制出线程间的父子关系、线程的创建和消亡时间,来帮助我们更好的理解业务的代码逻辑,如图9所示。另外,图中的线程名是线程id,而不是有含义的线程名,这个是因为业务代码中在创建线程时,没有指定线程名称,如果指定线程名称的话,线程关系图将有更好的可读性。

图9 线程创建时序和父子关系图举例

接着尝试扰乱一些线程的执行时序,一个简单的方式是模拟monkey test的思路,我们可以在每个线程的执行开始前随机sleep一段时间,尝试模拟不同线程间的执行顺序。当然这种方式存在一定的局限性,因为是随机sleep,测试的完备性是无法保证的,存在一些线程间的时序可能没有覆盖的情况。更合理的方式是首先对线程/进程间的关系进行时序建模,然后再通过控制各个线程的执行时间来模拟这些时序场景。

4.2.4 消息时序异常构造

对于一些复杂的业务应用来说,前端app之间以及前端app和后台服务会有比较多的消息往来。因为网络传输的不确定性,存在消息丢失、消息延迟、消息乱序等各类可能的场景出现。

我们先举个例子,图10是在线K歌合唱的简化版的时序图,我们有一个主唱,一个合唱者(听众A),一个听众(听众B)。主唱先上麦唱歌(消息1),K歌后台会通知所有听众(消息2,消息3),接着一个听众A(即合唱者)申请合唱(消息4,消息5),主唱同意后(消息6,消息7),K歌后台广播通知听众B(消息8),接着合唱者开始上麦唱歌(消息9),K歌后台将此消息通知主唱和听众B(消息10,消息11)。期间,合唱者可能会在在消息4之后的任意时刻选择放弃合唱(消息12),如在收到主唱的同意合唱消息7之前,也可能是在收到消息7之后。K歌后台会将该消息广播告知主唱及听众B(消息13,消息14)。

这是一个简化版的例子,还未涉及到鉴权之类的权限,也未涉及到多个听众同时申请合唱等复杂的场景。从上面的分析来看,这个简化的功能已经涉及跨网络跨App的多个消息,在现实中因为网络原因,消息会存在丢失和延迟,无法保证每个消息的顺序到达,也存在消息乱序的情况。比如:合唱者的收到同意合唱的消息(消息7)的时间点是无法保证的,主唱收到消息10和消息13的顺序性是无法保证的,同样听众B收到的消息11和消息14也同样无法保证。就需要我们验证这些不同场景下我们的App是否能够正常工作。

比如:合唱者请求合唱(消息4),在收到同意合唱前(消息7),直接放弃合唱(消息12),但后面又收到同意合唱消息(消息7),App侧可能会又变成允许合唱状态,这样就是个bug。

图10 在线K歌合唱示例时序图

那如何模拟时序异常呢?

常见的网络工具可以模拟网络抖动、延迟、丢包等异常,但这类工具不能针对某些特定的网络消息进行设置异常,无法确保消息时序异常场景模拟的完备性,故而存在一定的局限性。

AOP在消息时序异常构造和网络工具的不同在于:AOP通过对业务代码中特定的收发包函数进行拦截,可以更加精准的控制消息的收发,从而可以模拟和穷举各种消息相关的异常构造。

例如:收包函数的advice代码里:

(1)收到消息后不返回给消息消费函数,模拟消息丢失的场景;

(2)收到消息后sleep一段时间后再返回给消息消费函数,模拟消息延迟的场景;

(3)收到消息后先暂存下来,等收到多个特定的消息后,打乱它们的顺序,然后再依次返回给消息消费函数,模拟消息乱序的场景。可以通过穷举消息间的不同顺序,保证消息时序测试的完备性。

在现实的业务中,我们采用这种策略,发现了不少有价值的业务bug。因为这些实现和业务逻辑和业务代码比较强相关,这里就不展开具体讲解了,读者可以参考这个思路,根据自己的业务情况进行实现。

5. AOP的局限性

虽然AOP在一定程度上解决了程序异常行为监控及注入的测试难题,但基于AOP的代码注入方案仍有一定的局限性。

(1)切入点仅支持部分连接点,不能对所有的函数执行点进行插桩,如:条件分支、顺序执行的某条语句等,对于这类插桩点AOP将无法支持。

(2)插桩后的代码存在一定的性能开销,故不太适合收集性能数据的测试场景,但仍可以收集数据做趋势类的数据分析

6. 总结

本节介绍了一种基于AOP的测试方案,它可以有效的将测试代码和开发代码隔离,同时借助于AspectJ灵活的语法,高效注入测试代码到被测对象,进而发现程序的多种异常行为,同时还能够灵活的注入各类异常、控制程序的时序行为等,从而提升异常测试的覆盖度。

7. 参考文献

[1] AspectJ in Action. Second Edition. Manning Publications

[2] https://en.wikipedia.org/wiki/Aspect-oriented_programming

[3] https://www.eclipse.org/aspectj/

[4] https://www.baeldung.com/aspectj

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-02-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 MasterLU 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Spring3基于注释驱动的AOP
里面的doAfter方法上面有一行注释,指明这个方法将在UserManageServiceImpl.sayhi(..)方法运行结束之后来执行,参数JoinPoint主要携带了参数值和方法名什么的,到时候自己查查文档就ok了
py3study
2020/01/06
4210
Spring AOP 源码分析系列文章导读
前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解。在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅读了 AOP 方面的源码。开始以为 AOP 部分的源码也会比较复杂,所以原计划投入一周的时间用于阅读源码。但在我大致理清 AOP 源码逻辑后,发现没想的那么复杂,所以目前进度算是超前了。从今天(5.15)开始,我将对 AOP 部分的源码分析系列文章进行更新。包括本篇文章在内,本系列大概会有4篇文章,我将会在接下来一周时间内陆续进行更新。在本系列文章中,我将会分析 Spring AOP 是如何为 bean 筛选合适的通知器(Advisor),以及代理对象生成的过程。除此之外,还会对拦截器的调用过程进行分析。与前面的文章一样,本系列文章不会对 AOP 的 XML 配置解析过程进行分析。
田小波
2018/08/01
5130
Spring AOP 源码分析系列文章导读
android字节码框架——AspectJ
最常用的字节码处理框架有 AspectJ、ASM 等等,它们的相同之处在于输入输出都是 Class 文件。并且,它们都是 在 Java 文件编译成 .class 文件之后,生成 Dalvik 字节码之前执行。 而 AspectJ 作为 Java 中流行的 AOP(aspect-oriented programming) 编程扩展框架,其内部使用的是 BCEL框架 来完成其功能。
老马的编程之旅
2022/06/22
1.1K0
【小家Spring】Spring AOP的多种使用方式以及神一样的AspectJ-AOP使用介绍
AOP(Aspect-OrientedProgramming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善。
YourBatman
2019/09/03
2.6K0
【小家Spring】Spring AOP的多种使用方式以及神一样的AspectJ-AOP使用介绍
Spring AOP 是怎么运行的?彻底搞定这道面试必考题
其实, 接触了这么久的 AOP, 我感觉, AOP 给人难以理解的一个关键点是它的概念比较多, 而且坑爹的是, 这些概念经过了中文翻译后, 变得面目全非, 相同的一个术语, 在不同的翻译下, 含义总有着各种莫名其妙的差别. 鉴于此, 我在本章的开头, 着重为为大家介绍一个 Spring AOP 的各项术语的基本含义. 为了术语传达的准确性, 我在接下来的叙述中, 能使用英文术语的地方, 尽量使用英文。
南风
2020/02/17
4.7K0
Spring AOP(一) AOP基本概念
 Spring框架自诞生之日就拯救我等程序员于水火之中,它有两大法宝,一个是IoC控制反转,另一个便是AOP面向切面编程。今日我们就来破一下它的AOP法宝,以便以后也能自由使出一手AOP大法。
程序员历小冰
2019/02/11
4710
Spring AOP(一) AOP基本概念
Spring读源码系列之AOP--01---aop基本概念扫盲---上
ExpressionPointcut,它是用于解析String类型的切点表达式的接口,这个很重要,一会重点分析。
大忽悠爱学习
2022/05/10
8420
Spring读源码系列之AOP--01---aop基本概念扫盲---上
逐行阅读Spring5.X源码(十一)AOP概念、应用、原理
与OOP对比,面向切面,传统的OOP开发中的代码逻辑是自上而下的,而这些过程会产生一些横切性问题,这些横切性的问题和我们的主业务逻辑关系不大,这些横切性问题不会影响到主逻辑实现的,但是会散落到代码的各个部分,难以维护。AOP是处理一些横切性问题,AOP的编程思想就是把这些问题和主业务逻辑分开,达到与主业务逻辑解耦的目的。使代码的重用性和开发效率更高。
源码之路
2020/09/04
8890
逐行阅读Spring5.X源码(十一)AOP概念、应用、原理
漫谈AOP开发之开发Spring AOP程序
我们在Eclipse中创建一个新的工程,导入UserService、BookService两个类,并配置Spring的Bean:
张张
2019/12/26
5210
运用AOP思想更优雅地进行性能调优
在软件测试中,如果想在一个耗时严重的操作中找出其耗时的瓶颈时,一般采用的方法是在每个被调用的函数中写进测试代码,在运行时打出日志。如果该操作涉及到的业务逻辑特别复杂时,插入这些测试代码不仅工作量十分巨大,而且难以维护。如果后期剔除不干净,不仅增加了无关的代码量,还会在执行时造成不必要的资源浪费。 像在手机管家的清理加速模块中,垃圾扫描这个功能的耗时是性能优化的重点,如何快速测试和分析扫描过程中的函数耗时一直是性能测试想克服的难题。但是在数以千计的函数中插入测试代码简直是一场恶梦,所以优化过程一直是不知道从何
腾讯移动品质中心TMQ
2018/02/06
1.3K0
运用AOP思想更优雅地进行性能调优
Spring AOP
在专栏第一篇我们就简单说了Spring框架提供了对AOP的支持,那Spring AOP和AOP有什么不同吗?
终有救赎
2023/10/16
1940
Spring AOP
Spring AOP小记
一、概述 在通常的开发过程中,我们调用的顺序通常是controller->service-dao,其中,service中包含着太多的业务逻辑,并且还要不断调用dao来实现自身的业务逻辑,经常会导致业务
Zephery
2018/03/12
7370
Spring AOP小记
谈谈对Android上AspectJ使用的想法
概念:AOP是Aspect Oriented Programming的缩写,即『面向切面编程』;切面编程,就是在你项目原有的功能基础上,通过AOP去添加新的功能,这些功能是建立在原有功能的基础上的,而且原有的功能并不知道你已经添加了新的功能;AOP就是在某一个类或方法执行前后打个标记,声明在执行到这里之前要先执行什么,执行完这里之后要接着执行什么。插入了新的执行方法。
包子388321
2020/06/16
1.7K0
Spring 基于 XML 的 AOP
  AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。    AOP 是 Spring 框架的关键组件之一。虽然 Spring IoC 容器不依赖于 AOP,但在 Spring 应用中,经常会使用 AOP 来简化编程。在 Spring 框架中使用 AOP 主要有以下优势:  ♞ 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品。最重要的是,这种服务是声明式事务管理。  ♞ 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。  ♞ 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。 要使用 Spring AOP 需要添加 spring-aop 模块。
Demo_Null
2020/09/28
3100
Spring 基于 XML 的 AOP
AOP切面编程
如果说 IoC 是 Spring 的核心,那么面向切面编程就是 Spring 最为重要的功能之一了,在数据库事务中切面编程被广泛使用。
用户3467126
2019/08/12
6350
JAVAEE框架之Spring AOP
从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。
张哥编程
2024/12/19
990
JAVAEE框架之Spring AOP
Spring AOP 实现原理与 CGLIB 应用
AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等。AOP 实现的关键就在于 AOP 框架自动创建的 AOP 代理,AOP 代理则可分为静态代理和动态代理两大类,其中静态代理是指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于 JDK 动态代理、CGLIB 等在内存中“临时”生成 AOP 动态代理类,因此也被
java达人
2018/01/31
8790
Spring AOP 实现原理与 CGLIB 应用
关于Spring AOP,除了动态代理、CGLIB,你还知道什么?
Spring 作为 Java 中最流行的框架,主要归功于其提供的 IOC 和 AOP 功能。本文将讨论 Spring AOP 的实现。第一节将介绍 AOP 的相关概念,若熟悉可跳过,第二节中结合源码介绍 Spring 是如何实现 AOP 的各概念。
草捏子
2020/08/10
5480
关于Spring AOP,除了动态代理、CGLIB,你还知道什么?
Spring-AOP概述
Spring AOP是AOP技术在Spring中的具体实现,它是Spring框架的另外一个重要基石。
小小工匠
2021/08/16
4540
系统学习SpringFramework:Spring AOP
AOP(Aspect oriented programming),即面向切面编程,它是一个编程范式,是 OOP(面向对象编程)的一种延续,目的就是提高代码的模块性。
栗筝i
2022/12/01
2660
相关推荐
Spring3基于注释驱动的AOP
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验