How to create and use mybatis-3 source code cache

InfoQ 2020-11-06 01:15:46
create use mybatis-3 mybatis source


{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Mybatis-3 How is the source code cache created "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Mybatis In fact, the problem of cache is also the problem of high frequency in interview , Today we will talk about it from the source code level Mybatis Cache implementation of ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"( This article source code is in https:\/\/github.com\/ccqctljx\/Mybatis-3 in , Comments and Demo)."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" First, let's look at what caching is : Caching is general ORM The framework will provide functionality , The purpose is to improve the efficiency of query and reduce the pressure on the database . To put it bluntly, it is , After opening the cache , The same data query does not have to access the database again , Just take it from the cache ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Well, interviewers often ask First level cache and Second level cache What are they ?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" First level cache : First level cache is also called local cache , It's a conversation (SqlSession) Layer cache . With the beginning of the conversation , End of death .MyBatis The first level cache of is on by default , No configuration is required ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Second level cache : Because the first level cache comes with the session , You can't share it across sessions . Second level cache is used to solve this problem , His scope is namespace Grade , Can be multiple SqlSession share , Life cycle and SqlSessionFactory Sync . As long as it's the same SqlSessionFactory Created conversations , To share the same namespace Level cache . Level 2 cache needs to be configured in three places :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" The first is in mybaits-config.xml Set enable cache in configuration file :``"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"``"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" The second is to be in Mapper Configuration in file ``"},{"type":"codeinline","content":[{"type":"text","text":" "}]},{"type":"text","text":"`` label "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" The third is to add... To the statements that need to use the cache ``"},{"type":"codeinline","content":[{"type":"text","text":"useCache=\"true\" "}]},{"type":"text","text":"``"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" So the first level and second level cache has no execution order or something ? The answer is yes , If L2 cache is enabled, the execution order is :"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/resource\/image\/32\/52\/3208f20742e6fca73ddeba0934e4d852.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" So let's write an example code , Let's look at the effect of the next level two cache "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class Demo {\n public static void main(String[] args) throws IOException {\n\n String resource = \"mybatis\/mybatis-config.xml\";\n InputStream inputStream = Resources.getResourceAsStream(resource);\n SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);\n SqlSession sqlSession1 = sqlSessionFactory.openSession();\n SqlSession sqlSession2 = sqlSessionFactory.openSession();\n\n List bookInfoList1 = sqlSession1.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\" sqlSession 1 query 1 ----------------------------- \" + bookInfoList1);\n\n List bookInfoList2 = sqlSession1.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\"sqlSession 1 query 2 -----------------------------\" + bookInfoList2);\n\n sqlSession1.commit();\n System.out.println(\"sqlSession 1 commit -----------------------------\");\n\n List bookInfoList3 = sqlSession2.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\"sqlSession 2 query 1 ----------------------------- \" + bookInfoList3);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" The printout is :"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/resource\/image\/e1\/12\/e12a6b5b5f6e1fa753af2b06e1fac612.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" From this we can see that , Only the first query is executed sql, The other two queries did not go to the database . That's what caching does ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Let's go to the source code to see how it works ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":" Second level cache creation process 1 : Load configuration class "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" First , We created SqlSessionFactory In the factory , It will load all the configuration from the configuration file and generate Configuration object , And then Configuration The object is placed in SqlSessionFactory Maintain in instance objects . The parsing code is as follows "}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLConfigBuilder extends BaseBuilder {\n ……\n private void parseConfiguration(XNode root) {\n try {\n \/\/issue #117 read properties first\n propertiesElement(root.evalNode(\"properties\"));\n\n \/\/ Parse the configuration file setting label \n Properties settings = settingsAsProperties(root.evalNode(\"settings\"));\n loadCustomVfs(settings);\n loadCustomLogImpl(settings);\n \/\/ To generate an alias map In the configuration In the backup use \n typeAliasesElement(root.evalNode(\"typeAliases\"));\n pluginElement(root.evalNode(\"plugins\"));\n objectFactoryElement(root.evalNode(\"objectFactory\"));\n objectWrapperFactoryElement(root.evalNode(\"objectWrapperFactory\"));\n reflectorFactoryElement(root.evalNode(\"reflectorFactory\"));\n settingsElement(settings);\n \/\/ read it after objectFactory and objectWrapperFactory issue #631\n environmentsElement(root.evalNode(\"environments\"));\n databaseIdProviderElement(root.evalNode(\"databaseIdProvider\"));\n typeHandlerElement(root.evalNode(\"typeHandlers\"));\n\n \/\/ Parse the configuration file mappers label \n mapperElement(root.evalNode(\"mappers\"));\n } catch (Exception e) {\n throw new BuilderException(\"Error parsing SQL Mapper Configuration. Cause: \" + e, e);\n }\n }\n \n \/**\n * hold settings All configuration of the tag is loaded as Properties\n * @param context\n * @return\n *\/\n private Properties settingsAsProperties(XNode context) {\n if (context == null) {\n return new Properties();\n }\n Properties props = context.getChildrenAsProperties();\n \/\/ Check that all settings are known to the configuration class\n MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);\n for (Object key : props.keySet()) {\n if (!metaConfig.hasSetter(String.valueOf(key))) {\n throw new BuilderException(\"The setting \" + key + \" is not known. Make sure you spelled it correctly (case sensitive).\");\n }\n }\n return props;\n }\n \n \/**\n * Set global context properties \n *\/\n private void settingsElement(Properties props) {\n ……\n configuration.setCacheEnabled(booleanValueOf(props.getProperty(\"cacheEnabled\"), true));\n configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(\"localCacheScope\", \"SESSION\")));\n ……\n }\n ……\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Method settingsAsProperties In the configuration file setting The label reads Properties object , And then in settingsElement All of the methods are assigned to configuration object , One of them is right cache Label handling , take . This Configuration yes BaseBuilder A class that describes global configuration in , I'll throw it to SqlSessionFactory , As a global context ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Here's another way that's more important , Namely typeAliasesElement Method , This method is to configure some of our alias classes , Stored in the form of key value pairs in TypeAliasRegistry One of the classes HashMap in , for example \"byte\" -> Byte.class. This TypeAliasRegistry It will also be put into the global configuration Configuration in ."}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":" Second level cache creation process II : establish Cache Object and bind Mapper"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" After parsing the configuration file ,mybatis Know you need to turn on the secondary cache , So it's time to create a cache , First , Scan everything first Mapper file location , Then analyze the past one by one ( Here to resource For example, analyze ):"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLConfigBuilder extends BaseBuilder {\n private void mapperElement(XNode parent) throws Exception {\n if (parent != null) {\n \/\/ Traverse mybatis-config.xml Below the file mappers Child of a node \n for (XNode child : parent.getChildren()) {\n \/\/ Judge whether it is Package, If so, you can take it directly Package Load the package mapper file \n if (\"package\".equals(child.getName())) {\n String mapperPackage = child.getStringAttribute(\"name\");\n configuration.addMappers(mapperPackage);\n } else {\n \/\/ If not , Namely mapper label ( because xml Only these two tags are allowed in )\n \/\/ Then take the corresponding attributes , Go and analyze it separately \n String resource = child.getStringAttribute(\"resource\");\n String url = child.getStringAttribute(\"url\");\n String mapperClass = child.getStringAttribute(\"class\");\n\n \/\/ analysis resource Indicating position mapper\n if (resource != null && url == null && mapperClass == null) {\n \/\/ The error context is defined here , If the error log is loaded here, print (\"### The error may exist in xxx\");\n ErrorContext.instance().resource(resource);\n \/\/ Read The configuration file Forming current \n InputStream inputStream = Resources.getResourceAsStream(resource);\n XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());\n \/\/ Analyze the concrete mapper file \n mapperParser.parse();\n }\n\n \/\/ analysis url Indicating position mapper\n else if (resource == null && url != null && mapperClass == null) {\n ErrorContext.instance().resource(url);\n InputStream inputStream = Resources.getUrlAsStream(url);\n XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());\n mapperParser.parse();\n }\n\n \/\/ analysis mapperClass Indicating position mapper\n else if (resource == null && url == null && mapperClass != null) {\n Class mapperInterface = Resources.classForName(mapperClass);\n configuration.addMapper(mapperInterface);\n } else {\n throw new BuilderException(\"A mapper element may only specify a url, resource or class, but not more than one.\");\n }\n }\n }\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" find Mapper after , Start with Mapper Parsing :"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLMapperBuilder extends BaseBuilder {\n ……\n public void parse() {\n \/\/ Because it's a public method , Multiple calls , So first, judge whether it has been loaded \n if (!configuration.isResourceLoaded(resource)) {\n \/\/ If it's not loaded , Load resources first , So here we create Cache object \n configurationElement(parser.evalNode(\"\/mapper\"));\n configuration.addLoadedResource(resource);\n bindMapperForNamespace();\n }\n\n parsePendingResultMaps();\n parsePendingCacheRefs();\n parsePendingStatements();\n }\n \n private void configurationElement(XNode context) {\n try {\n String namespace = context.getStringAttribute(\"namespace\");\n if (namespace == null || namespace.isEmpty()) {\n throw new BuilderException(\"Mapper's namespace cannot be empty\");\n }\n builderAssistant.setCurrentNamespace(namespace);\n \/\/ These two lines are the key steps to enable L2 cache \n \/\/ This step took someone else's cache object Set it for yourself \n cacheRefElement(context.evalNode(\"cache-ref\"));\n \/\/ In this step Cache object \n cacheElement(context.evalNode(\"cache\"));\n \/\/ Analytical parameters Map\n parameterMapElement(context.evalNodes(\"\/mapper\/parameterMap\"));\n \/\/ analysis resultMap\n resultMapElements(context.evalNodes(\"\/mapper\/resultMap\"));\n \/\/ Resolve each sql label (mapper There are two kinds of sql, One is Here are four dozen tags to parse , And direct use sql Labeled )\n sqlElement(context.evalNodes(\"\/mapper\/sql\"));\n \/\/ Analyze the four labels , And put in configuration in , This will also turn on caching for each statement Set the cache object generated above , That is to say Cache\n buildStatementFromContext(context.evalNodes(\"select|insert|update|delete\"));\n } catch (Exception e) {\n throw new BuilderException(\"Error parsing Mapper XML. The XML location is '\" + resource + \"'. Cause: \" + e, e);\n }\n }\n ……\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" Here we have three steps related to caching , First step cacheRefElement It's to see mapper Is it marked with ``"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"`"},{"type":"codeinline","content":[{"type":"text","text":" label , This label means I can talk to other people namespace Of mapper Share one Cache. The source code is actually Configuration Loading good specification in mapper Of Cache Object refers to itself . Let's focus on creating Cache The object method is "}]},{"type":"text","text":"`"},{"type":"codeinline","content":[{"type":"text","text":"cacheElement(context.evalNode(\"cache\"));"}]},{"type":"text","text":"``"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void cacheElement(XNode context) {\n if (context != null) {\n \/\/ If the type is not specified , The default cache type is set to PERPETUAL\n String type = context.getStringAttribute(\"type\", \"PERPETUAL\");\n \/\/ typeAliasRegistry One was maintained internally HashMap And many kinds of aliases are preset , for example \"byte\" -> Byte.class\n \/\/ This refers to the previous loading of the configuration typeAliasesElement Methods do \n Class typeClass = typeAliasRegistry.resolveAlias(type);\n \/\/ eviction To expel 、 Drive out . Here it stands for Cache cleaning policy , How to clear the useless cache \n \/\/ The code can see , The default is LRU namely Removes the object that has not been used for the longest time .\n \/\/ There are four kinds of documents on the official website as follows :\n \/**\n LRU – Least Recently Used: Removes objects that haven't been used for the longst period of time.( Get rid of things that haven't been used for a long time )\n FIFO – First In First Out: Removes objects in the order that they entered the cache.( Get rid of what you put in first )\n SOFT – Soft Reference: Removes objects based on the garbage collector state and the rules of Soft References.( Soft reference cleanup )\n WEAK – Weak Reference: More aggressively removes objects based on the garbage collector state and rules of Weak References.( Weak referential cleanup )\n *\/\n String eviction = context.getStringAttribute(\"eviction\", \"LRU\");\n Class evictionClass = typeAliasRegistry.resolveAlias(eviction);\n \/\/ Refresh interval , Company millisecond , Represents a reasonable period of time in milliseconds . The default is no , There is no refresh interval , The cache just calls update Refresh when statement .\n Long flushInterval = context.getLongAttribute(\"flushInterval\");\n \/\/ Number of citations , Keep in mind the number of objects you cache and the number of memory resources available for your running environment . The default value is 1024.\n Integer size = context.getIntAttribute(\"size\");\n\n \/\/ The following is the configuration of whether the cache object instance is read-only \n \/\/ A read-only cache will return the same instance of the cache object to all callers . So these objects can't be modified ( Once modified , What others get is also modified ). This provides an important performance advantage .\n \/\/ A read-write cache will return a copy of the cache object ( By serializing ). It will be slower , But it's safe , So the default is false.\n boolean readWrite = !context.getBooleanAttribute(\"readOnly\", false);\n \/\/ Set whether it is a blocking cache , If it is true , When creating the cache, it will wrap a layer of BlockingCache . The default is false\n boolean blocking = context.getBooleanAttribute(\"blocking\", false);\n Properties props = context.getChildrenAsProperties();\n \/\/ This method builds a new Cache Object and set to configuration in .\n builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);\n }\n \n public Cache useNewCache(Class typeClass,\n Class evictionClass,\n Long flushInterval,\n Integer size,\n boolean readWrite,\n boolean blocking,\n Properties props) {\n \/\/ Here we create... Using builder mode Cache, And bind the current Mapper And as this Cache Of ID.\n Cache cache = new CacheBuilder(currentNamespace)\n \/\/ Cache implementation class \n .implementation(valueOrDefault(typeClass, PerpetualCache.class))\n \/\/ Packaging ( Cache recovery policy class )\n .addDecorator(valueOrDefault(evictionClass, LruCache.class))\n \/\/ Clear time \n .clearInterval(flushInterval)\n .size(size)\n .readWrite(readWrite)\n .blocking(blocking)\n .properties(props)\n .build();\n \/\/ Build up Cache after , Add to configuration Waiting to call .\n configuration.addCache(cache);\n currentCache = cache;\n return cache;\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" After creation , Here we call ``"},{"type":"codeinline","content":[{"type":"text","text":"configuration.addCache(cache)"}]},{"type":"text","text":"`` Method will generate good cache Put it in configuration In the object , It's actually going to be cache object put Enter Configuration Class internal maintenance StrictMap in , And this StrictMap It is inherited from HashMap, That is to say, in the final analysis, here will be cache With currentNamespace by Key Put in a HashMap in ."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":" Second level cache creation process 3 : For each sql Statement binding cache"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" It's generating Cache After the object ,Mapper The document will take this mapper All the statement tags in are generated one by one MappedStatement , In the process , Will give each statement Bind to the secondary cache , So that he can use it directly ."}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void parseStatementNode() {\n String id = context.getStringAttribute(\"id\");\n String databaseId = context.getStringAttribute(\"databaseId\");\n\n \/\/ If the database id If it is not empty and cannot match , Do not load the following \n if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {\n return;\n }\n\n String nodeName = context.getNode().getNodeName();\n \/\/ Here's the label ,insert | update | delete | select\n SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));\n \/\/ Whether it is select sentence \n boolean isSelect = sqlCommandType == SqlCommandType.SELECT;\n \/\/ Whether to clear the cache \n boolean flushCache = context.getBooleanAttribute(\"flushCache\", !isSelect);\n \/\/ Whether to use L2 cache or not \n boolean useCache = context.getBooleanAttribute(\"useCache\", isSelect);\n \/\/ Whether the results are sorted \n boolean resultOrdered = context.getBooleanAttribute(\"resultOrdered\", false);\n \n ······\n \n \/\/ Configure a series of properties , The corresponding attribute on the tag can be seen here \n StatementType statementType = StatementType.valueOf(context.getStringAttribute(\"statementType\", StatementType.PREPARED.toString()));\n Integer fetchSize = context.getIntAttribute(\"fetchSize\");\n Integer timeout = context.getIntAttribute(\"timeout\");\n String parameterMap = context.getStringAttribute(\"parameterMap\");\n String resultType = context.getStringAttribute(\"resultType\");\n Class resultTypeClass = resolveClass(resultType);\n String resultMap = context.getStringAttribute(\"resultMap\");\n String resultSetType = context.getStringAttribute(\"resultSetType\");\n ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);\n if (resultSetTypeEnum == null) {\n resultSetTypeEnum = configuration.getDefaultResultSetType();\n }\n String keyProperty = context.getStringAttribute(\"keyProperty\");\n String keyColumn = context.getStringAttribute(\"keyColumn\");\n String resultSets = context.getStringAttribute(\"resultSets\");\n\n \/\/ Build and parse completed MappedStatement , Also is to