面试官:说说代理在Spring中的应用吧?

前言

如果你阅读过我的上一篇说代理的文章, 你应该能充分的理解了代理到底是个什么东西,当谈论到代理相关问题的时候,也相信你能在面试官面前侃侃而谈。


动态代理在Spring中的使用

一、首先我们快速回顾一下Spring中的两种"员工"

1. JDK动态代理::严格遵守合同的"白领"​
less 复制代码
// 必须基于接口
public interface UserService {
    void saveUser();
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    @Transactional // 事务代理
    public void saveUser() {
        // 业务代码
    }
}

​​特点​​:

  • 只代理接口方法(像只按合同办事)
  • Spring默认选择(规范优先)
  • 生成类名:com.sun.proxy.$ProxyX
2. CGLIB代理:灵活多变的"蓝领"​
typescript 复制代码
@Service
public class OrderService { // 没有接口!
    @Cacheable // 缓存代理
    public Order getOrder() {
        // 业务代码
    }
}

特点​​:

  • 直接继承目标类(像"私生子"继承家产)
  • 能代理普通方法(不局限于接口)

二、在看一下Spring的智能选择策略​

Spring就像精明的HR,自动选择最佳代理:

objectivec 复制代码
if (有接口 && !强制CGLIB) {
    雇JDK代理;
} else {
    雇CGLIB代理;
}

三、简述一下Spring代理的四大"工作场景"​(快速浏览下就行)

1. 事务管理(@Transactional)
csharp 复制代码
// 代理前
public void transfer() {
    // 要自己写事务代码
}

// 代理后
public void transfer() {
    TransactionManager.begin();
    try {
        // 业务代码
        TransactionManager.commit();
    } catch (Exception e) {
        TransactionManager.rollback();
    }
}
2. 缓存优化(@Cacheable)
kotlin 复制代码
@Cacheable("users")
public User getUser(Long id) {
    // 代理会自动添加缓存逻辑:
    // 1. 先查缓存
    // 2. 缓存没有才查数据库
    // 3. 结果存入缓存
}
3. 安全控制(@Secured)​
java 复制代码
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser() {
    // 代理会先检查权限
    // 无权限直接抛异常
}
4. 性能监控(自定义切面)​
java 复制代码
@Around("execution(* com.example..*(..))")
public Object logTime(ProceedingJoinPoint pjp) {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();
    System.out.println("方法耗时:" + (System.currentTimeMillis() - start));
    return result;
}

鉴于上面提到工作场景,只是浅尝辄止,大家有个印象就行,下面将会详细的介绍一下AOP,详细的时候说一下用法,和代码实现。

AOP详解

1. 什么是AOP?

先看官方解释:AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它允许开发者将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,从而提高代码的模块化程度。

emmmmm, 什么是范式,什么是横切,什么是面向切面编程,当我第一眼看到这个概念的时候,眼前一黑。似乎好像懂了,但只是似乎。通俗地说,AOP就像一把"手术刀",可以精准地在程序执行的特定位置"切入"一些通用功能,而不需要修改原有业务代码。 如果你还不动,直接看下面的图片,你就明白了:

上述图片给了最简单的用户登录、登出、注册功能。调用的方式可能如下:

scss 复制代码
// 登录
userService.userLogin();
// 注册
userService.userRegister();
// 登出
useService.userLogout();

看图片中"打印调用记录"这个功能本身是个和我们用户登录,注册,登录是没有任何业务联系的。再反过来看一下AOP的定义,用于我们的代码中就可样这样描述: AOP它允许我们将打印日志功能(横向关注点),从业务中分离出来。

相信看完上述的解释,你已经知道了AOP到底是个什么东西。

AOP具体使用

为了能更清楚的展示AOP的功能,我还是用日志的功能进行讲解(没错, 他实现起来最简单。)我们先看一段没有AOP的日志打印。

csharp 复制代码
public class OrderService {
    public void createOrder() {
        // 记录日志
        System.out.println("开始执行createOrder方法");
        long start = System.currentTimeMillis();
        
        try {
            // 业务逻辑
            System.out.println("创建订单中...");
            // 模拟耗时
            Thread.sleep(1000);
            
            // 记录日志
            System.out.println("createOrder方法执行成功");
        } catch (Exception e) {
            // 异常处理
            System.out.println("createOrder方法执行失败: " + e.getMessage());
        } finally {
            // 性能监控
            long end = System.currentTimeMillis();
            System.out.println("createOrder方法执行耗时: " + (end - start) + "ms");
        }
    }
}

可以看到,日志、性能监控等代码与业务逻辑混杂在一起,导致:

  • 代码重复
  • 业务逻辑不清晰
  • 难以维护 在看下使用AOP后的代码:
csharp 复制代码
public class OrderService {
    public void createOrder() {
        System.out.println("创建订单中...");
        // 模拟耗时
        Thread.sleep(1000);
    }
}
java 复制代码
@Aspect
@Component
public class LoggingAspect {
    
    // 定义切入点(在哪些方法上应用切面)
    @Pointcut("execution(* com.example.service.OrderService.*(..))")
    public void orderServiceMethods() {}
    
    // 前置通知
    @Before("orderServiceMethods()")
    public void beforeMethod(JoinPoint joinPoint) {
        System.out.println("开始执行 " + joinPoint.getSignature().getName() + " 方法");
    }
    
    // 后置通知(方法成功执行后)
    @AfterReturning("orderServiceMethods()")
    public void afterReturningMethod(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行成功");
    }
    
    // 异常通知
    @AfterThrowing(pointcut = "orderServiceMethods()", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行失败: " + ex.getMessage());
    }
    
    // 环绕通知(可以控制方法执行)
    @Around("orderServiceMethods()")
    public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = null;
        
        try {
            result = joinPoint.proceed(); // 执行目标方法
        } finally {
            long end = System.currentTimeMillis();
            System.out.println(joinPoint.getSignature().getName() + 
                             " 方法执行耗时: " + (end - start) + "ms");
        }
        
        return result;
    }
}

我们可以观察到的是,在真正的业务代码实现那里,我们省去了大部分打印日志的语句,更聚焦于业务代码的实现。把相关日志都交给AOP做统一处理。


下面我将结合上述代码案例,再说下AOP中的"通知",没错你看错就是通知(Advice)

AOP通知

通知(Advice)就是AOP在特定时机执行的代码块,就像闹钟在特定时间响铃一样。AOP通知告诉系统:"当方法执行到某个点时,帮我插入这段代码"。我们以上述代码,进行详细说明。

前置通知(@Before)
java 复制代码
// 前置通知
@Before("orderServiceMethods()")
public void beforeMethod(JoinPoint joinPoint) {
    System.out.println("开始执行 " + joinPoint.getSignature().getName() + " 方法");
}

前置通知就是在代码执行的时候,先去执行前置通知中的代码。

后置通知(@AfterReturning
java 复制代码
   // 后置通知(方法成功执行后)
    @AfterReturning("orderServiceMethods()")
    public void afterReturningMethod(JoinPoint joinPoint) {
        System.out.println(joinPoint.getSignature().getName() + " 方法执行成功");
    }

后置通知就是在执行完业务功能后,再去执行后置通知的代码。

异常通知(@AfterThrowing)
less 复制代码
  // 异常通知
  @AfterThrowing(pointcut = "orderServiceMethods()", throwing = "ex")
  public void afterThrowingMethod(JoinPoint joinPoint, Exception ex) {
      System.out.println(joinPoint.getSignature().getName() + " 方法执行失败: " + ex.getMessage());
  }
}

业务代码执行的时候,如果有异常,会执行异常通知里的代码。

最终通知(@After)
kotlin 复制代码
@After("制作奶茶的方法")
public void 清理操作台() {
    System.out.println("清理操作台,准备下一单");
}

不管业务代码执行成功或失败,都执行最终通知。

环绕通知(@Around)
java 复制代码
// 环绕通知(可以控制方法执行)
@Around("orderServiceMethods()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = null;

    try {
        result = joinPoint.proceed(); // 执行目标方法
    } finally {
        long end = System.currentTimeMillis();
        System.out.println(joinPoint.getSignature().getName() + 
                         " 方法执行耗时: " + (end - start) + "ms");
    }

    return result;
}

全流程管控我们的业务代码。


上面就是AOP中的一些总结,下面再看下,真正在项目开发中的一些使用。

@Around​​:功能最全,适合需要控制方法执行的场景,比如事务管理、权限控制、日志记录 ​​@AfterReturning​​:适合方法成功后的处理,比如缓存更新、成功通知 ​​@AfterThrowing​​:统一异常处理,比如错误日志记录、异常报警

关于AOP所有的一些概念性的一些东西已经全部介绍完毕,这个内容旨在于加深我们对于AOP的理解和印象,可以结合我们自己的业务代码,看下AOP都为我们实现了什么功能。后续应该还会更新一篇AOP相关内容,是深入底层源码分析,看下Spring中是如何通过代码实现这个功能。

相关推荐
linweidong2 小时前
Go开发简历优化指南
分布式·后端·golang·高并发·简历优化·go面试·后端面经
咖啡啡不加糖3 小时前
雪花算法:分布式ID生成的优雅解决方案
java·分布式·后端
姑苏洛言3 小时前
基于微信公众号小程序的课表管理平台设计与实现
前端·后端
烛阴3 小时前
比UUID更快更小更强大!NanoID唯一ID生成神器全解析
前端·javascript·后端
why1513 小时前
字节golang后端二面
开发语言·后端·golang
还是鼠鼠3 小时前
单元测试-断言&常见注解
java·开发语言·后端·单元测试·maven
cainiao0806054 小时前
Spring Boot 4.0实战:构建高并发电商系统
java·spring boot·后端
Chandler244 小时前
Go 即时通讯系统:日志模块重构,并从main函数开始
后端·重构·golang·gin
酷爱码5 小时前
Spring Boot Starter 自动装配原理全解析:从概念到实践
java·开发语言·spring boot·后端·spring
小奏技术6 小时前
虚拟线程 vs. 传统线程池:Spring Boot 3.x I/O密集型任务性能对比
后端