@Transactional作用(成像原理)

事务主要保证了数据操作的原子性,一致性,隔离性和持久性。事务不会跨线程传播,事务不能跨数据源。

大家好,又见面了,我是你们的朋友全栈君。

    事务主要保证了数据操作的原子性,一致性,隔离性和持久性。
事务不会跨线程传播,事务不能跨数据源。

1、@Transactional的使用

  1. 导入相关依赖,数据源、数据库驱动、Spring-jdbc模块;
  2. 事务操作加@Transactional;
  3. 打开事务管理功能,对datasource进行控制;
  4. 注册事务管理器;
  5. 配置数据源、JdbcTemplate(Spring提供的简化数据库操作的工具)操作数据;

1.1、实践

使用注解@Transactional

    //第一步:导入相关数据库依赖;
    //第二步:加事务注解;
    @Transactional(rollbackFor = {Exception.class})
    public void updateAccount(int id) {
        int rows = accounMapper.deduction(id);
        if (rows > 0) {
            System.out.println("秒杀库存修改成功");
            insertGoodOrder();
        } else {
            System.out.println("秒杀修改失败");
        }
    }

开启事务管理器并注册事务管理器

其实我们之前在xml配置里, 会配置开启基于注解的事务管理功能,和AOP一样@EnableAspectJAutoProxy,@EnableTransactionalManagement开启基于注解的事务管理功能;


@Configuration
@ComponentScan("com.king.db")
@EnableTransactionManagement //第三步:开启事务管理功能,让@Transactional生效
public class DataSourceConfig {


    //创建数据源 这个c3p0封装了JDBC, dataSource 接口的实现
    @Bean
    public DataSource dataSource() throws PropertyVetoException {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser("root");
        dataSource.setPassword("kongyin");
        dataSource.setDriverClass("com.mysql.jdbc.Driver");
        dataSource.setJdbcUrl("jdbc:mysql//localhost:3306/order");
        return dataSource;
    }

    @Bean //第四步:注册事务管理器bean
    public PlatformTransactionManager platformTransactionManager() throws PropertyVetoException {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean //第五步:jdbcTemplate能简化增查改删的操作
    public JdbcTemplate jdbcTemplate() throws PropertyVetoException {
        return new JdbcTemplate(dataSource());
    }
}

或者在spring的xml中配置:

<!--事务管理器配置,market中对双数据源的配置-->
<tx:annotation-driven transaction-manager="transactionManager"/>

//注册事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
    <qualifier value="settleTransactionManager"/>
</bean>

可以看到@Transactional的使用非常简单,那么它是如何实现的呢?下面来看一下它的实现原理。

2、@Transactional原理分析

  • 思考一:
    为什么在方法上加了@Transactional就有了事务能力?
  • 思考二:
    为什么有时候加了@Transactional却不起作用?
先通过一段伪代码来解释一下注解事务的原理,例如一个方法加入@Transactional注解后,一个方法执行的伪代码执行如下,最终还是对数据库连接的控制使用。

@Transactional
public void doInvokeWithTransactional(){
    doBusiness()
}

如果在上述方法上添加一个@transactional后的等价操作:
public void doInvokeWithTransactional(){
    transactionalManager.beginTranscation; //事务管理器-开启事务-拿到一个事务
    autoCommit=false;     //关闭事务自动提交,需要手动提交
    public void invoke(){
        try{
            doBusiness(); //执行业务逻辑处理
        }cache(Exception e){
            Rollback();   //回滚
        }
        commit();         //提交
    }
}

@Transactional的实现我把它大致分为两个阶段:

  • 第一阶段:目标方法增强的初始化阶段;
  • 第二阶段:目标方法的执行阶段;

2.1、初始化阶段

当加了@Transactional注解后,需要做一系列的初始化工作,例如要使某个方法操作具有事务能力就的对该方法在容器启动的时候对其增强处理,赋予它事务的能力。为了了解其原理,我们从源码的开启事务管理功能入口开始:
@
EnableTransactionManagement

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class) //利用Import给容器添加一个Selector组件;
public @interface EnableTransactionManagement {

    //使用JDK或者是Cglib动态代理
   boolean proxyTargetClass() default false;

    //默认事务增强器是什么模式:代理
   AdviceMode mode() default AdviceMode.PROXY;

   //最低的优先级
   int order() default Ordered.LOWEST_PRECEDENCE;

}

2.1.1、@Import和[@ImportSelector + @ImportBeanDefinitionRegistor ]

可以看到EnableTransactionManagement利用@Import给容器添加了一个组件
TransactionManagementConfigurationSelector
注意:spring的源码中经常会出现
@Import和[ @ImportSelector + @ImportBeanDefinitionRegistor ] 的组合来为容器动态批量添加组件的套路实现。
  • ImportSelector是一个接口,只需要实现selectImport()方法,返回的是一个数组,即可给容器批量的注册Bean实例;
  • ImportBeanDefinitionRegistor也是一个接口,只需要实现registorBeanDefinition()方法就可以实现给容器添加bean实例;
public class TransactionManagementConfigurationSelector extends AdviceModeImportSelector<EnableTransactionManagement> {
   @Override
   protected String[] selectImports(AdviceMode adviceMode) {
     switch (adviceMode) {
       case PROXY:
         //利用ImportSelector开始往容器中注册2个组件;
         return new String[] {
                            AutoProxyRegistrar.class.getName(),                       //注册的第一个组件;
                            ProxyTransactionManagementConfiguration.class.getName()}; //注册的第二个组件
       case ASPECTJ:
            return new String[] {TransactionManagementConfigUtils.TRANSACTION_ASPECT_CONFIGURATION_CLASS_NAME};
       default:
            return null;
      }
   }
}

//AdviceModeImportSelector继承了ImportSelector,说明具有了向容器注册组件的能力;
public abstract class AdviceModeImportSelector<A extends Annotation> implements ImportSelector {

   /**
    * The default advice mode attribute name.
    */
   public static final String DEFAULT_ADVICE_MODE_ATTRIBUTE_NAME = "mode";

   protected String[] selectImports(AdviceMode adviceMode) {}
}

从上面可以看出,主要是利用@Import(TransactionManagementConfigurationSelector.class) 给容器导入两个重要的组件:

*
AutoProxyRegistrar – 创建事务动态代理创建器,给容器创建一个动态代理创建器:InfrastructureAdvisorAutoProxyCreator;
*
ProxyTransactionManagementConfiguration– 获取事务管理器生成事务拦截器,解析保存事务注解(传播属性-回滚方式)等等;
下面主要看一看这两个组件做了些什么?

2.1.2、InfrastructureAdvisorAutoProxyCreator组件

第一个组件:
InfrastructureAdvisorAutoProxyCreator
  事务动态代理创建器
AutoProxyRegistar利用ImporyBeanDefinitionRegistrar给容器中注册一个 InfrastructureAdvisorAutoProxyCreator 组件。

//通过ImportBeanDefinitionRegistrar给容器中添加组件:InfrastructureAdvisorAutoProxyCreator
public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

   private final Log logger = LogFactory.getLog(getClass());

   @Override
   public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
      boolean candidateFound = false;
      Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
      for (String annoType : annoTypes) {

         if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
               Boolean.class == proxyTargetClass.getClass()) {
            candidateFound = true;
            if (mode == AdviceMode.PROXY) {//看它给容器中注册了什么组件
               AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
               if ((Boolean) proxyTargetClass) {
                  AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                  return;
               }
            }
}

@Nullable //给容器中添加InfrastructureAdvisorAutoProxyCreator组件
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry,Object source) {
        //这里开始给容器注册:InfrastructureAdvisorAutoProxyCreator 事务动态代理创建器组件
   return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

那么InfrastructureAdvisorAutoProxyCreator主要做了什么呢?

        通过源码的不断跟踪,你会发现
InfrastructureAdvisorAutoProxyCreator其实就是实现了spring的后置处理器postProcessor接口,用来
创建增强的实例bean,也就是让doInvokeWithTransaction()方法具备事务的能力。
利用后置处理器机制在对象创建以后,包装对象Bean,返回
一个增强的代理对象,之后通过拦截链来完成调用。
下面主要是显示
InfrastructureAdvisorAutoProxyCreator这个类的继承关系,最终实现了BeanPostProcessor接口,就是一个后置处理器,用来对我们的业务bean实现增强。

public class InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator {
    public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator {
        public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
                      implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {}

public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationAwareBeanPostProcessor {
    public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {
        @Nullable  //Bean实例前置增强
        default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
           return bean;
        }

        @Nullable //Bean实例后置增强
        default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
           return bean;
        }
}

2.1.3、ProxyTransactionManagementConfiguration组件

第二个组件:ProxyTransactionManagementConfiguration  事务管理器的配置
ProxyTransactionManagementConfiguration 做了什么?
(1)给容器中注册生成的事务增强器Bean;
    * AnnotationTransactionAttributeSource 作用:解析事务注解元信息,传播属性,超时时间,隔离级别
(2)生成事务拦截器:
    * TransactionInterceptor,保存了事务属性信息,事务管理器;它是一个MethodInterceptor;

@Configuration
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {

   //开始事务的元数据属性解析
   @Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)
   @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
   //对属性元信息的一些增强,比如在注解中设置的一些参数:传播属性propagation,回滚的条件rollbackFor等等
   //@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
   public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {
        //对我们的事务进行属性增强;
      BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
      advisor.setTransactionAttributeSource(transactionAttributeSource());
      advisor.setAdvice(transactionInterceptor());
      if (this.enableTx != null) {
         advisor.setOrder(this.enableTx.<Integer>getNumber("order"));
      }
      return advisor;
   }

   @Bean//主要用于保存事务属性的信息,封装成一个TransactionInterceptor
   @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
   public TransactionInterceptor transactionInterceptor() {
      TransactionInterceptor interceptor = new TransactionInterceptor();
      interceptor.setTransactionAttributeSource(transactionAttributeSource());
      if (this.txManager != null) {
         interceptor.setTransactionManager(this.txManager);
      }
      return interceptor;
   }
}


//开始解析事务的属性值,会发现很多事务属性在这里都有:propagation/isolation/timeout等等
protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) {
   RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();
    //事务传播属性的设置
   Propagation propagation = attributes.getEnum("propagation");
   rbta.setPropagationBehavior(propagation.value());
    //事务的隔离属性的设置
   Isolation isolation = attributes.getEnum("isolation");
   rbta.setIsolationLevel(isolation.value());
    //事务的超时时间设置
   rbta.setTimeout(attributes.getNumber("timeout").intValue());
   rbta.setReadOnly(attributes.getBoolean("readOnly"));
   rbta.setQualifier(attributes.getString("value"));
   ArrayList<RollbackRuleAttribute> rollBackRules = new ArrayList<>();
    //事务的回滚条件设置
   Class<?>[] rbf = attributes.getClassArray("rollbackFor");
   for (Class<?> rbRule : rbf) {
      RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule);
      rollBackRules.add(rule);
   }
    //设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚
   String[] rbfc = attributes.getStringArray("rollbackForClassName");
   for (String rbRule : rbfc) {
      RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule);
      rollBackRules.add(rule);
   }
   Class<?>[] nrbf = attributes.getClassArray("noRollbackFor");
   for (Class<?> rbRule : nrbf) {
      NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule);
      rollBackRules.add(rule);
   }
    //设置不回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,事务不回滚
   String[] nrbfc = attributes.getStringArray("noRollbackForClassName");
   for (String rbRule : nrbfc) {
      NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule);
      rollBackRules.add(rule);
   }
   rbta.getRollbackRules().addAll(rollBackRules);
   return rbta;
}

到这里:第一就阶段的初始化任务就完成了,核心任务:

利用TransactionManagementConfigurationSelector给容器中导入两个组件:
(1)InfrastructureAdvisorAutoProxyCreator
        AutoProxyRegistrar给容器中注册一个 InfrastructureAdvisorAutoProxyCreator组件,它其实就是一个后置处理器,一个动态代理创建器,利用后置处理器和动态代理对目标方法进行增强,返回一个增强的实例对象,代理对象执行方法利用拦截器链进行调用;
(2)ProxyTransactionManagementConfiguration
        对事务管理器的获取,对事务的元信息进行处理,对目标方法本身的执行,主要是事务能力细节的代理实现,然后给容器中注册配置生成的事务增强器Bean;

2.2、调用执行阶段

目标方法的调用,开始调用TransactionInterceptor.invoke() 方法,这个是事务执行的核心,思路流程代码写的很清晰:

//这里拦截后封装成 MethodInterceptor,保存了事务的信息,和aop的逻辑一样
public class TransactionInterceptor extends TransactionAspectSupport implements MethodInterceptor, Serializable {


@Override
@Nullable //动态代理的调用
public Object invoke(final 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);
}

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

   // If the transaction attribute is null, the method is non-transactional.
    //(1):获取(实践第二步中)设置的事务属性信息(propagation = Propagation.REQUIRED, rollbackFor = Exception.class),直接从内存中加载;
   TransactionAttributeSource tas = getTransactionAttributeSource();
   final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
    //(2):获取(实践第四步中)注册的事务管理器-PlatformTransactionManager,加载到容器中;
   final PlatformTransactionManager tm = determineTransactionManager(txAttr);
   final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

   if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
      // Standard transaction demarcation with getTransaction and commit/rollback calls.
        //(3):得到事务管理器,关闭事务自动提交;
      TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
      Object retVal = null;
      try {
         // This is an around advice: Invoke the next interceptor in the chain.
         // This will normally result in a target object being invoked.
        //(4): 开始执行目标方法本身doBusiness();
         retVal = invocation.proceedWithInvocation();
      }
      catch (Throwable ex) {
         // target invocation exception
        //(4.1): 如果执行过程中抛出异常则回滚
         completeTransactionAfterThrowing(txInfo, ex);
         throw ex;
      }
      finally {
         cleanupTransactionInfo(txInfo);
      }
        //(4.2) 如果执行成功,则提交事务;
      commitTransactionAfterReturning(txInfo);
      return retVal;
   }


// (4.1) :回滚事务
protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {
   if (txInfo != null && txInfo.getTransactionStatus() != null) {
      if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {
         try {
            txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());
         }
      }

    Spring拿到数据库连接池的连接connection后,事务管理器关闭自动提交,通过方法执行的结果的成功和失败判断该事务是提交还是会滚。至此,我们的声明式事务@Transactional就完成了对方法的事务增强,使其具备了事务的能力。

3、事务失效原因分析

对于思考二问题:
为什么有时候加了@Transactional却不起作用?
现在就会很清楚了,从设计思想来看,@Transaction的设计是基于AOP的,所以首先就是具有事务能力的Bean实例一定是通过动态代理增强后的实例对象,也就是标签事务必须被spring代理增强,否则事务将失效,也就是类似本地调用,this的调用事务将会失效。
例如:


public class TransactionalTest{
    @Transactional
    public void doInvokeWithTransactional(){
        doBusiness();
        enjoy(); //不好意,这里等价于:this.enjoy(),注意,本地调用是不会走动态代理的,不会对enjoy()方法进行增强;所以这里enjoy()的事务会失效;
    }

    @Transactional
    public void enjoy(){
        doEnjoy();
    }

其他的事务失效的注意点:

原因一:入口的方法必须是public,如果是protected和private方法,则事务不起作用,
final 方法 和 static 方法不能添加事务,加了也不生效
一些在private方法上面加@Transactional,这件事有两重意思:
  • 1、你的方法是private的话,即使加上@Transactional注解,该注解也无效,不会开启事务,发生异常时不会回滚。
  • 2、即使你的方法是public的,但是如果被private的方法调用,@Transactional注解同样也会失效。
原因:出于安全考虑,因为是私有的方法,不能被访问增强,不应该用事务切入,这是合理的。
 
原因二
Spring的事务管理默认只对出
现非受检运行期异常(java.lang.RuntimeException及其子类)进行回滚,
Exception:受检异常 – Checked异常
事务@transaction 不回滚
 
原因三:请确保你的业务和事务入口在同一个线程里,
事务不能垮线程传播,否则事务也是不生效的,比如下面代码事务不生效:
@Transactional
@Override
public void save(Order orderInfo ) {
    new Thread(() -> {
          addOrder(orderInfo); //事务失效,不会垮线程传播;
          System.out.println(1 / 0);
    }).start();
}

4、小结

   注解事务设计思想:spring的声明式事务都是基于AOP的,其实
所有加了注解的方法都是利用spring的后置处理器,使用对应的处理类对当前的作用域的方法或者类做一个拦截增强处理,返回一个增强的代理类,实现注解的增强功能。
第一阶段:初始化阶段
  • 创建事务增强的后置处理器,主要用来对目标的方法和类进行增强;;
  • 使用动态代理,创建具有事务能力的增强代理类;
第二阶段:执行调用阶段
  • 2.1 通过AOP机制,调用动态增强代理对象的目标方法:   CglibAoProxy.
    intercept();
  • 2.2 获取目标方法事务
    拦截器链:也即设置的通知的方法 List<Object> chain = this.advised.getInterceptors()利用拦截器的链式机制,依次进入每一个拦截器通知进行执行;压栈的存储通知方法,再出栈调用通知方法;
  • 2.3 执行事务拦截器:
    TransactionalInterceptor实现了methodInterceptor:调用invokeWithTransaction()函数
        (1)获取事务的属性信息: 
AnnotationTransactionAttributeSource
        (2)获取事务管理器: 
PlatfromTransactionMavager
        (3)关闭事务的自动提交
        (4)执行目标方法: 
interceptor().
invoke()
            (4.1)执行异常:回滚;
            (4.2)执行正常:提交;
这里也就回答了开篇思考一的问题了。
 
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/129274.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

  • 操作系统第二章进程的描述与控制_进程同步和互斥的区别

    操作系统第二章进程的描述与控制_进程同步和互斥的区别什么是进程同步进程互斥的原则进程互斥的软件实现方法1、单标志法2、双标志先检查法3、双标志后检查法4、Peterson算法进程互斥的硬件实现方法1、中断屏蔽方法2、TestAndSetLock指令TSL和中断屏蔽的区别利用TSL完成进程间互斥-《现代操作系统》P713、XCHG指令信号量机制1、整型信号量2、记录型信号量(默认)记录型信号量定义P操作(wait操作)V操作(signal操作)信号量机制实现进程互斥信号量机制实现进程同步-前V后

  • 安卓应用程序开发_Android从入门到精通读书笔记

    安卓应用程序开发_Android从入门到精通读书笔记Android应用程序开发 第一章Android应用初体验1.1应用基础activity是AndroidSDK中Activity类的一个具体实例,负责管理用户与信息屏的交互。应用的功能是通过编写一个个Activity子类来实现的。布局定义了一系列用户界面对象以及它们显示在屏幕上的位置。组成布局的定义保存在XML文件中。…

  • gamma校正 matlab,Gamma校正 ——图像灰度变化 OpenCV (十)

    gamma校正 matlab,Gamma校正 ——图像灰度变化 OpenCV (十)Gamma校正(C++、OpenCV实现)1.作用:Gamma校正是对输入图像灰度值进行的非线性操作,使输出图像灰度值与输入图像灰度值呈指数关系:伽玛校正由以下幂律表达式定义:2.函数原型voidcalcHist(constMat*images,intnimages,constint*channels,InputArraymask,OutputArrayhist,int…

  • java注解生成xml和包含CDATA问题

    百度java生成xml,有一大推的文章,主要的生成方式一种使用Dom4J ,还有一种使用Jdk自带注解类! 下面主要整理我注解类的使用,(可以参考这篇文章Dom4J生成xml和包含CDATA问题)和xml中CDATA 问题的解决方法!

  • 网络编程_8(项目附件)[通俗易懂]

    网络编程_8(项目附件)[通俗易懂]dict.txtabandonmentn.放弃abbreviationn.缩写abeyancen.缓办,中止abidev.遵守abilityn.能力ableadj.有能力的,能干的abnormaladj.反常的,变态的aboardadv.船(车)上abolishv.废除,取消abolitionn.废除,取消abortionn.流产abortiveadj.无效果的,失败的aboutprep.关于,大约ab

  • pycharm最新激活码2021【2021.7最新】

    (pycharm最新激活码2021)JetBrains旗下有多款编译器工具(如:IntelliJ、WebStorm、PyCharm等)在各编程领域几乎都占据了垄断地位。建立在开源IntelliJ平台之上,过去15年以来,JetBrains一直在不断发展和完善这个平台。这个平台可以针对您的开发工作流进行微调并且能够提供…

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号