外贸网站建设与推广,wordpress显示文章全文,网站域名到期怎么回事,网站建设论文设计1. Mybatis整体三层设计 SSM中#xff0c;Spring、SpringMVC已经在前面文章源码分析总结过了#xff0c;Mybatis源码相对Spring和SpringMVC而言是的简单的#xff0c;只有一个项目#xff0c;项目下分了很多包。从宏观上了解Mybatis的整体框架分为三层#xff0c;分别是基…1. Mybatis整体三层设计 SSM中Spring、SpringMVC已经在前面文章源码分析总结过了Mybatis源码相对Spring和SpringMVC而言是的简单的只有一个项目项目下分了很多包。从宏观上了解Mybatis的整体框架分为三层分别是基础支持层、核心处理层、和接口层。如下图 MyBatis的主要工作流程图
2.1 接口层 首先接口层是我们打交道最多的。核心对象是SqlSession它是上层应用和MyBatis打交道的桥梁SqlSession上定义了非常多的对数据库的操作方法。接口层在接收到调用请求的时候会调用核心处理层的相应模块来完成具体的数据库操作。 2.2 核心处理层 核心处理层也就是跟数据库操作相关的动作都是在这一层完成的。核心处理层主要做了这几件事 把接口中传入的参数解析并且映射成JDBC类型解析xml文件中的SQL语句包括插入参数和动态SQL的生成执行SQL语句处理结果集并映射成Java对象。
插件也属于核心层这是由它的工作方式和拦截的对象决定的。
2.3 基础支持层 基础支持层主要是一些抽取出来的通用的功能实现复用用来支持核心处理层的功能。比如数据源、缓存、日志、xml解析、反射、IO、事务等等这些功能; 2. 核心流程
Spring的集成的原理分析看后面
这里只是单独的Mybatis分析不涉及集成的。/*** MyBatis getMapper 方法的使用*/Testpublic void test2() throws Exception{// 1.获取配置文件InputStream in Resources.getResourceAsStream(mybatis-config.xml);// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库UserMapper mapper sqlSession.getMapper(UserMapper.class);ListUser list mapper.selectUserList();for (User user : list) {System.out.println(user);}// 5.关闭会话sqlSession.close();}2.1 核心对象的生命周期
2.1.1 SqlSessionFactoryBuiler
SqlSessionFactoryBuiler是用来构建SqlSessionFactory的
而SqlSessionFactory只需要一个所以只要构建了这一个SqlSessionFactory
它的使命就完成了也就没有存在的意义了。所以它的生命周期只存在于方法的局部。2.1.2 SqlSessionFactory SqlSessionFactory是用来创建SqlSession的每次应用程序访问数据库都需要创建一个会话。因为我们一直有创建会话的需要所以SqlSessionFactory应该存在于应用的整个生命周期中作用域是应用作用域。创建SqlSession只需要一个实例来做这件事就行了否则会产生很多的混乱和浪费资源。所以我们要采用单例模式。 2.1.3 SqlSession SqlSession是一个会话因为它不是线程安全的不能在线程间共享。所以我们在请求开始的时候创建一个SqlSession对象在请求结束或者说方法执行完毕的时候要及时关闭它一次请求或者操作中。 2.1.4 Mapper Mapper实际上是一个代理对象是从SqlSession中获取的。它的作用是发送SQL来操作数据库的数据。它应该在一个SqlSession事务方法之内 UserMapper mapper sqlSession.getMapper(UserMapper.class);2.2 SqlSessionFactory
SqlSessionFactory factory new SqlSessionFactoryBuilder().build(in);2.2.1 SqlSessionFactoryBuilder 首先我们new了一个SqlSessionFactoryBuilder这是建造者模式的运用建造者模式用来创建复杂对象而不需要关注内部细节是一种封装的体现。MyBatis中很多地方用到了建造者模式名字以Builder结尾的类还有9个。 SqlSessionFactoryBuilder中用来创建SqlSessionFactory对象的方法是build()build()方法有9个重载可以用不同的方式来创建SqlSessionFactory对象。SqlSessionFactory对象默认是单例的。 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {// 用于解析 mybatis-config.xml同时创建了 Configuration 对象 XMLConfigBuilder parser new XMLConfigBuilder(inputStream, environment, properties);// 解析XML最终返回一个 DefaultSqlSessionFactory return build(parser.parse());} catch (Exception e) {throw ExceptionFactory.wrapException(Error building SqlSession., e);} finally {ErrorContext.instance().reset();try {inputStream.close();} catch (IOException e) {// Intentionally ignore. Prefer previous error.}}}在build方法中首先是创建了一个XMLConfigBuilder对象XMLConfigBuilder是抽象类BaseBuilder的一个子类专门用来解析全局配置文件针对不同的构建目标还有其他的一些子类关联到源码路 XMLMapperBuilder解析Mapper映射器XMLStatementBuilder解析增删改查标签XMLScriptBuilder解析动态SQL
build(parser.parse());parser.parse()方法返回的是一个Configuration对象build方法的如下 public SqlSessionFactory build(Configuration config) {return new DefaultSqlSessionFactory(config);}SessionFactory最终实现是DefaultSqlSessionFactory对象。
2.2.2 XMLConfigBuilder
XMLConfigBuilder初始化的时候做了哪些操作public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {// EntityResolver的实现类是XMLMapperEntityResolver 来完成配置文件的校验根据对应的DTD文件来实现this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);}再去进入重载的构造方法中 private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {super(new Configuration()); // 完成了Configuration的初始化ErrorContext.instance().resource(SQL Mapper Configuration);this.configuration.setVariables(props); // 设置对应的Properties属性this.parsed false; // 设置 是否解析的标志为 falsethis.environment environment; // 初始化environmentthis.parser parser; // 初始化 解析器}2.2.3 Configuration Configuration初始化完成了类型别名的注册工作通过上面的分析我们可以看到XMLConfigBuilder完成了XML文件的解析对应XPathParser和Configuration对象的初始化操作然后我们再来看下parse方法到底是如何解析配置文件的 2.2.4 parse解析
1、parser.parse()
parser.parse()进入具体的解析方法 public Configuration parse() {// 检查是否已经解析过了if (parsed) {throw new BuilderException(Each XMLConfigBuilder can only be used once.);}parsed true;// XPathParserdom 和 SAX 都有用到 开始解析parseConfiguration(parser.evalNode(/configuration));return configuration;}2、全局配置文件解析parseConfiguration() 对于全局配置文件各种标签的解析 private void parseConfiguration(XNode root) {try {//issue #117 read properties first// 对于全局配置文件各种标签的解析propertiesElement(root.evalNode(properties));// 解析 settings 标签Properties settings settingsAsProperties(root.evalNode(settings));// 读取文件loadCustomVfs(settings);// 日志设置loadCustomLogImpl(settings);// 类型别名typeAliasesElement(root.evalNode(typeAliases));// 插件pluginElement(root.evalNode(plugins));// 用于创建对象objectFactoryElement(root.evalNode(objectFactory));// 用于对对象进行加工objectWrapperFactoryElement(root.evalNode(objectWrapperFactory));// 反射工具箱reflectorFactoryElement(root.evalNode(reflectorFactory));// settings 子标签赋值默认值就是在这里提供的 settingsElement(settings);// read it after objectFactory and objectWrapperFactory issue #631// 创建了数据源 environmentsElement(root.evalNode(environments));databaseIdProviderElement(root.evalNode(databaseIdProvider));typeHandlerElement(root.evalNode(typeHandlers));// 解析引用的Mapper映射器mapperElement(root.evalNode(mappers));} catch (Exception e) {throw new BuilderException(Error parsing SQL Mapper Configuration. Cause: e, e);}}propertiesElement()
properties解析 private void propertiesElement(XNode context) throws Exception {if (context ! null) {// 创建了一个 Properties 对象后面可以用到Properties defaults context.getChildrenAsProperties();String resource context.getStringAttribute(resource);String url context.getStringAttribute(url);if (resource ! null url ! null) {// url 和 resource 不能同时存在throw new BuilderException(The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.);}// 加载resource或者url属性中指定的 properties 文件if (resource ! null) {defaults.putAll(Resources.getResourceAsProperties(resource));} else if (url ! null) {defaults.putAll(Resources.getUrlAsProperties(url));}Properties vars configuration.getVariables();if (vars ! null) {// 和 Configuration中的 variables 属性合并defaults.putAll(vars);}// 更新对应的属性信息parser.setVariables(defaults);configuration.setVariables(defaults);}}第一个是解析properties标签读取我们引入的外部配置文件例如db.properties。
这里面又有两种类型一种是放在resource目录下的是相对路径一种是写的绝对路径的url。
解析的最终结果就是我们会把所有的配置信息放到名为defaults的Properties对象里面Hashtable对象KV存储最后把XPathParser和Configuration的Properties属性都设置成我们填充后的Properties对象。
settings解析settingsAsProperties() private Properties settingsAsProperties(XNode context) {if (context null) {return new Properties();}// 获取settings节点下的所有的子节点Properties props context.getChildrenAsProperties();// Check that all settings are known to the configuration classMetaClass metaConfig MetaClass.forClass(Configuration.class, localReflectorFactory);for (Object key : props.keySet()) {// if (!metaConfig.hasSetter(String.valueOf(key))) {throw new BuilderException(The setting key is not known. Make sure you spelled it correctly (case sensitive).);}}return props;}getChildrenAsProperties方法就是具体的解析了 public Properties getChildrenAsProperties() {Properties properties new Properties();for (XNode child : getChildren()) {// 获取对应的name和value属性String name child.getStringAttribute(name);String value child.getStringAttribute(value);if (name ! null value ! null) {properties.setProperty(name, value);}}return properties;}loadCustomVfs(settings)解析 loadCustomVfs是获取Vitual File System的自定义实现类比如要读取本地文件或者FTP远程文件的时候就可以用到自定义的VFS类。 根据settings标签里面的vfsImpl标签生成了一个抽象类VFS的子类在MyBatis中有JBoss6VFS和DefaultVFS两个实现在io包中。 private void loadCustomVfs(Properties props) throws ClassNotFoundException {String value props.getProperty(vfsImpl);if (value ! null) {String[] clazzes value.split(,);for (String clazz : clazzes) {if (!clazz.isEmpty()) {SuppressWarnings(unchecked)Class? extends VFS vfsImpl (Class? extends VFS)Resources.classForName(clazz);configuration.setVfsImpl(vfsImpl);}}}}最后赋值到Configuration中。loadCustomLogImpl(settings)解析 loadCustomLogImpl是根据logImpl标签获取日志的实现类我们可以用到很多的日志的方案包括LOG4JLOG4J2SLF4J等等在logging包中。 private void loadCustomLogImpl(Properties props) {Class? extends Log logImpl resolveClass(props.getProperty(logImpl));configuration.setLogImpl(logImpl);}typeAliases解析
这一步是类型别名的解析private void typeAliasesElement(XNode parent) {// 放入 TypeAliasRegistryif (parent ! null) {for (XNode child : parent.getChildren()) {if (package.equals(child.getName())) {String typeAliasPackage child.getStringAttribute(name);configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);} else {String alias child.getStringAttribute(alias);String type child.getStringAttribute(type);try {Class? clazz Resources.classForName(type);if (alias null) {// 扫描 Alias 注解使用typeAliasRegistry.registerAlias(clazz);} else {// 直接注册typeAliasRegistry.registerAlias(alias, clazz);}} catch (ClassNotFoundException e) {throw new BuilderException(Error registering typeAlias for alias . Cause: e, e);}}}}}plugins解析
插件标签的解析private void pluginElement(XNode parent) throws Exception {if (parent ! null) {for (XNode child : parent.getChildren()) {// 获取plugin 节点的 interceptor 属性的值String interceptor child.getStringAttribute(interceptor);// 获取plugin 下的所有的properties子节点Properties properties child.getChildrenAsProperties();// 获取 Interceptor 对象Interceptor interceptorInstance (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();// 设置 interceptor的 属性interceptorInstance.setProperties(properties);// Configuration中记录 Interceptorconfiguration.addInterceptor(interceptorInstance);}}}插件的具体使用后面专门介绍
objectFactoryobjectWrapperFactory及reflectorFactory解析 private void objectFactoryElement(XNode context) throws Exception {if (context ! null) {// 获取objectFactory 节点的 type 属性String type context.getStringAttribute(type);// 获取 objectFactory 节点下的配置信息Properties properties context.getChildrenAsProperties();// 获取ObjectFactory 对象的对象 通过反射方式ObjectFactory factory (ObjectFactory) resolveClass(type).getDeclaredConstructor().newInstance();// ObjectFactory 和 对应的属性信息关联factory.setProperties(properties);// 将创建的ObjectFactory对象绑定到Configuration中configuration.setObjectFactory(factory);}}private void objectWrapperFactoryElement(XNode context) throws Exception {if (context ! null) {String type context.getStringAttribute(type);ObjectWrapperFactory factory (ObjectWrapperFactory) resolveClass(type).getDeclaredConstructor().newInstance();configuration.setObjectWrapperFactory(factory);}}private void reflectorFactoryElement(XNode context) throws Exception {if (context ! null) {String type context.getStringAttribute(type);ReflectorFactory factory (ReflectorFactory) resolveClass(type).getDeclaredConstructor().newInstance();configuration.setReflectorFactory(factory);}}ObjectFactory用来创建返回的对象。
ObjectWrapperFactory用来对对象做特殊的处理。比如select没有写别名查询返回的是一个Map可以在自定义的objectWrapperFactory中把下划线命名变成驼峰命名。
ReflectorFactory是反射的工具箱对反射的操作进行了封装官网和文档没有这个对象的描述。
以上四个对象都是用resolveClass创建的。
settingsElement(settings)解析 这里就是对settings标签里面所有子标签的处理了前面我们已经把子标签全部转换成了Properties对象所以在这里处理Properties对象就可以了。 settings二级标签中一共26个配置比如二级缓存、延迟加载、默认执行器类型等等。 需要注意的是我们之前提到的所有的默认值都是在这里赋值的。如果说后面我们不知道这个属性的值是什么也可以到这一步来确认一下。 所有的值都会赋值到Configuration的属性里面去。 private void settingsElement(Properties props) {configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty(autoMappingBehavior, PARTIAL)));configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty(autoMappingUnknownColumnBehavior, NONE)));configuration.setCacheEnabled(booleanValueOf(props.getProperty(cacheEnabled), true));configuration.setProxyFactory((ProxyFactory) createInstance(props.getProperty(proxyFactory)));configuration.setLazyLoadingEnabled(booleanValueOf(props.getProperty(lazyLoadingEnabled), false));configuration.setAggressiveLazyLoading(booleanValueOf(props.getProperty(aggressiveLazyLoading), false));configuration.setMultipleResultSetsEnabled(booleanValueOf(props.getProperty(multipleResultSetsEnabled), true));configuration.setUseColumnLabel(booleanValueOf(props.getProperty(useColumnLabel), true));configuration.setUseGeneratedKeys(booleanValueOf(props.getProperty(useGeneratedKeys), false));configuration.setDefaultExecutorType(ExecutorType.valueOf(props.getProperty(defaultExecutorType, SIMPLE)));configuration.setDefaultStatementTimeout(integerValueOf(props.getProperty(defaultStatementTimeout), null));configuration.setDefaultFetchSize(integerValueOf(props.getProperty(defaultFetchSize), null));configuration.setDefaultResultSetType(resolveResultSetType(props.getProperty(defaultResultSetType)));configuration.setMapUnderscoreToCamelCase(booleanValueOf(props.getProperty(mapUnderscoreToCamelCase), false));configuration.setSafeRowBoundsEnabled(booleanValueOf(props.getProperty(safeRowBoundsEnabled), false));configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(localCacheScope, SESSION)));configuration.setJdbcTypeForNull(JdbcType.valueOf(props.getProperty(jdbcTypeForNull, OTHER)));configuration.setLazyLoadTriggerMethods(stringSetValueOf(props.getProperty(lazyLoadTriggerMethods), equals,clone,hashCode,toString));configuration.setSafeResultHandlerEnabled(booleanValueOf(props.getProperty(safeResultHandlerEnabled), true));configuration.setDefaultScriptingLanguage(resolveClass(props.getProperty(defaultScriptingLanguage)));configuration.setDefaultEnumTypeHandler(resolveClass(props.getProperty(defaultEnumTypeHandler)));configuration.setCallSettersOnNulls(booleanValueOf(props.getProperty(callSettersOnNulls), false));configuration.setUseActualParamName(booleanValueOf(props.getProperty(useActualParamName), true));configuration.setReturnInstanceForEmptyRow(booleanValueOf(props.getProperty(returnInstanceForEmptyRow), false));configuration.setLogPrefix(props.getProperty(logPrefix));configuration.setConfigurationFactory(resolveClass(props.getProperty(configurationFactory)));}
environments解析 这一步是解析environments标签。一个environment就是对应一个数据源所以在这里我们会根据配置的transactionManager创建一个事务工厂根据dataSource标签创建一个数据源最后把这两个对象设置成Environment对象的属性放到Configuration里面。 private void environmentsElement(XNode context) throws Exception {if (context ! null) {if (environment null) {environment context.getStringAttribute(default);}for (XNode child : context.getChildren()) {String id child.getStringAttribute(id);if (isSpecifiedEnvironment(id)) {// 事务工厂TransactionFactory txFactory transactionManagerElement(child.evalNode(transactionManager));// 数据源工厂例如 DruidDataSourceFactory DataSourceFactory dsFactory dataSourceElement(child.evalNode(dataSource));// 数据源DataSource dataSource dsFactory.getDataSource();// 包含了 事务工厂和数据源的 EnvironmentEnvironment.Builder environmentBuilder new Environment.Builder(id).transactionFactory(txFactory).dataSource(dataSource);// 放入 Configurationconfiguration.setEnvironment(environmentBuilder.build());}}}}databaseIdProviderElement() 解析databaseIdProvider标签生成DatabaseIdProvider对象用来支持不同厂商的数据库。 typeHandlerElement() 跟TypeAlias一样TypeHandler有两种配置方式一种是单独配置一个类一种是指定一个package。最后我们得到的是JavaType和JdbcType以及用来做相互映射的TypeHandler之间的映射关系存放在TypeHandlerRegistry对象里面。 private void typeHandlerElement(XNode parent) {if (parent ! null) {for (XNode child : parent.getChildren()) {if (package.equals(child.getName())) {String typeHandlerPackage child.getStringAttribute(name);typeHandlerRegistry.register(typeHandlerPackage);} else {String javaTypeName child.getStringAttribute(javaType);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);}}}}}mapper解析-映射文件的解析 最后就是mappers标签的解析。 根据全局配置文件中不同的注册方式用不同的方式扫描但最终都是做了两件事情对于语句的注册和接口的注册。 扫描类型含义resource相对路径url绝对路径package包class单个接口 private void mapperElement(XNode parent) throws Exception {if (parent ! null) {for (XNode child : parent.getChildren()) {// 不同的定义方式的扫描最终都是调用 addMapper()方法添加到 MapperRegistry。这个方法和 getMapper() 对应// package 包if (package.equals(child.getName())) {String mapperPackage child.getStringAttribute(name);configuration.addMappers(mapperPackage);} else {String resource child.getStringAttribute(resource);String url child.getStringAttribute(url);String mapperClass child.getStringAttribute(class);if (resource ! null url null mapperClass null) {// resource 相对路径ErrorContext.instance().resource(resource);InputStream inputStream Resources.getResourceAsStream(resource);XMLMapperBuilder mapperParser new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());// 解析 Mapper.xml总体上做了两件事情 mapperParser.parse();} else if (resource null url ! null mapperClass null) {// url 绝对路径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 单个接口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.);}}}}}然后开始进入具体的配置文件的解析操作
mapperParser.parse()
首先进入parse方法public void parse() {// 总体上做了两件事情对于语句的注册和接口的注册if (!configuration.isResourceLoaded(resource)) {// 1、具体增删改查标签的解析。// 一个标签一个MappedStatement。 configurationElement(parser.evalNode(/mapper));configuration.addLoadedResource(resource);// 2、把namespace接口类型和工厂类绑定起来放到一个map。// 一个namespace 一个 MapperProxyFactory bindMapperForNamespace();}parsePendingResultMaps();parsePendingCacheRefs();parsePendingStatements();}configurationElement()——解析所有的子标签最终获得MappedStatement对象。
bindMapperForNamespace()——把namespace接口类型和工厂类MapperProxyFactory绑定起来。
configurationElement() configurationElement是对Mapper.xml中所有具体的标签的解析包括namespace、cache、parameterMap、resultMap、sql和select|insert|update|delete。 private void configurationElement(XNode context) {try {String namespace context.getStringAttribute(namespace);if (namespace null || namespace.equals()) {throw new BuilderException(Mappers namespace cannot be empty);}builderAssistant.setCurrentNamespace(namespace);// 添加缓存对象cacheRefElement(context.evalNode(cache-ref));// 解析 cache 属性添加缓存对象cacheElement(context.evalNode(cache));// 创建 ParameterMapping 对象parameterMapElement(context.evalNodes(/mapper/parameterMap));// 创建 ListResultMappingresultMapElements(context.evalNodes(/mapper/resultMap));// 解析可以复用的SQLsqlElement(context.evalNodes(/mapper/sql));// 解析增删改查标签得到 MappedStatement 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);}}在buildStatementFromContext()方法中创建了用来解析增删改查标签的XMLStatementBuilder并且把创建的MappedStatement添加到mappedStatements中。 MappedStatement statement statementBuilder.build();// 最关键的一步在 Configuration 添加了 MappedStatement configuration.addMappedStatement(statement);bindMapperForNamespace()方法 private void bindMapperForNamespace() {String namespace builderAssistant.getCurrentNamespace();if (namespace ! null) {Class? boundType null;try {// 根据名称空间加载对应的接口类型boundType Resources.classForName(namespace);} catch (ClassNotFoundException e) {//ignore, bound type is not required}if (boundType ! null) {if (!configuration.hasMapper(boundType)) {// Spring may not know the real resource name so we set a flag// to prevent loading again this resource from the mapper interface// look at MapperAnnotationBuilder#loadXmlResourceconfiguration.addLoadedResource(namespace: namespace);// 添加到 MapperRegistry本质是一个 map里面也有 Configuration configuration.addMapper(boundType);}}}}通过源码分析发现主要是是调用了addMapper()。addMapper()方法中把接口类型注册到MapperRegistry中实际上是为接口创建一个对应的MapperProxyFactory用于为这个type提供工厂类创建MapperProxy。 public T void addMapper(ClassT type) {if (type.isInterface()) { // 检测 type 是否为接口if (hasMapper(type)) { // 检测是否已经加装过该接口throw new BindingException(Type type is already known to the MapperRegistry.);}boolean loadCompleted false;try {// MapClass?, MapperProxyFactory? 存放的是接口类型和对应的工厂类的关系knownMappers.put(type, new MapperProxyFactory(type));// Its important that the type is added before the parser is run// otherwise the binding may automatically be attempted by the// mapper parser. If the type is already known, it wont try.// 注册了接口之后根据接口开始解析所有方法上的注解例如 Select MapperAnnotationBuilder parser new MapperAnnotationBuilder(config, type);parser.parse();loadCompleted true;} finally {if (!loadCompleted) {knownMappers.remove(type);}}}}同样的再进入parse方法中查看 public void parse() {String resource type.toString();if (!configuration.isResourceLoaded(resource)) {// 先判断 Mapper.xml 有没有解析没有的话先解析 Mapper.xml例如定义 package 方式loadXmlResource();configuration.addLoadedResource(resource);assistant.setCurrentNamespace(type.getName());// 处理 CacheNamespaceparseCache();// 处理 CacheNamespaceRefparseCacheRef();// 获取所有方法Method[] methods type.getMethods();for (Method method : methods) {try {// issue #237if (!method.isBridge()) {// 解析方法上的注解添加到 MappedStatement 集合中 parseStatement(method);}} catch (IncompleteElementException e) {configuration.addIncompleteMethod(new MethodResolver(this, method));}}}parsePendingMethods();}3、总结
我们主要完成了config配置文件、Mapper文件、Mapper接口中注解的解析。我们得到了一个最重要的对象Configuration这里面存放了全部的配置信息它在属性里面还有各种各样的容器。最后返回了一个DefaultSqlSessionFactory里面持有了Configuration的实例。 2.3 SqlSession 程序每一次操作数据库都需要创建一个会话我们用openSession()方法来创建。接下来我们看看SqlSession创建过程中做了哪些操作 2.3.1 factory.openSession()
SqlSession sqlSession factory.openSession();Overridepublic SqlSession openSession() {return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}首先会获取默认的执行器类型, 默认的是simple。
2.3.1.1 openSessionFromDataSource() private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {Transaction tx null;try {final Environment environment configuration.getEnvironment();// 获取事务工厂final TransactionFactory transactionFactory getTransactionFactoryFromEnvironment(environment);// 创建事务tx transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);// 根据事务工厂和默认的执行器类型创建执行器 final Executor executor configuration.newExecutor(tx, execType);return new DefaultSqlSession(configuration, executor, autoCommit);} catch (Exception e) {closeTransaction(tx); // may have fetched a connection so lets call close()throw ExceptionFactory.wrapException(Error opening session. Cause: e, e);} finally {ErrorContext.instance().reset();}}我们在解析environment标签的时候有创建TransactionFactory对象 根据事务工厂和默认的执行器类型创建执行器 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {executorType executorType null ? defaultExecutorType : executorType;executorType executorType null ? ExecutorType.SIMPLE : executorType;Executor executor;if (ExecutorType.BATCH executorType) {executor new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE executorType) {executor new ReuseExecutor(this, transaction);} else {// 默认 SimpleExecutorexecutor new SimpleExecutor(this, transaction);}// 二级缓存开关settings 中的 cacheEnabled 默认是 trueif (cacheEnabled) {executor new CachingExecutor(executor);}// 植入插件的逻辑至此四大对象已经全部拦截完毕executor (Executor) interceptorChain.pluginAll(executor);return executor;}最后返回的是一个DefaultSqlSession对象 在这个DefaultSqlSession对象中包括了Configuration和Executor对象 2.3.2 总结 创建会话的过程我们获得了一个DefaultSqlSession里面包含了一个ExecutorExecutor是SQL的实际执行对象。 2.4 Mapper代理对象
接下来看下通过getMapper方法获取对应的接口的代理对象的实现原理2.4.1 getMapper()
// 4.通过SqlSession中提供的 API方法来操作数据库
UserMapper mapper sqlSession.getMapper(UserMapper.class);进入DefaultSqlSession中查看 public T T getMapper(ClassT type, SqlSession sqlSession) {// mapperRegistry中注册的有Mapper的相关信息 在解析映射文件时 调用过addMapper方法return mapperRegistry.getMapper(type, sqlSession);}进入getMapper方法 /*** 获取Mapper接口对应的代理对象*/public T T getMapper(ClassT type, SqlSession sqlSession) {// 获取Mapper接口对应的 MapperProxyFactory 对象final MapperProxyFactoryT mapperProxyFactory (MapperProxyFactoryT) knownMappers.get(type);if (mapperProxyFactory null) {throw new BindingException(Type type is not known to the MapperRegistry.);}try {return mapperProxyFactory.newInstance(sqlSession);} catch (Exception e) {throw new BindingException(Error getting mapper instance. Cause: e, e);}}2.4.2 newInstance()
进入newInstance方法 public T newInstance(SqlSession sqlSession) {final MapperProxyT mapperProxy new MapperProxy(sqlSession, mapperInterface, methodCache);return newInstance(mapperProxy);}/*** 创建实现了 mapperInterface 接口的代理对象*/protected T newInstance(MapperProxyT mapperProxy) {// 1类加载器:2被代理类实现的接口、3实现了 InvocationHandler 的触发管理类return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);}最终我们在代码中发现代理对象是通过JDK动态代理创建返回的代理对象。而且里面也传递了一个实现了InvocationHandler接口的触发管理类。 2.4.3 总结 获得Mapper对象的过程实质上是获取了一个JDK动态代理对象类型是$ProxyN。这个代理类会继承Proxy类实现被代理的接口里面持有了一个MapperProxy类型的触发管理类。 2.5 SQL执行
接下来我们看看SQL语句的具体执行过程是怎么样的ListUser list mapper.selectUserList();由于所有的Mapper都是JDK动态代理对象所以任意的方法都是执行触发管理类MapperProxy的invoke()方法2.5.1 MapperProxy.invoke()
我们直接进入到invoke方法中Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// toString hashCode equals getClass等方法无需走到执行SQL的流程if (Object.class.equals(method.getDeclaringClass())) {return method.invoke(this, args);} else {// 提升获取 mapperMethod 的效率到 MapperMethodInvoker内部接口 的 invoke// 普通方法会走到 PlainMethodInvoker内部类 的 invokereturn cachedInvoker(method).invoke(proxy, method, args, sqlSession);}} catch (Throwable t) {throw ExceptionUtil.unwrapThrowable(t);}
}然后进入到PlainMethodInvoker的invoke方法 Overridepublic Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {// SQL执行的真正起点return mapperMethod.execute(sqlSession, args);}2.5.2 mapperMethod.execute() public Object execute(SqlSession sqlSession, Object[] args) {Object result;switch (command.getType()) { // 根据SQL语句的类型调用SqlSession对应的方法case INSERT: {// 通过 ParamNameResolver 处理args[] 数组 将用户传入的实参和指定参数名称关联起来Object param method.convertArgsToSqlCommandParam(args);// sqlSession.insert(command.getName(), param) 调用SqlSession的insert方法// rowCountResult 方法会根据 method 字段中记录的方法的返回值类型对结果进行转换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()) {// 返回值为空 且 ResultSet通过 ResultHandler处理的方法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);// 普通 select 语句的执行入口 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;}
在这一步根据不同的typeINSERT、UPDATE、DELETE、SELECT和返回类型
1调用convertArgsToSqlCommandParam()将方法参数转换为SQL的参数。
2调用sqlSession的insert()、update()、delete()、selectOne ()方法。我们以查询为例会走到selectOne()方法。
Object param method.convertArgsToSqlCommandParam(args);
result sqlSession.selectOne(command.getName(), param);2.5.3 sqlSession.selectOne()
这里来到了对外的接口的默认实现类DefaultSqlSession。
selectOne()最终也是调用了selectList() Overridepublic T T selectOne(String statement, Object parameter) {// 来到了 DefaultSqlSession// Popular vote was to return null on 0 results and throw exception on too many.ListT list this.selectList(statement, parameter);if (list.size() 1) {return list.get(0);} else if (list.size() 1) {throw new TooManyResultsException(Expected one result (or null) to be returned by selectOne(), but found: list.size());} else {return null;}}在SelectList()中我们先根据command nameStatement ID从Configuration中拿到MappedStatement。ms里面有xml中增删改查标签配置的所有属性包括id、statementType、sqlSource、useCache、入参、出参等等 Overridepublic E ListE selectList(String statement, Object parameter, RowBounds rowBounds) {try {MappedStatement ms configuration.getMappedStatement(statement);// 如果 cacheEnabled true默认Executor会被 CachingExecutor装饰return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);} catch (Exception e) {throw ExceptionFactory.wrapException(Error querying database. Cause: e, e);} finally {ErrorContext.instance().reset();}}然后执行了Executor的query()方法。
Executor是第二步openSession的时候创建的创建了执行器基本类型之后依次执行了二级缓存装饰和插件包装。
所以如果有被插件包装这里会先走到插件的逻辑。如果没有显式地在settings中配置cacheEnabledfalse再走到CachingExecutor的逻辑然后会走到BaseExecutor的query()方法。
插件后面讲这里先跳过。
2.5.4 CachingExecutor.query() Overridepublic E ListE query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {// 获取SQLBoundSql boundSql ms.getBoundSql(parameterObject);// 创建CacheKey什么样的SQL是同一条SQL CacheKey key createCacheKey(ms, parameterObject, rowBounds, boundSql);return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}二级缓存的CacheKey是怎么构成的呢或者说什么样的查询才能确定是同一个查询呢
在BaseExecutor的createCacheKey方法中用到了六个要素
cacheKey.update(ms.getId()); // com.msb.mapper.BlogMapper.selectBlogById
cacheKey.update(rowBounds.getOffset()); // 0
cacheKey.update(rowBounds.getLimit()); // 2147483647 2^31-1
cacheKey.update(boundSql.getSql());
cacheKey.update(value);
cacheKey.update(configuration.getEnvironment().getId());也就是说方法相同、翻页偏移相同、SQL相同、参数值相同、数据源环境相同才会被认为是同一个查询。
CacheKey的实际值举例toString()生成的debug可以看到
-1381545870:4796102018:com.msb.mapper.BlogMapper.selectBlogById:0:2147483647:select * from blog where bid ?:1:development注意看一下CacheKey的属性里面有一个List按顺序存放了这些要素。 private static final int DEFAULT_MULTIPLIER 37;private static final int DEFAULT_HASHCODE 17;private final int multiplier;private int hashcode;private long checksum;private int count;private ListObject updateList怎么比较两个CacheKey是否相等呢如果一上来就是依次比较六个要素是否相等要比较6次这样效率不高。有没有更高效的方法呢继承Object的每个类都有一个hashCode ()方法用来生成哈希码。它是用来在集合中快速判重的。
在生成CacheKey的时候update方法也更新了CacheKey的hashCode它是用乘法哈希生成的基数baseHashCode17乘法因子multiplier37。
hashcode multiplier * hashcode baseHashCode;Object中的hashCode()是一个本地方法通过随机数算法生成OpenJDK8 默认可以通过-XX:hashCode修改。CacheKey中的hashCode()方法进行了重写返回自己生成的hashCode。为什么要用37作为乘法因子呢跟String中的31类似。
CacheKey中的equals也进行了重写比较CacheKey是否相等。 Overridepublic boolean equals(Object object) {if (this object) {return true;}if (!(object instanceof CacheKey)) {return false;}final CacheKey cacheKey (CacheKey) object;if (hashcode ! cacheKey.hashcode) {return false;}if (checksum ! cacheKey.checksum) {return false;}if (count ! cacheKey.count) {return false;}for (int i 0; i updateList.size(); i) {Object thisObject updateList.get(i);Object thatObject cacheKey.updateList.get(i);if (!ArrayUtil.equals(thisObject, thatObject)) {return false;}}return true;
}如果哈希值乘法哈希、校验值加法哈希、要素个数任何一个不相等都不是同一个查询最后才循环比较要素防止哈希碰撞。
CacheKey生成之后调用另一个query()方法。
2.5.5 BaseExecutor.query方法 Overridepublic E ListE query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)throws SQLException {Cache cache ms.getCache();// cache 对象是在哪里创建的 XMLMapperBuilder类 xmlconfigurationElement()// 由 cache 标签决定if (cache ! null) {// flushCachetrue 清空一级二级缓存 flushCacheIfRequired(ms);if (ms.isUseCache() resultHandler null) {ensureNoOutParams(ms, boundSql);// 获取二级缓存// 缓存通过 TransactionalCacheManager、TransactionalCache 管理SuppressWarnings(unchecked)ListE list (ListE) tcm.getObject(cache, key);if (list null) {list delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);// 写入二级缓存tcm.putObject(cache, key, list); // issue #578 and #116}return list;}}// 走到 SimpleExecutor | ReuseExecutor | BatchExecutorreturn delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);}public E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {// 异常体系之 ErrorContextErrorContext.instance().resource(ms.getResource()).activity(executing a query).object(ms.getId());if (closed) {throw new ExecutorException(Executor was closed.);}if (queryStack 0 ms.isFlushCacheRequired()) {// flushCachetrue时即使是查询也清空一级缓存clearLocalCache();}ListE list;try {// 防止递归查询重复处理缓存queryStack;// 查询一级缓存// ResultHandler 和 ResultSetHandler的区别list resultHandler null ? (ListE) localCache.getObject(key) : null;if (list ! null) {handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);} else {// 真正的查询流程list queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);}} finally {queryStack--;}if (queryStack 0) {for (DeferredLoad deferredLoad : deferredLoads) {deferredLoad.load();}// issue #601deferredLoads.clear();if (configuration.getLocalCacheScope() LocalCacheScope.STATEMENT) {// issue #482clearLocalCache();}}return list;}private E ListE queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {ListE list;// 先占位localCache.putObject(key, EXECUTION_PLACEHOLDER);try {// 三种 Executor 的区别看doUpdate// 默认Simplelist doQuery(ms, parameter, rowBounds, resultHandler, boundSql);} finally {// 移除占位符localCache.removeObject(key);}// 写入一级缓存localCache.putObject(key, list);if (ms.getStatementType() StatementType.CALLABLE) {localOutputParameterCache.putObject(key, parameter);}return list;}2.5.6 SimpleExecutor.doQuery方法 Overridepublic E ListE doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {Statement stmt null;try {Configuration configuration ms.getConfiguration();// 注意已经来到SQL处理的关键对象 StatementHandler StatementHandler handler configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);// 获取一个 Statement对象stmt prepareStatement(handler, ms.getStatementLog());// 执行查询return handler.query(stmt, resultHandler);} finally {// 用完就关闭closeStatement(stmt);}}在configuration.newStatementHandler()中new一个StatementHandler先得到RoutingStatementHandler。RoutingStatementHandler里面没有任何的实现是用来创建基本的StatementHandler的。这里会根据MappedStatement里面的statementType决定StatementHandler的类型。默认是PREPAREDSTATEMENT、PREPARED、CALLABLE。 public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {// StatementType 是怎么来的 增删改查标签中的 statementTypePREPARED默认值 PREPAREDswitch (ms.getStatementType()) {case STATEMENT:delegate new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case PREPARED:// 创建 StatementHandler 的时候做了什么 delegate new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;case CALLABLE:delegate new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);break;default:throw new ExecutorException(Unknown statement type: ms.getStatementType());}}StatementHandler里面包含了处理参数的ParameterHandler和处理结果集的ResultSetHandler。
这两个对象都是在上面new的时候创建的。 protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {this.configuration mappedStatement.getConfiguration();this.executor executor;this.mappedStatement mappedStatement;this.rowBounds rowBounds;this.typeHandlerRegistry configuration.getTypeHandlerRegistry();this.objectFactory configuration.getObjectFactory();if (boundSql null) { // issue #435, get the key before calculating the statementgenerateKeys(parameterObject);boundSql mappedStatement.getBoundSql(parameterObject);}this.boundSql boundSql;// 创建了四大对象的其它两大对象 // 创建这两大对象的时候分别做了什么this.parameterHandler configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);this.resultSetHandler configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);}这三个对象都是可以被插件拦截的四大对象之一所以在创建之后都要用拦截器进行包装的方法。public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {ParameterHandler parameterHandler mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);// 植入插件逻辑返回代理对象parameterHandler (ParameterHandler) interceptorChain.pluginAll(parameterHandler);return parameterHandler;}public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,ResultHandler resultHandler, BoundSql boundSql) {ResultSetHandler resultSetHandler new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);// 植入插件逻辑返回代理对象resultSetHandler (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);return resultSetHandler;}public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {StatementHandler statementHandler new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);// 植入插件逻辑返回代理对象statementHandler (StatementHandler) interceptorChain.pluginAll(statementHandler);return statementHandler;}创建Statement
用new出来的StatementHandler创建Statement对象。 执行查询操作,如果有插件包装会先走到被拦截的业务逻辑。
// 执行查询
return handler.query(stmt, resultHandler);进入到PreparedStatementHandler中处理 Overridepublic E ListE query(Statement statement, ResultHandler resultHandler) throws SQLException {PreparedStatement ps (PreparedStatement) statement;// 到了JDBC的流程ps.execute();// 处理结果集return resultSetHandler.handleResultSets(ps);}执行PreparedStatement的execute()方法后面就是JDBC包中的PreparedStatement的执行了。 ResultSetHandler处理结果集如果有插件包装会先走到被拦截的业务逻辑。
总结调用代理对象执行SQL操作的流程 2.6 MyBatis核心对象
对象相关对象作用ConfigurationMapperRegistryTypeAliasRegistryTypeHandlerRegistry包含了MyBatis的所有的配置信息SqlSessionSqlSessionFactoryDefaultSqlSession对操作数据库的增删改查的API进行了封装提供给应用层使用ExecutorBaseExecutorSimpleExecutorBatchExecutorReuseExecutorMyBatis执行器是MyBatis 调度的核心负责SQL语句的生成和查询缓存的维护StatementHandlerBaseStatementHandlerSimpleStatementHandlerPreparedStatementHandlerCallableStatementHandler封装了JDBC Statement操作负责对JDBC statement 的操作如设置参数、将Statement结果集转换成List集合ParameterHandlerDefaultParameterHandler把用户传递的参数转换成JDBC Statement 所需要的参数ResultSetHandlerDefaultResultSetHandler把JDBC返回的ResultSet结果集对象转换成List类型的集合MapperProxyMapperProxyFactory触发管理类用于代理Mapper接口方法MappedStatementSqlSourceBoundSqlMappedStatement维护了一条select|update|delete|insert节点的封装表示一条SQL包括了SQL信息、入参信息、出参信息 文章转载自: http://www.morning.pxwzk.cn.gov.cn.pxwzk.cn http://www.morning.nswcw.cn.gov.cn.nswcw.cn http://www.morning.fyglg.cn.gov.cn.fyglg.cn http://www.morning.cybch.cn.gov.cn.cybch.cn http://www.morning.gskzy.cn.gov.cn.gskzy.cn http://www.morning.lbpfl.cn.gov.cn.lbpfl.cn http://www.morning.rcntx.cn.gov.cn.rcntx.cn http://www.morning.bscsp.cn.gov.cn.bscsp.cn http://www.morning.oioini.com.gov.cn.oioini.com http://www.morning.plfy.cn.gov.cn.plfy.cn http://www.morning.qswws.cn.gov.cn.qswws.cn http://www.morning.ktcrr.cn.gov.cn.ktcrr.cn http://www.morning.xfxnq.cn.gov.cn.xfxnq.cn http://www.morning.plwfx.cn.gov.cn.plwfx.cn http://www.morning.bdzps.cn.gov.cn.bdzps.cn http://www.morning.npmx.cn.gov.cn.npmx.cn http://www.morning.hhboyus.cn.gov.cn.hhboyus.cn http://www.morning.mrbzq.cn.gov.cn.mrbzq.cn http://www.morning.ltspm.cn.gov.cn.ltspm.cn http://www.morning.fwblh.cn.gov.cn.fwblh.cn http://www.morning.sxygc.cn.gov.cn.sxygc.cn http://www.morning.lbgfz.cn.gov.cn.lbgfz.cn http://www.morning.jwqqd.cn.gov.cn.jwqqd.cn http://www.morning.tlfmr.cn.gov.cn.tlfmr.cn http://www.morning.brfxt.cn.gov.cn.brfxt.cn http://www.morning.gbjxj.cn.gov.cn.gbjxj.cn http://www.morning.xcjbk.cn.gov.cn.xcjbk.cn http://www.morning.clxpp.cn.gov.cn.clxpp.cn http://www.morning.ptmgq.cn.gov.cn.ptmgq.cn http://www.morning.jxzfg.cn.gov.cn.jxzfg.cn http://www.morning.nkbfc.cn.gov.cn.nkbfc.cn http://www.morning.dfmjm.cn.gov.cn.dfmjm.cn http://www.morning.nykzl.cn.gov.cn.nykzl.cn http://www.morning.zhnpj.cn.gov.cn.zhnpj.cn http://www.morning.qxxj.cn.gov.cn.qxxj.cn http://www.morning.zypnt.cn.gov.cn.zypnt.cn http://www.morning.mrlkr.cn.gov.cn.mrlkr.cn http://www.morning.pbgnx.cn.gov.cn.pbgnx.cn http://www.morning.zlxrg.cn.gov.cn.zlxrg.cn http://www.morning.wcft.cn.gov.cn.wcft.cn http://www.morning.ymwny.cn.gov.cn.ymwny.cn http://www.morning.hymmq.cn.gov.cn.hymmq.cn http://www.morning.bhqlj.cn.gov.cn.bhqlj.cn http://www.morning.rzrbw.cn.gov.cn.rzrbw.cn http://www.morning.prfrb.cn.gov.cn.prfrb.cn http://www.morning.plfy.cn.gov.cn.plfy.cn http://www.morning.gygfx.cn.gov.cn.gygfx.cn http://www.morning.rdlxh.cn.gov.cn.rdlxh.cn http://www.morning.xinxianzhi005.com.gov.cn.xinxianzhi005.com http://www.morning.pkfpl.cn.gov.cn.pkfpl.cn http://www.morning.wnjwb.cn.gov.cn.wnjwb.cn http://www.morning.qynpw.cn.gov.cn.qynpw.cn http://www.morning.hclplus.com.gov.cn.hclplus.com http://www.morning.hxcrd.cn.gov.cn.hxcrd.cn http://www.morning.jnbsx.cn.gov.cn.jnbsx.cn http://www.morning.fwrr.cn.gov.cn.fwrr.cn http://www.morning.blbys.cn.gov.cn.blbys.cn http://www.morning.pbpcj.cn.gov.cn.pbpcj.cn http://www.morning.zbhfs.cn.gov.cn.zbhfs.cn http://www.morning.rqhbt.cn.gov.cn.rqhbt.cn http://www.morning.kwyq.cn.gov.cn.kwyq.cn http://www.morning.ltksw.cn.gov.cn.ltksw.cn http://www.morning.nkhdt.cn.gov.cn.nkhdt.cn http://www.morning.xtlty.cn.gov.cn.xtlty.cn http://www.morning.fgtls.cn.gov.cn.fgtls.cn http://www.morning.gkgr.cn.gov.cn.gkgr.cn http://www.morning.qkkmd.cn.gov.cn.qkkmd.cn http://www.morning.jxtbr.cn.gov.cn.jxtbr.cn http://www.morning.bwdnx.cn.gov.cn.bwdnx.cn http://www.morning.rdnkx.cn.gov.cn.rdnkx.cn http://www.morning.ns3nt8.cn.gov.cn.ns3nt8.cn http://www.morning.cwkcq.cn.gov.cn.cwkcq.cn http://www.morning.tdfyj.cn.gov.cn.tdfyj.cn http://www.morning.tnkwj.cn.gov.cn.tnkwj.cn http://www.morning.bnbzd.cn.gov.cn.bnbzd.cn http://www.morning.wngpq.cn.gov.cn.wngpq.cn http://www.morning.wqmpd.cn.gov.cn.wqmpd.cn http://www.morning.rwbh.cn.gov.cn.rwbh.cn http://www.morning.zlrsy.cn.gov.cn.zlrsy.cn http://www.morning.rchsr.cn.gov.cn.rchsr.cn