文章目录
- TransactionTemplate使用
- **1. 定义异常枚举类**
- **2. 配置 TransactionTemplate**
- **3. Service 层实现**
- 4. DAO 层实现 / 替换为MySQL
- **5. 全局异常处理器**
- **6. 事务操作边界说明**
- **7. 对比表格:`@Transactional` vs `TransactionTemplate`**
- **场景描述**
- **1. 定义异常和枚举**
- **2. 配置 TransactionTemplate**
- **3. Service 层实现(动态事务控制)**
- **4. DAO 和 Service 层实现**
- **5. 动态事务控制的关键点**
- **6. 异常捕获与全局异常处理器**
- **7. 事务边界说明**
- **8. 对比:声明式事务 vs 编程式事务**
- **总结**
- Sa-Token
- string.intern()
- Spring官网为什么推荐构造器注入
- 多账号认证
- HttpBasic / Digest认证
- OAuth2
- 单点登录
TransactionTemplate使用
Spring提供的编程式事务类,支持自定义事务范围,以及更灵活更细粒度的事务操作。
以下是您要求的完整示例,包含使用 Lombok
的 @RequiredArgsConstructor
和 final
构造函数注入 TransactionTemplate
,并通过事务操作、异常处理和全局异常处理器的实现。最后附上 @Transactional
和 TransactionTemplate
的对比表格。
1. 定义异常枚举类
// BizException.java
public enum BizExceptionEnum {
DATABASE_ERROR(5001, "数据库操作失败"),
ILLEGAL_ARGUMENT(4001, "非法参数");
private final int code;
private final String message;
BizExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
// 自定义异常类
public class BizException extends RuntimeException {
private final int code;
public BizException(BizExceptionEnum enumType) {
super(enumType.getMessage());
this.code = enumType.getCode();
}
public int getCode() {
return code;
}
}
2. 配置 TransactionTemplate
// AppConfig.java
@Configuration
public class AppConfig {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
TransactionTemplate template = new TransactionTemplate(transactionManager);
template.setPropagationBehavior(Propagation.REQUIRED.value()); // 默认传播行为
return template;
}
}
3. Service 层实现
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@RequiredArgsConstructor // Lombok 生成构造函数,参数为所有 final 字段
public class UserService {
private final TransactionTemplate transactionTemplate;
private final UserRepository userRepository; // 假设这是 DAO 层
public void updateUserInTransaction(Long userId, String newName) {
transactionTemplate.execute(status -> {
try {
// 执行数据库操作
User user = userRepository.findById(userId)
.orElseThrow(() -> new BizException(BizExceptionEnum.ILLEGAL_ARGUMENT));
user.setName(newName);
userRepository.save(user);
// 模拟可能的异常
if (newName.equals("error")) {
throw new RuntimeException("Simulated database error");
}
return null;
} catch (Exception e) {
// 异常时手动回滚事务
status.setRollbackOnly();
throw new BizException(BizExceptionEnum.DATABASE_ERROR);
}
});
}
}
4. DAO 层实现 / 替换为MySQL
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
@Autowired
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public Optional<User> findById(Long userId) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.query(sql, (rs, rowNum) ->
new User(rs.getLong("id"), rs.getString("name"))
).stream().findFirst();
}
public void save(User user) {
String sql = "UPDATE users SET name = ? WHERE id = ?";
jdbcTemplate.update(sql, user.getName(), user.getId());
}
}
5. 全局异常处理器
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BizException.class)
public ResponseEntity<ErrorInfo> handleBizException(BizException ex) {
ErrorInfo errorInfo = new ErrorInfo(ex.getCode(), ex.getMessage());
return new ResponseEntity<>(errorInfo, HttpStatus.INTERNAL_SERVER_ERROR);
}
private static class ErrorInfo {
private final int code;
private final String message;
public ErrorInfo(int code, String message) {
this.code = code;
this.message = message;
}
// getters
}
}
6. 事务操作边界说明
- 事务开始:调用
transactionTemplate.execute(...)
时,事务开始。 - 事务提交:若回调方法正常执行完毕,事务自动提交。
- 事务回滚:
- 默认情况下,未捕获的
RuntimeException
会触发回滚。 - 在
catch
块中调用status.setRollbackOnly()
显式回滚。
- 默认情况下,未捕获的
7. 对比表格:@Transactional
vs TransactionTemplate
特性 | @Transactional(声明式事务) | TransactionTemplate(编程式事务) |
---|---|---|
定义方式 | 通过注解定义(如 @Transactional ) | 通过代码显式调用 TransactionTemplate.execute() |
配置复杂度 | 简单(只需配置事务管理器) | 需要手动配置 TransactionTemplate 和事务管理器 |
灵活性 | 固定的事务边界(方法级别) | 可动态控制事务边界(如嵌套事务、条件性事务) |
异常处理 | 自动回滚 RuntimeException ,需配置 rollbackFor | 手动控制回滚(通过 status.setRollbackOnly() ) |
适用场景 | 简单事务(如方法级事务) | 复杂事务逻辑(如动态事务、嵌套事务) |
代码侵入性 | 低(只需注解) | 高(需显式编写事务逻辑) |
事务传播行为 | 通过注解参数(如 propagation )设置 | 通过 TransactionTemplate 的配置设置 |
场景描述
假设有一个订单处理流程,包含以下步骤:
- 保存订单:将订单信息保存到数据库。
- 扣减库存:调用库存服务扣减商品库存。
- 调用支付服务:调用第三方支付接口完成支付。
- 更新订单状态:根据支付结果更新订单状态。
要求:
- 如果任何步骤失败,需回滚前面的操作。
- 如果扣减库存失败,仅回滚库存操作(模拟嵌套事务)。
- 如果支付失败,回滚整个订单操作。
1. 定义异常和枚举
// 自定义异常枚举
public enum OrderExceptionEnum {
ORDER_SAVE_FAILED(5001, "保存订单失败"),
INVENTORY_DEDUCT_FAILED(5002, "库存扣减失败"),
PAYMENT_FAILED(5003, "支付失败");
private final int code;
private final String message;
OrderExceptionEnum(int code, String message) {
this.code = code;
this.message = message;
}
// 省略 getter 方法
}
// 自定义异常类
public class OrderException extends RuntimeException {
private final int code;
public OrderException(OrderExceptionEnum enumType, String message) {
super(message);
this.code = enumType.getCode();
}
// 省略 getter 方法
}
2. 配置 TransactionTemplate
@Configuration
public class AppConfig {
@Bean
public DataSourceTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
public TransactionTemplate transactionTemplate(DataSourceTransactionManager transactionManager) {
return new TransactionTemplate(transactionManager);
}
}
3. Service 层实现(动态事务控制)
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
@Service
@RequiredArgsConstructor
public class OrderService {
private final TransactionTemplate transactionTemplate;
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentService paymentService;
public void processOrder(Order order) {
transactionTemplate.execute(status -> {
try {
// 步骤1:保存订单
orderRepository.save(order);
// 步骤2:扣减库存(模拟嵌套事务)
executeInventoryDeduction(order);
// 步骤3:调用支付服务
PaymentResult paymentResult = paymentService.pay(order);
if (!paymentResult.isSuccess()) {
throw new OrderException(OrderExceptionEnum.PAYMENT_FAILED, "支付失败");
}
// 步骤4:更新订单状态
order.setStatus(OrderStatus.PAID);
orderRepository.update(order);
return "Success";
} catch (OrderException e) {
// 根据异常类型决定是否回滚
status.setRollbackOnly();
throw e;
} catch (Exception e) {
// 其他异常一律回滚
status.setRollbackOnly();
throw new OrderException(OrderExceptionEnum.ORDER_SAVE_FAILED, "未知错误", e);
}
});
}
// 执行扣减库存的嵌套事务
private void executeInventoryDeduction(Order order) {
TransactionTemplate nestedTemplate = new TransactionTemplate(transactionTemplate);
nestedTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_NESTED);
nestedTemplate.execute(nestedStatus -> {
try {
inventoryService.deduct(order.getProductId(), order.getQuantity());
} catch (InventoryException e) {
nestedStatus.setRollbackOnly();
throw new OrderException(OrderExceptionEnum.INVENTORY_DEDUCT_FAILED, e.getMessage());
}
return null;
});
}
}
4. DAO 和 Service 层实现
// 订单仓储
@Repository
public class OrderRepository {
public void save(Order order) {
// 保存订单到数据库
}
public void update(Order order) {
// 更新订单状态
}
}
// 库存服务
@Service
public class InventoryService {
public void deduct(String productId, int quantity) {
// 扣减库存,可能抛出 InventoryException
}
}
// 支付服务
@Service
public class PaymentService {
public PaymentResult pay(Order order) {
// 调用支付接口,返回 PaymentResult
return new PaymentResult(true); // 模拟成功
}
}
5. 动态事务控制的关键点
-
动态回滚控制:
- 在
catch
块中,根据异常类型(如OrderException
)决定是否调用status.setRollbackOnly()
。 - 如果支付失败,直接回滚整个事务;如果库存扣减失败,仅回滚子事务(通过嵌套事务)。
- 在
-
嵌套事务的模拟:
- 使用
TransactionDefinition.PROPAGATION_NESTED
设置传播行为,创建子事务。 - 如果子事务(扣减库存)失败,仅回滚到保存点,主事务继续执行后续步骤(但实际中需依赖数据库支持保存点)。
- 使用
6. 异常捕获与全局异常处理器
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(OrderException.class)
public ResponseEntity<ErrorInfo> handleOrderException(OrderException ex) {
ErrorInfo errorInfo = new ErrorInfo(ex.getCode(), ex.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorInfo);
}
private static class ErrorInfo {
private final int code;
private final String message;
public ErrorInfo(int code, String message) {
this.code = code;
this.message = message;
}
}
}
7. 事务边界说明
步骤 | 事务边界 | 回滚条件 |
---|---|---|
保存订单 | 主事务开始 | 任何异常触发回滚 |
扣减库存 | 嵌套事务(保存点) | 库存不足时仅回滚子事务 |
调用支付 | 主事务中执行 | 支付失败时回滚整个主事务 |
更新订单状态 | 主事务中执行 | 异常时回滚整个主事务 |
8. 对比:声明式事务 vs 编程式事务
场景 | 声明式事务(@Transactional) | 编程式事务(TransactionTemplate) |
---|---|---|
动态回滚控制 | 无法根据异常类型动态决定回滚 | 可通过 status.setRollbackOnly() 精确控制回滚 |
嵌套事务支持 | 仅支持传播行为(如 PROPAGATION_NESTED ) | 可通过 TransactionDefinition.PROPAGATION_NESTED 模拟 |
复杂逻辑处理 | 无法在事务中嵌套条件判断或分阶段提交 | 可在 execute 回调中实现复杂的条件逻辑和分阶段操作 |
代码侵入性 | 低(仅需注解) | 高(需编写事务逻辑代码) |
总结
- 动态事务控制:通过
TransactionTemplate
的execute
回调,可以灵活处理不同异常类型,并决定是否回滚事务。 - 嵌套事务:通过
PROPAGATION_NESTED
实现局部回滚,但需依赖数据库支持保存点(如 MySQL 的SAVEPOINT
)。 - 适用场景:当需要根据业务逻辑动态控制事务边界、处理复杂条件或与第三方服务交互时,
TransactionTemplate
是更灵活的选择。
Sa-Token
对比维度 | Sa-Token | Spring Security |
---|---|---|
核心定位 | 轻量级权限认证框架,专注于登录认证、权限控制、单点登录(SSO)、OAuth2 等核心功能。 | 企业级安全框架,提供全面的安全解决方案,包括认证、授权、加密、OAuth2、CSRF 防护等。 |
上手难度 | 简单易用,API 设计简洁,开箱即用,适合快速集成。 | 复杂,配置繁琐,学习曲线陡峭,需要熟悉 Spring 生态系统和安全概念。 |
功能特性 | - 登录认证 - 权限验证(RBAC 模型) - 分布式 Session - SSO(三种模式) - OAuth2 支持(四种授权模式) - 自动续签、踢人下线、封禁账号等。 | - 完整的认证与授权体系 - 支持 OAuth2、OpenID、JWT - CSRF 防护 - 细粒度权限控制 - 与 Spring 生态无缝集成。 |
社区与生态 | 社区活跃,文档完善,提供大量示例和插件(如 SSO、OAuth2)。 | 社区庞大,资源丰富,Spring 家族成员,与 Spring Boot/Spring MVC 等深度整合。 |
适用场景 | - 小型到中型项目 - 需要快速实现基础权限控制和 SSO - 对轻量级和易用性要求高。 | - 复杂企业级应用 - 需要细粒度权限控制和高级安全功能 - 依赖 Spring 生态系统的项目。 |
扩展性 | 通过插件和自定义接口扩展,但灵活性有限。 | 极强,支持自定义认证提供者、权限表达式(如 SpEL)、过滤器链等,适合复杂需求。 |
性能与资源占用 | 轻量级,资源占用低,适合对性能敏感的场景。 | 功能全面但配置复杂,可能增加资源开销,需合理优化。 |
与 Spring 的集成 | 提供 Spring Boot Starter,开箱即用,但非 Spring 生态核心组件。 | 原生 Spring 家族成员,与 Spring Boot、Spring MVC 等无缝集成,依赖关系紧密。 |
安全特性 | 内置防 Ticket 劫持、参数加密等,适合通用场景。 | 支持更全面的安全机制(如加密算法、安全响应处理、攻击防护),适合高安全需求场景。 |
分布式支持 | 支持分布式 Session 和 SSO(通过 Redis 或 HTTP 接口),提供多种模式适配不同架构。 | 需借助第三方组件(如 Redis、JWT)实现分布式安全,配置复杂度较高。 |
如果需要 SSO 或 OAuth2 的 开箱即用支持,Sa-Token 的实现更简单;而 Spring Security 需要更多自定义配置。
对于 微服务架构,两者均支持,但 Sa-Token 通过插件化设计更易快速集成,而 Spring Security 需借助 Zuul/Gateway 等组件。
string.intern()
将某个字符串引用指向其常量池,避免额外空间浪费。但是这个值如果用来加锁会有释放不及时的问题。
Spring官网为什么推荐构造器注入
Spring 官方推荐构造器注入(Constructor Injection)而非字段注入(Field Injection)或Setter注入,主要基于以下核心原因,结合知识库中的信息总结如下:
**1. ** 显式声明依赖关系
- 显式性:通过构造函数的参数,可以一目了然地看到一个类的所有依赖项。例如:
public class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; @Autowired public OrderService(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; }
- 优点:依赖关系清晰,开发者无需查看类内部属性即可了解类的依赖项。
- 对比字段注入:字段注入(如
@Autowired private PaymentService paymentService;
)会隐藏依赖关系,导致类的可读性下降。
**2. ** 强制依赖完整性
- 依赖必须存在:构造器注入要求在对象实例化时所有依赖项必须被提供,否则无法创建对象实例。例如:
// 如果 PaymentService 或 InventoryService 未被正确注入,构造器将无法实例化 OrderService
- 优点:避免了依赖项为
null
的风险,确保对象在创建时就处于有效状态。 - 对比Setter注入:Setter注入允许依赖项在对象创建后延迟注入,可能导致依赖未被正确初始化时调用方法,引发
NullPointerException
。
- 优点:避免了依赖项为
**3. ** 支持不可变性(Immutability)
- 依赖不可变:通过将依赖项声明为
final
,可以确保依赖在对象初始化后无法被修改:private final PaymentService paymentService;
- 优点:提高代码的线程安全性和稳定性,避免多线程环境下依赖被意外修改。
- 对比Setter注入:Setter注入允许依赖在运行时被修改,可能引入不可预测的行为。
**4. ** 提高可测试性
- 易于单元测试:构造器注入使得在测试时可以方便地传入模拟对象(Mock):
@Test public void testOrderService() { PaymentService mockPayment = mock(PaymentService.class); InventoryService mockInventory = mock(InventoryService.class); OrderService service = new OrderService(mockPayment, mockInventory); // 测试逻辑... }
- 优点:无需依赖Spring容器即可手动实例化对象,测试更灵活、可控。
- 对比字段注入:字段注入需要通过反射或复杂的配置来模拟依赖,测试代码更复杂。
**5. ** 避免与框架的强耦合
- POJO(普通Java对象)原则:构造器注入使类保持为简单的POJO,不依赖于Spring的注解或框架特性。例如:
// 即使不使用Spring,也可以通过构造器手动实例化对象 OrderService service = new OrderService(new PaymentService(), new InventoryService());
- 优点:类的设计更独立,可移植性更强,不绑定到特定框架。
- 对比字段注入:字段注入直接依赖于
@Autowired
注解,增加了与Spring框架的耦合。
**6. ** 防止代码异味(Code Smell)
- 减少过度依赖:构造函数参数过多可能提示类承担了过多职责(违反单一职责原则)。例如:
// 过多的参数可能提示需要重构 public OrderService(Dependency1 d1, Dependency2 d2, ..., DependencyN dN) { ... }
- 优点:通过构造函数参数的臃肿问题,促使开发者对类进行合理拆分,提升代码质量。
- 对比Setter注入:Setter注入可能使类的依赖项无限制增加,隐藏设计问题。
**7. ** Spring框架的优化支持
- 自动装配的灵活性:
- Spring 4.3+ 支持通过无注解构造函数进行自动装配,只要类只有一个构造函数:
public class OrderService { private final PaymentService paymentService; private final InventoryService inventoryService; // 无需 @Autowired 注解,Spring 自动识别 public OrderService(PaymentService paymentService, InventoryService inventoryService) { // ... } }
- 优点:减少注解的冗余,代码更简洁。
- Spring 4.3+ 支持通过无注解构造函数进行自动装配,只要类只有一个构造函数:
对比其他注入方式的缺点
字段注入(Field Injection)
- 问题:
- 隐藏依赖:依赖关系不显式,难以通过类构造函数快速了解依赖。
- 延迟初始化风险:字段可能未被及时注入,导致运行时
NullPointerException
。 - 破坏封装性:直接暴露字段,可能被外部修改。
- 测试困难:需通过反射或框架支持注入模拟对象。
Setter注入(Setter Injection)
- 适用场景:
- 非必需的依赖(可选配置)。
- 需要在对象创建后动态修改依赖的情况。
- 问题:
- 依赖可能未被及时注入,需添加
@Required
注解确保必填性。 - 依赖可变性可能引入不可预测的行为。
- 依赖可能未被及时注入,需添加
总结:Spring推荐构造器注入的核心原因
原因 | 具体表现 |
---|---|
显式性 | 依赖关系清晰可见,无需深入类内部即可了解依赖项。 |
强制完整性 | 确保依赖在对象创建时必须存在,避免 null 引发的异常。 |
不可变性 | 通过 final 修饰符保证依赖不可变,提升线程安全性和代码稳定性。 |
可测试性 | 方便单元测试中传入模拟对象,无需依赖框架。 |
减少框架耦合 | 类保持为POJO,可独立于Spring实例化。 |
代码质量保障 | 通过参数臃肿问题提示设计缺陷,避免类承担过多职责。 |
最佳实践建议
- 强制依赖:使用构造器注入(如服务、数据库连接等必需依赖)。
- 可选/可变依赖:使用Setter注入(如配置参数、非核心组件)。
- 避免字段注入:除非有特殊需求,否则不推荐直接通过字段注解注入。
通过构造器注入,可以编写出更健壮、可维护且符合设计原则的代码,这也是Spring官方推荐的核心原因。
多账号认证
场景:当我们一个系统有不同的权限体系。例如模块与模块之间的功能并不相同。这个时候怎么区分开不同的权限认证体系。