Spring面向切面编程(AOP)是企业级应用的基石,可以这样说,如果大家要升级到高级程序员,这部分的知识必不可少。
这里我们将结合一些具体的案例来讲述这部分的知识,并且还将给出AOP部分的一些常见面试题。本文的文字和案例根据java web轻量级开发面试教程改编。
1 面向切面编程的使用场景
现在有如下需求,在调用项目里的很多方法之后,需要调用内存回收的方法,用如下方式可以来实现这个需求。
1 2 3 4 5 6 7 8 9 10 | 1 void f1(){ 2 f1的正常业务 3 //回收内存 4 clearMem(); 5 } 6 void f2(){ 7 f2的正常业务 8 //回收内存 9 clearMem(); 10 } |
这种实现方法的问题在于,我们不得不在所有必要的方法里写上调用回收内存的代码,这样会造成代码重复。如果有修改,还不得不修改很多处重复的地方。
更糟糕的是,假如回收内存的方法由内存管理团队来维护,而正常的f1和f2之类的业务由业务团队来维护。假如哪天内存管理团队升级代码,比如修改了clearMem方法名,或者变更了参数,那么会连带着业务团队也要变动代码。之前说过,项目代码如果要部署到生产环境上,一般需要比较大的代价,内存管理团队部署代码,这是情理中的事,但就没有必要牵连到业务团队了。在这种场景下,面向对象的编程方式就帮不到我们了,就需要用面向切面的编程方式了。
2 案例演示
有这样的需求,在银行项目里,当我们调用给账户加钱和扣钱的方法前后,需要完成一些固定的动作,比如记流水或者安全性检查,可以通过如下步骤用AOP的方式实现。 队升级代码,比如修改了clearMem方法名,或者变更了参数,那么会连带着业务团队也要变动代码。之前说过,项目代码如果要部署到生产环境上,一般需要比较大的代价,内存管理团队部署代码,这是情理中的事,但就没有必要牵连到业务团队了。在这种场景下,面向对象的编程方式就帮不到我们了,就需要用面向切面的编程方式了。
步骤一,编写账户的接口和实现类。这里依然遵循着面向接口编程的思路。接口代码是Account.java,其中在第2行和第3行定义了两个方法。
1 2 3 4 | 1 public interface Account{ 2 void add( int money); 3 void minus( int money); 4 } |
实现类的代码是AccountImpl.java,其中在第2行定义了账户名这个属性,并在第3行和第6行定义了它的get和set方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 1 public class AccountImpl implements Account { 2 private String name; 3 public String getName() { 4 return name; 5 } 6 public void setName(String name) { 7 this .name = name; 8 } 9 public void add( int money) { 10 System.out.println( "给" +name+ "账户加钱 " + money + "元" ); 11 } 12 public void minus( int money) { 13 System.out.println( "从" +name+ "账户扣钱: " + money + "元" ); 14 } 15 } |
步骤二 编写前置处理类BeforeAdvice.java,在项目里需要在调用加钱和扣钱方法之前,调用封装在其中的before方法。
1 2 3 4 5 6 7 8 9 10 11 12 | 1 import java.lang.reflect.Method; 2 import org.springframework.aop.MethodBeforeAdvice; 3 public class BeforeAdvice implements MethodBeforeAdvice 4 { 5 public void before(Method m, Object[] args, Object target) throws Throwable 6 { 7 System.out.println( "在方法调用之前" ); 8 System.out.println( "执行的方法是:" + m); 9 System.out.println( "方法的参数是:" + args[ 0 ]); 10 System.out.println( "目标对象是:" + target); 11 } 12 } |
请大家注意第3行,需要实现MethodBeforeAdvice接口,第5行,可以在before方法里定义前置操作的业务动作。这里我们是打印了一些信息。
同样,在AfterAdvice.java里,定义了后置操作。它和前置操作很像,这里是在第5行实现了AfterReturningAdvice接口里的afterReturning方法,同样是打印了一些信息。
1 2 3 4 5 6 7 8 9 10 11 12 | 1 import org.springframework.aop.AfterReturningAdvice; 2 import java.lang.reflect.Method; 3 public class AfterAdvice implements AfterReturningAdvice 4 { 5 public void afterReturning(Object returnValue, Method m, Object[] args, Object target) throws Throwable 6 { 7 System.out.println( "在方法调用之后" ); 8 System.out.println( "执行的方法是:" + m); 9 System.out.println( "方法的参数是:" + args[ 0 ]); 10 System.out.println( "目标对象是:" + target); 11 } 12 } |
此外,还可以定义环绕操作,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 | 1 import org.aopalliance.intercept.MethodInterceptor; 2 import org.aopalliance.intercept.MethodInvocation; 3 public class AroundInterceptor implements MethodInterceptor 4 { 5 public Object invoke(MethodInvocation invocation) throws Throwable 6 { 7 System.out.println( "调用方法之前: invocation对象:[" + invocation + "]" ); 8 Object rval = invocation.proceed(); 9 System.out.println( "调用结束..." ); 10 return rval; 11 } 12 } |
这里是在第3行实现了MethodInterceptor接口,一旦调用到第5行定义的invoke方法时,Spring容器就会把本尊方法(比如加钱或扣钱方法)的控制权交给invoke方法,而一般在本方法里,会加些前置(比如第7行的打印)和后置(比如第9行的打印)操作,而后通过第8行的动作执行本尊方法。
到目前为止,共定义了两套方法:第一套是本尊的加钱和扣钱方法,第二套是前置、后置以及环绕的动作。大家可以看到在本尊方法里,并没有任何前后环绕处理的业务,它们两者是完全分离的,用专业的话来说,它们之间的耦合度很低。
步骤三 如何在Main类里使用,代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 1 import org.springframework.context.ApplicationContext; 2 import org.springframework.context.support.FileSystemXmlApplicationContext; 3 public class Main 4 { 5 public static void main(String[] args) throws Exception 6 { 7 ApplicationContext ctx = new FileSystemXmlApplicationContext( "src/ApplicationContext.xml" ); 8 Account account = (Account)ctx.getBean( "account" ); 9 System.out.println( "第一个add方法" ); 10 account.add( 100 ); 11 System.out.println( "第二个minus方法" ); 12 account.minus( 100 ); 13 } 14 } |
在第7行和第8行里,装载并获取了Account的实例,并在第10行和第12行调用了加钱和扣钱的方法。
从调用的代码来看,依然看不出任何前置环绕处理的痕迹,那么它们是怎么装配起来的呢?
最后来看下配置文件,给出主体部分的代码。
1 2 3 4 5 6 7 8 | 1 <beans> 2 <!-- 配置目标对象--> 3 <bean id= "accountTarget" class = "AccountImpl" > 4 <!-- 为目标对象注入name属性值--> 5 <property name= "name" > 6 <value>Java</value> 7 </property> 8 </bean> |
在第3行里,定义了accountTarget这个Bean,并在第5行到第7行,设置了name的初始化值是Java。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 9 <!-- 第一个处理类--> 10 <bean id= "myAdvice" class = "BeforeAdvice" /> 11 <!-- 第二个处理类--> 12 <bean id= "myAroundInterceptor" class = "AroundInterceptor" /> 13 <!--指定了对哪些方法增加处理--> 14 <bean id= "addAdvisor" class = "org.springframework.aop.support.RegexpMethodPointcutAdvisor" > 15 <!-- advice属性确定处理bean--> 16 <property name= "advice" > 17 <!-- 此处的处理bean定义采用嵌套bean,也可引用容器的另一个bean--> 18 <bean class = "AfterAdvice" /> 19 </property> 20 <!-- 确定正则表达式模式--> 21 <property name= "patterns" > 22 <list> 23 <!-- 确定正则表达式列表--> 24 <value>.*add*.</value> 25 </list> 26 </property> 27 </bean> |
从第9行到第27行,定义了3个处理类的Bean,其中前两个比较简单,只是使用了前置处理类和环绕处理类。对于第三个后置处理类,我们把第18行定义好的AfterAdvice这个Bean包装到第14行到第27行的addAdvisor这个Bean里,同时,通过第21行和第24行指定了该拦截器只能作用在add方法上,对其它方法是没有效果的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 28 <!-- 使用ProxyFactoryBean 产生代理对象--> 29 <bean id= "account" class = "org.springframework.aop.framework.ProxyFactoryBean" > 30 <!-- 代理对象所实现的接口--> 31 <property name= "proxyInterfaces" > 32 <value>Account</value> 33 </property> 34 <!-- 设置目标对象--> 35 <property name= "target" > 36 <ref local= "accountTarget" /> 37 </property> 38 <!-- 代理对象所使用的拦截器--> 39 <property name= "interceptorNames" > 40 <list> 41 <value>runAdvisor</value> 42 <value>myAdvice</value> 43 <value>myAroundInterceptor</value> 44 </list> 45 </property> 46 </bean> 47 </beans> |
定义的前后环绕处理是作用在Accout类的add和minus方法上的,体现在第29行到第46行的Account这个Bean里,而第41行到第43行里指定了要使用上文里定义好的三个处理类。
当运行Main这个主类时,会看到如下输出。
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 | 1 第一个add方法 2 这个是在Main类里调用add方法之前的输出 3 在方法调用之前 4 在add方法调用之前,调用定义在BeforeAdvice里的before方法 5 执行的方法是: public abstract void Account.add( int ) 6 方法的参数是: 100 7 目标对象是:AccountImpl @11d5b59 8 从第 3 行到第 5 行是before方法的输出,从中能看到执行的方法是add,参数是 100 ,目标对象是AccountImpl 9 调用方法之前。 invocation对象:[ReflectiveMethodInvocation: public abstract void Account.add( int ); target is of class [AccountImpl]] 10 这个是针对add方法的环绕操作的invocation.proceed()之前的输出 11 给Java账户加钱 100 元 12 这个是正常的加钱操作 13 调用结束... 14 这个是针对add方法的环绕操作的invocation.proceed()之后的输出 15 在方法调用之后 16 这个是调用AfterAdivce里的afterReturning方法。 17 执行的方法是: public abstract void Account.add( int ) 18 方法的参数是: 100 19 目标对象是:AccountImpl @11d5b59 20 从第 10 行到第 12 行是afterReturning的输出 21 第二个minus方法 22 这个是在Main类里调用minus方法前的输出 23 在方法调用之前 24 同样是在minus方法调用之前,调用定义在BeforeAdvice里的before方法 25 执行的方法是: public abstract void Account.minus( int ) 26 方法的参数是: 100 27 目标对象是:AccountImpl @11d5b59 28 从第 15 行到第 17 行,同样是看到before方法里的输出。 29 调用方法之前。 invocation对象:[ReflectiveMethodInvocation: public abstract void Account.minus( int ); target is of class [AccountImpl]] 30 这个是针对minus方法环绕操作的invocation.proceed()之前的输出 31 从Java账户扣钱: 100 元 32 调用minus方法时的输出 33 调用结束... 34 这个是针对add方法的环绕操作的invocation.proceed()之前的输出 35 在配置文件里,由于AfterAdvice这个类是仅仅作用在add方法上,所以在minus方法后不会执行这个 |
3 面试点说明
在实际使用过程中,大家可以不用太多地了解概念,而是应该掌握如下知识点:
①有哪些通知的类型?
②结合项目说明你是如何使用AOP的?
③如果前置通知只想作用在某些方法上,不想作用在全部方法上,该怎么做?
同样可以通过注解(而不是通过配置文件)来实现面向切面编程,这种做法的局限性在上文已经提到过,所以这里就不再一一赘述了。
4 如何在面试中证明自己掌握Spring基本技能
我们经常会用到Spring的Web开发技能、其他组件(比如Hibernate)整合的技能以及数据库事务等技能,而IoC和AOP作为Spring的基础,更是会被频繁用到。
针对这些基础技能,我们一般不会问概念,因为这没法区分稍懂(刚学但没商业项目经验)和精通(工作3年左右正在往高级程序员升)的差别。
首先,我们会查看候选人对Spring的综合理解,一般来说,希望候选人能够结合项目实际说出Spring的优势,比如能告诉我们面向接口自由组装降低耦合之类特性在他的项目里是怎么实现的,或者给他们的项目带来了哪些具体的好处。这里我们很关注结合项目,因为这是判断候选人是否用过Spring以及是否精通Spring的必要条件。
其次,候选人既然做了Spring的不少项目,那么可以讲讲项目开发过程中用Spring时走的一些弯路,有哪些经验体会。我们听到的回答有大量用到了注解会对项目维护带来问题,或者是用单例创建Bean的时候对多线程之间的调用带来了一定的问题。
这方面我们一般不拘泥于答案本身,甚至只要别错得离谱就行,我们关注的是候选人对Spring的熟悉程度,以此来衡量他的Spring方面的经验。
最后,我们会从Spring里Bean的生命周期和源代码这两个方面来提问。这部分确实是比较资深的内容,对工作经验3年以内正要升级的程序员来说,我们的期望要求是能知道Bean的生命周期,遇到一些需求时,知道该调用哪些节点方法来实现。至于Spring内核实现IoC和AOP的源代码,这是对资深程序员(工作经验5年左右)的要求,初级程序员如果能回答上,就是个加分项。
下面总结下我们经常提问的问题点,请大家结合这些问题来检查本章的学习情况。
第一,阅读简历发现候选人做过Spring项目后,会请他结合实际项目说明是如何使用IoC的。
IoC本身的技术点并不难描述,但千万记得要结合具体的项目,比如在银行项目里一个管理账户的类可以通过IoC的特性动态地加载另外一个管理安全风险的类,这样能减少耦合度。
第二,请结合项目说明你用过哪些autowired的种类。
第三,请说明你用过哪些注解。
@Autowired之类的注解本身并不难描述,但这里请大家同样说下用注解的坏处,这样就能说明你做过不少 Spring的项目。
第四,先问如何实现单例,如何实现多例,然后请候选人结合项目说下在哪种情况下该用单例模式(或多例)创建Bean。
这里首先请了解设置单例的语法,然后请了解有状态和无状态Bean的概念和用途,最后再说明如何创建。这里的回答要点是,根据你的需求确认该用单例创建就行了。
第五,具体说下Bean的生命周期,说下你重写过其中的哪些方法。或者这样问,如果要给Bean编号,该怎么做?或者该如何设置Spring的初始化方法。总之,当你了解了Spring生命周期里各节点,遇到实际需求后,你就能清楚地了解该重写哪些方法来解决。
这里请大家熟悉整个Spring Bean的生命周期以及其中各节点的方法。
第六,说下你在项目里如何使用AOP,大多候选人都说用AOP来实现日志打印,这确实是非常合适的用途。
这里同样请大家结合项目实际来说明。
第七,说下你在项目里具体用过哪些AOP,比如前置、后置、环绕等。
这其实是个通俗的说法,更专业的问法是你用过哪些AOP的通知类型。我们知道AOP的通知类型一共有5种,在项目里不是 用得越多越好,而是应该用在合适的场合,哪怕你们项目就用了前置,但用得很恰当,那就行。我们还真见过有人说5种都用过但用的场合不对,这可能就属于画蛇添足了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构