diff --git a/README.md b/README.md index 48ae099..0093d2f 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ [![issues](https://badgen.net/github/open-issues/doocs/source-code-hunter)](https://github.com/doocs/source-code-hunter/issues) [![PRs Welcome](https://badgen.net/badge/PRs/welcome/green)](http://makeapullrequest.com) -有被“读过哪些知名的开源项目源码?”这种问题所困扰过吗?加入我们,一起通读互联网公司主流框架及中间件源码,成为强大的“源码猎人”,目前开放的有 Spring 系列框架、Mybatis 框架、Netty 框架,及Redis中间件等,让我们一起开拓新的领地,揭开这些源码的神秘面纱。本项目主要用于记录框架及中间件源码的阅读经验、个人理解及解析,希望能够使阅读源码变成一件更简单有趣,且有价值的事情,抽空更新中... +有被“读过哪些知名的开源项目源码?”这种问题所困扰过吗?加入我们,一起通读互联网公司主流框架及中间件源码,成为强大的“源码猎人”,目前开放的有 Spring 系列框架、Mybatis 框架、Netty 框架,及Redis中间件等,让我们一起开拓新的领地,揭开这些源码的神秘面纱。本项目主要用于记录框架及中间件源码的阅读经验、个人理解及解析,希望能够使阅读源码变成一件更简单有趣,且有价值的事情,抽空更新中...(如果本项目对您有帮助,请watch、star、fork 素质三连一波,鼓励一下作者,谢谢) ## spring系列 ### IoC容器 - [BeanDefinition 的资源定位过程](/docs/Spring/IoC/1、BeanDefinition的资源定位过程.md) - [将 bean 解析封装成 BeanDefinition](/docs/Spring/IoC/2、将bean解析封装成BeanDefinition.md) - [将 BeanDefinition 注册进 IoC 容器](/docs/Spring/IoC/3、将BeanDefinition注册进IoC容器.md) -- [依赖注入(DI)](/docs/Spring/IoC/四、依赖注入(DI).md) +- [依赖注入(DI)](/docs/Spring/IoC/4、依赖注入(DI).md) ### AOP - [AOP 源码实现及分析](/docs/Spring/AOP/AOP源码实现及分析.md) @@ -24,10 +24,12 @@ ### SpringJDBC + ### Spring事务 + ### Spring源码故事(瞎编版) -- [面筋哥 IoC 容器的一天(上)](/docs/Spring/IoC/面筋哥IoC容器的一天(上).md) +- [面筋哥 IoC 容器的一天(上)](/docs/Spring/Spring源码故事(瞎编版)/面筋哥IoC容器的一天(上).md) ## MyBatis ### 基础支持层 @@ -37,6 +39,10 @@ - [缓存模块](docs/Mybatis/基础支持层/4、缓存模块.md) ### 核心处理层 - [MyBatis初始化](docs/Mybatis/核心处理层/1、MyBatis初始化.md) +- [SqlNode和SqlSource](docs/Mybatis/核心处理层/2、SqlNode和SqlSource.md) +- [ResultSetHandler](docs/Mybatis/核心处理层/3、ResultSetHandler.md) +- [StatementHandler](docs/Mybatis/核心处理层/4、StatementHandler.md) +- [Executor组件](docs/Mybatis/核心处理层/5、Executor组件.md) ## Netty ### IO @@ -46,6 +52,8 @@ ## Redis +## Tomcat + ## 学习心得 ### 个人经验 - [初级开发者应该从 spring 源码中学什么](docs/学习心得/个人经验/初级开发者应该从spring源码中学什么.md) diff --git a/docs/Mybatis/基础支持层/3、binding模块.md b/docs/Mybatis/基础支持层/3、binding模块.md index 844942c..18b2518 100644 --- a/docs/Mybatis/基础支持层/3、binding模块.md +++ b/docs/Mybatis/基础支持层/3、binding模块.md @@ -1,5 +1,5 @@ -binding模块主要为了解决一个历史遗留问题,原先查询一个VO对象时需要调用SqlSession.queryForObject(“selectXXVOById”, primaryKey)方法,执行指定的sql语句,第一个参数selectXXVOById指定了执行的sql语句id,如果我们不小心写错了参数,mybatis是无法在初始化时发现这个错误的,只会在实际调用queryForObject(“selectXXVOById”, primaryKey)方法时才会抛出异常,这对于工程师来说是非常难受的,就像泛型出来之前,很多类型转换不会在编译期发现错误一样。 -而binding模块就像Java的泛型机制一样,将程序的错误提前暴露出来,为开发人员省去不少排查问题的精力。 +binding模块主要为了解决一个历史遗留问题,原先查询一个VO对象时需要调用SqlSession.queryForObject(“selectXXVOById”, primaryKey)方法,执行指定的sql语句,第一个参数selectXXVOById指定了执行的sql语句id,如果我们不小心写错了参数,mybatis是无法在初始化时发现这个错误的,只会在实际调用queryForObject(“selectXXVOById”, primaryKey)方法时才会抛出异常,这对于工程师来说是非常难受的,就像泛型出来之前,很多类型转换不会在编译期发现错误一样。而binding模块就像Java的泛型机制一样,将程序的错误提前暴露出来,为开发人员省去不少排查问题的精力。 + binding模块的解决方案是,定义一个Mapper接口,在接口中定义sql语句对应的方法名(Id)及参数,这些方法在mybatis的初始化过程中,会与该Mapper接口对应的映射配置文件中的sql语句相关联,如果存在无法关联的sql语句,mybatis就会抛出异常,帮助我们及时发现问题。示例代码如下: ```java public interface HeroMapper { @@ -544,4 +544,4 @@ public class MapperMethod { return result; } } -``` \ No newline at end of file +``` diff --git a/docs/Mybatis/核心处理层/1、MyBatis初始化.md b/docs/Mybatis/核心处理层/1、MyBatis初始化.md index e69de29..84e79ed 100644 --- a/docs/Mybatis/核心处理层/1、MyBatis初始化.md +++ b/docs/Mybatis/核心处理层/1、MyBatis初始化.md @@ -0,0 +1,698 @@ +和spring框架的IoC容器初始化一样,mybatis也会通过定位、解析相应的配置文件完成自己的初始化。mybatis的配置文件主要有mybatis-config.xml核心配置文件及一系列映射配置文件,另外,mybatis也会根据注解进行配置。 +## 1 BaseBuilder +mybatis初始化的主要内容是加载并解析mybatis-config.xml配置文件、映射配置文件以及相关的注解信息。mybatis的初始化入口是SqlSessionFactoryBuilder的build()方法。 +```java +public class SqlSessionFactoryBuilder { + + public SqlSessionFactory build(Reader reader) { + return build(reader, null, null); + } + + public SqlSessionFactory build(Reader reader, String environment) { + return build(reader, environment, null); + } + + public SqlSessionFactory build(Reader reader, Properties properties) { + return build(reader, null, properties); + } + + /** + * build方法的主要实现 + */ + public SqlSessionFactory build(Reader reader, String environment, Properties properties) { + try { + // SqlSessionFactory会创建XMLConfigBuilder对象来解析mybatis-config.xml配置文件 + // XMLConfigBuilder继承自BaseBuilder抽象类,顾名思义这一系的类使用了 建造者设计模式 + XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties); + // 解析配置文件的内容 到Configuration对象,根据到Configuration对象 + // 创建DefaultSqlSessionFactory对象,然后返回 + return build(parser.parse()); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error building SqlSession.", e); + } finally { + ErrorContext.instance().reset(); + try { + // 关闭配置文件输入流 + reader.close(); + } catch (IOException e) { + // Intentionally ignore. Prefer previous error. + } + } + } + + public SqlSessionFactory build(Configuration config) { + return new DefaultSqlSessionFactory(config); + } +``` +BaseBuilder中的核心字段如下: +```java +public abstract class BaseBuilder { + + // 保存了mybatis的几乎所以核心配置信息,全局唯一 + protected final Configuration configuration; + // 在mybatis-config.xml中可以通过标签定义别名 + protected final TypeAliasRegistry typeAliasRegistry; + // 在mybatis-config.xml中可以通过标签添加自定义TypeHandler + // TypeHandler用于完成JDBC数据类型与Java类型的相互转换,所有的TypeHandler + // 都保存在typeHandlerRegistry中 + protected final TypeHandlerRegistry typeHandlerRegistry; + + public BaseBuilder(Configuration configuration) { + this.configuration = configuration; + this.typeAliasRegistry = this.configuration.getTypeAliasRegistry(); + this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry(); + } +} +``` +BaseBuilder中的typeAliasRegistry和typeHandlerRegistry字段均来自于configuration,通过BaseBuilder的构造方法可以看到详细内容。 +## 2 XMLConfigBuilder +XMLConfigBuilder是BaseBuilder的众多子类之一,主要负责解析mybatis-config.xml配置文件。它通过调用parseConfiguration()方法实现整个解析过程,其中,mybatis-config.xml配置文件中的每个节点都被封装成了一个个相应的解析方法,parseConfiguration()方法只是依次调用了这些解析方法而已。 +```java +public class XMLConfigBuilder extends BaseBuilder { + + // 标记是否解析过mybatis-config.xml文件 + private boolean parsed; + // 用于解析mybatis-config.xml的解析器 + private final XPathParser parser; + // 标识配置的名称,默认读取标签的default属性 + private String environment; + // 创建并缓存Reflector对象 + private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory(); + + /** + * 解析的入口,调用了parseConfiguration()进行后续的解析 + */ + public Configuration parse() { + // parsed标志位的处理 + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + parsed = true; + // 在mybatis-config.xml配置文件中查找节点,并开始解析 + parseConfiguration(parser.evalNode("/configuration")); + return configuration; + } + + private void parseConfiguration(XNode root) { + try { + // 根据root.evalNode("properties")中的值就可以知道具体是解析哪个标签的方法咯 + propertiesElement(root.evalNode("properties")); + Properties settings = settingsAsProperties(root.evalNode("settings")); + loadCustomVfs(settings); + typeAliasesElement(root.evalNode("typeAliases")); + pluginElement(root.evalNode("plugins")); + objectFactoryElement(root.evalNode("objectFactory")); + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + reflectorFactoryElement(root.evalNode("reflectorFactory")); + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + environmentsElement(root.evalNode("environments")); + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + typeHandlerElement(root.evalNode("typeHandlers")); + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } + } +``` +mybatis中的标签很多,所以相对应的解析方法也很多,这里挑几个比较重要的标签进行分析。 +### 2.1 解析<typeHandlers>标签 +```java + private void typeHandlerElement(XNode parent) throws Exception { + if (parent != null) { + // 处理下的所有子标签 + for (XNode child : parent.getChildren()) { + // 处理标签 + if ("package".equals(child.getName())) { + // 获取指定的包名 + String typeHandlerPackage = child.getStringAttribute("name"); + // 通过typeHandlerRegistry的register(packageName)方法 + // 扫描指定包中的所有TypeHandler类,并进行注册 + typeHandlerRegistry.register(typeHandlerPackage); + } else { + // Java数据类型 + String javaTypeName = child.getStringAttribute("javaType"); + // JDBC数据类型 + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + Class javaTypeClass = resolveClass(javaTypeName); + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + Class typeHandlerClass = resolveClass(handlerTypeName); + // 注册 + if (javaTypeClass != null) { + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } + } +``` +### 2.2 解析<environments>标签 +```java + /** + * mybatis可以配置多个环境,分别用于开发、测试及生产等, + * 但每个SqlSessionFactory实例只能选择其一 + */ + private void environmentsElement(XNode context) throws Exception { + if (context != null) { + // 如果未指定XMLConfigBuilder的environment字段,则使用default属性指定的环境 + if (environment == null) { + environment = context.getStringAttribute("default"); + } + // 遍历节点 + for (XNode child : context.getChildren()) { + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + // 实例化TransactionFactory + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + // 创建DataSourceFactory和DataSource + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + DataSource dataSource = dsFactory.getDataSource(); + // 创建的Environment对象中封装了上面的TransactionFactory对象和DataSource对象 + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + // 为configuration注入environment属性值 + configuration.setEnvironment(environmentBuilder.build()); + } + } + } + } +``` +### 2.3 解析<databaseIdProvider>标签 +mybatis不像hibernate那样,通过hql的方式直接帮助开发人员屏蔽不同数据库产品在sql语法上的差异,针对不同的数据库产品,mybatis往往要编写不同的sql语句。但在mybatis-config.xml配置文件中,可以通过<databaseIdProvider>定义所有支持的数据库产品的databaseId,然后在映射配置文件中定义sql语句节点时,通过databaseId指定该sql语句应用的数据库产品,也可以达到类似的屏蔽数据库产品的功能。 + +mybatis初始化时,会根据前面解析到的DataSource来确认当前使用的数据库产品,然后在解析映射文件时,加载不带databaseId属性的sql语句 及 带有databaseId属性的sql语句,其中,带有databaseId属性的sql语句优先级更高,会被优先选中。 +```java + /** + * 解析节点,并创建指定的DatabaseIdProvider对象, + * 该对象会返回databaseId的值,mybatis会根据databaseId选择对应的sql语句去执行 + */ + private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + String type = context.getStringAttribute("type"); + // 为了保证兼容性,修改type取值 + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + // 解析相关配置信息 + Properties properties = context.getChildrenAsProperties(); + // 创建DatabaseIdProvider对象 + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); + // 配置DatabaseIdProvider,完成初始化 + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + // 根据前面解析到的DataSource获取databaseId,并记录到configuration的configuration属性上 + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + configuration.setDatabaseId(databaseId); + } + } +``` +mybatis提供了DatabaseIdProvider接口,该接口的核心方法为getDatabaseId(DataSource dataSource),主要根据dataSource查找对应的databaseId并返回。该接口的主要实现类为VendorDatabaseIdProvider。 +```java +public class VendorDatabaseIdProvider implements DatabaseIdProvider { + + private static final Log log = LogFactory.getLog(VendorDatabaseIdProvider.class); + + private Properties properties; + + @Override + public void setProperties(Properties p) { + this.properties = p; + } + + @Override + public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + return getDatabaseName(dataSource); + } catch (Exception e) { + log.error("Could not get a databaseId from dataSource", e); + } + return null; + } + + private String getDatabaseName(DataSource dataSource) throws SQLException { + // 解析到数据库产品名 + String productName = getDatabaseProductName(dataSource); + if (this.properties != null) { + // 根据子节点配置的数据库产品 和 databaseId之间的对应关系, + // 确定最终使用的databaseId + for (Map.Entry property : properties.entrySet()) { + if (productName.contains((String) property.getKey())) { + return (String) property.getValue(); + } + } + // 没有合适的databaseId,则返回null + return null; + } + return productName; + } + + // 根据dataSource获取 数据库产品名的具体实现 + private String getDatabaseProductName(DataSource dataSource) throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getDatabaseProductName(); + } finally { + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + // ignored + } + } + } + } +} +``` +### 2.4 解析<mappers>标签 +mybatis初始化时,除了加载mybatis-config.xml文件,还会加载全部的映射配置文件,mybatis-config.xml文件的<mapper>节点会告诉mybatis去哪里查找映射配置文件,及使用了配置注解标识的接口。 +```java + /** + * 解析节点,本方法会创建XMLMapperBuilder对象加载映射文件,如果映射配置文件存在 + * 相应的Mapper接口,也会加载相应的Mapper接口,解析其中的注解 并完成向MapperRegistry的注册 + */ + private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + // 处理的子节点 + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 获取子节点中的包名 + String mapperPackage = child.getStringAttribute("name"); + // 扫描指定的包目录,然后向MapperRegistry注册Mapper接口 + configuration.addMappers(mapperPackage); + } else { + // 获取节点的resource、url、mapperClass属性,这三个属性互斥,只能有一个不为空 + // mybatis提供了通过包名、映射文件路径、类全名、URL四种方式引入映射器。 + // 映射器由一个接口和一个XML配置文件组成,XML文件中定义了一个命名空间namespace, + // 它的值就是接口对应的全路径。 + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + // 如果节点指定了resource或是url属性,则创建XMLMapperBuilder对象解析 + // resource或是url属性指定的Mapper配置文件 + if (resource != null && url == null && mapperClass == null) { + ErrorContext.instance().resource(resource); + InputStream inputStream = Resources.getResourceAsStream(resource); + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url != null && mapperClass == null) { + ErrorContext.instance().resource(url); + InputStream inputStream = Resources.getUrlAsStream(url); + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url == null && mapperClass != null) { + // 如果节点指定了class属性,则向MapperRegistry注册该Mapper接口 + Class mapperInterface = Resources.classForName(mapperClass); + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } + } +``` +## 3 XMLMapperBuilder +和XMLConfigBuilder一样,XMLMapperBuilder也继承了BaseBuilder,其主要负责解析映射配置文件,其解析配置文件的入口方法也是parse(),另外,XMLMapperBuilder也将各个节点的解析过程拆分成了一个个小方法,然后由configurationElement()方法统一调用。 +```java +public class XMLMapperBuilder extends BaseBuilder { + public void parse() { + // 是否已经加载过该配置文件 + if (!configuration.isResourceLoaded(resource)) { + // 解析节点 + configurationElement(parser.evalNode("/mapper")); + // 将resource添加到configuration的loadedResources属性中, + // 该属性是一个HashSet类型的集合,其中记录了已经加载过的映射文件 + configuration.addLoadedResource(resource); + // 注册Mapper接口 + bindMapperForNamespace(); + } + // 处理configurationElement()方法中解析失败的节点 + parsePendingResultMaps(); + // 处理configurationElement()方法中解析失败的节点 + parsePendingCacheRefs(); + // 处理configurationElement()方法中解析失败的节点 + parsePendingStatements(); + } + + private void configurationElement(XNode context) { + try { + // 获取节点的namespace属性 + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + // 使用MapperBuilderAssistant对象的currentNamespace属性 记录namespace命名空间 + builderAssistant.setCurrentNamespace(namespace); + // 解析节点,后面的解析方法 也都见名知意 + cacheRefElement(context.evalNode("cache-ref")); + cacheElement(context.evalNode("cache")); + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + resultMapElements(context.evalNodes("/mapper/resultMap")); + sqlElement(context.evalNodes("/mapper/sql")); + buildStatementFromContext(context.evalNodes("select|insert|update|delete")); + } catch (Exception e) { + throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); + } + } +} +``` +XMLMapperBuilder也根据配置文件进行了一系列节点解析,我们着重分析一下比较重要且常见的<resultMap>节点和<sql>节点 +### 3.1 解析<resultMap>节点 +select语句查询得到的结果是一张二维表,水平方向上是一个个字段,垂直方向上是一条条记录。而Java是面向对象的程序设计语言,对象是根据类的定义创建的,类之间的引用关系可以认为是嵌套结构。JDBC编程中,为了将结果集中的数据映射成VO对象,我们需要自己写代码从结果集中获取数据,然后将数据封装成对应的VO对象,并设置好对象之间的关系,这种ORM的过程中存在大量重复的代码。 + +mybatis通过<resultMap>节点定义了ORM规则,可以满足大部分的映射需求,减少重复代码,提高开发效率。 + +在分析<resultMap>节点的解析过程之前,先看一下该过程使用的数据结构。每个ResultMapping对象记录了结果集中的一列与JavaBean中一个属性之间的映射关系。<resultMap>节点下除了<discriminator>子节点的其它子节点 都会被解析成对应的ResultMapping对象。 +```java +public class ResultMapping { + + private Configuration configuration; + // 对应节点的property属性,表示 该列进行映射的属性 + private String property; + // 对应节点的column属性,表示 从数据库中得到的列名或列名的别名 + private String column; + // 表示 一个JavaBean的完全限定名,或一个类型别名 + private Class javaType; + // 进行映射列的JDBC类型 + private JdbcType jdbcType; + // 类型处理器 + private TypeHandler typeHandler; + // 该属性通过id引用了另一个节点,它负责将结果集中的一部分列映射成 + // 它所关联的结果对象。这样我们就可以通过join方式进行关联查询,然后直接映射成 + // 多个对象,并同时设置这些对象之间的组合关系(nested嵌套的) + private String nestedResultMapId; + // 该属性通过id引用了另一个节点配置的flushCache属性为true时,才会清空一级缓存 + clearLocalCache(); + } + List list; + try { + // 增加查询层数 + queryStack++; + // 根据传入的CacheKey对象 查询一级缓存 + list = resultHandler == null ? (List) localCache.getObject(key) : null; + if (list != null) { + // 针对存储过程调用的处理,在一级缓存命中时,获取缓存中保存的输出类型参数 + // 并设置到用户传入的实参parameter对象中 + handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); + } else { + // 缓存未命中,则从数据库查询结果集,其中会调用doQuery()方法完成数据库查询操作, + // 该方法为抽象方法,由BaseExecutor的子类实现 + list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); + } + } finally { + // 当前查询完成,查询层数减少 + queryStack--; + } + if (queryStack == 0) { + // 延迟加载的相关内容 + for (DeferredLoad deferredLoad : deferredLoads) { + deferredLoad.load(); + } + deferredLoads.clear(); + if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { + // issue #482 + clearLocalCache(); + } + } + return list; + } +} +``` +从上面的代码中可以看到,BaseExecutor的query()方法会根据flushCache属性和localCacheScope配置 决定是否清空一级缓存。 +另外,BaseExecutor的update()方法在调用doUpdate()方法之前,也会清除一级缓存。update()方法负责执行insert、update、delete三类SQL 语句,它是调用doUpdate()方法实现的。 +```java + @Override + public int update(MappedStatement ms, Object parameter) throws SQLException { + // 判断当前的Executor是否已经关闭 + ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + // 清除一级缓存,该方法会调用localCache和localOutputParameterCache + // 的clear()方法清除缓存 + clearLocalCache(); + // 抽象方法,交由子类实现 + return doUpdate(ms, parameter); + } + + @Override + public void clearLocalCache() { + if (!closed) { + localCache.clear(); + localOutputParameterCache.clear(); + } + } +``` +### 1.3 事务相关操作 +在BatchExecutor实现中,可以缓存多条SQL语句,等待合适时机将缓存的多条SQL 语句一并发送到数据库执行。Executor的flushStatements()方法主要是针对批处理多条SQL语句的,它会调用doFlushStatements()这个基本方法处理Executor中缓存的多条SQL语句。在BaseExecutor的commit()及rollback()等方法中都会首先调用flushStatements()方法,然后再执行相关事务操作。 +```java + @Override + public void commit(boolean required) throws SQLException { + // 检查当前连接是否已关闭 + if (closed) { + throw new ExecutorException("Cannot commit, transaction is already closed"); + } + // 清除一级缓存 + clearLocalCache(); + // 不执行Executor中缓存的SQL语句 + flushStatements(); + // 根据参数required决定是否提交事务 + if (required) { + transaction.commit(); + } + } + + @Override + public List flushStatements() throws SQLException { + return flushStatements(false); + } + + public List flushStatements(boolean isRollBack) throws SQLException { + if (closed) { + throw new ExecutorException("Executor was closed."); + } + // 这是一个交由子类实现的抽象方法,参数isRollBack表示 + // 是否执行Executor中缓存的SQL语句,false表示执行,true表示不执行 + return doFlushStatements(isRollBack); + } + + @Override + public void rollback(boolean required) throws SQLException { + if (!closed) { + try { + // 清除一级缓存 + clearLocalCache(); + // 批量执行缓存的sql语句 + flushStatements(true); + } finally { + // 根据required决定是否回滚事务 + if (required) { + transaction.rollback(); + } + } + } + } + + @Override + public void close(boolean forceRollback) { + try { + try { + // 根据forceRollback参数决定 是否强制回滚该事务 + rollback(forceRollback); + } finally { + if (transaction != null) { + transaction.close(); + } + } + } catch (SQLException e) { + // Ignore. There's nothing that can be done at this point. + log.warn("Unexpected exception on closing transaction. Cause: " + e); + } finally { + transaction = null; + deferredLoads = null; + localCache = null; + localOutputParameterCache = null; + closed = true; + } + } +``` +## 2 SimpleExecutor +SimpleExecutor继承了BaseExecutor抽象类,它是最简单的Executor接口实现。Executor组件使用了模板方法模式,一级缓存等固定不变的操作都封装到了BaseExecutor中,在SimpleExecutor中就不必再关心一级缓存等操作,只需要专注实现4 个基本方法的实现即可。 +```java +public class SimpleExecutor extends BaseExecutor { + + public SimpleExecutor(Configuration configuration, Transaction transaction) { + super(configuration, transaction); + } + + @Override + public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + Statement stmt = null; + try { + // 获取配置对象 + Configuration configuration = ms.getConfiguration(); + // 创建StatementHandler对象 + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); + // 完成Statement的创建和初始化,该方法首先会调用StatementHandler的prepare()方法 + // 创建Statement对象,然后调用StatementHandler的parameterize()方法处理占位符 + stmt = prepareStatement(handler, ms.getStatementLog()); + // 调用StatementHandler的query()方法,执行sql语句,并通过ResultSetHandler + // 完成结果集的映射 + return handler.query(stmt, resultHandler); + } finally { + // 关闭Statement对象 + closeStatement(stmt); + } + } + + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + Connection connection = getConnection(statementLog); + // 创建Statement对象 + stmt = handler.prepare(connection, transaction.getTimeout()); + // 处理占位符 + handler.parameterize(stmt); + return stmt; + } + + /** + * 与前面doQuery()方法的实现非常类似 + */ + @Override + public int doUpdate(MappedStatement ms, Object parameter) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null); + stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.update(stmt); + } finally { + closeStatement(stmt); + } + } + + @Override + protected Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException { + Configuration configuration = ms.getConfiguration(); + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql); + Statement stmt = prepareStatement(handler, ms.getStatementLog()); + return handler.queryCursor(stmt); + } + + @Override + public List doFlushStatements(boolean isRollback) throws SQLException { + // SimpleExecutor不提供sql语句批处理,所以直接返回空集合 + return Collections.emptyList(); + } + +} +``` +## 3 ReuseExecutor + + +## 4 BatchExecutor + + +## 5 CachingExecutor + + diff --git a/docs/Spring/IoC/1、BeanDefinition的资源定位过程.md b/docs/Spring/IoC/1、BeanDefinition的资源定位过程.md index 79be178..6d6ed1b 100644 --- a/docs/Spring/IoC/1、BeanDefinition的资源定位过程.md +++ b/docs/Spring/IoC/1、BeanDefinition的资源定位过程.md @@ -1,8 +1,12 @@ ## 前言 之前一直想系统的拜读一下 spring 的源码,看看它到底是如何吸引身边的大神们对它的设计赞不绝口,虽然每天工作很忙,每天下班后总感觉脑子内存溢出,想去放松一下,但总是以此为借口,恐怕会一直拖下去。所以每天下班虽然有些疲惫,但还是按住自己啃下这块硬骨头。 + spring 源码这种东西真的是一回生二回熟,第一遍会被各种设计模式和繁杂的方法调用搞得晕头转向,不知道这个方法调用的是哪个父类的实现,另一个方法又调的是哪个子类的实现,但当你耐下心来多走几遍,会发现越看越熟练,每次都能 get 到新的点。 + 另外,对于第一次看 spring 源码的同学,建议先在 B 站上搜索相关视频看一下,然后再结合计文柯老师的《spring 技术内幕》深入理解,最后再输出自己的理解加强印象。 + 首先对于我们新手来说,还是从我们最常用的两个 IoC 容器开始分析,这次我们先分析 FileSystemXmlApplicationContext 这个 IoC 容器的具体实现,ClassPathXmlApplicationContext 留着下次讲解。 + (PS:可以结合我 GitHub 上对 spring 框架源码的翻译注解一起看,会更有助于各位开发姥爷的理解。 地址: spring-beans https://github.com/AmyliaY/spring-beans-reading @@ -345,4 +349,4 @@ FileSystemXmlApplicationContext 从上层体系的各抽象类中继承了大量 return new FileSystemResource(path); } ``` -至此,我们可以看到,FileSystemXmlApplicationContext 的 getResourceByPath() 方法返回了一个 FileSystemResource 对象,接下来 spring 就可以对这个对象进行相关的 I/O 操作,进行 BeanDefinition 的读取和载入了。 \ No newline at end of file +至此,我们可以看到,FileSystemXmlApplicationContext 的 getResourceByPath() 方法返回了一个 FileSystemResource 对象,接下来 spring 就可以对这个对象进行相关的 I/O 操作,进行 BeanDefinition 的读取和载入了。 diff --git a/docs/Spring/IoC/2、将bean解析封装成BeanDefinition.md b/docs/Spring/IoC/2、将bean解析封装成BeanDefinition.md index 813e6da..f46538f 100644 --- a/docs/Spring/IoC/2、将bean解析封装成BeanDefinition.md +++ b/docs/Spring/IoC/2、将bean解析封装成BeanDefinition.md @@ -1,4 +1,5 @@ 接着上一篇的 BeanDefinition 资源定位开始讲。Spring IoC 容器 BeanDefinition 解析过程就是把用户在配置文件中定义好的 bean,解析并封装成容器可以装载的 BeanDefinition,BeanDefinition 是 spring 定义的基本数据结构,也是为了方便对 bean 进行管理和操作。 + (PS:可以结合我 GitHub 上对 spring 框架源码的阅读及个人理解一起看,会更有助于各位开发大佬理解。 spring-beans https://github.com/AmyliaY/spring-beans-reading spring-context https://github.com/AmyliaY/spring-context-reading @@ -884,4 +885,4 @@ spring-context https://github.com/AmyliaY/spring-context-reading } } ``` -经过这样逐层地解析,我们在配置文件中定义的 Bean 就被整个解析成了可以被 IoC 容器装载和使用的 BeanDefinition,这种数据结构可以让 IoC 容器执行索引、查询等操作。经过上述解析得到的 BeanDefinition,接下来我们就可以将它注册到 IoC 容器中咯。 \ No newline at end of file +经过这样逐层地解析,我们在配置文件中定义的 Bean 就被整个解析成了可以被 IoC 容器装载和使用的 BeanDefinition,这种数据结构可以让 IoC 容器执行索引、查询等操作。经过上述解析得到的 BeanDefinition,接下来我们就可以将它注册到 IoC 容器中咯。 diff --git a/docs/Spring/IoC/4、依赖注入(DI).md b/docs/Spring/IoC/4、依赖注入(DI).md index 2bc0370..24d77a2 100644 --- a/docs/Spring/IoC/4、依赖注入(DI).md +++ b/docs/Spring/IoC/4、依赖注入(DI).md @@ -1,5 +1,7 @@ 前面我们主要分析了 FileSystemXmlApplicationContext 这个具体的 IoC 容器的初始化源码实现,在 IoC 容器中建立了 BeanDefinition 的数据映射,将其和 beanName 一起绑定在一个 ConcurrentHashMap 中。现在我们来看一下 spring 是如何将 IoC 容器中的 Bean 根据配置关联在一起的。 + Spring 中触发 IoC 容器“依赖注入”的方式有两种,一个是通过 getBean() 向容器索要 bean 时触发依赖注入;另一个是给 bean 配置 lazy-init 属性,spring 会自动调用此 bean 的 getBean() 方法,提前完成依赖注入。总的来说,想提高运行时获取 bean 的效率,可以考虑配置此属性。 + 下面我将分别解读这两种依赖注入的触发方式,先看 getBean() 的,因为 lazy-init 最后也是通过调用 getBean 完成的依赖注入。 (PS:可以结合我 GitHub 上对 spring 框架源码的阅读及个人理解一起看,会更有助于各位开发姥爷理解,地址: @@ -1443,4 +1445,4 @@ lazy-init 触发的预实例化和依赖注入,发生在 IoC 容器完成对 B beanFactory.preInstantiateSingletons(); } -``` \ No newline at end of file +``` diff --git a/docs/学习心得/设计模式/设计模式.md b/docs/学习心得/设计模式/设计模式.md index af5ceb6..46c08ac 100644 --- a/docs/学习心得/设计模式/设计模式.md +++ b/docs/学习心得/设计模式/设计模式.md @@ -1,10 +1,13 @@ ## 六大原则 - 1. 单一职责:一个类只负责唯一一项职责 2. 依赖倒置:即面向接口编程,系统的高层模块(顶层接口、顶层抽象类等)不应该依赖底层模块(具体实现类),当需求发生变化时,对外接口不变,只要提供新的实现类即可。 3. 接口隔离:尽量设计出功能单一的接口,避免实现类实现很多不必要的接口方法 4. **开放-封闭:对扩展开放,对修改关闭**,本原则是设计模式的终极目标 5. 迪米特法则:尽量减少类之间的耦合性 6. 里氏替换:继承体系的设计要合理 + ## 装饰器模式 + + +