Spring中每次访问数据库都要创建SqlSession吗?

news2024/11/30 6:19:55

    • 一、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的创建过程:

  1. 从Configuration配置类中拿到Environment数据源;
  2. 从数据源中获取TransactionFactory和DataSource,并创建一个Transaction连接管理对象;
  3. 创建Executor对象(SqlSession只是所有操作的门面,真正要干活的是Executor,它封装了底层JDBC所有的操作细节);
  4. 创建SqlSession会话。

每次创建一个SqlSession会话,都会伴随创建一个专属SqlSession的连接管理对象,如果SqlSession共享,就会出现事务问题。

二、源码分析

1)mybatis获取Mapper流程

先回顾以下传统mybatis创建Mapper接口的代理对象流畅如下:

  1. 如果没有引入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.获取代理对象,执行SQL
  UserDao userDao = sqlSession.getMapper(UserDao.class);
  List<User> userList = userDao.getUserList();
  for (User user : userList) {
      System.out.println(user);
  }

  //关闭sqlSession
  sqlSession.close();

  1. 查看DefaultSqlSession的getMapper方法如下:
@Override
public <T> T getMapper(Class<T> type) {
		return configuration.getMapper(type, this);
}
  1. 接着会调用到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);
    }
  }
  1. 接着调用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);
  }
}
  1. 因为MapperProxy实现了InvocationHandler接口,所以代理对象调用方法时,会先经过MapperProxy#invoke方法
  private final Map<Method, MapperMethod> methodCache;
  
  @Override
  public 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#execute
    final 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()));
  }
  1. 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 对象会调用ParameterHandlersetParameters方法来实现参数设置,里面会调用TypeHandler 对象方法来完成Java类型到数据库类型的转换
  • 在处理结果集时,PreparedStatementHandler 对象会调用ResultSetHandlerhandleResultSets方法来实现结果集映射,里面会调用TypeHandler 对象方法来完数据库类型到Java类型的转换

在这里插入图片描述

回顾了mybatis执行的大致原理,都是依靠DefaultSqlSession的方法,那引入了spring为什么就不需要我们手动创建sqlSession了呢,接下来接着分析

2)Spring创建Mapper接口的代理对象流程

  1. 当我们在接口标注一个@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;
  }

  @Override
  protected 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对象
  @Override
  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

 
  @Override
  public Class<T> getObjectType() {
    return this.mapperInterface;
  }

  @Override
  public boolean isSingleton() {
    return true;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }
}

  1. 当Spring填充某个Bean的字段时,如果根据字段名称能从一级缓存获取到了Bean实例,并且该Bean实现了FactoryBean接口,则会调用该Bean的getObject方法,来获取真正的Bean来注入到对应字段中:
@Override
public T getObject() throws Exception {
  return getSqlSession().getMapper(this.mapperInterface);
}
  1. 里面会调用父类的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;
  }

  @Override
  protected void checkDaoConfig() {
    notNull(this.sqlSessionTemplate, "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");
  }

}

可以看到,getSqlSession方法最终会返回一个已经创建好的SqlSessionTemplate对象,它底层实现了SqlSession接口,并且每个MapperFactoryBean对象都会持有同一个SqlSessionTemplate 对象,因为它们都继承了同一个抽象父类SqlSessionDaoSupport 的sqlSessionTemplate字段

打断点发现一级缓存中的两个MapperFactoryBean对象确实持有相同的SqlSessionTemplate 引用,如下所示:

在这里插入图片描述

在这里插入图片描述

  1. 当返回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对象
	@Override
	public 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调用时机

  1. 当我注入一个Mapper接口对象时,它会调用doGetBean方法,根据bean的名称从一级缓存中获取到对应的MapperFactoryBean对象:

在这里插入图片描述

  1. 来看看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
	@Nullable
	protected Object getCachedObjectForFactoryBean(String beanName) {
		return this.factoryBeanObjectCache.get(beanName);
	}
	
	// 缓存代理Bean
	private final Map<String, Object> factoryBeanObjectCache = new ConcurrentHashMap<>(16);
  1. 此时传进来的beanInstance是MapperFactoryBean实例,显然是工厂Bean对象,所以接下来会执行getObjectFromFactoryBean方法:
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
    // 1.判断是不是单例,一级缓存中是否存在该工厂Bean对象,很显然是有的
		if (factory.isSingleton() && containsSingleton(beanName)) {
			synchronized (getSingletonMutex()) {
				// 2.再次从缓存中获取代理Bean
				Object 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;
		}
	}
  1. doGetObjectFromFactoryBean方法:

可以看到这里面就会通过getObject方法来获取代理了我们Mapper接口的对象,并且它持有一个MybatisMapperProxy 引用(mybatis-plus框架的对象,也实现了InvocationHandler 接口),MybatisMapperProxy 对象里面又会持有SqlSessionTemplate对象的引用,假设没有引入mybatis-plus框架,最终代理对象持有的是MapperProxy 引用

在这里插入图片描述

这里的SqlSessionTemplate对象和前面图片的SqlSessionTemplate对象不同,是因为我重启了项目。。

小结:

  1. 当标注Mapper注解的接口被扫描到时,会生成一个该接口对应的MapperFactoryBean对象,然后将接口名称(第一个字母小写)作为key,MapperFactoryBean对象作为value放入到一级缓存中,注意此时还没有创建Mapper接口的代理对象
  2. 当我们给一些类的字段注入Mapper接口的对象时,此时会走getBean流程,根据接口名称从一级缓存获取到了MapperFactoryBean对象,接着Spring会判断该Bean是不是FactoryBean类型
  3. 如果该Bean不是FactoryBean类型,直接返回
  4. 如果该Bean是FactoryBean类型,此时会尝试从factoryBeanObjectCache这个缓存中根据接口名称获取Mapper接口的代理对象
  5. 如果获取到,直接返回该代理Bean对象
  6. 如果获取不到,说明是第一次注入该Mapper接口的对象,则会去调用MapperFactoryBean的getObject方法来创建一个代理Mapper接口的对象返回
  7. 此时拿到的代理Bean对象还不能返回,会拿到所有的后置处理器尝试对该代理Bean对象做增强
  8. 将增强后的代理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 的创建流程:

  1. 调用有参构造方法,将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));
  }
  1. 最终会调用下面的构造方法:
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拦截到

  1. 我们看下SqlSessionTemplate的一些其他方法:

  @Override
  public <E> List<E> selectList(String statement, Object parameter) {
    return this.sqlSessionProxy.selectList(statement, parameter);
  }

  @Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    return this.sqlSessionProxy.selectList(statement, parameter, rowBounds);
  }
 
  @Override
  public int insert(String statement) {
    return this.sqlSessionProxy.insert(statement);
  }

  @Override
  public int update(String statement, Object parameter) {
    return this.sqlSessionProxy.update(statement, parameter);
  }

  @Override
  public 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 {
  @Override
  public 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对象流程

  1. 首先会调用如下方法获取一个SqlSession对象:
SqlSession sqlSession = getSqlSession(SqlSessionTemplate.this.sqlSessionFactory,
          SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator);
  1. 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是否有SqlSessionHolder
    SqlSessionHolder 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中调用的方法

  1. 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方法:

@Nullable
	private 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;
	}
  1. 当拿到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;
  }
  1. 如果能从SqlSessionHolder中获取到SqlSession 对象,则直接返回,否则会执行下面的方法去创建一个DefaultSqlSession对象:
 session = sessionFactory.openSession(executorType);
  1. 当创建了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同步回调器
        TransactionSynchronizationManager
            .registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
        holder.setSynchronizedWithTransaction(true);
        // 会话引用次数+1
        holder.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,以激活当前线程事务管理器:

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<>());
	}

所以当前有事务时,比如方法上标注了@Transactional 注解,会注册SqlSession到当前线程ThreadLocal中,保证事务方法里面的所有方法共用同一个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);
		// 从线程的本地变量中获取SqlSessionHolder
    SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
		// 如果SqlSessionHolder不为null说明开启了事务,返回true
    return (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注解的提交事务时机又有所不同,这里不展开代码分析了,分析下大致逻辑:

  1. 当一个public方法被标注**@Transaction注解之后(后续简称目标方法),**Spring会基于AOP给这个方法所在的类创建一个代理对象
  2. 并且会给这个代理对象,创建出一个方法拦截器TransactionInterceptor
  3. 假设这个代理对象是JDK代理的,那当我们执行这个代理对象的方法时,最终会执行到JdkDynamicAopProxy 的invoke方法
  4. 接着里面会根据方法对象和方法对象的hashcode去Map<MethodCacheKey, List<Object>> methodCache 这个缓存中尝试获取拦截器链
  5. 如果没有获取到,说明是第一次执行方法,则会从ProxyFactory (此对象在创建代理对象时会被保存在JdkDynamicAopProxy 中)获取增强器链
  6. 接着遍历增强链,如果不是方法拦截器则适配成方法拦截器,此时就获取到了TransactionInterceptor 这个拦截器对象
  7. 拿到拦截器链之后,就会按照顺序执行拦截器链中的拦截器方法以及目标方法
  8. 其中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注解的方法什么时候才会关闭会话呢?

  1. 当方法标注@Transaction注解之后,Spring会给当前类生成一个代理对象,并且事务处理的拦截器为TransactionInterceptor ,所以当我们执行事务注解标注的方法时,假设没有异常的情况下,最终调用链路如下:

TransactionInterceptor#invokeTransactionAspectSupport#invokeWithinTransactionTransactionAspectSupport#commitTransactionAfterReturningAbstractPlatformTransactionManager#commit

AbstractPlatformTransactionManager#processCommitAbstractPlatformTransactionManager#triggerBeforeCompletionSqlSessionUtils#beforeCompletion

  1. 从上面得知,在整体事务提交之前会执行SqlSessionUtilsbeforeCompletion 方法:
@Override
public void beforeCompletion() {
  // Issue #18 Close SqlSession and deregister it now
  // because afterCompletion may be called from a different thread
  // 1.判断会话引用次数是否大于0
  if (!this.holder.isOpen()) {
		// 2.小于等于0,说明@Transaction注解标注的方法里面的所有增删改查方法都执行完成了,可以进行会话关闭了
    LOGGER
        .debug(() -> "Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");
    // 3.从线程的本地变量中移除SqlSessionHolder
    TransactionSynchronizationManager.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);
}
  1. 当执行insertData方法时,其实调用的是代理对象的方法,最终会被TransactionInterceptor拦截到,在目标对象的insertData执行前,会由AbstractPlatformTransactionManager 开启事务

  2. 当执行到itemStockMapper.save(itemStock)时,此时执行的也是代理了ItemStockMapper接口的对象方法,最终会执行到SqlSessionInterceptor 的invoke方法:

    • 前面也分析过了,第一次执行invoke方法,此时线程的本地变量没有SqlSessionHolder,所以会去创建SqlSession对象,并把它放入SqlSessionHolder对象中,接着会把SqlSessionHolder放入本地变量中,并对会话引用计数加一
    • 当itemStockMapper.save(itemStock)执行完成之后,此时会把会话引用计数减一,并没有提交事务
  3. 当执行到itemMapper.save(item)时,此时执行的也是代理了ItemMapper接口的对象方法,最终会执行到SqlSessionInterceptor 的invoke方法:

    • 这是第二次调用invoke方法,所以可以在本地变量中获取到SqlSession对象,并对会话引用计数加一
    • 当itemMapper.save(item)执行完成之后,此时会话引用计数减一变为0了,此时还没有提交事务
  4. 当整个insertData方法都执行完成之后,代表整个事务都完成了,此时会由AbstractPlatformTransactionManager 来提交事务,并且在提交的时候会判断会话引用计数是否大于0,如果小于等于0则关闭会话

三、总结

本文大致讲解了Mybatis手动创建SqlSession的流程,引入Spring之后为什么就不需要手动去创建SqlSession,以及Spring创建SqlSession的时机原理。

当我们引入Spring之后:

  • 自动扫描标注@Mapper的接口,生成一个代理对象,其中代理对象的增删改查操作最终会由SqlSessionTemplate 来执行
  • SqlSessionTemplate 会生成一个代理SqlSession接口的对象,由该代理对象帮我们管理SqlSession的创建,当方法上标注了@Transactional 注解,则该方法里面多次访问数据库用的是同一个SqlSession,否则每次调用方法都会去创建一个SqlSession
  • TransactionInterceptor 会拦截标注@Transaction注解的方法,通过事务管理器PlatformTransactionManager 来对当前事务进行管理,包括正常提交、异常回滚、关闭会话等操作

其实底层都是基于动态代理和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 回滚:

  1. 这里相当于两个线程调用不同的事务方法,而每个线程不会共享自己的SqlSession
  2. saveItem无法回滚是因为没有捕获到新线程中抛出的异常
  3. saveItemStock方法可以回滚,是因为事务管理器只对当前线程中的事务有效

所以开启事务后,在多线程环境下事务管理器并不会跨线程传播事务,事务的状态是存储在线程的本地ThreadLocal 中, 方便后续管理当前线程的事务上下文信息。这也意味着每个线程都有一个独立的事务上下文,事务信息在不同线程之间不会共享。

这篇文章断断续续写了好几天,再加上自己的表达能力有限,所以写起来有点乱,见谅见谅,如果有错误的地方欢迎指正!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2250252.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据结构】填空集

基本术语 顺序队列在实现的时候&#xff0c;通常将数组看成是一个首尾相连的循环队列&#xff0c;这样做的目的是为避免产生&#xff08;溢出&#xff09;现象 数组q[M]&#xff08;M等于6&#xff09;存储一个循环队&#xff0c;first和last分别指向首尾指针。已知first2,la…

【趣味升级版】斗破苍穹修炼文字游戏HTML,CSS,JS

目录 图片展示 开始游戏 手动升级&#xff08;满100%即可升级&#xff09; 升级完成&#xff0c;即可解锁打怪模式 新增功能说明&#xff1a; 如何操作&#xff1a; 完整代码 实现一个简单的斗破苍穹修炼文字游戏&#xff0c;你可以使用HTML、CSS和JavaScript结合来构建…

【在Linux世界中追寻伟大的One Piece】多线程(三)

目录 1 -> Linux线程同步 1.1 -> 条件变量 1.2 -> 同步概念与竞态条件 1.3 -> 条件变量函数 1.4 -> 为什么pthread_cond_wait需要互斥量 1.5 -> 条件变量使用规范 2 -> 生产者消费者模型 2.1 -> 为什么要使用生产者消费者模型 2.2 -> 生产…

AI数据分析工具(一)

Looker Studio&#xff08;谷歌&#xff09;-免费 优点 免费使用&#xff1a;对于中小型企业和个人用户来说&#xff0c;没有任何费用压力&#xff0c;可以免费享受到数据可视化和报表创建的功能。与Google服务集成&#xff1a;特别适合使用Google产品生态的企业&#xff0c;…

个人博客接入github issue风格的评论,utteranc,gitment

在做个人博客的时候&#xff0c;如果你需要评论功能&#xff0c;但是又不想构建用户体系和评论模块&#xff0c;那么可以直接使用github的issue提供的接口&#xff0c;对应的开源项目有utteranc和gitment&#xff0c;尤其是前者。 它们的原理是一样的&#xff1a;在博客文章下…

springboot 配置跨域访问

什么是 CORS&#xff1f; CORS&#xff0c;全称是“跨源资源共享”&#xff08;Cross-Origin Resource Sharing&#xff09;&#xff0c;是一种Web应用程序的安全机制&#xff0c;用于控制不同源的资源之间的交互。 在Web应用程序中&#xff0c;CORS定义了一种机制&#xff0…

探索Python WebSocket新境界:picows库揭秘

文章目录 探索Python WebSocket新境界&#xff1a;picows库揭秘第一部分&#xff1a;背景介绍第二部分&#xff1a;picows库概述第三部分&#xff1a;安装picows库第四部分&#xff1a;简单库函数使用方法第五部分&#xff1a;场景应用第六部分&#xff1a;常见Bug及解决方案第…

【Linux】-学习笔记06

第二章、时间同步服务器 2.1时间同步服务器的使用 2.1.1系统时区时间的管理 timedatectl set-time "2024-02-13 10:41:55" ##设定系统时间 timedatectl list-timezones ##显示系统的所有时区 timedatectl set-timezone "Asia/Shangh…

Mac使用charles抓包

一、官网下载安装 二、配置Help--->SSL Proxying 有证书选择全部信任即可 三、设置系统代理&#xff0c;mac每次重启都需要选择&#xff0c;否则会没有数据 四、设置端口&#xff08;如果无法获取https&#xff09; 五、手机链接&#xff0c;从网页下载证书保存到手机&…

3d扫描建模产品开发-三维扫描检测蓝光检测

现当下&#xff0c;汽车制造、航空航天&#xff0c;还是消费电子、医疗器械&#xff0c;三维扫描检测与蓝光技术正以前所未有的精度和效率&#xff0c;推动着产品从概念到实物的快速转化。 三维扫描技术&#xff0c;简而言之&#xff0c;就是通过激光、结构光&#xff08;如蓝…

Hive中的基本数据类型和表的类型

Hive支持关系数据库中大多数据基本数据类型&#xff0c;同时还支持三种复杂类型。 示例&#xff1a; Hive表 创建表 – 直接建表法 create table t_page_view ( page_id bigint comment ‘页面ID’, page_name string comment ‘页面名称’, page_url string comment ‘页面…

Python PDF转JPG图片小工具

Python PDF转JPG图片小工具 1.简介 将单个pdf装换成jpg格式图片 Tip: 1、软件窗口默认最前端&#xff0c;不支持调整窗口大小&#xff1b; 2、可通过按钮选择PDF文件&#xff0c;也可以直接拖拽文件到窗口&#xff1b; 3、转换质量有5个档位&#xff0c;&#xff08;0.25&a…

剖析 SpringBoot 于夕阳红公寓管理系统架构搭建的核心作用

3 系统分析 本文作者在确定了研究的课题之后&#xff0c;从各大数字图书馆下载文献来阅读&#xff0c;并了解同类型的网站具备的大致功能&#xff0c;然后与本系统用户的实际需求结合进行分析&#xff0c;得出本系统要研究的具体功能与性能。虽然分析系统这一阶段性工作主要是确…

springboot336社区物资交易互助平台pf(论文+源码)_kaic

毕 业 设 计&#xff08;论 文&#xff09; 社区物资交易互助平台设计与实现 摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff…

嵌入式linux系统中网络编程的具体实现

1.网络编程简介 要编写通过计算机网络通信的程序,首先要确定这些程序同通信的协议(protocol),在设计一个协议的细节之前,首先要分清程序是由哪个程序发起以及响应何时产生。 举例来说,一般认为WEB服务器程序是一个长时间运行的程序(守护进程deamon),它只在响应来自网…

vue3.0 根据富文本html页面生成压缩包(含视频在线地址、图片在线地址、前端截图、前端文档)

vue3.0生成压缩包&#xff08;含在线地址、前端截图、前端文档&#xff09; 需求描述效果开始下载插件包基本代码构造 点击下载按钮1.截图content元素&#xff0c;并转化为pdfcanvas putImageData、getImageDatagetImageData 获取指定矩形区域的像素信息putImageData 将这些数据…

由于导包而引发的错误

今天在调试时发现删除功能无论如何都无法实现&#xff0c;于是调试找到了mapper层的错误但不知道为什么报错。以下是报错信息。 Caused by: org.apache.ibatis.binding.BindingException: Parameter userIds not found. Available parameters are [arg0, collection, list]at o…

黑马2024AI+JavaWeb开发入门Day04-SpringBootWeb入门-HTTP协议-分层解耦-IOCDI飞书作业

视频地址&#xff1a;哔哩哔哩 讲义作业飞书地址&#xff1a;day04作业&#xff08;IOC&DI&#xff09; 作业很简单&#xff0c;主要是练习拆分为三层架构controller、service、dao&#xff0c;并基于IOC & DI进行解耦。 1、结构&#xff1a; 2、代码 网盘链接&…

【LeetCode: 145. 二叉树的后序遍历 + 栈】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

哈希表算法题

目录 题目一——1. 两数之和 - 力扣&#xff08;LeetCode&#xff09; 1.1.暴力解法1 1.2.暴力解法2 1.2.哈希表解法 题目二——面试题 01.02. 判定是否互为字符重排 - 力扣&#xff08;LeetCode&#xff09; 题目三——217. 存在重复元素 - 力扣&#xff08;LeetCode&…