在 Spring Boot 开发中,事务管理是核心场景之一,而事务的实现依赖于 Spring 的代理机制。这一机制初学时往往让人困惑:为何加了 @Transactional 注解的方法,调用时并非原始对象执行?带着这个问题,我们从 Spring Boot 代理对象与事务的关联入手,逐步回溯 Java SE 基础中的对象、引用、equals 方法、字符串常量池等核心知识点,打通从框架到基础的逻辑链路,帮你彻底理解底层原理与实际开发中的避坑点。
一、Spring Boot 中的对象与代理对象引入及 @Transactional 事务实践 1.1 核心问题:为何事务方法需通过代理对象调用? 在 Spring Boot 的 MVC 三层架构中,我们通常在 Service 层添加 @Transactional 注解实现事务管理。但很多开发者会遇到一个经典问题:同一 Service 类中,非事务方法调用事务方法时,事务失效。这一问题的根源就是 Spring 的代理机制——@Autowired 注入的 Service 实例,并非我们编写的原始类对象,而是 Spring 动态生成的代理对象。
Spring 为了实现 AOP(面向切面编程)的无侵入式增强,会对包含 @Transactional、@Cacheable、@Async 等注解的 Bean 生成代理对象。代理对象的核心作用是在原始方法执行前后,嵌入增强逻辑(如事务的开启、提交、回滚),而原始对象仅包含核心业务逻辑。这种设计让业务代码与横切逻辑(事务、缓存、日志等)解耦,符合“单一职责原则”。
1.2 Spring Boot 中代理对象的生成场景与触发条件 通过 @Autowired 注入的实例,并非都会被转为代理对象,仅在满足特定条件时,Spring 才会触发动态代理机制。核心场景及条件如下:
1.2.1 基于注解的 AOP 增强场景(最常见) 当 Bean 中存在被 Spring AOP 注解标记的方法时,Spring 会为该 Bean 生成代理对象,核心注解包括:
事务相关 :@Transactional(类或方法上添加,无论是否设置属性,均会触发代理);
缓存相关 :@Cacheable、@CacheEvict、@CachePut 等;
异步相关 :@Async(异步方法注解,需配合@EnableAsync 开启);
自定义 AOP 注解 :通过 @Aspect 定义切面,结合 @Pointcut 切入自定义注解的方法。
注意:若注解添加在类上,该类所有 public 方法都会被代理增强;若仅添加在方法上,仅目标方法会被增强。非 public 方法即使添加注解,也无法被代理(JDK 动态代理不支持 private 方法,CGLIB 虽能继承但无法重写 private 方法)。
1.2.2 基于接口/类的 AOP 配置场景 除了注解方式,通过 XML 配置(如 <aop:config>)或 Java 配置类(通过 @Bean 定义 Aspect 切面)指定切入规则时,符合规则的 Bean 会被生成代理对象。例如,配置切入所有 Service 层的 transfer* 方法,此时所有 Service 中匹配该规则的方法都会被代理。
1.2.3 特殊 Bean 类型的代理场景 部分 Spring 内置 Bean 或自定义 Bean 实现特定接口时,会被强制生成代理对象,例如:
1.2.4 不会生成代理对象的场景 以下情况中,@Autowired 注入的是原始对象,而非代理对象:
Bean 中无任何 AOP 注解,且未被自定义切面切入;
通过 new 关键字手动创建的对象(未交给 Spring 容器管理);
被 @Scope("prototype") 标记的 Bean,虽每次获取都是新实例,但仅当满足 AOP 条件时才会生成代理,否则为原始对象;
静态方法、final 方法(无法被代理重写,即使添加注解也不会触发增强)。
1.3 为何 Spring Boot 偏爱依赖注入(DI)方式? Spring Boot 核心设计思想之一就是“依赖注入”(Dependency Injection,DI),通过@Autowired、@Resource、构造器注入等方式管理 Bean 依赖,而非手动通过 new 创建对象。这种方式的优势的体现在多方面,是企业级开发的核心需求:
1.3.1 解耦依赖关系,提升代码可维护性 传统开发中,对象间依赖通过 new 硬编码实现,例如 UserService userService = new UserServiceImpl();。这种方式导致对象间耦合度极高,若UserServiceImpl 构造器变化,所有调用处都需修改。而依赖注入将对象的创建权交给 Spring 容器,开发者只需通过注解声明依赖,容器自动完成对象创建与注入,彻底解除对象间的硬耦合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class UserController { private UserService userService = new UserServiceImpl (); } @RestController public class UserController { @Autowired private UserService userService; }
1.3.2 支持 AOP 动态代理,实现横切逻辑增强 如前文所述,Spring 的代理机制依赖于容器管理的 Bean。若对象通过 new 手动创建,Spring 无法对其生成代理对象,也就无法植入事务、缓存等横切逻辑。而依赖注入获取的 Bean,本质是容器管理的代理对象(满足条件时),这为 AOP 增强提供了基础,是 Spring 事务、缓存等核心功能的实现前提。
1.3.3 统一管理 Bean 生命周期,优化资源占用 Spring 容器对所有注入的 Bean 进行统一生命周期管理,包括初始化(@PostConstruct)、销毁(@PreDestroy)、作用域控制(单例/多例)等。默认情况下,Bean 为单例(@Scope("singleton")),容器仅创建一个实例并复用,避免了频繁创建对象导致的内存浪费和性能损耗。若手动new 对象,需自行管理生命周期,易出现资源泄漏(如数据库连接未关闭)。
1.3.4 简化测试,提升开发效率 依赖注入便于单元测试时进行 Mock 替换。例如,测试 UserController 时,可通过 Mockito 模拟 UserService 实例,注入到控制器中,无需依赖真实的 Service 实现和数据库环境,大幅降低测试成本。若为硬编码创建对象,Mock 替换需修改源码,测试灵活性极差。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @SpringBootTest public class UserControllerTest { @MockBean private UserService userService; @Autowired private UserController userController; @Test public void testAddUser () { when (userService.addUser(any(User.class))).thenReturn(true ); Result result = userController.addUser(new User ()); Assertions.assertTrue(result.isSuccess()); } }
1.3.5 支持依赖传递与循环依赖解决 Spring 容器能自动处理 Bean 间的依赖传递(如 A 依赖 B,B 依赖 C,容器自动按顺序创建 C、B、A 并注入),同时通过三级缓存机制解决合法的循环依赖(如 A 依赖 B,B 依赖 A)。若手动管理依赖,需自行处理依赖顺序和循环依赖问题,极易出现逻辑错误。
1.4 代理对象的实现原理与两种代理方式 Spring 生成代理对象有两种主流方式,根据目标类是否实现接口自动选择(Spring Boot 2.x 默认优先使用 CGLIB 代理,Spring Boot 1.x 默认优先使用 JDK 动态代理)。
方式一:CGLIB 代理(基于继承,无接口场景) CGLIB(Code Generation Library)通过继承目标类,动态生成子类作为代理对象,重写目标类的方法并嵌入增强逻辑。代理对象持有原始对象的引用,执行方法时先触发增强逻辑,再调用原始对象的核心方法。
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 51 52 53 54 55 56 @Service public class UserService { @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } public void deductBalance (Long userId, BigDecimal amount) { System.out.println("扣减用户" + userId + "余额:" + amount); } public void addBalance (Long userId, BigDecimal amount) { System.out.println("增加用户" + userId + "余额:" + amount); } } public class UserService$$EnhancerBySpringCGLIB extends UserService { private UserService target; @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { TransactionStatus status = null ; try { status = transactionManager.getTransaction(new DefaultTransactionDefinition ()); target.transferMoney(fromId, toId, amount); transactionManager.commit(status); } catch (Exception e) { if (status != null ) { transactionManager.rollback(status); } throw e; } } @Override public void deductBalance (Long userId, BigDecimal amount) { target.deductBalance(userId, amount); } }
方式二:JDK 动态代理(基于接口,有接口场景) 若目标类实现了接口,Spring 会使用 JDK 动态代理,生成实现该接口的代理类。代理类持有原始对象引用,实现接口方法时嵌入增强逻辑,本质是“基于接口的委托模式”。
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 public interface IUserService { void transferMoney (Long fromId, Long toId, BigDecimal amount) ; } @Service public class UserService implements IUserService { @Transactional(rollbackFor = Exception.class) @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } } public class UserServiceJdkProxy implements IUserService { private IUserService target; @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { TransactionStatus status = null ; try { status = transactionManager.getTransaction(new DefaultTransactionDefinition ()); target.transferMoney(fromId, toId, amount); transactionManager.commit(status); } catch (Exception e) { if (status != null ) { transactionManager.rollback(status); } throw e; } } }
1.5 事务失效的经典场景与解决方案 最常见的事务失效场景:同一 Service 类中,非事务方法通过 this 调用事务方法。因为 this 代表原始对象,而非代理对象,跳过了增强逻辑,导致事务无法触发。
场景复现(事务失效) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class UserService { public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); this .transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } public void saveUser (User user) { System.out.println("新增用户:" + user.getName()); } }
解决方案:获取代理对象调用事务方法 核心思路:放弃 this 调用,通过代理对象调用事务方法,常用两种方式:
方案1:通过 AopContext 获取代理对象(需开启暴露代理) 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 @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } } @Service public class UserService { public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); UserService proxy = (UserService) AopContext.currentProxy(); proxy.transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { } }
方案2:自注入代理对象(更优雅,无需额外配置) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class UserService { @Autowired private UserService userServiceProxy; public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); userServiceProxy.transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { } }
方案3:拆分 Service 类(彻底解决同一类调用问题) 将事务方法拆分到另一个 Service 类中,通过依赖注入调用,避免同一类中 this 调用的问题,同时符合“单一职责原则”。
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 @Service public class TransactionService { @Autowired private UserService userService; @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { userService.deductBalance(fromId, amount); userService.addBalance(toId, amount); } } @Service public class UserService { @Autowired private TransactionService transactionService; public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); transactionService.transferMoney(fromId, toId, amount); } public void deductBalance (Long userId, BigDecimal amount) { } public void addBalance (Long userId, BigDecimal amount) { } public void saveUser (User user) { } }
1.6 核心问题回顾:为何事务方法需通过代理对象调用? (复盘小节,保留原知识点)在 Spring Boot 的 MVC 三层架构中,我们通常在 Service 层添加 @Transactional 注解实现事务管理。但很多开发者会遇到一个经典问题:同一 Service 类中,非事务方法调用事务方法时,事务失效。这一问题的根源的就是 Spring 的代理机制——@Autowired 注入的 Service 实例,并非我们编写的原始类对象,而是 Spring 动态生成的代理对象。
Spring 为了实现 AOP(面向切面编程)的无侵入式增强,会对包含 @Transactional、@Cacheable、@Async 等注解的 Bean 生成代理对象。代理对象的核心作用是在原始方法执行前后,嵌入增强逻辑(如事务的开启、提交、回滚),而原始对象仅包含核心业务逻辑。
1.7 代理对象的实现原理与两种代理方式 Spring 生成代理对象有两种主流方式,根据目标类是否实现接口自动选择(Spring Boot 默认优先使用 CGLIB 代理)。
方式一:CGLIB 代理(基于继承,无接口场景) CGLIB(Code Generation Library)通过继承目标类,动态生成子类作为代理对象,重写目标类的方法并嵌入增强逻辑。代理对象持有原始对象的引用,执行方法时先触发增强逻辑,再调用原始对象的核心方法。
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 51 52 53 54 55 56 @Service public class UserService { @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } public void deductBalance (Long userId, BigDecimal amount) { System.out.println("扣减用户" + userId + "余额:" + amount); } public void addBalance (Long userId, BigDecimal amount) { System.out.println("增加用户" + userId + "余额:" + amount); } } public class UserService$$EnhancerBySpringCGLIB extends UserService { private UserService target; @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { TransactionStatus status = null ; try { status = transactionManager.getTransaction(new DefaultTransactionDefinition ()); target.transferMoney(fromId, toId, amount); transactionManager.commit(status); } catch (Exception e) { if (status != null ) { transactionManager.rollback(status); } throw e; } } @Override public void deductBalance (Long userId, BigDecimal amount) { target.deductBalance(userId, amount); } }
方式二:JDK 动态代理(基于接口,有接口场景) 若目标类实现了接口,Spring 会使用 JDK 动态代理,生成实现该接口的代理类。代理类持有原始对象引用,实现接口方法时嵌入增强逻辑,本质是“基于接口的委托模式”。
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 public interface IUserService { void transferMoney (Long fromId, Long toId, BigDecimal amount) ; } @Service public class UserService implements IUserService { @Transactional(rollbackFor = Exception.class) @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } } public class UserServiceJdkProxy implements IUserService { private IUserService target; @Override public void transferMoney (Long fromId, Long toId, BigDecimal amount) { TransactionStatus status = null ; try { status = transactionManager.getTransaction(new DefaultTransactionDefinition ()); target.transferMoney(fromId, toId, amount); transactionManager.commit(status); } catch (Exception e) { if (status != null ) { transactionManager.rollback(status); } throw e; } } }
1.8 事务失效的经典场景与解决方案 最常见的事务失效场景:同一 Service 类中,非事务方法通过 this 调用事务方法。因为 this 代表原始对象,而非代理对象,跳过了增强逻辑,导致事务无法触发。
场景复现(事务失效) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class UserService { public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); this .transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { deductBalance(fromId, amount); addBalance(toId, amount); } public void saveUser (User user) { System.out.println("新增用户:" + user.getName()); } }
解决方案:获取代理对象调用事务方法 核心思路:放弃 this 调用,通过代理对象调用事务方法,常用两种方式:
方案1:通过 AopContext 获取代理对象(需开启暴露代理) 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 @SpringBootApplication @EnableAspectJAutoProxy(exposeProxy = true) public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } } @Service public class UserService { public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); UserService proxy = (UserService) AopContext.currentProxy(); proxy.transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { } }
方案2:自注入代理对象(更优雅,无需额外配置) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Service public class UserService { @Autowired private UserService userServiceProxy; public void addUserAndTransfer (User user, Long fromId, Long toId, BigDecimal amount) { saveUser(user); userServiceProxy.transferMoney(fromId, toId, amount); } @Transactional(rollbackFor = Exception.class) public void transferMoney (Long fromId, Long toId, BigDecimal amount) { } }
二、Java SE 基础回顾:对象与引用的核心区别 理解 Spring 代理机制的前提,是掌握 Java 中对象与引用的本质区别。很多开发者混淆了“对象”和“引用”,导致无法理解代理对象的调用逻辑、空指针异常等问题。
2.1 对象与引用的定义与内存模型 Java 是面向对象语言,但我们无法直接操作对象,必须通过“引用”间接操作。核心关系如下:
代码拆解与内存图解 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 class Student { String name; int age; public Student (String name, int age) { this .name = name; this .age = age; } public void study () { System.out.println(name + "正在学习" ); } } public class Test { public static void main (String[] args) { Student student = new Student ("张三" , 20 ); student.age = 21 ; student.study(); } }
内存模型(简化):
2.2 引用的核心特性与实战场景 特性1:多个引用可指向同一个对象 多个引用变量存储同一个对象的地址,修改其中一个引用操作的对象属性,会影响所有指向该对象的引用。
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { Student s1 = new Student ("张三" , 20 ); Student s2 = s1; s2.age = 21 ; System.out.println(s1.age); System.out.println(s1 == s2); } }
特性2:引用可重新指向其他对象 引用变量的值(对象地址)可以修改,重新指向其他对象,原对象若无人引用,会被 GC(垃圾回收器)回收。
1 2 3 4 5 6 7 8 public class Test { public static void main (String[] args) { Student s1 = new Student ("张三" , 20 ); s1 = new Student ("李四" , 22 ); } }
特性3:空引用(null) 引用变量可赋值为 null,表示不指向任何对象。通过空引用调用方法/属性,会抛出 NullPointerException(NPE),这是 Java 开发中最常见的异常之一。
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) { Student s = null ; } }
2.3 == 与 equals() 方法的区别(面试核心) 判断两个对象是否“相等”,是 Java 基础高频考点,核心在于区分 == 运算符和 equals() 方法的作用。
2.3.1 == 运算符的作用 == 是运算符,无法被重写,作用分两种场景:
2.3.2 equals() 方法的作用 equals() 是 Object 类的方法,可被重写,核心作用是判断两个对象的“内容是否相等”,但默认实现等价于 ==。
1 2 3 4 5 public boolean equals (Object obj) { return (this == obj); }
2.3.3 重写 equals() 方法(自定义内容比较) 实际开发中,我们通常需要按对象属性判断内容是否相等,此时需重写equals() 方法,同时必须重写 hashCode() 方法(遵循“相等的对象必须有相等的哈希码”规则,否则 HashMap、HashSet 等集合无法正常工作)。
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 import java.util.Objects;class Student { String name; int age; public Student (String name, int age) { this .name = name; this .age = age; } @Override public boolean equals (Object o) { if (this == o) return true ; if (o == null || getClass() != o.getClass()) return false ; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); } @Override public int hashCode () { return Objects.hash(name, age); } } public class Test { public static void main (String[] args) { Student s1 = new Student ("张三" , 20 ); Student s2 = new Student ("张三" , 20 ); Student s3 = s1; System.out.println(s1 == s2); System.out.println(s1.equals(s2)); System.out.println(s1 == s3); } }
关键说明:getClass() 的作用 代码中 getClass() != o.getClass() 等价于 this.getClass() != o.getClass():
作用是严格判断两个对象是否属于同一个类的实例,避免子类与父类对象误判(比 instanceof 更严格,instanceof 允许子类对象匹配父类类型)。
三、JVM 字符串常量池与字符串操作全解析 字符串是 Java 中最常用的引用类型,其特殊的内存管理机制(字符串常量池)是面试高频考点。与其他语言不同,Java 为字符串设计了常量池,目的是缓存字符串字面量,避免重复创建相同内容的对象,节省内存。
3.1 字符串常量池的核心原理 字符串常量池(String Constant Pool)是 JVM 中专门缓存字符串字面量的区域,位置随 JDK 版本变化:
核心规则:编译期确定的字符串字面量(如"张三")会自动入池;运行期创建的字符串(如 new String()、变量拼接)不会自动入池,需通过 intern() 方法手动入池。
3.2 字符串的三种创建方式与内存差异 方式1:字面量赋值(自动入池,复用对象) 直接用 String s = "xxx" 赋值时,JVM 先检查常量池是否存在该字符串:
1 2 3 4 5 6 7 8 9 public class Test { public static void main (String[] args) { String s1 = "张三" ; String s2 = "张三" ; System.out.println(s1 == s2); System.out.println(s1.equals(s2)); }
方式2:new String() 创建(堆中新建对象,不复用) 用 new String("xxx") 创建字符串时,JVM 分两步执行:
因此,new String() 必然在堆中创建新对象,地址与常量池对象不同。
1 2 3 4 5 6 7 8 9 10 11 public class Test { public static void main (String[] args) { String s1 = "张三" ; String s2 = new String ("张三" ); String s3 = new String ("张三" ); System.out.println(s1 == s2); System.out.println(s2 == s3); System.out.println(s1.equals(s2)); }
方式3:字符串拼接(字面量拼接 vs 变量拼接) 字符串拼接的逻辑随拼接元素不同而变化,是面试高频坑点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Test { public static void main (String[] args) { String s1 = "张" + "三" ; String s2 = "张三" ; System.out.println(s1 == s2); String a = "张" ; String s3 = a + "三" ; System.out.println(s3 == s2); final String b = "张" ; String s4 = b + "三" ; System.out.println(s4 == s2); }
3.3 intern() 方法的作用与使用场景 intern() 方法是 String 类的核心方法,作用是手动将字符串对象加入常量池,返回常量池中的引用(无论是否已存在)。
intern() 核心逻辑
代码示例 1 2 3 4 5 6 7 8 9 10 public class Test { public static void main (String[] args) { String s1 = new String ("张三" ); String s2 = s1.intern(); String s3 = "张三" ; System.out.println(s1 == s2); System.out.println(s2 == s3); }
关键注意点 intern() 方法不会改变原字符串变量的指向,仅返回常量池引用。若要让变量指向常量池,需接收其返回值。
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) { String s = new String ("张三" ); s.intern(); System.out.println(s == "张三" ); }
四、高频面试题(含详细解析) 面试题1:Spring 中 @Transactional 事务失效的常见场景有哪些?如何避免?(中等难度) 答案解析: 常见失效场景及解决方案如下:
同一 Service 类中,非事务方法通过 this 调用事务方法 :this 代表原始对象,跳过代理,事务增强逻辑无法触发。解决方案:通过 AopContext 获取代理对象,或自注入代理对象调用。
事务方法未抛出异常,或异常被捕获未抛出 :默认情况下,@Transactional 仅对 RuntimeException 和 Error 回滚。解决方案:指定 rollbackFor 属性(如 rollbackFor = Exception.class),确保异常能触发回滚;避免在事务方法内捕获异常不抛出。
目标类未被 Spring 管理 :未加 @Service、@Component 等注解,或通过 new 手动创建对象,Spring 无法生成代理。解决方案:确保类被 Spring 扫描管理,通过 @Autowired 注入而非 new 创建。
事务方法为 private、static 或 final 修饰 :private 方法无法被代理重写;static/final 方法无法被动态代理重写(CGLIB 也无法重写 final 方法)。解决方案:事务方法改为 public 修饰,避免 static/final。
propagation 传播行为设置不当 :如设置为 PROPAGATION_NOT_SUPPORTED(不支持事务)、PROPAGATION_NEVER(禁止事务)。解决方案:根据业务场景选择合适的传播行为(默认 PROPAGATION_REQUIRED 即可)。
面试题2:Java 中 == 与 equals() 的区别,为何重写 equals() 必须重写 hashCode()?(基础+进阶) 答案解析:
== 与 equals() 的区别:
重写 equals() 必须重写 hashCode() 的原因:
遵循 JDK 规范:相等的对象必须有相等的哈希码(hashCode) ,反之不成立(哈希码相等的对象不一定相等)。
若仅重写 equals() 不重写 hashCode(),会导致两个内容相等的对象哈希码不同。当这两个对象存入 HashMap、HashSet 等基于哈希表的集合时,会被存入不同的桶位,导致集合无法识别它们是同一个对象,出现重复元素、查询失效等问题。
示例:Student s1 和 s2 内容相等(equals() 返回 true),若未重写 hashCode(),s1.hashCode() != s2.hashCode(),存入 HashSet 会被视为两个元素,违反集合去重规则。
面试题3:JDK 1.6 与 JDK 1.7+ 中字符串常量池的位置变化,对 intern() 方法有何影响?(进阶难度) 答案解析:
常量池位置变化:
对 intern() 方法的影响:
当常量池不存在目标字符串时,intern() 方法的行为差异:
代码验证(JDK 1.7+):
1 2 3 4 5 6 7 public class Test { public static void main (String[] args) { String s = new String ("a" ) + new String ("b" ); String s2 = s.intern(); System.out.println(s == s2); }
面试题4:Spring 中 CGLIB 代理与 JDK 动态代理的区别,如何强制使用 CGLIB 代理?(中等难度) 答案解析:
核心区别:
对比维度
JDK 动态代理
CGLIB 代理
实现原理
基于接口实现,生成实现目标接口的代理类
基于继承,生成目标类的子类作为代理类
依赖条件
目标类必须实现接口
目标类不能是 final 修饰(无法继承)
方法限制
仅能代理接口中声明的方法
可代理目标类的所有 public 方法(不能代理 private/final 方法)
性能
代理创建速度快,运行速度稍慢
代理创建速度慢(需生成子类字节码),运行速度快
2. 强制使用 CGLIB 代理的方式:
Spring Boot 2.x 默认优先使用 CGLIB 代理,若需明确配置,可通过以下方式:
1 2 3 4 5 6 7 8 9 10 @SpringBootApplication @EnableAspectJAutoProxy(proxyTargetClass = true) public class DemoApplication { public static void main (String[] args) { SpringApplication.run(DemoApplication.class, args); } }
方式2:配置文件(application.yml)
1 2 3 4 5 spring: aop: proxy-target-class: true auto: true
面试题5:以下代码的输出结果是什么?请解释原因(综合难度,考察字符串常量池+intern()) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class StringTest { public static void main (String[] args) { String str1 = new String ("abc" ); String str2 = str1.intern(); String str3 = "abc" ; System.out.println(str1 == str2); System.out.println(str2 == str3); System.out.println(str1 == str3); String str4 = new String ("a" ) + new String ("b" ); String str5 = str4.intern(); String str6 = "ab" ; System.out.println(str4 == str5); System.out.println(str5 == str6); System.out.println(str4 == str6); } }
答案解析:(基于 JDK 1.7+) 输出结果:
原因分析:
str1、str2、str3 部分 :
str1 = new String(“abc”):常量池创建 “abc”,堆创建新对象,str1 指向堆对象。
str1.intern():常量池已存在 “abc”,返回常量池引用给 str2。
str3 = “abc”:指向常量池对象。
因此:str1(堆)== str2(常量池)→ false;str2 == str3(均为常量池)→ true;str1 == str3 → false。
str4、str5、str6 部分 :
str4 = new String(“a”) + new String(“b”):运行期拼接,堆创建 “ab” 对象,常量池无 “ab”。
str4.intern():常量池无 “ab”,将堆对象 str4 的引用存入常量池,返回 str4 的引用给 str5。
str6 = “ab”:常量池已存储 str4 的引用,直接指向 str4 对应的堆对象。
因此:str4 == str5(同一堆对象)→ true;str5 == str6(均指向堆对象)→ true;str4 == str6 → true。
若在 JDK 1.6 中,str4 == str5 会返回 false,因为 JDK 1.6 中 intern() 会复制堆对象内容到常量池,str5 指向常量池新对象,与 str4(堆对象)地址不同。