目录

一、概述

二、事务的ACID属性

三、事务的隔离级别

四、事务的传播行为

五、Spring声明式事务环境搭建

六、@EnableTransactionManagement分析

七、AutoProxyRegistrar类分析

八、ProxyTransactionManagementConfiguration类分析

九、总结


一、概述

在分析Spring事务原理之前,我们有必要先回顾下数据库事务相关的知识。如事务的概念、事务的属性、事务隔离级别、事务传播行为等。

首先介绍一些什么是事务?

事务由单独单元的一个或者多个sql语句组成,在这个单元中,每个mysql语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条sql语句一旦执行失败或者产生错误,整个单元将会回滚,也就是所有受到影响的数据将会返回到事务开始以前的状态;如果单元中的所有sql语句均执行成功,则事务被顺利执行。一般来说,我们说的事务单指“数据库事务”,接下来我们会以MySQL数据库、Spring声明式事务为主要研究对象。

二、事务的ACID属性

提到事务,不可避免需要涉及到事务的ACID属性:

  • 原子性(atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行;
  • 一致性(consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束;
  • 隔离性(isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行;
  • 持久性(durability): 事务一旦提交,对数据库的修改应该永久保存在数据库中;

三、事务的隔离级别

MySQL的InnoDB引擎提供四种隔离级别:

  • ①读未提交(READ UNCOMMITTED)

事务中的修改,即使没有提交,对其他事务也是可见的,也就是说事务可以读取到未提交的数据,这也被称为脏读。

  • ②读已提交(READ COMMITTED)

一个事务从开始到提交之前,所做的任何修改对其他事务都是不可见的,这个级别有时候也叫不可重复读,因为两次执行同样的查询,可能会得到不一样的结果。

  • ③可重复读(REPEATABLE READ)

该隔离级别保证了在同一个事务中多次读取同样的记录的结果是一致的,但是无法解决另外一个幻读的问题,所谓的幻读就是指当某个事务在读取某个范围内的记录是,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时就会产生幻行。

  • ④可串行化(SERIALIZABLE)

SERIALIZABLE是最高的隔离级别,通过强制事务的串行执行,避免了前面说的幻读问题,简单来说,SERIALIZABLE会在读取的每一行数据上加上锁。

四、事务的传播行为

Spring针对方法嵌套调用时事务的创建行为定义了七种事务传播机制,分别是:

传播行为

含义

PROPAGATION_REQUIRED

这是默认的传播属性,如果外部调用方有事务,将会加入到事务,没有的话新建一个。

PROPAGATION_SUPPORTS

表示当前方法不必需要具有一个事务上下文,但是如果有一个事务的话,它也可以在这个事务中运行(如果当前存在事务,则加入到该事务;如果当前没有事务,则以非事务的方式继续运行。)

PROPAGATION_MANDATORY

表示当前方法必须在一个事务中运行,如果没有事务,将抛出异常

PROPAGATION_NESTED

表示如果当前方法正有一个事务在运行中,则该方法应该运行在一个嵌套事务中,被嵌套的事务可以独立于被封装的事务中进行提交或者回滚。如果封装事务存在,并且外层事务抛出异常回滚,那么内层事务必须回滚,反之,内层事务并不影响外层事务。如果封装事务不存在,则同PROPAGATION_REQUIRED的一样

PROPAGATION_NEVER

表示当前方法务不应该在一个事务中运行,如果存在一个事务,则抛出异常

PROPAGATION_REQUIRES_NEW

表示当前方法必须运行在它自己的事务中。一个新的事务将启动,而且如果有一个现有的事务在运行的话,则这个方法将在运行期被挂起,直到新的事务提交或者回滚才恢复执行。

PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起

五、Spring声明式事务环境搭建

笔者这里是在Spring源码里面新建了一个子模块搭建事务功能测试环境,所以使用的是Gradle进行构建。当然也可以使用maven搭建。

  • (一)、build.gradle:添加Druid数据源、mysql驱动等依赖
description = "spring test demo"dependencies {compile(project(":spring-beans"))compile(project(":spring-core"))compile(project(":spring-context"))compile(project(":spring-tx"))compile(project(":spring-jdbc"))implementation 'com.alibaba:druid:1.1.10'compile ("mysql:mysql-connector-java:5.1.24")
}repositories {mavenLocal()maven { url "https://maven.aliyun.com/nexus/content/groups/public" }maven { url "https://repo.springsource.org/plugins-release" }mavenCentral()
}
  • (二)、定义Service接口以及实现类,实现类对应方法添加@Transactional开启声明式事务功能
public interface UserService {void insert();}@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserDao userDao;@Override// 声明式事务@Transactionalpublic void insert() {userDao.insert();System.out.println("插入完成");
//      int value = 1 / 0;}}
  • (三)、定义持久层接口
@Repository
public class UserDao {@Autowiredprivate JdbcTemplate jdbcTemplate;public void insert() {String sql = "INSERT INTO user(name, age) VALUES(?,?)";jdbcTemplate.update(sql, "lisi", 20);}}
  • (四)、定义事务配置类

使用@EnableTransactionManagement开启Spring注解版事务功能,并往容器中注册DataSource、JdbcTemplate、PlatformTransactionManager这三个bean对象。

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.sql.DataSource;// 开启Spring注解版事务功能
@EnableTransactionManagement
@Configuration
@ComponentScan("com.wsh.transaction")
public class TxConfig {/*** 数据源配置*/@Beanpublic DataSource dataSource() {DruidDataSource dataSource = new DruidDataSource();dataSource.setUsername("root");dataSource.setPassword("0905");dataSource.setUrl("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=Asia/Shanghai");dataSource.setDriverClassName("com.mysql.jdbc.Driver");return dataSource;}@Beanpublic JdbcTemplate jdbcTemplate(DataSource dataSource) {return new JdbcTemplate(dataSource);}/*** 事务管理器*/@Beanpublic PlatformTransactionManager platformTransactionManager() {return new DataSourceTransactionManager(dataSource());}
}
  • (五)、客户端类
public class Client {public static void main(String[] args) {AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TxConfig.class);UserService userService = (UserService) annotationConfigApplicationContext.getBean("userServiceImpl");System.out.println(userService);userService.insert();}}

六、@EnableTransactionManagement分析

在前面的例子中,我们使用@EnableTransactionManagement开启了Spring注解版本的事务功能,所以我们从@EnableTransactionManagement注解开始分析Spring事务的整体流程。

先来看看@EnableTransactionManagement注解的源码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 开启Spring事务支持
// 通过@EnableTransactionManagement引入TransactionManagementConfigurationSelector组件,进而导入两个bean:
// a、AutoProxyRegistrar
// b、ProxyTransactionManagementConfiguration
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {/*** 使用JDK动态代理还是Cglib动态代理,默认是false,但前提是AdviceMode必须是PROXY才适用*/boolean proxyTargetClass() default false;/*** 通知的模式,默认是PROXY,还有一种是ASPECTJ*/AdviceMode mode() default AdviceMode.PROXY;/*** 指定通知器的执行顺序*/int order() default Ordered.LOWEST_PRECEDENCE;}

通过@EnableTransactionManagement的源码,我们看到,使用@Import注解@Import(TransactionManagementConfigurationSelector.class)注入了TransactionManagementConfigurationSelector类。

TransactionManagementConfigurationSelector间接实现了ImportSelector接口,先来看看ImportSelector有什么作用?

public interface ImportSelector {String[] selectImports(AnnotationMetadata importingClassMetadata);}

ImportSelector接口只定义了一个方法selectImports(),用于指定需要注册为bean的Class名称。当在@Configuration标注的Class上使用@Import引入了一个ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。

我们再回来看TransactionManagementConfigurationSelector类的selectImports()方法返回了什么Class名称。

protected String[] selectImports(AdviceMode adviceMode) {switch (adviceMode) {case PROXY:return new String[] {AutoProxyRegistrar.class.getName(),// 导入了BeanFactoryTransactionAttributeSourceAdvisor组件,bean名称为"org.springframework.transaction.config.internalTransactionAdvisor"ProxyTransactionManagementConfiguration.class.getName()};case ASPECTJ:return new String[] {determineTransactionAspectClass()};default:return null;}
}

selectImports()方法根据@EnableTransactionManagement注解指定的AdviceMode,分别返回不同的Class名称。

这里我们以默认的AdviceMode.PROXY模式为例,返回了两个bean的名称:

  • (1). AutoProxyRegistrar
  • (2). ProxyTransactionManagementConfiguration

七、AutoProxyRegistrar类分析

public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {private final Log logger = LogFactory.getLog(getClass());@Override// org.springframework.context.annotation.AutoProxyRegistrar.registerBeanDefinitionspublic void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {boolean candidateFound = false;Set<String> annTypes = importingClassMetadata.getAnnotationTypes();for (String annType : annTypes) {// 获取注解的属性AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);if (candidate == null) {continue;}// 模式Object mode = candidate.get("mode");Object proxyTargetClass = candidate.get("proxyTargetClass");if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&Boolean.class == proxyTargetClass.getClass()) {candidateFound = true;if (mode == AdviceMode.PROXY) {// 注册自动代理创建器:InfrastructureAdvisorAutoProxyCreatorAopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);if ((Boolean) proxyTargetClass) {AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);return;}}}}if (!candidateFound && logger.isInfoEnabled()) {String name = getClass().getSimpleName();logger.info(String.format("%s was imported but no annotations were found " +"having both 'mode' and 'proxyTargetClass' attributes of type " +"AdviceMode and boolean respectively. This means that auto proxy " +"creator registration and configuration may not have occurred as " +"intended, and components may not be proxied as expected. Check to " +"ensure that %s has been @Import'ed on the same class where these " +"annotations are declared; otherwise remove the import of %s " +"altogether.", name, name, name));}}}// org.springframework.aop.config.AopConfigUtils#registerAutoProxyCreatorIfNecessary
public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {return registerAutoProxyCreatorIfNecessary(registry, null);
}public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, @Nullable Object source) {// 注册或者升级InfrastructureAdvisorAutoProxyCreator组件return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
}

从源码可以看到,AutoProxyRegistrar实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar接口内部定义了registerBeanDefinitions()方法实现向容器中注册bean的功能。

AutoProxyRegistrar#registerBeanDefinitions()方法中注册了自动代理创建器:InfrastructureAdvisorAutoProxyCreator。接下来看看InfrastructureAdvisorAutoProxyCreator是什么东西?

先看看InfrastructureAdvisorAutoProxyCreator的层级关系:

我们看到,InfrastructureAdvisorAutoProxyCreator类跟我们前面介绍到的AOP类一样,根父类也是AbstractAutoProxyCreator。主要分析下面两点:

  • (1)、实现了InstantiationAwareBeanPostProcessor接口

该接口有2个方法postProcessBeforeInstantiation()和postProcessAfterInstantiation(),其中实例化之前会执行postProcessBeforeInstantiation()方法:

InfrastructureAdvisorAutoProxyCreator中并没有实现postProcessBeforeInstantiation(),而是在其父类AbstractAutoProxyCreator中实现。

// AbstractAutoProxyCreator#postProcessBeforeInstantiation
// postProcessBeforeInstantiation()方法在bean实例化之前调用
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {// 组装缓存keyObject cacheKey = getCacheKey(beanClass, beanName);if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {// 如果advisedBeans缓存中已经存在,即当前正在创建的Bean已经被解析过,则直接返回nullif (this.advisedBeans.containsKey(cacheKey)) {return null;}// 注意:AnnotationAwareAspectJAutoProxyCreator重写了isInfrastructureClass()方法.// 1.isInfrastructureClass():判断当前正在创建的Bean是否是基础的Bean(Advice、PointCut、Advisor、AopInfrastructureBean)// 2.shouldSkip():判断是否需要跳过(在这个方法内部,Spring Aop解析直接解析出我们的切面信息(并且把我们的切面信息进行缓存))// 满足两个条件其中之一,都将跳过,直接返回nullif (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {this.advisedBeans.put(cacheKey, Boolean.FALSE);return null;}}// Create proxy here if we have a custom TargetSource.// Suppresses unnecessary default instantiation of the target bean:// The TargetSource will handle target instances in a custom fashion.// 获取用户自定义TargetSource(目标源)TargetSource targetSource = getCustomTargetSource(beanClass, beanName);if (targetSource != null) {if (StringUtils.hasLength(beanName)) {this.targetSourcedBeans.add(beanName);}// 获取目标对象的拦截器链Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);// 创建AOP代理Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);this.proxyTypes.put(cacheKey, proxy.getClass());return proxy;}return null;
}

postProcessBeforeInstantiation()方法在bean实例化之前调用,主要是通过isInfrastructureClass(beanClass)方法判断当前正在创建的Bean是否是基础的Bean(Advice、PointCut、Advisor、AopInfrastructureBean);以及通过

shouldSkip(beanClass, beanName)方法判断是否需要跳过,在shouldSkip()方法内部,Spring Aop解析直接解析出我们的切面信息,并且把我们的切面信息进行缓存,后面我们创建代理对象时直接从缓存中获取使用。

关于isInfrastructureClass(beanClass)和shouldSkip(beanClass, beanName)方法的详细分析,读者朋友可以参考笔者在前面的Spring AOP原理分析中的文章呦~~

  • (2)、实现了BeanPostProcessor接口

该接口有2个方法postProcessBeforeInitialization()和postProcessAfterInitialization(),其中组件初始化之后会执行postProcessAfterInitialization()(该方法创建Aop和事务的代理对象)方法:

// 所有Spring管理的bean在初始化后都会去调用所有BeanPostProcessor的postProcessAfterInitialization()方法
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {// 如果bean非空的话,判断是否需要进行代理,需要代理的话则会进行包装if (bean != null) {// 获取缓存key:// a.如果beanName非空的话,则还会判断是否是FactoryBean,是FactoryBean的话使用"&+beanName"作为缓存key,否则直接使用beanName;// b.如果beanName为空,直接使用beanClass作为缓存key;Object cacheKey = getCacheKey(bean.getClass(), beanName);if (this.earlyProxyReferences.remove(cacheKey) != bean) {// 如果有必要的话,则执行具体的包装return wrapIfNecessary(bean, beanName, cacheKey);}}// 如果bean为空,则直接返回return bean;
}

所有Spring管理的bean在初始化后都会去调用所有BeanPostProcessor的postProcessAfterInitialization()方法,postProcessAfterInitialization()方法内部最主要的是完成了代理对象的创建工作,返回给容器中。

同样的,关于wrapIfNecessary(bean, beanName, cacheKey)方法的详细分析,读者朋友可以参考笔者在前面的Spring AOP原理分析中的文章呦~~

八、ProxyTransactionManagementConfiguration类分析

前面我们介绍了AutoProxyRegistrar组件,主要是向容器中注册了自动代理创建器---InfrastructureAdvisorAutoProxyCreator,它间接实现了InstantiationAwareBeanPostProcessor接口,在postProcessBeforeInstantiation()方法中完成了切面信息的解析工作,并进行缓存,后面创建代理对象时直接从缓存中获取使用。同时它还实现了BeanPostProcessor接口,在postProcessAfterInitialization()方法内部完成了代理对象的创建工作。

接下来,我们看TransactionManagementConfigurationSelector导入的另外一个组件---ProxyTransactionManagementConfiguration发挥了什么作用。

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyTransactionManagementConfiguration extends AbstractTransactionManagementConfiguration {/*** 注册BeanFactoryTransactionAttributeSourceAdvisor增强器*/@Bean(name = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME)@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public BeanFactoryTransactionAttributeSourceAdvisor transactionAdvisor() {// 注册BeanFactoryTransactionAttributeSourceAdvisor增强器,beanName = "org.springframework.transaction.config.internalTransactionAdvisor"BeanFactoryTransactionAttributeSourceAdvisor advisor = new BeanFactoryTransactionAttributeSourceAdvisor();advisor.setTransactionAttributeSource(transactionAttributeSource());advisor.setAdvice(transactionInterceptor());if (this.enableTx != null) {advisor.setOrder(this.enableTx.<Integer>getNumber("order"));}return advisor;}/*** 注册TransactionAttributeSource,类型是AnnotationTransactionAttributeSource*/@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public TransactionAttributeSource transactionAttributeSource() {return new AnnotationTransactionAttributeSource();}/*** 注册TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行*/@Bean@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public TransactionInterceptor transactionInterceptor() {TransactionInterceptor interceptor = new TransactionInterceptor();interceptor.setTransactionAttributeSource(transactionAttributeSource());if (this.txManager != null) {interceptor.setTransactionManager(this.txManager);}return interceptor;}}

从源码中我们看到,ProxyTransactionManagementConfiguration是一个配置类,通过@Bean + @Configuration注解往容器中注册了三个Bean:

  • (1). BeanFactoryTransactionAttributeSourceAdvisor增强器
  • (2). TransactionAttributeSource,类型是AnnotationTransactionAttributeSource
  • (3). TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行

九、总结

最后,通过一张图总结一下@EnableTransactionManagement注解的作用:

  • AutoProxyRegistrar组件

主要是向容器中注册了自动代理创建器---InfrastructureAdvisorAutoProxyCreator,它间接实现了InstantiationAwareBeanPostProcessor接口,在postProcessBeforeInstantiation()方法中完成了切面信息的解析工作,并进行缓存,后面创建代理对象时直接从缓存中获取使用。同时它还实现了BeanPostProcessor接口,在postProcessAfterInitialization()方法内部完成了代理对象的创建工作。

  • ProxyTransactionManagementConfiguration组件

通过@Bean + @Configuration注解往容器中注册了三个Bean:

  • (1). BeanFactoryTransactionAttributeSourceAdvisor增强器;
  • (2). TransactionAttributeSource,类型是AnnotationTransactionAttributeSource;
  • (3). TransactionInterceptor,实现了MethodInterceptor接口,主要用于拦截事务方法的执行;

Spring事务原理分析(一)--@EnableTransactionManagement 到底做了什么?相关推荐

  1. Spring事务原理分析-部分一

    Spring事务原理分析-部分一 什么事务 事务:逻辑上的一组操作,组成这组操作的各个单元,要么全都成功,要么全都失败. 事务基本特性 ⑴ 原子性(Atomicity) 原子性是指事务包含的所有操作要 ...

  2. Spring 事务原理篇:@EnableTransactionManagement注解底层原理分析技巧,就算你看不懂源码,也要学会这个技巧!

    前言 学习了关于Spring AOP原理以及事务的基础知识后,今天咱们来聊聊Spring在底层是如何操作事务的.如果阅读到此文章,并且对Spring AOP原理不太了解的话,建议先阅读下本人的这篇文章 ...

  3. 【笔记】Spring 事务原理分析和源码剖析

    文章目录 概述 源码解析 xml 配置解析 事务代理类的创建 事务拦截器的实现 切面实现 事务处理实现 总结: 资料 概述 事务处理是一个重要并且涉及范围很广的领域,涉及到并发和数据一致性方面的问题. ...

  4. 不同类的方法 事务问题_深入理解 Spring 事务原理

    Spring事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的.对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行: 获 ...

  5. Spring事务原理-1-transactionManager以及Connection的本质

    Spring事务原理 1.spring事务配置 2.Connection本质的探究 3. Spring事务的底层原理 1.spring事务配置 <bean id="transactio ...

  6. spring ioc原理分析

    spring ioc原理分析 spring ioc 的概念 简单工厂方法 spirng ioc实现原理 spring ioc的概念 ioc: 控制反转 将对象的创建由spring管理.比如,我们以前用 ...

  7. MySQL事务原理分析(ACID特性、隔离级别、锁、MVCC、并发读异常、并发死锁以及如何避免死锁)

    MySQL事务原理分析(ACID特性.隔离级别.锁.MVCC.并发读异常.并发死锁以及如何避免死锁) 一.事务 目的 组成 特征 事务空间语句 二.ACID特性 原子性(A) 隔离性(I) 持久性(d ...

  8. 【Mybatis+spring整合源码探秘】--- mybatis整合spring事务原理

    文章目录 1 mybatis整合spring事务原理 1 mybatis整合spring事务原理 本篇文章不再对源码进行具体的解读了,仅仅做了下面一张图: 该图整理了spring+mybatis整合后 ...

  9. 浅谈:Spring Boot原理分析,切换内置web服务器,SpringBoot监听项目(使用springboot-admin),将springboot的项目打成war包

    浅谈:Spring Boot原理分析(更多细节解释在代码注释中) 通过@EnableAutoConfiguration注解加载Springboot内置的自动初始化类(加载什么类是配置在spring.f ...

最新文章

  1. 各种光学仪器成像技术(上)
  2. iOS实现动态区域裁剪图片
  3. 2017还有29天,你的目标实现了吗?|内有彩蛋
  4. 公有云账单:忽略这四项成本,后果很严重!
  5. Dataset之Cityscapes:Cityscapes数据集的简介、安装、使用方法之详细攻略
  6. [css] css3和css2的区别是什么?
  7. WebClient上传文件至服务器和下载服务器文件至客户端
  8. springboot统一校验validator实现
  9. xuperchain 事件订阅 判断交易是否上链 交易状态
  10. JXTA第一步:HelloWorld
  11. 《计算机网络(第7版)》-谢希仁
  12. gem5——向简单脚本中添加缓存
  13. 嵌入式软件开发做什么?嵌入式开发培训学哪些
  14. SpringCloud(4)— 统一网关Gateway
  15. Photoshop对图片加边框
  16. 用户太多:互联网巨头之惑
  17. javascript编码调试环境-ide和调试工具
  18. Python django 会议室管理系统
  19. 3D动作捕捉实施推流虚拟人物角色动画的实时运动捕捉系统
  20. ZLib的数据压缩和解压缩

热门文章

  1. 陀螺仪计算姿态待完善
  2. 非法破解?击键声也会出卖你?嘘,小心隔墙有耳……
  3. Excel批量将文本中的【】替换为[]
  4. 在Excel中批量生成标签,支持单排/双排标签,也支持二维码和条形码
  5. js生成随机数概率算法
  6. 数据结构——优先级队列(堆)
  7. Jetson TX2 上安装Pycharm
  8. linux安装pycharm详细步骤
  9. 从01背包说起(上)
  10. 网络流最大流进阶---最小费用最大流