Spring中每次访问数据库都要创建SqlSession吗?
- 一、SqlSession是什么
- 二、源码分析
- 1)mybatis获取Mapper流程
- 2)Spring创建Mapper接口的代理对象流程
- 3)MapperFactoryBean#getObject调用时机
- 4)SqlSessionTemplate创建流程
- 5)SqlSessionInterceptor拦截逻辑
- 6)开启事务后,关闭会话的时机分析
- 三、总结
参考:https://www.zhihu.com/question/57568941/answer/2062846449
先来说结论:
- 如果方法上标注了
@Transactional
注解,则该方法里面多次访问数据库用的是同一个SqlSession(多线程调用除外) - 如果方法上没有标注该注解,则每访问一次数据库,都会创建新的SqlSession
一、SqlSession是什么
SqlSession是Mybatis工作的最顶层API会话接口,所有的数据库操作都经由它来实现。由于它是一个会话,即SqlSession对应这一次数据库会话,不是永久存活的,因此每次访问数据库时都需要创建它。
并且它不是线程安全的,如果将一个SqlSession搞成单例形式,或者静态域和实例变量的形式,都会导致SqlSession出现事务问题,这也就是为什么同一事务中的多个访问数据库请求会共用一个SqlSession会话,而不同事务则会创建不同SqlSession的原因。
SqlSession的创建过程:
- 从Configuration配置类中拿到Environment数据源;
- 从数据源中获取TransactionFactory和DataSource,并创建一个Transaction连接管理对象;
- 创建Executor对象(SqlSession只是所有操作的门面,真正要干活的是Executor,它封装了底层JDBC所有的操作细节);
- 创建SqlSession会话。
每次创建一个SqlSession会话,都会伴随创建一个专属SqlSession的连接管理对象,如果SqlSession共享,就会出现事务问题。
二、源码分析
1)mybatis获取Mapper流程
先回顾以下传统mybatis创建Mapper接口的代理对象流畅如下:
- 如果没有引入spring的依赖,以前做法是通过sqlSession手动去获取Mapper对象,第一步是先创建SqlSession工厂对象,由它来创建SqlSession对象:
//sqlSessionFactory --> sqlSession
public class MybatisUtils {static SqlSessionFactory sqlSessionFactory = null;static {try {String resource = "mybatis-config.xml";InputStream inputStream = Resources.getResourceAsStream(resource);// 1.build方法会解析xml文件,包括我们写的mapper接口的xml文件,最终会把解析的信息封装到configuration对象中,// 特别是我们xml文件中的sql和相关信息都会被封装成一个个的MappedStatement对象存进一个Map中,key为全限定类名+方法名,value为MappedStatement对象,// 然后创建一个持有configuration引用的工厂对象返回,这里面就不展开分析了sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);} catch (IOException e) {e.printStackTrace();}}// 2.这里会创建一个持有configuration对象引用的DefaultSqlSession对象返回,并且Executor对象也是在这一步创建的,提供了在数据库执行 SQL 命令所需的所有方法,这里面不展开分析public static SqlSession getSqlSession(){return sqlSessionFactory.openSession();}
}
使用:
//1.获取SqlSession对象SqlSession sqlSession = MybatisUtils.getSqlSession();//2.获取代理对象,执行SQLUserDao userDao = sqlSession.getMapper(UserDao.class);List<User> userList = userDao.getUserList();for (User user : userList) {System.out.println(user);}//关闭sqlSessionsqlSession.close();
- 查看DefaultSqlSession的getMapper方法如下:
@Override
public <T> T getMapper(Class<T> type) {return configuration.getMapper(type, this);
}
- 接着会调用到
MapperRegistry#getMapper
方法如下:
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {// 1.knownMappers会在解析mapper接口的xml文件时设置,key为接口的class对象,value为持有接口字节码对象引用的MapperProxyFactory对象final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);if (mapperProxyFactory == null) {throw new BindingException("Type " + type + " is not known to the MapperRegistry.");}try {// 2.创建代理对象逻辑return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException("Error getting mapper instance. Cause: " + e, e);}}
- 接着调用
MapperRegistry#newInstance
方法,MapperProxyFactory源码如下:
public class MapperProxyFactory<T> {private final Class<T> mapperInterface;private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<>();public MapperProxyFactory(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}public Class<T> getMapperInterface() {return mapperInterface;}public Map<Method, MapperMethod> getMethodCache() {return methodCache;}@SuppressWarnings("unchecked")protected T newInstance(MapperProxy<T> mapperProxy) {// 3.用JDK的方式去创建一个代理了mapper接口的代理对象返回,然后可以拿这个对象来执行增删改查查方法了,具体逻辑是现在MapperProxy中return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}public T newInstance(SqlSession sqlSession) {// 1.创建MapperProxy对象,它持有sqlSession对象、接口字节码对象引用,并且它实现了InvocationHandler接口,这是动态代理的关键final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);// 2.创建mapper的代理对象return newInstance(mapperProxy);}
}
- 因为MapperProxy实现了InvocationHandler接口,所以代理对象调用方法时,会先经过
MapperProxy#invoke
方法
private final Map<Method, MapperMethod> methodCache;@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 1.如果是父类Object的方法就直接反射调用if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else if (method.isDefault()) {// 2.如果是接口的默认方法,则调用invokeDefaultMethod方法return invokeDefaultMethod(proxy, method, args);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);} // 3.我们自己mapper接口定义的方法,会接着调用MapperMethod#executefinal MapperMethod mapperMethod = cachedMapperMethod(method);return mapperMethod.execute(sqlSession, args);}// 4.如果mapperMethod 对象存在就不创建了,直接从缓存取private MapperMethod cachedMapperMethod(Method method) {// MapperMethod对象持有mapper接口字节码对象、要执行的目标方法对象、configuration对象引用return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));}
-
MapperMethod#execute
方法如下:public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {// 根据全限定类名+方法名去获取MappedStatement对象,然后获取MappedStatement对象的标签类型,其中它的字段type为标签类型,name为全限定类名+方法名this.command = new SqlCommand(config, mapperInterface, method);this.method = new MethodSignature(config, mapperInterface, method);}public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) {case INSERT: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.insert(command.getName(), param));break;}case UPDATE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.update(command.getName(), param));break;}case DELETE: {Object param = method.convertArgsToSqlCommandParam(args);result = rowCountResult(sqlSession.delete(command.getName(), param));break;}case SELECT:if (method.returnsVoid() && method.hasResultHandler()) {executeWithResultHandler(sqlSession, args);result = null;} else if (method.returnsMany()) {result = executeForMany(sqlSession, args);} else if (method.returnsMap()) {result = executeForMap(sqlSession, args);} else if (method.returnsCursor()) {result = executeForCursor(sqlSession, args);} else {Object param = method.convertArgsToSqlCommandParam(args);result = sqlSession.selectOne(command.getName(), param);if (method.returnsOptional()&& (result == null || !method.getReturnType().equals(result.getClass()))) {result = Optional.ofNullable(result);}}break;case FLUSH:result = sqlSession.flushStatements();break;default:throw new BindingException("Unknown execution method for: " + command.getName());}if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {throw new BindingException("Mapper method '" + command.getName()+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");}return result;}
这里面不具体展开了,大体逻辑是:
1)根据获取的MappedStatement对象的标签类型(对应xml文件的增删改查查标签:SELECT、DELETE、UPDATE、INSERT)来执行DefaultSqlSession对象对应的增删改查查方法
2)然后从configuration对象中根据全限定类名+方法名去获取一个MappedStatement对象
3)接着调用对应Executor对象的方法,并把MappedStatement对象传进去,Executor对象里面封装了JDBC的逻辑,以查询为例,大致逻辑为:
- 首先会创建
PreparedStatementHandler
对象,接着会创建ParameterHandler
对象和ResultSetHandler
对象,这些对象在创建时都会用InterceptorChain
拦截器链的pluginAll
方法去判断是否需要增强这三个对象,如果要增强则会用动态代理来创建这些对象的代理对象,这也是mybatis插件原理的实现 PreparedStatementHandler
对象相当于JDBC的预编译语句对象,它会处理sgl语句预编译,设置参数等相关工作- 在设置预编译参数时(sql语句的占位符替换),
PreparedStatementHandler
对象会调用ParameterHandler
的setParameters
方法来实现参数设置,里面会调用TypeHandler
对象方法来完成Java类型到数据库类型的转换 - 在处理结果集时,
PreparedStatementHandler
对象会调用ResultSetHandler
的handleResultSets
方法来实现结果集映射,里面会调用TypeHandler
对象方法来完数据库类型到Java类型的转换
回顾了mybatis执行的大致原理,都是依靠DefaultSqlSession的方法,那引入了spring为什么就不需要我们手动创建sqlSession了呢,接下来接着分析
2)Spring创建Mapper接口的代理对象流程
- 当我们在接口标注一个
@Mapper
注解,并且@MapperScan
注解的包路径能扫描到该接口时,则会对该接口生成一个工厂Bean对象MapperFactoryBean
放入一级缓存中:
// 省略其他代码...
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {private Class<T> mapperInterface;private boolean addToConfig = true;public MapperFactoryBean() {// intentionally empty}// 1.Mapper注解所在接口的字节码对象public MapperFactoryBean(Class<T> mapperInterface) {this.mapperInterface = mapperInterface;}@Overrideprotected void checkDaoConfig() {super.checkDaoConfig();notNull(this.mapperInterface, "Property 'mapperInterface' is required");Configuration configuration = getSqlSession().getConfiguration();if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {try {configuration.addMapper(this.mapperInterface);} catch (Exception e) {logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", e);throw new IllegalArgumentException(e);} finally {ErrorContext.instance().reset();}}}// 2.注册真正的Mapper对象@Overridepublic T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);}@Overridepublic Class<T> getObjectType() {return this.mapperInterface;}@Overridepublic boolean isSingleton() {return true;}public Class<T> getMapperInterface() {return mapperInterface;}
}
- 当Spring填充某个Bean的字段时,如果根据字段名称能从一级缓存获取到了Bean实例,并且该Bean实现了FactoryBean接口,则会调用该Bean的getObject方法,来获取真正的Bean来注入到对应字段中:
@Override
public T getObject() throws Exception {return getSqlSession().getMapper(this.mapperInterface);
}
- 里面会调用父类的
getSqlSession
方法:
public abstract class SqlSessionDaoSupport extends DaoSupport {private SqlSessionTemplate sqlSessionTemplate;public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {// 2.如果sqlSessionTemplate为空,则创建该对象if (this.sqlSessionTemplate == null || sqlSessionFactory != this.sqlSessionTemplate.getSqlSessionFactory()) {this.sqlSessionTemplate = createSqlSessionTemplate(sqlSessionFactory);}}// 3.创建SqlSessionTemplate对象,并把sqlSessionFactory对象传进去(持有Mapper.xml文件解析后的数据)@SuppressWarnings("WeakerAccess")protected SqlSessionTemplate createSqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}public final SqlSessionFactory getSqlSessionFactory() {return (this.sqlSessionTemplate != null ? this.sqlSessionTemplate.getSqlSessionFactory() : null);}public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {this.sqlSessionTemplate = sqlSessionTemplate;}// 1.获取SqlSessionTemplate对象,它实现了SqlSession接口public SqlSession getSqlSession() {return this.sqlSessionTemplate;}public SqlSessionTemplate getSqlSessionTemplate() {return this.sqlSessionTemplate;}@Overrideprotected void checkDaoConfig() {notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");}}
可以看到,getSqlSession方法最终会返回一个已经创建好的SqlSessionTemplate
对象,它底层实现了SqlSession接口,并且每个MapperFactoryBean
对象都会持有同一个SqlSessionTemplate
对象,因为它们都继承了同一个抽象父类SqlSessionDaoSupport
的sqlSessionTemplate字段
打断点发现一级缓存中的两个MapperFactoryBean对象确实持有相同的SqlSessionTemplate
引用,如下所示:
- 当返回SqlSessionTemplate对象之后,就会调用它的getMapper方法来获取Mapper接口的代理对象:
// 省略其他代码...
public class SqlSessionTemplate implements SqlSession, DisposableBean {public <T> T getMapper(Class<T> type) {// 2.关键部分:创建一个代理Mapper接口的对象返回return getConfiguration().getMapper(type, this);}// 1.获取创建时传进来的sqlSessionFactory对象中的Configuration对象@Overridepublic Configuration getConfiguration() {return this.sqlSessionFactory.getConfiguration();}}
在创建代理对象时,关键在于这个this引用是当前的SqlSessionTemplate
对象,在前面的mybatis获取Mapper流程中分析了getMapper方法的逻辑,这里不在展开分析。
总之SqlSessionTemplate
对象最终会被MapperProxy
对象所持有,后续调用代理对象的方法时,都会由SqlSessionTemplate
对象的方法来处理,所以我们引入Spring之后,会自动创建一个SqlSessionTemplate
对象,由该对象代替mybatis手动创建的DefaultSqlSession
来处理我们的增删改查查方法。
小结:
为什么引入Spring就不用手动去创建SqlSession对象了?
因为在注册MapperFactoryBean时,都会调用它的getObject方法,里面会返回一个实现了SqlSession接口的SqlSessionTemplate
对象,并且由会调用它的getMapper方法来获取代理Mapper接口的对象,其中实现了InvocationHandler
接口的MapperProxy
对象会持有SqlSessionTemplate
对象引用,最终调用代理对象的方法时,都会经过MapperProxy
的invoke方法来处理,具体是由SqlSessionTemplate
对象来处理的。
3)MapperFactoryBean#getObject调用时机
- 当我注入一个Mapper接口对象时,它会调用doGetBean方法,根据bean的名称从一级缓存中获取到对应的MapperFactoryBean对象:
- 来看看
getObjectForBeanInstance
方法逻辑:
protected Object getObjectForBeanInstance(Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {if (BeanFactoryUtils.isFactoryDereference(name)) {if (beanInstance instanceof NullBean) {return beanInstance;}if (!(beanInstance instanceof FactoryBean)) {throw new BeanIsNotAFactoryException(beanName, beanInstance.getClass());}if (mbd != null) {mbd.isFactoryBean = true;}return beanInstance;}// 1.不是工厂Bean直接返回if (!(beanInstance instanceof FactoryBean)) {return beanInstance;}Object object = null;if (mbd != null) {mbd.isFactoryBean = true;}else {// 2.从缓存中获取代理Bean对象object = getCachedObjectForFactoryBean(beanName);}// 3.缓存中获取不到,说明第一次获取if (object == null) {FactoryBean<?> factory = (FactoryBean<?>) beanInstance;// Caches object obtained from FactoryBean if it is a singleton.if (mbd == null && containsBeanDefinition(beanName)) {mbd = getMergedLocalBeanDefinition(beanName);}boolean synthetic = (mbd != null && mbd.isSynthetic());// 4.里面会调用MapperFactoryBean#getObject方法object = getObjectFromFactoryBean(factory, beanName, !synthetic);}return object;}// 从缓存中获取代理Bean@Nullableprotected Object getCachedObjectForFactoryBean(String beanName) {return this.factoryBeanObjectCache.get(beanName);}// 缓存代理Beanprivate final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<>(16);
- 此时传进来的beanInstance是MapperFactoryBean实例,显然是工厂Bean对象,所以接下来会执行getObjectFromFactoryBean方法:
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {// 1.判断是不是单例,一级缓存中是否存在该工厂Bean对象,很显然是有的if (factory.isSingleton() && containsSingleton(beanName)) {synchronized (getSingletonMutex()) {// 2.再次从缓存中获取代理BeanObject object = this.factoryBeanObjectCache.get(beanName);if (object == null) {// 3.缓存还是没有,这下才回去调用MapperFactoryBean#getObject方法获取代理Bean对象object = doGetObjectFromFactoryBean(factory, beanName);Object alreadyThere = this.factoryBeanObjectCache.get(beanName);if (alreadyThere != null) {object = alreadyThere;}else {if (shouldPostProcess) {if (isSingletonCurrentlyInCreation(beanName)) {// Temporarily return non-post-processed object, not storing it yet..return object;}beforeSingletonCreation(beanName);try {// 4.这个方法最终会遍历所有的BeanPostProcessor,尝试执行postProcessAfterInitialization方法来对该代理Bean对象做后置增强,这里不在展开分析object = postProcessObjectFromFactoryBean(object, beanName);}catch (Throwable ex) {throw new BeanCreationException(beanName,"Post-processing of FactoryBean's singleton object failed", ex);}finally {afterSingletonCreation(beanName);}}if (containsSingleton(beanName)) {// 5.将增强后的代理Bean对象放入到缓存中,这样当别的类注入这个Mapper对象时,就不需要再走一遍后置增强的逻辑了。。直接从这个缓存获取即可this.factoryBeanObjectCache.put(beanName, object);}}}return object;}}else {Object object = doGetObjectFromFactoryBean(factory, beanName);if (shouldPostProcess) {try {object = postProcessObjectFromFactoryBean(object, beanName);}catch (Throwable ex) {throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);}}return object;}}
- doGetObjectFromFactoryBean方法:
可以看到这里面就会通过getObject方法来获取代理了我们Mapper接口的对象,并且它持有一个MybatisMapperProxy
引用(mybatis-plus框架的对象,也实现了InvocationHandler
接口),MybatisMapperProxy
对象里面又会持有SqlSessionTemplate对象的引用,假设没有引入mybatis-plus框架,最终代理对象持有的是MapperProxy
引用
这里的SqlSessionTemplate对象和前面图片的SqlSessionTemplate对象不同,是因为我重启了项目。。
小结:
- 当标注Mapper注解的接口被扫描到时,会生成一个该接口对应的MapperFactoryBean对象,然后将接口名称(第一个字母小写)作为key,MapperFactoryBean对象作为value放入到一级缓存中,注意此时还没有创建Mapper接口的代理对象
- 当我们给一些类的字段注入Mapper接口的对象时,此时会走getBean流程,根据接口名称从一级缓存获取到了MapperFactoryBean对象,接着Spring会判断该Bean是不是FactoryBean类型
- 如果该Bean不是FactoryBean类型,直接返回
- 如果该Bean是FactoryBean类型,此时会尝试从
factoryBeanObjectCache
这个缓存中根据接口名称获取Mapper接口的代理对象 - 如果获取到,直接返回该代理Bean对象
- 如果获取不到,说明是第一次注入该Mapper接口的对象,则会去调用MapperFactoryBean的getObject方法来创建一个代理Mapper接口的对象返回
- 此时拿到的代理Bean对象还不能返回,会拿到所有的后置处理器尝试对该代理Bean对象做增强
- 将增强后的代理Bean对象放入到
factoryBeanObjectCache
缓存中,并将该对象返回
4)SqlSessionTemplate创建流程
可以看到SqlSessionTemplate
确实是实现了SqlSession
接口
public class SqlSessionTemplate implements SqlSession, DisposableBean {// 持有Configuration对象引用private final SqlSessionFactory sqlSessionFactory;private final ExecutorType executorType;// 代理SqlSession接口的对象,也是SqlSessionTemplate的核心private final SqlSession sqlSessionProxy;private final PersistenceExceptionTranslator exceptionTranslator;//省略其他代码...
}
经过前面的分析,我们知道当调用代理Mapper接口的对象方法时,SqlSessionTemplate
最终会代替DefaultSqlSession
来完成Mapper接口的增删改查查操作,所以我们先来看下SqlSessionTemplate
的创建流程:
- 调用有参构造方法,将sqlSessionFactory传进来:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {this(sqlSessionFactory, sqlSessionFactory.getConfiguration().getDefaultExecutorType());}public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType) {this(sqlSessionFactory, executorType,new MyBatisExceptionTranslator(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(), true));}
- 最终会调用下面的构造方法:
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");notNull(executorType, "Property 'executorType' is required");this.sqlSessionFactory = sqlSessionFactory;this.executorType = executorType;this.exceptionTranslator = exceptionTranslator;// 关键:是不是很熟悉这种代码,这里面创建了一个代理了SqlSession接口的对象,并且最终该代理对象的逻辑会被SqlSessionInterceptor拦截到,因为它实现了InvocationHandler接口this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),new Class[] { SqlSession.class }, new SqlSessionInterceptor());}
可以看到,它创建了一个代理SqlSession接口的对象,最终代理对象的方法都会被SqlSessionInterceptor拦截到
- 我们看下SqlSessionTemplate的一些其他方法:
@Overridepublic <E> List<E> selectList(String statement, Object parameter) {return this.sqlSessionProxy.selectList(statement, parameter);}@Overridepublic <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {return this.sqlSessionProxy.selectList(statement, parameter, rowBounds);}@Overridepublic int insert(String statement) {return this.sqlSessionProxy.insert(statement);}@Overridepublic int update(String statement, Object parameter) {return this.sqlSessionProxy.update(statement, parameter);}@Overridepublic int delete(String statement, Object parameter) {return this.sqlSessionProxy.delete(statement, parameter);}
可以看到,当代理Mapper接口的对象执行增删改查查方法时,会被MapperProxy对象拦截到,然后由SqlSessionTemplate对象来处理,最终都会交由自己内部的sqlSessionProxy对象处理,而由于sqlSessionProxy也是个代理对象,它又会被SqlSessionInterceptor拦截来处理,所以接下来看下SqlSessionInterceptor做了什么处理,也是本篇文章问题的答案所在
5)SqlSessionInterceptor拦截逻辑
先来看下SqlSessionInterceptor的源码如下,它是SqlSessionTemplate的内部类:
private class SqlSessionInterceptor implements InvocationHandler {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1.获取SqlSession对象,见后面分析SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);try {// 2.执行DefaultSqlSession的方法Object result = method.invoke(sqlSession, args);// 3.判断是否开启了事务,见后面分析if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// force commit even on non-dirty sessions because some databases require// a commit/rollback before calling close()// 4.没有开启,则提交事务sqlSession.commit(true);}return result;} catch (Throwable t) {Throwable unwrapped = unwrapThrowable(t);if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {// 5.关闭会话,见后面分析closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);// 6.置为null,finally块就不会重复执行closeSqlSession方法了sqlSession = null;Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);if (translated != null) {unwrapped = translated;}}throw unwrapped;} finally {if (sqlSession != null) {// 7.关闭会话closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);}}}
}
当我们执行Mapper接口的增删改查查方法时,最终都会执行到SqlSessionInterceptor的invoke方法,接下来分析下invoke方法的逻辑。
①获取SqlSession对象流程
- 首先会调用如下方法获取一个SqlSession对象:
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
- SqlSessionUtils#getSqlSession 方法如下:
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator) {notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);// 1.从TransactionSynchronizationManager(以下称当前线程事务管理器)获取当前线程threadLocal是否有SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);// 2.如果获取到了,则从SqlSessionHolder中拿到SqlSession对象返回SqlSession session = sessionHolder(executorType, holder);if (session != null) {return session;}LOGGER.debug(() -> "Creating a new SqlSession");// 3.由SqlSessionFactory创建一个DefaultSqlSession对象,和使用mybaits手动创建DefaultSqlSession的方法一样session = sessionFactory.openSession(executorType);// 4.将SqlSession对象封装到SqlSessionHolder对象中,并保存到当前线程事务管理器中registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);return session;}
接下来依次分析getSqlSession中调用的方法
- TransactionSynchronizationManager#getResource方法流程:
先来看看当前线程事务管理器的结构:
public abstract class TransactionSynchronizationManager {// 存储当前线程事务资源,比如Connection、session等private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal<>("Transactional resources");// 存储当前线程事务同步回调器// 当有事务,该字段会被初始化,即激活当前线程事务管理器private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal<>("Transaction synchronizations");// 省略其他代码...
}
TransactionSynchronizationManager#getResource方法如下:
public static Object getResource(Object key) {Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key);// 根据SqlSessionFactory对象,从resources中获取SqlSessionHolder对象Object value = doGetResource(actualKey);if (value != null && logger.isTraceEnabled()) {logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" +Thread.currentThread().getName() + "]");}return value;}
它会接着调用doGetResource方法:
@Nullableprivate static Object doGetResource(Object actualKey) {// 1.从resources中获取当前线程的事务资源Map<Object, Object> map = resources.get();if (map == null) {return null;}// 2.如果事务资源存在,则根据将SqlSessionFactory对象作为Key,去获取一个SqlSessionHolder对象Object value = map.get(actualKey);// Transparently remove ResourceHolder that was marked as void...if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) {map.remove(actualKey);// Remove entire ThreadLocal if empty...if (map.isEmpty()) {resources.remove();}value = null;}// 3.返回SqlSessionHolder对象return value;}
- 当拿到SqlSessionHolder对象后,会执行sessionHolder方法来获取SqlSession对象:
private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {SqlSession session = null;if (holder != null && holder.isSynchronizedWithTransaction()) {if (holder.getExecutorType() != executorType) {throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");}holder.requested();LOGGER.debug(() -> "Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");// 从SqlSessionHolder中获取到SqlSession对象session = holder.getSqlSession();}return session;}
- 如果能从SqlSessionHolder中获取到SqlSession 对象,则直接返回,否则会执行下面的方法去创建一个DefaultSqlSession对象:
session = sessionFactory.openSession(executorType);
- 当创建了SqlSession对象之后,会接着执行registerSessionHolder方法:
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {SqlSessionHolder holder;// 1.判断当前是否有事务if (TransactionSynchronizationManager.isSynchronizationActive()) {// 2.判断当前环境配置的事务管理工厂是否是SpringManagedTransactionFactory(默认)Environment environment = sessionFactory.getConfiguration().getEnvironment();if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {LOGGER.debug(() -> "Registering transaction synchronization for SqlSession [" + session + "]");// 3.创建一个SqlSessionHolder对象holder = new SqlSessionHolder(session, executorType, exceptionTranslator);// 4.绑定当前SqlSessionHolder到线程ThreadLocal中,即ThreadLocal<Map<Object, Object>> resources中TransactionSynchronizationManager.bindResource(sessionFactory, holder);// 5.注册SqlSession同步回调器到线程的本地变量synchronizations中TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));holder.setSynchronizedWithTransaction(true);// 会话引用次数+1holder.requested();} else {if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {LOGGER.debug(() -> "SqlSession [" + session+ "] was not registered for synchronization because DataSource is not transactional");} else {throw new TransientDataAccessResourceException("SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");}}} else {LOGGER.debug(() -> "SqlSession [" + session+ "] was not registered for synchronization because synchronization is not active");}}
注册SqlSession到当前线程事务管理器的条件首先是当前环境中有事务,否则不注册,判断是否有事务的条件是synchronizations的ThreadLocal是否为空:
public static boolean isSynchronizationActive() {return (synchronizations.get() != null);
}
每当我们开启一个事务(声明式、编程式),会调用initSynchronization()方法进行初始化synchronizations,以激活当前线程事务管理器:
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =new NamedThreadLocal<>("Transaction synchronizations");public static void initSynchronization() throws IllegalStateException {if (isSynchronizationActive()) {throw new IllegalStateException("Cannot activate transaction synchronization - already active");}logger.trace("Initializing transaction synchronization");synchronizations.set(new LinkedHashSet<>());}
后续事务管理器AbstractPlatformTransactionManager 可以从synchronizations获取到SqlSessionHolder对象中的SqlSession来对事务管理,比如关闭Sqlsession。
②事务提交时机
当获取到SqlSession对象之后,接下来会执行以下方法:
Object result = method.invoke(sqlSession, args);// 判断有没有开启事务if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {// 提交当前事务sqlSession.commit(true);}
查看SqlSessionUtils#isSqlSessionTransactional方法如下:
public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {notNull(session, NO_SQL_SESSION_SPECIFIED);notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);// 从线程的本地变量中获取SqlSessionHolderSqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);// 如果SqlSessionHolder不为null说明开启了事务,返回truereturn (holder != null) && (holder.getSqlSession() == session);}
从前面的分析中,我们知道只有当开启事务时,才会将SqlSessionHolder 对象保存到线程的本地变量ThreadLocal<Map<Object, Object>> resources
中,所以如果没有开启事务的话,是不会保存的。
为什么要判断是否开启事务以控制当前事务提交?
例如:在一个方法上标注了@Transaction注解说明开启了事务,里面执行的方法都为增删改查的逻辑:
@Transactional(rollbackFor = Exception.class)
public void insertData(Item item, ItemStock itemStock) {itemStockMapper.save(itemStock);int i = 1 / 0;itemMapper.save(item);
}
如果没有这个判断逻辑,当该方法执行完itemStockMapper.save(itemStock),便会提交事务了,后面即使报错了也不会回滚了。正是因为有了这个判断,才不会出现这种情况,将标注@Transaction注解方法内的所有增删改查操作都看作一个整体事务,只有第一个增删改查方法执行时才会创建SqlSession对象,后续的每个增删改查方法执行时都能从线程的本地变量中获取到同一个SqlSession对象来使用,而只有当全部增删改查操作执行完成,才会提交事务。
那标注了@Transaction注解的方法是怎么提交的事务?
刚才看到了,只要没有开启事务并且没有报错,Spring会自动帮我们把事务提交了,这也就是为什么我们平常写代码不需要手动提交事务的原因。
而标注了@Transaction注解的提交事务时机又有所不同,这里不展开代码分析了,分析下大致逻辑:
- 当一个public方法被标注
@Transaction
注解之后(后续简称目标方法),Spring会基于AOP给这个方法所在的类创建一个代理对象 - 并且会给这个代理对象,创建出一个方法拦截器
TransactionInterceptor
- 假设这个代理对象是JDK代理的,那当我们执行这个代理对象的方法时,最终会执行到
JdkDynamicAopProxy
的invoke方法 - 接着里面会根据方法对象和方法对象的hashcode去
Map<MethodCacheKey, List<Object>> methodCache
这个缓存中尝试获取拦截器链 - 如果没有获取到,说明是第一次执行方法,则会从
ProxyFactory
(此对象在创建代理对象时会被保存在JdkDynamicAopProxy
中)获取增强器链 - 接着遍历增强链,如果不是方法拦截器则适配成方法拦截器,此时就获取到了
TransactionInterceptor
这个拦截器对象 - 拿到拦截器链之后,就会按照顺序执行拦截器链中的拦截器方法以及目标方法
- 其中
TransactionInterceptor
会比目标方法先执行,它会在目标方法执行之前开启事务,如果目标方法执行过程中报错,它会控制事务回滚,当目标方法执行完成之后,它才会控制事务提交。不过事务的处理是交由PlatformTransactionManager
这个事务管理器来处理的。
③closeSqlSession方法分析
无论是正常提交还是异常回滚,都会执行这个关闭会话的方法:
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {notNull(session, NO_SQL_SESSION_SPECIFIED);notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);// 1.从线程本地变量中获取SqlSessionHolder SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);if ((holder != null) && (holder.getSqlSession() == session)) {LOGGER.debug(() -> "Releasing transactional SqlSession [" + session + "]");// 2.能获取到,说明开启了事务,则不能关闭会话,减少会话引用次数holder.released();} else {LOGGER.debug(() -> "Closing non transactional SqlSession [" + session + "]");// 3.如果没有开启事务,则直接关闭会话session.close();}}
6)开启事务后,关闭会话的时机分析
在前面的分析中,当方法标注了@Transaction注解代表开了事务,则每次执行里面的子方法时都会从本地变量中获取到SqlSession对象,并且会话引用次数加一。在closeSqlSession方法逻辑中,只是将会话引用次数减一,并没有执行关闭会话的逻辑,那标注了@Transaction注解的方法什么时候才会关闭会话呢?
- 当方法标注@Transaction注解之后,Spring会给当前类生成一个代理对象,并且事务处理的拦截器为
TransactionInterceptor
,所以当我们执行事务注解标注的方法时,假设没有异常的情况下,最终调用链路如下:
TransactionInterceptor#invoke
→TransactionAspectSupport#invokeWithinTransaction
→TransactionAspectSupport#commitTransactionAfterReturning
→AbstractPlatformTransactionManager#commit
→AbstractPlatformTransactionManager#processCommit
→AbstractPlatformTransactionManager#triggerBeforeCompletion
→ SqlSessionSynchronization#beforeCompletion
- 首先会从前面分析获取SqlSession对象时的
synchronizations
本地变量中获取到SqlSessionSynchronization
对象,里面保存了SqlSessionHolder
对象,在整体事务提交之前会执行SqlSessionSynchronization
对象的beforeCompletion
方法:
@Override
public void beforeCompletion() {// Issue #18 Close SqlSession and deregister it now// because afterCompletion may be called from a different thread// 1.判断会话引用次数是否大于0if (!this.holder.isOpen()) {// 2.小于等于0,说明@Transaction注解标注的方法里面的所有增删改查方法都执行完成了,可以进行会话关闭了LOGGER.debug(() -> "Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");// 3.从线程的本地变量中移除SqlSessionHolderTransactionSynchronizationManager.unbindResource(sessionFactory);this.holderActive = false;LOGGER.debug(() -> "Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");// 4.从SqlSessionHolder中获取SqlSession对象,执行会话关闭方法this.holder.getSqlSession().close();}
}// ResourceHolderSupport类的方法,方便查看
public boolean isOpen() {return (this.referenceCount > 0);}
因此,当我们开启事务之后,同一个事务的方法执行时,由于它们同属于一个SqlSession
会话,都会将会话引用次数加一,每个方法执行完成会将会话引用次数减一,当整个方法都执行完成之后,会话引用次数递减为0,最终Spring会判断会话引用次数是否大于0,如果大于0则不关闭会话,小于等于0才会关闭。
来个例子说明下:
@Transactional(rollbackFor = Exception.class)
public void insertData(Item item, ItemStock itemStock) {itemStockMapper.save(itemStock);itemMapper.save(item);
}
-
当执行insertData方法时,其实调用的是代理对象的方法,最终会被TransactionInterceptor拦截到,在目标对象的insertData执行前,会由
AbstractPlatformTransactionManager
开启事务 -
当执行到itemStockMapper.save(itemStock)时,此时执行的也是代理了ItemStockMapper接口的对象方法,最终会执行到
SqlSessionInterceptor
的invoke方法:- 前面也分析过了,第一次执行invoke方法,此时线程的本地变量没有SqlSessionHolder,所以会去创建SqlSession对象,并把它放入SqlSessionHolder对象中,接着会把SqlSessionHolder放入本地变量中,并对会话引用计数加一
- 当itemStockMapper.save(itemStock)执行完成之后,此时会把会话引用计数减一,并没有提交事务
-
当执行到itemMapper.save(item)时,此时执行的也是代理了ItemMapper接口的对象方法,最终会执行到
SqlSessionInterceptor
的invoke方法:- 这是第二次调用invoke方法,所以可以在本地变量中获取到SqlSession对象,并对会话引用计数加一
- 当itemMapper.save(item)执行完成之后,此时会话引用计数减一变为0了,此时还没有提交事务
-
当整个insertData方法都执行完成之后,代表整个事务都完成了,此时会由
AbstractPlatformTransactionManager
来提交事务,并且在提交的时候会判断会话引用计数是否大于0,如果小于等于0则关闭会话
三、总结
本文大致讲解了Mybatis手动创建SqlSession的流程,引入Spring之后为什么就不需要手动去创建SqlSession,以及Spring创建SqlSession的时机原理。
当我们引入Spring之后的处理:
- 自动扫描标注@Mapper的接口,生成一个代理对象,其中代理对象的增删改查操作最终会由
SqlSessionTemplate
来执行 SqlSessionTemplate
会生成一个代理SqlSession接口的对象,由该代理对象帮我们管理SqlSession
的创建,当方法上标注了@Transactional
注解,则该方法里面多次访问数据库用的是同一个SqlSession,否则每次调用方法都会去创建一个SqlSessionTransactionInterceptor
会拦截标注@Transaction注解的方法,通过事务管理器PlatformTransactionManager
来对当前事务进行管理,包括正常提交、异常回滚、关闭会话等操作- 在开启事务后,
SqlSessionHolder
对象起着很大重用,它保存着首次创建的SqlSession对象,并且它会存储在线程的两个本地变量中:TransactionSynchronizationManager.resources
:主要作用是给事务方法里面的多个子方法使用,保证了它们能从这个本地变量中获取同一个SqlSession对象(多线程情况除外)TransactionSynchronizationManager.synchronizations
:主要作用是由事务管理器AbstractPlatformTransactionManager从本地变量中获取SqlSession对象,来对全局事务进行管理。并且当它获取的值不为空时说明开启了事务,才会将SqlSessionHolder
对象保存在TransactionSynchronizationManager.resources
中
其实底层都是基于动态代理和AOP切面拦截的思想,通过这些机制和线程本地变量,让不同事务创建不同的SqlSession对象,让同一个事务共享同一个SqlSession对象,保证了线程安全。
最后再来一个例子:哪些方法会回滚?
在 insertData方法中里面调用了saveItem方法和saveItemStock方法,并且通过一个新线程调用了 saveItemStock,在 saveItemStock中抛出了异常,这些方法都开启了事务。
@Transactional
public void insertData(Item item, ItemStock itemStock) {itemStockService.saveItemStock(itemStock);new Thread(() -> {try {itemService.saveItem(item);} catch (Exception e) {throw new RuntimeException();}}).start();
}@Transactional
public void saveItemStock(ItemStock itemStock) {save(itemStock);throw new RuntimeException("111");
}@Transactional
public void saveItem(Item item) {save(item);
}
结果:saveItem不回滚、 saveItemStock 回滚:
- 这里相当于两个线程调用不同的事务方法,而每个线程不会共享自己的SqlSession
- saveItem无法回滚是因为没有捕获到新线程中抛出的异常
- saveItemStock方法可以回滚,是因为事务管理器只对当前线程中的事务有效
所以开启事务后,在多线程环境下事务管理器并不会跨线程传播事务,事务的状态是存储在线程的本地ThreadLocal
中, 方便后续管理当前线程的事务上下文信息。这也意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。
这篇文章断断续续写了好几天,再加上自己的表达能力有限,所以写起来有点乱,见谅见谅,如果有错误的地方欢迎指正!
相关文章:
Spring中每次访问数据库都要创建SqlSession吗?
一、SqlSession是什么二、源码分析1)mybatis获取Mapper流程2)Spring创建Mapper接口的代理对象流程3)MapperFactoryBean#getObject调用时机4)SqlSessionTemplate创建流程5)SqlSessionInterceptor拦截逻辑6)开…...
力扣刷题TOP101:6.BM7 链表中环的入口结点
目录: 目的 思路 复杂度 记忆秘诀 python代码 目的 {1,2},{3,4,5}, 3 是环入口。 思路 这个任务是找到带环链表的环入口。可以看作是上一题龟兔赛跑(Floyd 判圈算法)的延续版:乌龟愤愤不平地举报兔子跑得太快,偷偷…...
浅谈telnet和ping
telnet 和 ping 是网络诊断工具,用于测试网络连接性和故障排查,但它们有不同的用途和功能。以下是它们的主要区别: 1. ping 功能描述 用途:ping 命令用于测试主机与目标地址(IP或域名)之间的连通性。工作…...
P4-3【应用数组进行程序设计 | 第三节】——知识要点:字符数组
知识要点:字符数组 视频: P4-3【应用数组进行程序设计 | 第三节】——知识要点:字符数组 目录 一、任务分析 二、必备知识与理论 三、任务实施 一、任务分析 本任务要求输入一行字符,统计其中的单词数,单词之间用…...
彻底理解微服务配置中心的作用
常见的配置中心有SpringCloudConfig、Apollo、Nacos等,理解它的作用,无非两点,一是配置中心能做什么,不使用配置中心会出现什么问题。 作用:配置中心是用来集中管理服务的配置,它是用来提高系统配置的维护…...
SpringBoot开发——详细讲解 Spring Boot 项目中的 POM 配置
文章目录 一、POM 文件简介二、单模块项目的 POM 配置1. 创建基本的 Spring Boot 单模块项目2. 重点解析三、多模块项目的 POM 配置1. 多模块项目结构2. 父模块 POM 文件3. 子模块 POM 文件4. 重点解析结语在 Spring Boot 项目中,POM(Project Object Model)文件起着关键作用…...
pyspark实现基于协同过滤的电影推荐系统
最近在学一门大数据的课,课程要求很开放,任意做一个大数据相关的项目即可,不知道为什么我就想到推荐算法,一直到着手要做之前还没有新的更好的来代替,那就这个吧。 推荐算法 推荐算法的发展由来已久,但和…...
视觉语言模型(VLM)学习笔记
目录 应用场景举例 VLM 的总体架构包括: 深度解析:图像编码器的实现 图像编码器:视觉 Transformer 注意力机制 视觉-语言投影器 综合实现 训练及注意事项 总结 应用场景举例 基于文本的图像生成或编辑:你输入 “生成一张…...
学习笔记:黑马程序员JavaWeb开发教程(2024.11.29)
10.5 案例-部门管理-新增 如何接收来自前端的数据: 接收到json数据之后,利用RequestBody注解,将前端响应回来的json格式的数据封装到实体类中 对代码中Controller层的优化 发现路径中都有/depts,可以将每个方法对应请求路径中的…...
文档加密怎么做才安全?
公司的文档包含很多机密文件,这些文件不仅关乎公司的核心竞争力,还涉及到客户隐私、商业策略等敏感信息。因此,文档的保管和传递一直是我们工作的重中之重。 为了确保机密文件的安全,公司需要制定了一系列严格的保密措施。从文件的…...
使用Setup Factory将C#的程序打包成安装包
一、软件下载 https://download.csdn.net/download/qq_65356682/90042701 可以直接下载 二、软件使用 打开 1、创建一个新的项目 2、设置如下信息,也可以不设置,最好填非空的、 产品名就是你安装成功后生成文件的名称 3、如下文件夹路径就是你C#中ex…...
解决 java -jar 报错:xxx.jar 中没有主清单属性
问题复现 在使用 java -jar xxx.jar 命令运行 Java 应用程序时,遇到了以下错误: xxx.jar 中没有主清单属性这个错误表示 JAR 文件缺少必要的启动信息,Java 虚拟机无法找到应用程序的入口点。本文将介绍该错误的原因以及如何通过修改 pom.xm…...
Java HashSet 介绍
怀旧网个人博客网站地址:怀旧网,博客详情:Java HashSet 介绍 哈希值介绍 创建一个实体类 public class Student {private String name;private int age;public Student(String name, int age) {this.name name;this.age age;} }使用测试…...
2024年几款免费的AI对话工具介绍
目前几款免费的AI对话工具介绍 文章目录 目前几款免费的AI对话工具介绍一、前言二、AI对话工具介绍1、讯飞星火认知大模型2、百度文心一言3、通义千问4、豆包5、百川大模型6、智谱清言7、月子暗面-KIMI下面是国外的 AI 对话工具: 8、Replika8、Cleverbot9、Coze 三、…...
Gazebo构建模型(含GNSS、IMU、LiDAR、Camera传感器)
将GNSS、IMU、LiDAR、Camera传感器和机器人的base分别放在不同的文件中。这样可以提高模型的可维护性和模块化。下面是一个示例,展示如何将这些部分分别放在不同的.xacro文件中,然后通过导入的方式组合在一起。 1. 创建基础文件:my_robot.xa…...
#Js篇: 链式判断运算符 ?.和Null判断运算符 ??和逻辑赋值运算符||= = ??=
链式判断运算符 ?. ?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。如果是的,就不再往下运算,而是返回undefined。 链判断运算符?.有三种写法。 obj?.prop // 对象属性是否存在 obj?.[expr] // 同上…...
IDEA敲Web前端快捷键
1.html基础格式 英文符号TAB键 <!doctype html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport"content"widthdevice-width, user-scalableno, initial-scale1.0, maximum-scale1.0, mini…...
【Vue3】【Naive UI】<NDropdown>标签
【Vue3】【Naive UI】 标签 基本设置自定义渲染交互事件其他属性 【VUE3】【Naive UI】<NCard> 标签 【VUE3】【Naive UI】<n-button> 标签 【VUE3】【Naive UI】<a> 标签 【VUE3】【Naive UI】<…...
技术总结(四十一)
一、MySQL 索引概述 索引的概念:索引就好比一本书的目录,它能帮助 MySQL 快速定位到表中的数据行,而不用全表扫描。通过创建合适的索引,可以大大提高查询的效率。例如,在一个存储了大量员工信息的表中,如果…...
Android布局
一、线性布局 属性:orientation vertical horizontal layout_weight【水平均分,width"0dp"】 layout_height layout_width 小动物连连看 1<?xml version"1.0" encoding"utf-8"?>2<LinearLayout xmlns:and…...
k8s集成skywalking
如果能科学上网的话,安装应该不难,如果有问题可以给我留言 本篇文章我将给大家介绍“分布式链路追踪”的内容,对于目前大部分采用微服务架构的公司来说,分布式链路追踪都是必备的,无论它是传统微服务体系亦或是新一代…...
如何写一份优质技术文档
作者简介: 本文作者拥有区块链创新专利30,是元宇宙标准化工作组成员、香港web3标准工作组成员,参与编写《数据资产确权与交易安全评价标准》、《链接元宇宙:应用与实践》、《香港Web3.0标准化白皮书》等标准,下面提供…...
LeetCode:206.反转链表
跟着carl学算法,本系列博客仅做个人记录,建议大家都去看carl本人的博客,写的真的很好的! 代码随想录 LeetCode:206.反转链表 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。 示例…...
详解高斯消元
详解高斯消元 好东西,可以求所有一次方程组的解。 \color {red} 好东西,可以求所有一次方程组的解。 好东西,可以求所有一次方程组的解。 前置知识 一般消元法的公理: 两方程互换,解不变; 一方程乘以非零数 k k k,解不变; 一方程乘以数 k k k加上另一方程,解不变。 …...
Maven - 优雅的管理多模块应用的统一版本号
文章目录 概述一、使用 versions-maven-plugin 插件1. 在主 pom.xml 中定义插件2. 修改版本号3. 回退修改4. 提交修改 二、使用占位符统一管理版本号1. 在主 pom.xml 中定义占位符2. 使用 flatten-maven-plugin 插件自动替换占位符3. 修改版本号4. 为什么这种方式更方便&#x…...
国际网络安全趋势
1. 亲近拥抱人工智能自动化。 随着安全协调、人工智能自动化和响应(SOAR)的日益普及,人工智能自动化开始成为现实并将继续扩展到其他安全行动领域。寻求将人工智能自动化整合到原有的工具中,通过将威胁情报整合在一起,将其转换为可用格式并主…...
基于米尔全志T527开发板的FacenetPytorch人脸识别方案
本篇测评由优秀测评者“小火苗”提供。 本文将介绍基于米尔电子MYD-LT527开发板(米尔基于全志 T527开发板)的FacenetPytorch人脸识别方案测试。 一、facenet_pytorch算法实现人脸识别 深度神经网络 1.简介 Facenet-PyTorch 是一个基于 PyTorch 框架实…...
Altium Designer脚本工具定制
原理图设计自动化 ➡️Altium原理图检查工具 ➡️元器件参数集导入导出 ➡️原理图符号自动创建 ➡️原理图高级查找 ➡️原理图库文档高级查找 ➡️原理图文档对比 ➡️原理图库文档对比 PCB设计自动化 ➡️各种各样的PCB线圈自动创建 ➡️PCB文档导出成SVG格式文档…...
贝锐自研智慧网关系统OrayOS升级,适配Banana PI开发板BPI-R3 Mini
为了满足多元化的应用场景,贝锐与Banana PI携手合作,贝锐自研新一代云智慧网关系统OrayOS不仅已成功适配BPI-R3,还进一步扩展至BPI-R3 Mini,提供了更丰富的选择。在全球工业物联网、视频监控管理以及企业级办公存储等领域…...
搭建环境-PHP简介及环境搭建教程
搭建环境-PHP简介及环境搭建教程 前言 在现代Web开发中,PHP是一种广泛使用的服务器端脚本语言,它以简洁、高效和跨平台的特性受到开发者的青睐。无论是小型网站还是大型企业应用,PHP都能提供强大的支持。本文将为您详细介绍PHP的基本概念、特点,以及如何搭建PHP开发环境。…...
网站怎么弄模板/出词
UR-介绍1 资料1.1 简介1.2 资料1.3 网站2 远程用户密码3 通信3.1 The overview of four options is as below3.2 UR CB-Series3.3 UR e-Series4 URScript5 机器人状态读取6 工控机7 控制器芯片8 数据定义参考1 资料 1.1 简介 UR机器人作为目前使用广泛的协作机器人࿰…...
东莞网站建设_东莞网页设计/自己如何注册网站
B2C(Business to Customer)。B2C中的B是Business,意思是企业,2则是to的谐音,C是Customer,意思是消费者,所以B2C是企业对消费者的电子商务模式。这种形式的电子商务一般以网络零售业为主,主要借助于Internet…...
上海专业网站建设网站/百度搜索竞价推广
Xor and Sum 之前做过一道异或的。感觉有点眼熟,发现不是。由于对异或一点也不熟悉。所以直接放弃了 首先写出来几项看看。 a: 1 2 4 1 1 2 4 prex : 1 3 7 6 7 5 1 prey: 1 3 7 8 9 11 15 可以…...
温州网站建设制作设计公司/定制网站制作公司
--创建一个person表 create table person( pid number(20), pname varchar2(10) );--添加一列 alter table person add( gender number(1) );--修改列类型 char为定长 alter table person modify( gender char(1) );--修改列名称 gender为sex alter table pers…...
织梦网站地图生成/百度收录提交网站后多久收录
最近遇到考题:一个互联网产品如何从无到有聚集用户?对此,我分了3个阶段来进行论述。(中间加了一些孙子兵法的观点,学习孙子兵法,对做产品也有一定的指导思想。希望更多的人能学习国学,爱国学。多学国学,就…...
三六五网做网站吗/免费网站模板
作为局域网的主要连接设备,以太网交换机成为应用普及最快的网络设备之一,同时,也是随着这种快速的发展,交换机的功能不断增强,随之而来则是交换机端口的更新换代以及各种特殊设备连接端口不断的添加到交换机上…...