没有源码,如何修改代码逻辑?
当我咨询对方团队:“大哥,我们这边要对你们在xxx项目上的代码进行二次开发,想了解下你们的二开机制是怎样的?”
对方回复道:“我们没有特别明确的二开机制。不过标品这边会提供jar包给你们,你们只需新建一个Spring Boot工程,然后通过依赖这个标品jar包来搭建二开工程。要是想修改业务逻辑,基于Spring的AOP机制进行操作就行,自由度还是比较高的。”
我听后有些惊讶:“原来是这样操作啊,我一开始还以为是从你们的代码仓库拉分支或者fork一份代码,然后我们在这个基础上进行修改呢。”
不得不说,基于AOP进行二次开发确实有很大的灵活性,开发者可以根据自己的需求自由发挥。但这种方式也存在明显的弊端,一旦项目出现问题,排查起来会比较困难。而且过度使用AOP,会导致业务逻辑分散在各个地方,不利于维护。回顾以往的开发经验,我使用AOP主要是进行一些横向的功能补充或扩展,比如权限控制、异常处理等方面,还真没试过用它来实现核心功能
方式一:基于Spring AOP
至于AOP的基础概念我就不啰嗦了,请自行前往官网了解。通过一段简单代码来看下如何通过AOP来修改代码逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | import jakarta.annotation.Resource; import org.springframework.stereotype.Service; @Service public class BizService { //原有逻辑 public void testBiz(){ System. out .println( "testBiz" ); } } 通过切面修改BizService的testBiz逻辑 @Aspect @Component public class BizAspectTest { //Around切面拦截BizService的testBiz方法 @Around( "execution(* com.example.demo.BizService.testBiz())" ) public void around(ProceedingJoinPoint joinPoint) throws Throwable { System. out .println( "around" ); //继续原逻辑。如果想完全修改,也可以不继续 joinPoint.proceed(); } } |
到这里,我们成功修改了 BizService 的 testBiz 方法逻辑,却没有对 BizService 的代码做任何改动。
一切看似风平浪静,可不出意外的话,新问题马上就要接踵而至了。
方式二:类覆盖
方式一基于Spring 的AOP机制来做逻辑的更改,前提是技术框架基于Spring,且Bean的生命周期还必须交给Spring托管才可以,如果是直接new出来的Bean,AOP也无能为力,比如:
1 2 3 4 5 6 7 8 | public class BizService2 { public void testBiz(){ System. out .println( "testBiz in BizService2" ); } } BizService2 bizService2 = new BizService2(); bizService2.testBiz() |
遇到这种情况该如何处理呢?起初,我想到的办法是在自己的二开工程里定义一个与原类同名的 BizService2 。接着,把需要修改的代码复制过来,针对要二次开发的部分进行调整。倘若我们要修改的 BizService2 位于 biz.jar 中,那么就在这个二次开发工程内,定义一个类路径与原类完全一致的 BizService2 ,如此一来,便可以按照需求自由编写代码了。其实,这里运用的是Spring Boot类加载机制的顺序原理,通过这种方式巧妙地实现了代码的定制化修改。
Spring Boot 使用 LaunchedURLClassLoader 来加载类,该类加载器在加载类时会按照一定的顺序搜索类路径。在搜索过程中,会先从 BOOT-INF/classes 目录查找所需的类,如果在该目录下找到了对应的类文件,就会直接加载该类,而不会再去 BOOT-INF/lib 目录下的 JAR 文件中查找同名类。(来源于网络)
不难发现,这种方法简单直接,弊端也很明显,会留下大量冗余代码。就算仅仅是修改一行代码,也不得不把其余代码都保留下来,这无疑增加了后续维护的难度和成本。既然这种方式存在不足,那么接下来,就让我们一起看看方法三,说不定能找到更优解。
方式三:Mojo's AspectJ Maven Plugin
这是一个Maven的插件,可以在编译期对代码进行植入,和Spring AOP的最终效果类似,Spring AOP是运行时植入,而它是编译时植入,第一次知道它的时候确实让我眼前一亮,以植入BizService2为例看看怎么操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | //1.依然定义切面类,语法和使用Spring AOP时一样 @Aspect public class BizAspect2Test { //Around切面拦截BizService的testBiz方法 @Around( "execution(* com.example.demo.BizService2.testBiz())" ) public void around(ProceedingJoinPoint joinPoint) throws Throwable { System. out .println( "around" ); joinPoint.proceed(); } } //2.配置maven插件 <build> <plugins> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.15.0</version> <configuration> <complianceLevel>16</complianceLevel> <source>16</source> <showWeaveInfo> true </showWeaveInfo> <!--如果要植入的类在jar包内,一定要这里指定--> <weaveDependencies> <weaveDependency> <groupId>com.xxx</groupId> <artifactId>yyy</artifactId> </weaveDependency> </weaveDependencies> </configuration> <executions> <execution> <configuration> <skip> false </skip> </configuration> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin> </plugins> </build> //3.引入aspectjrt <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.9.19</version> </dependency> |
接下来就是使用maven正常编译,看看编译以后BizService2会发生什么变化,我们反编译BizService2看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | import com.example.demo.BizAspect2Test; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.runtime.reflect.Factory; public class BizService2 { private static /* synthetic */ JoinPoint.StaticPart ajc$tjp_0; static { // 1 BizService2.ajc$preClinit(); } public void testBiz() { // 1 JoinPoint joinPoint = Factory.makeJP(ajc$tjp_0, this , this ); BizService2.testBiz_aroundBody1$advice( this , joinPoint, BizAspect2Test.aspectOf(), (ProceedingJoinPoint) joinPoint); } private static /* synthetic */ void ajc$preClinit() { Factory factory = new Factory( "BizService2.java" , BizService2. class ); ajc$tjp_0 = factory.makeSJP( "method-execution" , (Signature) factory.makeMethodSig( "1" , "testBiz" , "com.example.demo.BizService2" , "" , "" , "" , "void" ), 4); } private static final /* synthetic */ void testBiz_aroundBody1$advice(BizService2 ajc$ this , JoinPoint thisJoinPoint, BizAspect2Test ajc$aspectInstance, ProceedingJoinPoint joinPoint) { // 13 System. out .println( "around" ); // 14 ProceedingJoinPoint proceedingJoinPoint = joinPoint; System. out .println( "testBiz in BizService2" ); } } |
可以看到BizServer2内部已经融合了BizAspect2Test的相关逻辑,是不是很强大。最后借助豆包的回答来看看aspectjrt和Spring AOP实现原理的对比:
- aspectjrt
- AspectJ 采用了编译时织入(Compile-time weaving)、加载时织入(Load-time weaving)等方式。编译时织入是在 Java 源代码编译成字节码的过程中,就将切面逻辑合并到目标类的字节码中;加载时织入则是在类加载到 JVM 时,通过特殊的类加载器对字节码进行修改,插入切面逻辑。
- 这种方式生成的代码在运行时没有额外的性能开销,因为切面逻辑已经成为目标类的一部分。
- Spring AOP
- Spring AOP 主要基于代理模式实现,分为 JDK 动态代理和 CGLIB 代理。当目标对象实现了接口时,使用 JDK 动态代理;当目标对象没有实现接口时,使用 CGLIB 代理。
- 代理模式在运行时创建代理对象,通过代理对象来调用目标对象的方法,并在方法调用前后插入切面逻辑。因此,在运行时会有一定的性能开销,尤其是在频繁调用方法时。
推荐阅读
https://www.mojohaus.org/aspectj-maven-plugin/index.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)