Spring基于AOP事务控制实现原理

简介: 对于一个系统应用而言,使用数据库进行数据存储是必然的,意味着开发过程中事务的使用及控制也是必不可少的,当然事务是数据库层面的知识点并不是`Spring`框架所提出的。使用JDBC开发时,我们使用`connnection`对事务进行控制,使用`MyBatis`时,我们使用`SqlSession`对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化,所以`Spring` 就在这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制和声明式事务控制

1.概述

对于一个系统应用而言,使用数据库进行数据存储是必然的,意味着开发过程中事务的使用及控制也是必不可少的,当然事务是数据库层面的知识点并不是Spring框架所提出的。使用JDBC开发时,我们使用connnection对事务进行控制,使用MyBatis时,我们使用SqlSession对事务进行控制,缺点显而易见,当我们切换数据库访问技术时,事务控制的方式总会变化,所以Spring 就在这些技术基础上,提供了统一的控制事务的接口。Spring的事务分为:编程式事务控制和声明式事务控制。

  • 编程式事务控制:Spring提供了事务控制的类和方法,使用编码的方式对业务代码进行事务控制,事务控制代码和业务操作代码耦合到了一起,开发中几乎不使用
  • 声明式事务控制:Spring将事务控制的代码封装,对外提供了Xml和注解配置方式,通过配置的方式完成事务的控制,可以达到事务控制与业务操作代码解耦合,开发中推荐使用

2.Spring事务管理和封装

2.1 原生事务控制

在没有框架对事务进行封装之前,我们都是使用底层的原生api来进行事务控制,如JDBC操作数据库控制事务

 // 加载数据库驱动 
 Class.forName("com.mysql.jdbc.Driver");
 // 获取mysql数据库连接
 Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8","root", "root");
 conn.setAutoCommit(false);
 // 获取statement
 statement = conn.createStatement();
 // 执行sql,返回结果集
 resultSet = statement.executeQuery("xxxx");
 // 提交
 conn.commit();
 // 回滚
 // conn.rollback();

这就是原生操作事务的流程,在我们使用Spring框架开发业务系统时也是离不了这样的事务操作的,如果每与数据库交互都需要按上面步骤进行操作,就会显得十分臃肿、重复编码,所以Spring对此进行了封装来提高编程的效率,让事务控制这一过程自动化、透明化,从而做到让开发者专注业务逻辑编码,无需关注事务控制,由Spring框架AOP切面完成即可。

2.2 Spring提供的事务API

Spring基于模版方法设计模式实现了事务控制的封装,核心API和模板类如下:

核心类 解释
平台事务管理器PlatformTransactionManager 是一个接口标准,实现类都具备事务提交、回滚和获得事务对象的功能,不同持久层框架可能会有不同实现方案
事务定义TransactionDefinition 封装事务的隔离级别、传播行为、过期时间等属性信息
事务状态TransactionStatus 存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等

事务管理器—PlatformTransactionManager

PlatformTransactionManager是事务管理器的顶层接口,只规定了事务的基本操作:创建事务,提交事物和回滚事务。

public interface PlatformTransactionManager extends TransactionManager {
   
   

 // 打开事务
 TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
   throws TransactionException;

 // 提交事务
 void commit(TransactionStatus status) throws TransactionException;

  // 回滚事务
 void rollback(TransactionStatus status) throws TransactionException;
}

Spring使用模板方法模式提供了一个抽象类AbstractPlatformTransactionManager,规定了事务管理器的基本框架,仅将依赖于具体平台的特性作为抽象方法留给子类实现,如mybatis框架的事务管理器是DatasourceTransactionManagerhibernate框架的事务管理器是HibernateTransactionManager,我曾经见过一个项目服务里的ORM框架同时使用了mybatis,hibernate两个框架,至于为啥?大概是想从hibernate转为mybatis吧....然后有这么一个问题,一个逻辑方法有两个数据库操作,一个是用mybatis实现的,一个是用hibernate实现,这个逻辑方法使用了@Transactional(rollbackFor = Exception.class),但是事务竟然没控制住~前面就是问题的原因所在,mybatishibernate的事务管理器都不是同一个,肯定控制不住事务的。

事务状态—TransactionStatus

存储当前事务的状态信息,如果事务是否提交、是否回滚、是否有回滚点等。

public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
   
   

 /**
  * 是否有Savepoint Savepoint是当事务回滚时需要恢复的状态
  */
 boolean hasSavepoint();

 /**
  * flush()操作和底层数据源有关,并非强制所有数据源都要支持
  */
 @Override
 void flush();

}

还从父接口TransactionExecution,SavepointManager中继承了其他方法

/**
  * 是否是新事务(或是其他事务的一部分)
  */
 boolean isNewTransaction();

 /**
  * 设置rollback-only 表示之后需要回滚
  */
 void setRollbackOnly();

 /**
  * 是否rollback-only
  */
 boolean isRollbackOnly();

 /**
  * 判断该事务已经完成
  */
 boolean isCompleted();


 /**
  * 创建一个Savepoint
  */
 Object createSavepoint() throws TransactionException;

 /**
  * 回滚到指定Savepoint
  */
 void rollbackToSavepoint(Object savepoint) throws TransactionException;

 /**
  * 释放Savepoint 当事务完成后,事务管理器基本上自动释放该事务所有的savepoint
  */
 void releaseSavepoint(Object savepoint) throws TransactionException;

事务属性的定义—TransactionDefinition

TransactionDefinition封装事务的隔离级别、传播行为、过期时间等属性信息

 /**
  * 返回事务的传播级别
  */
 default int getPropagationBehavior() {
   
   
  return PROPAGATION_REQUIRED;
 }

 /**
  * 返回事务的隔离级别
  */
 default int getIsolationLevel() {
   
   
  return ISOLATION_DEFAULT;
 }

 /**
  * 事务超时时间
  */
 default int getTimeout() {
   
   
  return TIMEOUT_DEFAULT;
 }

 /**
  * 是否为只读事务(只读事务在处理上能有一些优化)
  */
 default boolean isReadOnly() {
   
   
  return false;
 }

 /**
  * 返回事务的名称
  */
 @Nullable
 default String getName() {
   
   
  return null;
 }


 /**
  * 默认的事务配置
  */
 static TransactionDefinition withDefaults() {
   
   
  return StaticTransactionDefinition.INSTANCE;
 }

2.3 Spring编程式事务实现

基于上面底层的API,开发者可以在代码中手动的管理事务的开启、提交、回滚等操作来完成编程式事务控制。在spring项目中可以使用TransactionTemplateTransactionCallback进行实现手动控制事务。

   //设置事务的各种属性;可以猜测TransactionTemplate应该是实现了TransactionDefinition
        transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
        transactionTemplate.setTimeout(30000);

        //执行事务 将业务逻辑封装在TransactionCallback中
        transactionTemplate.execute(new TransactionCallback<Object>() {
   
   
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
   
   
                    //....   业务代码
            }
        });

但是我们在开发过程中一般不使用编程式事务控制,因为比较繁琐不够优雅,一般是使用声明式进行事务控制,所以接下来就重点讲讲声明式事务。

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

交流探讨qun:Shepherd_126

3.声明式事务

3.1 示例

@Transactional是Spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚,直接来看看我们添加用户和角色的示例代码:

  @Transactional(rollbackFor = Exception.class)
  public void addUser(UserParam param) {
   
   
      String username = param.getUsername();
      checkUsernameUnique(username);
      User user = PtcBeanUtils.copy(param, User.class);
      userDAO.insert(user);
      if (!CollectionUtils.isEmpty(param.getRoleIds())) {
   
   
          userRoleService.addUserRole(user.getId(), param.getRoleIds());
      }
  }

可以看出在Spring中进行事务管理非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。

3.2 实现原理

@EnableTransactionManagement说起,该注解开启注解声明式事务,所以我们就先来看看其定义:

 @Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {
   
   

 /**
  * 用来表示默认使用JDK Dynamic Proxy还是CGLIB Proxy
  */
 boolean proxyTargetClass() default false;

 /**
  * 表示以Proxy-based方式实现AOP还是以Weaving-based方式实现AOP
  */
 AdviceMode mode() default AdviceMode.PROXY;

 /**
  * 顺序
  */
 int order() default Ordered.LOWEST_PRECEDENCE;

}

可以看出,和注解@EnableAspectJAutoProxy开启aop代理差不多,核心逻辑:@Import(TransactionManagementConfigurationSelector.class)TransactionManangementConfigurationSelector主要是往Spring容器中注入相关bean,核心逻辑如下:

public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
   
   

    /**
     * Returns {@link ProxyTransactionManagementConfiguration} or
     * {@code AspectJ(Jta)TransactionManagementConfiguration} for {@code PROXY}
     * and {@code ASPECTJ} values of {@link EnableTransactionManagement#mode()},
     * respectively.
     */
    @Override
    protected String[] selectImports(AdviceMode adviceMode) {
   
   
        switch (adviceMode) {
   
   
            case PROXY:
                return new String[] {
   
   AutoProxyRegistrar.class.getName(),
                        ProxyTransactionManagementConfiguration.class.getName()};
            case ASPECTJ:
                return new String[] {
   
   determineTransactionAspectClass()};
            default:
                return null;
        }
    }
}

3.2.1 如何生成代理类

AutoProxyRegistrar通过调用AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);向容器中注册AbstractAdvisorAutoProxyCreator,这个类在之前总结的 Spring切面编程实现原理一文中重点解析过,是生成AOP代理类的核心实现所在。

UserService实现类中使用了@Transational来进行数据库事务控制,AuthService中不涉及到数据库事务处理,从图中可知UserService是被CGLIB动态代理生成的代理类,而AuthService是原生类,这就是AbstractAdvisorAutoProxyCreator实现的,

#getAdvicesAndAdvisorsForBean()会判断bean是否有advisor,有的话就通过动态代理生成代理对象注入到Spring容器中,这就是前面UserService代理对象的由来。

    protected Object[] getAdvicesAndAdvisorsForBean(
            Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
   
   

        List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
        if (advisors.isEmpty()) {
   
   
            return DO_NOT_PROXY;
        }
        return advisors.toArray();
    }

#findEligibleAdvisors()顾名思义就是找到符合条件的advisor

    protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
   
   
        List<Advisor> candidateAdvisors = findCandidateAdvisors();
        List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
        extendAdvisors(eligibleAdvisors);
        if (!eligibleAdvisors.isEmpty()) {
   
   
            eligibleAdvisors = sortAdvisors(eligibleAdvisors);
        }
        return eligibleAdvisors;
    }

#findCandidateAdvisors()查找所有候选的advisor

    @Override
    protected List<Advisor> findCandidateAdvisors() {
        // Add all the Spring advisors found according to superclass rules.
        List<Advisor> advisors = super.findCandidateAdvisors();
        // Build Advisors for all AspectJ aspects in the bean factory.
        if (this.aspectJAdvisorsBuilder != null) {
            advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
        }
        return advisors;
    }

super.findCandidateAdvisors()就是获取spring内部规则的advisor,比如说事务控制的advisor:BeanFactoryTransactionAttributeSourceAdvisor

this.aspectJAdvisorsBuilder.buildAspectJAdvisors()是解析使用了@Aspect的切面类,根据切点表达式生成advisor。

#findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName)调用AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);筛选出能匹配当前bean的advisor。

AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass)会调用#canApply()方法:

    public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
   
   
        if (advisor instanceof IntroductionAdvisor) {
   
   
            return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
        }
        else if (advisor instanceof PointcutAdvisor) {
   
   
            PointcutAdvisor pca = (PointcutAdvisor) advisor;
            return canApply(pca.getPointcut(), targetClass, hasIntroductions);
        }
        else {
   
   
            // It doesn't have a pointcut so we assume it applies.
            return true;
        }
    }

这里判断如果是PointcutAdvisor类型,就会调用canApply(pca.getPointcut(), targetClass, hasIntroductions);,上面提到的事务advisor:BeanFactoryTransactionAttributeSourceAdvisor正好符合。执行BeanFactoryTransactionAttributeSourceAdvisorTransactionAttributeSourcePointcut对象的matches()方法来进行是否匹配判断,然后根据当前bean的所有method遍历执行判断使用有@Transational注解,来到AbstractFallbackTransactionAttributeSourcecomputeTransactionAttribute()方法

    @Nullable
    protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
   
   
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
   
   
            return null;
        }

        // The method may be on an interface, but we need attributes from the target class.
        // If the target class is null, the method will be unchanged.
        Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

        // First try is the method in the target class.
        TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
        if (txAttr != null) {
   
   
            return txAttr;
        }

        // Second try is the transaction attribute on the target class.
        txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
        if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
   
   
            return txAttr;
        }

        if (specificMethod != method) {
   
   
            // Fallback is to look at the original method.
            txAttr = findTransactionAttribute(method);
            if (txAttr != null) {
   
   
                return txAttr;
            }
            // Last fallback is the class of the original method.
            txAttr = findTransactionAttribute(method.getDeclaringClass());
            if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
   
   
                return txAttr;
            }
        }

        return null;
    }

最终来到SpringTransactionAnnotationParser#parseTransactionAnnotation()

@Override
public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) {
    //这里就是分析Method是否被@Transactional注解标注,有的话,不用说BeanFactoryTransactionAttributeSourceAdvisor适配当前bean,进行代理,并且注入切点
    //BeanFactoryTransactionAttributeSourceAdvisor
   AnnotationAttributes attributes = AnnotatedElementUtils.getMergedAnnotationAttributes(ae, Transactional.class);
   if (attributes != null) {
      return parseTransactionAnnotation(attributes);
   }
   else {
      return null;
   }
}

上面就是判断是否需要根据@Transactional进行代理对象创建的判断过程。@Transactional的作用就是标识方法需要被代理,同时携带事务管理需要的属性信息。

3.2.2 如何进行事务控制

在之前Spring切面编程实现原理一文中我碍于文章篇幅只分析了JDK代理的实现方式,但在 SpringBoot 2.x AOP中会默认使用Cglib来实现,所以今天就来分析一下CGLIB这种方式。

Spring的CGLIB方式生存代理对象是靠ObjenesisCglibAopProxy完成的,ObjenesisCglibAopProxy继承自CglibAopProxy,调用方法#createProxyClassAndInstance()得到基于Cglib动态代理的对象。最终的代理对象的代理方法DynamicAdvisedInterceptor#intercept()方法

        @Override
        @Nullable
        public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
   
   
            Object oldProxy = null;
            boolean setProxyContext = false;
            Object target = null;
            TargetSource targetSource = this.advised.getTargetSource();
            try {
   
   
                if (this.advised.exposeProxy) {
   
   
                    // Make invocation available if necessary.
                    oldProxy = AopContext.setCurrentProxy(proxy);
                    setProxyContext = true;
                }
                // Get as late as possible to minimize the time we "own" the target, in case it comes from a pool...
                target = targetSource.getTarget();
                Class<?> targetClass = (target != null ? target.getClass() : null);
                List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
                Object retVal;
                // Check whether we only have one InvokerInterceptor: that is,
                // no real advice, but just reflective invocation of the target.
                if (chain.isEmpty() && CglibMethodInvocation.isMethodProxyCompatible(method)) {
   
   
                    // We can skip creating a MethodInvocation: just invoke the target directly.
                    // Note that the final invoker must be an InvokerInterceptor, so we know
                    // it does nothing but a reflective operation on the target, and no hot
                    // swapping or fancy proxying.
                    Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
                    try {
   
   
                        retVal = methodProxy.invoke(target, argsToUse);
                    }
                    catch (CodeGenerationException ex) {
   
   
                        CglibMethodInvocation.logFastClassGenerationFailure(method);
                        retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
                    }
                }
                else {
   
   
                    // We need to create a method invocation...
                    retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
                }
                retVal = processReturnType(proxy, target, method, retVal);
                return retVal;
            }
            finally {
   
   
                if (target != null && !targetSource.isStatic()) {
   
   
                    targetSource.releaseTarget(target);
                }
                if (setProxyContext) {
   
   
                    // Restore old proxy.
                    AopContext.setCurrentProxy(oldProxy);
                }
            }
        }

这和以JDK实现的动态代理JdkDynamicAopProxy实现了InvocationHandler执行invoke()来进行逻辑增强套路是一样的。

通过分析 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)返回的是TransactionInterceptor,然后来到new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed(),最终调用TransactionInterceptor#invoke()方法

    @Override
    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
   
   
        // Work out the target class: may be {@code null}.
        // The TransactionAttributeSource should be passed the target class
        // as well as the method, which may be from an interface.
        Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);

        // Adapt to TransactionAspectSupport's invokeWithinTransaction...
        return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed);
    }

#invokeWithinTransaction()就是通过切面实现事务控制的核心逻辑所在:

 @Nullable
 protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
   final InvocationCallback invocation) throws Throwable {
   
   


  TransactionAttributeSource tas = getTransactionAttributeSource();
  final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
  final TransactionManager tm = determineTransactionManager(txAttr);

  //省略部分代码

        //获取事物管理器
  PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
  final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

  if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
   
   
   // 打开事务(内部就是getTransactionStatus的过程)
   TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

   Object retVal;
   try {
   
   
    // 执行业务逻辑 invocation.proceedWithInvocation();
   }
   catch (Throwable ex) {
   
   
    // 异常回滚
    completeTransactionAfterThrowing(txInfo, ex);
    throw ex;
   }
   finally {
   
   
    cleanupTransactionInfo(txInfo);
   }

   //省略部分代码

            //提交事物
   commitTransactionAfterReturning(txInfo);
   return retVal;
  }

4.总结

行文至此,Spring基于AOP自动完成事务控制的逻辑分析就完结了。Spring的声明式事务注解开发非常简单,只需要在方法上加上注解@Transactional,Spring就可以自动帮我们进行事务的开启、提交、回滚操作。但是在日常开发中却经常出现事务失效的情况,所以了解Spring事务控制实现还是很有必要的,同时还可以加深对Spring AOP应用的认识。

目录
相关文章
|
5天前
|
运维 Java 程序员
Spring5深入浅出篇:基于注解实现的AOP
# Spring5 AOP 深入理解:注解实现 本文介绍了基于注解的AOP编程步骤,包括原始对象、额外功能、切点和组装切面。步骤1-3旨在构建切面,与传统AOP相似。示例代码展示了如何使用`@Around`定义切面和执行逻辑。配置中,通过`@Aspect`和`@Around`注解定义切点,并在Spring配置中启用AOP自动代理。 进一步讨论了切点复用,避免重复代码以提高代码维护性。通过`@Pointcut`定义通用切点表达式,然后在多个通知中引用。此外,解释了AOP底层实现的两种动态代理方式:JDK动态代理和Cglib字节码增强,默认使用JDK,可通过配置切换到Cglib
|
1天前
|
监控 安全 Java
Spring cloud原理详解
Spring cloud原理详解
10 0
|
1天前
|
Java 数据库连接 数据库
AOP&事务
AOP&事务
5 0
|
3天前
|
SQL Java 关系型数据库
Spring 事务
Spring 事务
7 1
|
5天前
|
XML Java 数据格式
Spring使用AOP 的其他方式
Spring使用AOP 的其他方式
15 2
|
5天前
|
XML Java 数据格式
Spring 项目如何使用AOP
Spring 项目如何使用AOP
19 2
|
5天前
|
Java 数据库连接 数据库
Spring事务简介,事务角色,事务属性
Spring事务简介,事务角色,事务属性
16 2
|
7天前
|
Java 开发者 微服务
Spring Cloud原理详解
【5月更文挑战第4天】Spring Cloud是Spring生态系统中的微服务框架,包含配置管理、服务发现、断路器、API网关等工具,简化分布式系统开发。核心组件如Eureka(服务发现)、Config Server(配置中心)、Ribbon(负载均衡)、Hystrix(断路器)、Zuul(API网关)等。本文讨论了Spring Cloud的基本概念、核心组件、常见问题及解决策略,并提供代码示例,帮助开发者更好地理解和实践微服务架构。此外,还涵盖了服务通信方式、安全性、性能优化、自动化部署、服务网格和无服务器架构的融合等话题,揭示了微服务架构的未来趋势。
32 6
|
10天前
|
Java 数据库连接 数据库
16:事务-Java Spring
16:事务-Java Spring
26 5
|
11天前
|
Java 开发者 Spring
Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
【5月更文挑战第1天】Spring AOP的切点是通过使用AspectJ的切点表达式语言来定义的。
23 5