Spring核心深度解析:AOP与事务管理(TX)全指南

在Spring框架生态中,AOP(面向切面编程)与TX(事务管理)是支撑企业级应用稳定运行的关键技术。AOP以"解耦横切逻辑"为核心价值,为日志、权限等通用功能提供统一实现方案;而TX则基于AOP机制,保障数据操作的一致性,是业务系统可靠性的基石。两者相辅相成,共同构建了Spring应用的高效开发与稳定运行体系。

一、Spring AOP:面向切面的解耦艺术

传统OOP(面向对象编程)通过类与继承构建业务逻辑体系,但对于日志记录、权限校验、异常处理等"横切逻辑",其分散在各个业务类中的特性会导致代码冗余、耦合度高------修改日志格式需改动所有业务类,权限规则调整则涉及多处接口。Spring AOP通过"将横切逻辑抽取为独立切面,动态织入业务流程"的方式,彻底解决了这一问题,实现了业务逻辑与通用逻辑的解耦。

1.1 AOP核心概念:构建切面的核心要素

理解AOP需先明确其核心术语,这些术语共同定义了切面与业务流程的关联方式,是后续实践的基础:

  • 切面(Aspect):封装横切逻辑的组件,是AOP的核心载体。例如"日志切面""权限切面",一个切面可包含多个通知和切入点,负责定义"要做什么"以及"在何处做"。

  • 连接点(JoinPoint):业务流程中可被切面织入的"时机点",如方法执行前、执行后、异常抛出时、方法返回时等。Spring AOP仅支持方法级别的连接点,这是其与AspectJ(更强大的AOP框架)的主要区别之一。

  • 切入点(Pointcut):从所有连接点中筛选出"需要织入切面"的具体位置,通过表达式定义。例如"所有Service层以query开头的方法",切入点决定了切面逻辑的作用范围。

  • 通知(Advice):切面的具体执行逻辑,同时定义了"在切入点的哪个时机执行"。Spring AOP支持5种通知类型,覆盖方法执行的全流程。

  • 织入(Weaving):将切面逻辑融入业务流程的过程,分为编译期织入(AspectJ)、类加载期织入(AspectJ)和运行期织入(Spring AOP)。Spring AOP采用运行期织入,通过动态代理技术实现,无需修改字节码文件。

  • 目标对象(Target):被切面织入的业务对象,即包含核心业务逻辑的类实例。

  • 代理对象(Proxy):Spring AOP通过动态代理为目标对象创建的代理实例,切面逻辑最终通过代理对象执行------客户端调用的是代理对象,代理对象先执行切面逻辑,再调用目标对象的业务方法。

1.2 Spring AOP的5种通知类型:覆盖方法执行全流程

通知是切面的执行逻辑,不同类型的通知对应方法执行的不同时机,开发者可根据需求选择合适的通知类型:

  1. 前置通知(@Before):在目标方法执行前执行,可用于参数校验、权限判断等。例如"在用户查询接口执行前,校验用户是否登录"。

  2. 后置通知(@After):在目标方法执行后执行(无论是否抛出异常),可用于资源清理。例如"接口执行完毕后,关闭数据库连接"。

  3. 返回通知(@AfterReturning):在目标方法正常返回后执行,可获取方法返回值,用于日志记录、结果处理等。例如"记录接口的返回数据"。

  4. 异常通知(@AfterThrowing):在目标方法抛出异常时执行,可获取异常信息,用于异常告警、错误日志记录。例如"接口抛出异常时,发送告警邮件"。

  5. 环绕通知(@Around):包裹目标方法,可在方法执行前后自定义逻辑,甚至控制方法是否执行、修改返回值。是功能最强大的通知类型,例如"统计接口执行耗时"。

1.3 实现原理:动态代理的两种方式

Spring AOP基于动态代理实现运行期织入,根据目标对象是否实现接口,采用两种不同的代理方式,确保代理逻辑的无缝执行:

  • JDK动态代理 :若目标对象实现了至少一个接口,Spring AOP默认使用JDK动态代理。其核心是java.lang.reflect.Proxy类和InvocationHandler接口------通过Proxy类创建代理实例,InvocationHandler接口的invoke()方法中封装了代理逻辑(切面逻辑+目标方法调用)。

  • CGLIB动态代理 :若目标对象未实现接口,Spring AOP会使用CGLIB(Code Generation Library)动态代理。其原理是通过字节码技术生成目标对象的子类,子类中重写目标方法,将切面逻辑融入其中。需要注意的是,若目标方法被final修饰,CGLIB无法重写该方法,切面逻辑将无法织入。

Spring Boot 2.x后,默认使用CGLIB代理(即使目标对象实现接口),可通过配置spring.aop.proxy-target-class=false切换为JDK动态代理。

1.4 实践:基于注解的Spring AOP实现

Spring AOP支持XML配置和注解配置两种方式,注解配置因简洁高效成为主流。以下以"日志切面"为例,完整演示AOP的实现流程:

1.4.1 步骤1:引入依赖(Spring Boot项目)

Spring Boot已整合AOP,引入spring-boot-starter-aop即可:

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

1.4.2 步骤2:定义业务接口与实现(目标对象)

java 复制代码
// 业务接口
public interface UserService {
    // 查询用户
    String queryUser(Long id);
    // 新增用户
    void addUser(String username);
}

// 业务实现(目标对象)
@Service
public class UserServiceImpl implements UserService {
    @Override
    public String queryUser(Long id) {
        if (id == null || id <= 0) {
            throw new IllegalArgumentException("用户ID非法");
        }
        return "用户名:张三,ID:" + id;
    }

    @Override
    public void addUser(String username) {
        if (username == null || username.isEmpty()) {
            throw new NullPointerException("用户名为空");
        }
        System.out.println("新增用户:" + username);
    }
}

1.4.3 步骤3:定义切面类(核心)

通过@Aspect注解声明切面类,@Component确保切面被Spring容器管理;通过切入点表达式定义作用范围,通过通知注解定义执行逻辑:

java 复制代码
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

// 声明为切面类 + 交给Spring管理
@Aspect
@Component
public class LogAspect {
    // 切入点表达式:匹配com.example.service包下所有类的所有方法
    @Pointcut("execution(* com.example.service..*(..))")
    public void servicePointcut() {}

    // 前置通知:目标方法执行前执行,获取方法参数
    @Before("servicePointcut()")
    public void beforeAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName(); // 获取方法名
        Object[] args = joinPoint.getArgs(); // 获取方法参数
        System.out.println("【前置通知】方法名:" + methodName + ",参数:" + (args.length > 0 ? args[0] : "无"));
    }

    // 环绕通知:统计方法执行耗时
    @Around("servicePointcut()")
    public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        // 执行目标方法(必须调用,否则目标方法不会执行)
        Object result = proceedingJoinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("【环绕通知】方法执行耗时:" + (end - start) + "ms");
        return result; // 返回目标方法的返回值
    }

    // 返回通知:获取方法返回值
    @AfterReturning(pointcut = "servicePointcut()", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【返回通知】方法名:" + methodName + ",返回值:" + result);
    }

    // 异常通知:获取方法抛出的异常
    @AfterThrowing(pointcut = "servicePointcut()", throwing = "ex")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【异常通知】方法名:" + methodName + ",异常信息:" + ex.getMessage());
    }

    // 后置通知:方法执行后执行(无论是否异常)
    @After("servicePointcut()")
    public void afterAdvice(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("【后置通知】方法名:" + methodName + ",执行完毕");
    }
}

1.4.4 步骤4:测试AOP效果

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class AopDemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(AopDemoApplication.class, args);
        UserService userService = context.getBean(UserService.class);
        
        // 测试正常执行的方法
        System.out.println("=== 测试queryUser方法 ===");
        userService.queryUser(1L);
        
        // 测试抛出异常的方法
        System.out.println("\n=== 测试addUser方法(空用户名) ===");
        try {
            userService.addUser(null);
        } catch (Exception e) {
            // 捕获异常,避免程序终止
        }
        context.close();
    }
}

1.4.5 输出结果(切面逻辑生效)

text 复制代码
=== 测试queryUser方法 ===
【前置通知】方法名:queryUser,参数:1
【环绕通知】方法执行耗时:1ms
【返回通知】方法名:queryUser,返回值:用户名:张三,ID:1
【后置通知】方法名:queryUser,执行完毕

=== 测试addUser方法(空用户名) ===
【前置通知】方法名:addUser,参数:null
【异常通知】方法名:addUser,异常信息:用户名为空
【后置通知】方法名:addUser,执行完毕

1.5 切入点表达式:精准定位织入位置

切入点表达式是AOP的核心,用于定义"哪些方法需要被切面织入"。Spring AOP支持多种切入点表达式,其中execution表达式最常用,格式如下:

text 复制代码
execution(访问修饰符? 返回值类型 包名.类名.方法名(参数类型) 异常类型?)

各部分说明:

  • 访问修饰符:可选,如public、private,省略则匹配所有修饰符。

  • 返回值类型:必填,如String、void,*表示匹配所有返回值类型。

  • 包名.类名:必填,com.example.service..表示com.example.service包及其子包;*表示匹配该包下所有类。

  • 方法名:必填,query*表示匹配以query开头的方法;*表示匹配所有方法。

  • 参数类型:必填,(..)表示匹配任意参数;(Long)表示匹配单个Long类型参数。

  • 异常类型:可选,指定方法抛出的异常类型,省略则匹配所有异常。

常用表达式示例:

  • 匹配com.example.service包下所有类的所有方法:execution(* com.example.service.*.*(..))

  • 匹配com.example.service包及其子包下所有类的public方法:execution(public * com.example.service..*(..))

  • 匹配UserService接口中所有返回String类型的方法:execution(String com.example.service.UserService.*(..))

  • 匹配所有以add开头且参数为String的方法:execution(* *.add*(String))

二、Spring TX:基于AOP的事务管理机制

在企业级应用中,数据一致性是核心需求------例如"转账业务"中,"扣减转出方余额"与"增加转入方余额"两个操作必须同时成功或同时失败,若其中一个操作成功而另一个失败,将导致数据错误。事务管理(TX)就是保障这种"原子性操作"的核心机制,而Spring TX则通过AOP技术,将事务控制逻辑封装为切面,实现了事务管理与业务逻辑的解耦。

2.1 事务核心特性:ACID原则

事务是数据库操作的基本单元,必须遵循ACID原则,这是保障数据一致性的基础:

  • 原子性(Atomicity):事务中的所有操作要么全部成功,要么全部失败,不存在"部分成功"的情况。例如转账业务中,若扣减余额成功但增加余额失败,事务会回滚到初始状态。

  • 一致性(Consistency):事务执行前后,数据的完整性约束保持一致。例如转账前双方余额总和为1000元,转账后总和仍为1000元。

  • 隔离性(Isolation):多个事务并发执行时,一个事务的操作不会被其他事务干扰,每个事务都感觉不到其他事务的存在。

  • 持久性(Durability):事务一旦提交,其对数据的修改将永久保存到数据库中,即使数据库宕机也不会丢失。

2.2 Spring TX的核心机制:事务管理器与AOP织入

Spring TX本身不直接管理事务,而是通过"事务管理器"适配不同的持久层框架(如JDBC、MyBatis、Hibernate),再通过AOP将事务控制逻辑织入业务流程。其核心工作流程如下:

  1. 事务管理器适配 :Spring提供PlatformTransactionManager接口作为事务管理器的顶层接口,不同持久层框架有对应的实现类,例如:

    • JDBC/MyBatis:DataSourceTransactionManager

    • Hibernate:HibernateTransactionManager

    • JPA:JpaTransactionManager

  2. 事务配置解析 :Spring通过注解(如@Transactional)或XML配置,解析事务的传播行为、隔离级别、超时时间等属性。

  3. AOP切面织入:Spring将事务控制逻辑封装为切面,在目标方法执行前开启事务,执行后根据是否抛出异常决定提交或回滚事务------若方法正常执行,提交事务;若抛出异常,回滚事务。

2.3 Spring TX的两种实现方式

Spring TX支持编程式事务和声明式事务两种方式,声明式事务基于AOP实现,无需侵入业务代码,是实际开发中的首选。

2.3.1 编程式事务:手动控制事务(灵活性高,侵入性强)

编程式事务通过TransactionTemplatePlatformTransactionManager手动控制事务的开启、提交与回滚,适用于事务逻辑复杂的场景。

java 复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class TransferService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private TransactionTemplate transactionTemplate;

    // 基于TransactionTemplate的编程式事务
    public void transfer(Long fromId, Long toId, Double amount) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    // 扣减转出方余额
                    jdbcTemplate.update("UPDATE user SET balance = balance - ? WHERE id = ?", amount, fromId);
                    // 模拟异常(测试事务回滚)
                    // int i = 1 / 0;
                    // 增加转入方余额
                    jdbcTemplate.update("UPDATE user SET balance = balance + ? WHERE id = ?", amount, toId);
                } catch (Exception e) {
                    // 手动回滚事务
                    status.setRollbackOnly();
                    throw e;
                }
            }
        });
    }
}

2.3.2 声明式事务:基于注解的AOP事务(推荐)

声明式事务通过@Transactional注解实现,Spring自动通过AOP织入事务控制逻辑,无需修改业务代码,侵入性极低。

步骤1:引入依赖(Spring Boot项目)

整合MyBatis与Spring TX,引入以下依赖:

xml 复制代码
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Boot MyBatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>3.0.3</version>
    </dependency>
    <!-- MySQL驱动 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- Spring Boot 事务(已包含在web依赖中,可省略) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
</dependencies>
步骤2:配置数据库连接(application.yml)
yaml 复制代码
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/spring_demo?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
  mapper-locations: classpath:mapper/*.xml #  mapper文件路径
  type-aliases-package: com.example.entity # 实体类包路径
步骤3:开启事务支持(主启动类)

通过@EnableTransactionManagement注解开启Spring TX支持(Spring Boot 2.x后可省略,默认自动开启):

java 复制代码
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描Mapper接口
@EnableTransactionManagement // 开启事务支持(可选)
public class TxDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(TxDemoApplication.class, args);
    }
}
步骤4:在业务方法上添加@Transactional注解
java 复制代码
import com.example.entity.User;
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;

    // 声明式事务:该方法受事务管理
    @Transactional(rollbackFor = Exception.class) // 所有异常都回滚
    public void transfer(Long fromId, Long toId, Double amount) {
        // 1. 查询转出方余额
        User fromUser = userMapper.selectById(fromId);
        if (fromUser.getBalance() < amount) {
            throw new IllegalArgumentException("余额不足");
        }
        // 2. 扣减转出方余额
        userMapper.updateBalance(fromId, -amount);
        // 3. 模拟异常(测试事务回滚)
        // int i = 1 / 0;
        // 4. 增加转入方余额
        userMapper.updateBalance(toId, amount);
    }
}

2.4 @Transactional注解核心属性:定制事务行为

@Transactional注解提供了多个属性,用于定制事务的传播行为、隔离级别、超时时间等,满足不同业务场景的需求:

2.4.1 传播行为(propagation):控制事务的嵌套规则

当一个事务方法调用另一个事务方法时,传播行为决定了子方法的事务归属,是Spring TX中最常用的属性。Spring支持7种传播行为,核心常用的有4种:

  • REQUIRED(默认):若当前存在事务,则加入该事务;若当前无事务,则创建新事务。例如"转账方法调用扣减余额方法",两者共用一个事务,任一操作失败则整体回滚。

  • REQUIRES_NEW:无论当前是否存在事务,都创建新事务,且新事务与原事务相互独立。例如"订单创建方法调用日志记录方法",日志记录失败不影响订单创建,订单创建失败也不影响日志记录。

  • SUPPORTS:若当前存在事务,则加入该事务;若当前无事务,则以非事务方式执行。适用于"可选事务"的场景,如查询方法。

  • NESTED:若当前存在事务,则在嵌套事务中执行;若当前无事务,则创建新事务。嵌套事务依赖于原事务,原事务回滚时嵌套事务也回滚,但嵌套事务回滚不影响原事务。

2.4.2 隔离级别(isolation):解决并发事务问题

多个事务并发执行时,可能出现脏读、不可重复读、幻读等问题,隔离级别通过控制事务间的可见性,解决这些问题。Spring支持5种隔离级别,对应数据库的隔离级别:

  • DEFAULT(默认):使用数据库的默认隔离级别(MySQL默认REPEATABLE READ,Oracle默认READ COMMITTED)。

  • READ_UNCOMMITTED:最低隔离级别,允许读取未提交的事务数据,可能出现脏读、不可重复读、幻读。

  • READ_COMMITTED:允许读取已提交的事务数据,避免脏读,但可能出现不可重复读、幻读(Oracle默认)。

  • REPEATABLE_READ:保证同一事务中多次读取同一数据的结果一致,避免脏读、不可重复读,但可能出现幻读(MySQL默认)。

  • SERIALIZABLE:最高隔离级别,事务串行执行,避免所有并发问题,但性能极低,仅适用于数据一致性要求极高的场景。

并发问题说明:

  1. 脏读:读取到其他事务未提交的数据,若该事务回滚,读取的数据为"无效数据";
  2. 不可重复读:同一事务中多次读取同一数据,结果不一致(因其他事务修改并提交了该数据);
  3. 幻读:同一事务中多次执行同一查询,结果集行数不一致(因其他事务新增或删除了数据)。

2.4.3 其他核心属性

  • rollbackFor :指定需要回滚事务的异常类型,默认仅对RuntimeException和Error回滚。例如rollbackFor = Exception.class表示所有异常都回滚。

  • noRollbackFor:指定不需要回滚事务的异常类型,与rollbackFor相反。

  • timeout :事务超时时间(单位:秒),若事务执行超过该时间则自动回滚,默认-1(无超时限制)。例如timeout = 30表示30秒超时。

  • readOnly:指定事务是否为只读事务,若为true,数据库可优化事务性能(如禁用写操作),适用于查询方法。默认false。

2.5 事务不生效的常见原因(避坑指南)

声明式事务看似简单,但实际开发中容易因配置或代码问题导致事务不生效,以下是典型场景及解决方案:

  • 原因1:方法不是public修饰 :Spring TX默认仅对public方法生效,非public方法(如private、protected)的@Transactional注解无效。

    解决方案:将方法改为public,或通过XML配置修改事务切面的切入点表达式。

  • 原因2:异常类型不匹配 :默认仅对RuntimeException和Error回滚,若抛出Checked异常(如IOException),事务不会回滚。

    解决方案:通过rollbackFor = Exception.class指定所有异常都回滚。

  • 原因3:手动捕获异常未抛出 :若在方法内部捕获了异常且未重新抛出,Spring无法感知异常,事务不会回滚。

    解决方案:捕获异常后手动抛出,或通过TransactionStatus.setRollbackOnly()手动触发回滚。

  • 原因4:目标对象未被Spring管理 :若业务类未添加@Service、@Component等注解,未被Spring容器管理,事务切面无法织入。

    解决方案:为业务类添加Spring组件注解,确保被容器扫描。

  • 原因5:自调用问题 :同一类中,无事务的方法调用有事务的方法,因未通过代理对象调用,事务切面无法织入。

    解决方案:1. 注入自身代理对象(通过@Autowired注入当前类);2. 将方法拆分到不同类中。

  • 原因6:事务管理器配置错误 :未配置对应的事务管理器,或事务管理器与持久层框架不匹配。

    解决方案:确保配置了正确的事务管理器(如MyBatis对应DataSourceTransactionManager)。

三、AOP与TX的关联:事务管理的底层实现

Spring TX的底层核心是AOP------事务管理本身就是一种特殊的"横切逻辑",Spring将事务的开启、提交、回滚封装为一个切面,通过以下流程织入业务方法:

  1. 当业务方法添加@Transactional注解后,Spring通过AOP的切入点表达式匹配该方法。

  2. Spring为目标业务类创建动态代理对象,客户端调用业务方法时,实际调用的是代理对象。

  3. 代理对象先执行事务切面的逻辑:开启事务(通过事务管理器获取数据库连接,设置事务自动提交为false)。

  4. 代理对象调用目标对象的业务方法,执行核心业务逻辑。

  5. 若业务方法正常执行,事务切面执行提交事务逻辑;若抛出异常,执行回滚事务逻辑。

可以说,没有AOP就没有Spring TX的"声明式事务"------AOP为TX提供了"无侵入式"的织入能力,而TX则是AOP在数据一致性场景下的典型应用。

四、总结:AOP与TX的核心价值

Spring AOP与TX是企业级应用开发的"基石技术",两者分别解决了不同的核心问题,但又紧密关联:

  • AOP的核心价值:解耦横切逻辑与业务逻辑,减少代码冗余,提升代码可维护性。无论是日志、权限还是事务,都可通过AOP实现统一管理,修改横切逻辑时无需改动业务代码。

  • TX的核心价值:基于AOP实现声明式事务,保障数据一致性,开发者无需关注事务的底层细节,只需通过注解即可完成事务配置,大幅提升开发效率。

在实际开发中,掌握AOP的核心概念与切入点表达式,理解TX的事务属性与常见问题,是构建高效、稳定Spring应用的关键。同时,要深刻理解两者的关联------TX是AOP的典型应用,AOP是TX的底层支撑,只有将两者结合使用,才能充分发挥Spring框架的优势。

相关推荐
一水鉴天5 小时前
整体设计 定稿 之6 完整设计文档讨论及定稿 之1(豆包周助手)
java·前端·数据库
五阿哥永琪5 小时前
Spring Boot 权限控制三件套:JWT 登录校验 + 拦截器 + AOP 角色注解实战
java·spring boot·python
光算科技5 小时前
商品颜色/尺码选项太多|谷歌爬虫不收录怎么办
java·javascript·爬虫
派大鑫wink5 小时前
分享一些在Spring Boot中进行参数配置的最佳实践
java·spring boot·后端
想学习java初学者5 小时前
SpringBoot整合MQTT多租户(优化版)
java·spring boot·后端
代码栈上的思考5 小时前
MyBatis XML的方式来实现
xml·java·mybatis
阿拉斯攀登5 小时前
Spring Boot 深度解析:核心原理与自动配置全解
java·spring boot
AM越.5 小时前
Java设计模式超详解--观察者设计模式
java·开发语言·设计模式
专注VB编程开发20年5 小时前
c#语法和java相差多少
java·开发语言·microsoft·c#