建设自己的网站首页,中山网站建设文化方案,长治网站制作一般多少钱,html5电影网站设计论文相信在很多使用MybatisPlus框架的小伙伴都会遇到多数据源的配置问题#xff0c;并且官网也给出了推荐使用多数据源 (dynamic-datasource-spring-boot-starter) 组件来实现。由于最近项目也在使用这个组件来实现多数据源切换#xff0c;因此想了解一下该组件是如何运行的…相信在很多使用MybatisPlus框架的小伙伴都会遇到多数据源的配置问题并且官网也给出了推荐使用多数据源 (dynamic-datasource-spring-boot-starter) 组件来实现。由于最近项目也在使用这个组件来实现多数据源切换因此想了解一下该组件是如何运行的经过自己的调试简单记录一下这个组件的实现也以便日后组件如果出问题了或者某些地方需要开次开发时有个参考。1 简单实现数据源切换1.1 数据库demo本例子使用的是同一个MYSQL服务不同数据库来进行调试的具体如图所示建表语句如下:CREATE TABLE class_t (name varchar(30) DEFAULT NULL,number varchar(30) DEFAULT NULL
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE user_t (id int(11) NOT NULL AUTO_INCREMENT,user_name varchar(30) DEFAULT NULL,user_sex varchar(30) DEFAULT NULL,PRIMARY KEY (id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb4;1.2 SpringBoot demo1.2.1 添加依赖 dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependencydependencygroupIdcom.baomidou/groupIdartifactIdmybatis-plus-boot-starter/artifactIdversion3.4.0/version/dependencydependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactIdscoperuntime/scope/dependencydependencygroupIdcom.baomidou/groupIdartifactIddynamic-datasource-spring-boot-starter/artifactIdversion3.4.0/version/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactIdversion1.18.20/versionscopeprovided/scope/dependency1.2.2 配置YML文件spring:datasource:type: com.zaxxer.hikari.HikariDataSourcehikari:minimum-idle: 5maximum-pool-size: 15idle-timeout: 30000max-lifetime: 1800000connection-timeout: 30000pool-name: OasisHikariCPconnection-test-query: SELECT 1dynamic:primary: db1 #默认主数据源datasource:db1: #配置主数据源url: jdbc:mysql://127.0.0.1:3306/test?useSSLtruerequireSSLfalseserverTimezoneUTCcharacterEncodingUTF-8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driverdb2: #配置其他数据源url: jdbc:mysql://127.0.0.1:3306/test2?useSSLtruerequireSSLfalseserverTimezoneUTCcharacterEncodingUTF-8username: rootpassword: 123456driver-class-name: com.mysql.cj.jdbc.Driver1.2.3 实体层UserEntity.java/*** description: DB1中的实体* date: 2021/7/13 13:38 br* author: wuKeFan br* version: 1.0 br*/
TableName(user_t)
public class UserEntity {private long id;private String userName;private String userSex;public long getId() {return id;}public void setId(long id) {this.id id;}public String getUserName() {return userName;}public void setUserName(String userName) {this.userName userName;}public String getUserSex() {return userSex;}public void setUserSex(String userSex) {this.userSex userSex;}
}ClassEntity.java/*** description: DB2中的实体* date: 2021/7/13 13:40 br* author: wuKeFan br* version: 1.0 br*/
TableName(class_t)
public class ClassEntity {private String name;private String number;public String getName() {return name;}public void setName(String name) {this.name name;}public String getNumber() {return number;}public void setNumber(String number) {this.number number;}
}1.2.4 mapper层UserMapper.java(使用默认数据源)/*** description: UserMapper br* date: 2021/7/13 13:41 br* author: wuKeFan br* version: 1.0 br*/
public interface UserMapper extends BaseMapperUserEntity {
}ClassMapper.java(使用另外一个数据源)/*** description: ClassMapper br* date: 2021/7/13 13:41 br* author: wuKeFan br* version: 1.0 br*/
DS(db2) //使用另外一个数据源
public interface ClassMapper extends BaseMapperClassEntity {
}
1.2.5 单元测试结果已经是可以完美运行多数据源。2 源码解析在我们搞项目中不仅要学会用这些组件更重要的是 要知其所以然知道他是如何实现的其实原理也就是网上能搜到的基于切面的代理处理方式但是其中有些内容还是值得去学习。2.1 自动装配首先我们从 dynamic-datasource组件的自动装配开始接下来让我们来看一下 这个自动装配类所装配的BeanSlf4j
Configuration
//启动SpringBoot 自动装配 DynamicDataSourceProperties外部化配置
EnableConfigurationProperties(DynamicDataSourceProperties.class)
//声明装配加载顺序,在 DataSourceAutoConfiguration 之前加载
AutoConfigureBefore(value DataSourceAutoConfiguration.class, name com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure)
//当自动装配时引入并自动装配下列三个自动装配类
Import(value {DruidDynamicDataSourceConfiguration.class, DynamicDataSourceCreatorAutoConfiguration.class, DynamicDataSourceHealthCheckConfiguration.class})
//自动装配加载条件 当 spring.datasource.dynamic true时 进行自动装配的加载默认缺省为true
ConditionalOnProperty(prefix DynamicDataSourceProperties.PREFIX, name enabled, havingValue true, matchIfMissing true)
public class DynamicDataSourceAutoConfiguration implements InitializingBean {//注入外部化配置private final DynamicDataSourceProperties properties;private final ListDynamicDataSourcePropertiesCustomizer dataSourcePropertiesCustomizers;//构造函数注入public DynamicDataSourceAutoConfiguration(DynamicDataSourceProperties properties,ObjectProviderListDynamicDataSourcePropertiesCustomizer dataSourcePropertiesCustomizers) {this.properties properties;this.dataSourcePropertiesCustomizers dataSourcePropertiesCustomizers.getIfAvailable();}//多数据源加载接口默认的实现为从yml信息中加载所有数据源 Beanpublic DynamicDataSourceProvider ymlDynamicDataSourceProvider() {return new YmlDynamicDataSourceProvider(properties.getDatasource());}//实现DataSource JAVA JNDI 后期Spring 容器中 所有的数据库连接都从该实现Bean 中获取BeanConditionalOnMissingBeanpublic DataSource dataSource() {DynamicRoutingDataSource dataSource new DynamicRoutingDataSource();dataSource.setPrimary(properties.getPrimary());dataSource.setStrict(properties.getStrict());dataSource.setStrategy(properties.getStrategy());dataSource.setP6spy(properties.getP6spy());dataSource.setSeata(properties.getSeata());return dataSource;}//设置动态数据源转换切换配置器Role(value BeanDefinition.ROLE_INFRASTRUCTURE)Beanpublic Advisor dynamicDatasourceAnnotationAdvisor(DsProcessor dsProcessor) {DynamicDataSourceAnnotationInterceptor interceptor new DynamicDataSourceAnnotationInterceptor(properties.isAllowedPublicOnly(), dsProcessor);DynamicDataSourceAnnotationAdvisor advisor new DynamicDataSourceAnnotationAdvisor(interceptor);advisor.setOrder(properties.getOrder());return advisor;}//数据库事务的切面配置类Role(value BeanDefinition.ROLE_INFRASTRUCTURE)ConditionalOnProperty(prefix DynamicDataSourceProperties.PREFIX, name seata, havingValue false, matchIfMissing true)Beanpublic Advisor dynamicTransactionAdvisor() {AspectJExpressionPointcut pointcut new AspectJExpressionPointcut();pointcut.setExpression(annotation(com.baomidou.dynamic.datasource.annotation.DSTransactional));return new DefaultPointcutAdvisor(pointcut, new DynamicLocalTransactionAdvisor());}//DynamicDataSourceAnnotationInterceptor 切面配置器中所需要的执行链//主要用来确定使用哪个数据源BeanConditionalOnMissingBeanpublic DsProcessor dsProcessor(BeanFactory beanFactory) {DsHeaderProcessor headerProcessor new DsHeaderProcessor();DsSessionProcessor sessionProcessor new DsSessionProcessor();DsSpelExpressionProcessor spelExpressionProcessor new DsSpelExpressionProcessor();spelExpressionProcessor.setBeanResolver(new BeanFactoryResolver(beanFactory));headerProcessor.setNextProcessor(sessionProcessor);sessionProcessor.setNextProcessor(spelExpressionProcessor);return headerProcessor;}//Bean注入后所执行的方法本Demo中目前暂无使用Overridepublic void afterPropertiesSet() {if (!CollectionUtils.isEmpty(dataSourcePropertiesCustomizers)) {for (DynamicDataSourcePropertiesCustomizer customizer : dataSourcePropertiesCustomizers) {customizer.customize(properties);}}}}大体上的自动装配已经介绍完了接下来我们逐个将重要的代码段或者类来进行解释2.2 DynamicDataSourceCreatorAutoConfiguration类分析这个类主要是进行数据源加载的 主要代码如下Slf4j
Configuration
AllArgsConstructor
EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceCreatorAutoConfiguration {//描述Bean的 注入顺序public static final int JNDI_ORDER 1000;public static final int DRUID_ORDER 2000;public static final int HIKARI_ORDER 3000;public static final int BEECP_ORDER 4000;public static final int DBCP2_ORDER 5000;public static final int DEFAULT_ORDER 6000;private final DynamicDataSourceProperties properties;//默认的数据源创造器PrimaryBeanConditionalOnMissingBeanpublic DefaultDataSourceCreator dataSourceCreator(ListDataSourceCreator dataSourceCreators) {DefaultDataSourceCreator defaultDataSourceCreator new DefaultDataSourceCreator();defaultDataSourceCreator.setProperties(properties);defaultDataSourceCreator.setCreators(dataSourceCreators);return defaultDataSourceCreator;}//省略部分代码/*** 存在Hikari数据源时, 加入创建器*/ConditionalOnClass(HikariDataSource.class)Configurationpublic class HikariDataSourceCreatorConfiguration {BeanOrder(HIKARI_ORDER)ConditionalOnMissingBeanpublic HikariDataSourceCreator hikariDataSourceCreator() {return new HikariDataSourceCreator(properties.getHikari());}}//省略部分代码}
当Spring 容器注入 DefaultDataSourceCreator 实例后 接下来就被 DynamicDataSourceProvider 这个类所使用。2.3 DynamicDataSourceProvider 分析Slf4j
AllArgsConstructor
public class YmlDynamicDataSourceProvider extends AbstractDataSourceProvider {/*** 所有数据源*/private final MapString, DataSourceProperty dataSourcePropertiesMap;//通过构造函数注入所有的 数据源 然后调用该父类方法创建数据源集合Overridepublic MapString, DataSource loadDataSources() {return createDataSourceMap(dataSourcePropertiesMap);}
}Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {//从Spring 容器中获取注入好的 DefaultDataSourceCreator Autowiredprivate DefaultDataSourceCreator defaultDataSourceCreator;//创建数据源集合protected MapString, DataSource createDataSourceMap(MapString, DataSourceProperty dataSourcePropertiesMap) {MapString, DataSource dataSourceMap new HashMap(dataSourcePropertiesMap.size() * 2);for (Map.EntryString, DataSourceProperty item : dataSourcePropertiesMap.entrySet()) {DataSourceProperty dataSourceProperty item.getValue();String poolName dataSourceProperty.getPoolName();if (poolName null || .equals(poolName)) {poolName item.getKey();}dataSourceProperty.setPoolName(poolName);dataSourceMap.put(poolName, defaultDataSourceCreator.createDataSource(dataSourceProperty));}return dataSourceMap;}
}2.4 DynamicDataSourceAnnotationAdvisor 分析这个其实就是Spring AOP的切面配置器 主要代码如下public class DynamicDataSourceAnnotationAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {//切面增强方法private final Advice advice;private final Pointcut pointcut;//构造方法注入public DynamicDataSourceAnnotationAdvisor(NonNull DynamicDataSourceAnnotationInterceptor dynamicDataSourceAnnotationInterceptor) {this.advice dynamicDataSourceAnnotationInterceptor;this.pointcut buildPointcut();}Overridepublic Pointcut getPointcut() {return this.pointcut;}Overridepublic Advice getAdvice() {return this.advice;}//省略部分代码//当有类或者方法中有 DS.class 注解时 进行 切面增强private Pointcut buildPointcut() {Pointcut cpc new AnnotationMatchingPointcut(DS.class, true);Pointcut mpc new AnnotationMethodPoint(DS.class);return new ComposablePointcut(cpc).union(mpc);}//省略部分代码
}
2.5 DynamicDataSourceAnnotationInterceptor 分析该类为切面增强即当上面的DynamicDataSourceAnnotationAdvisor 拦截到类或者方法中有 DS.class 注解时 调用该增强类进行处理public class DynamicDataSourceAnnotationInterceptor implements MethodInterceptor {/*** The identification of SPEL.*/private static final String DYNAMIC_PREFIX #;private final DataSourceClassResolver dataSourceClassResolver;private final DsProcessor dsProcessor;public DynamicDataSourceAnnotationInterceptor(Boolean allowedPublicOnly, DsProcessor dsProcessor) {dataSourceClassResolver new DataSourceClassResolver(allowedPublicOnly);this.dsProcessor dsProcessor;}//AOP拦截后进行 切面增强方法Overridepublic Object invoke(MethodInvocation invocation) throws Throwable {//选择数据源String dsKey determineDatasourceKey(invocation);//使用基于ThreadLocal的实现切换数据源DynamicDataSourceContextHolder.push(dsKey);try {return invocation.proceed();} finally {DynamicDataSourceContextHolder.poll();}}//通过调用 DsProcessor 来链式调用进行 数据源的确认private String determineDatasourceKey(MethodInvocation invocation) {String key dataSourceClassResolver.findDSKey(invocation.getMethod(), invocation.getThis());return (!key.isEmpty() key.startsWith(DYNAMIC_PREFIX)) ? dsProcessor.determineDatasource(invocation, key) : key;}
}
2.6 DefaultPointcutAdvisor 分析该切面增强为事务增强设置此增强类后不能与Spring 源事务或者 Transactional 注解共用。Slf4j
public class DynamicLocalTransactionAdvisor implements MethodInterceptor {Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {if (!StringUtils.isEmpty(TransactionContext.getXID())) {return methodInvocation.proceed();}boolean state true;Object o;String xid UUID.randomUUID().toString();TransactionContext.bind(xid);try {o methodInvocation.proceed();} catch (Exception e) {state false;throw e;} finally {ConnectionFactory.notify(state);TransactionContext.remove();}return o;}
}2.7 DynamicDataSourceContextHolder 核心切换类public final class DynamicDataSourceContextHolder {/*** 为什么要用链表存储(准确的是栈)* pre* 为了支持嵌套切换如ABC三个service都是不同的数据源* 其中A的某个业务要调B的方法B的方法需要调用C的方法。一级一级调用切换形成了链。* 传统的只设置当前线程的方式不能满足此业务需求必须使用栈后进先出。* /pre*/private static final ThreadLocalDequeString LOOKUP_KEY_HOLDER new NamedThreadLocalDequeString(dynamic-datasource) {Overrideprotected DequeString initialValue() {return new ArrayDeque();}};private DynamicDataSourceContextHolder() {}/*** 获得当前线程数据源** return 数据源名称*/public static String peek() {return LOOKUP_KEY_HOLDER.get().peek();}/*** 设置当前线程数据源* p* 如非必要不要手动调用调用后确保最终清除* /p** param ds 数据源名称*/public static String push(String ds) {String dataSourceStr StringUtils.isEmpty(ds) ? : ds;LOOKUP_KEY_HOLDER.get().push(dataSourceStr);return dataSourceStr;}/*** 清空当前线程数据源* p* 如果当前线程是连续切换数据源 只会移除掉当前线程的数据源名称* /p*/public static void poll() {DequeString deque LOOKUP_KEY_HOLDER.get();deque.poll();if (deque.isEmpty()) {LOOKUP_KEY_HOLDER.remove();}}/*** 强制清空本地线程* p* 防止内存泄漏如手动调用了push可调用此方法确保清除* /p*/public static void clear() {LOOKUP_KEY_HOLDER.remove();}
}大致核心的代码已经介绍完了接下来我们逐步debugger,摸清其执行流程。2.8 数据源切换执行流程现在当我们执行上面的SpringBoot demo中的 调用注解 DS(db2) 的 Mapper 查询数据库时他的顺序如下只给出涉及到该组件的相关类ClassMapper#selectList() : 执行Mybatis查询操作DynamicDataSourceAnnotationInterceptor#invoke() : Spring AOP 拦截到带有 DS(db2) 并执行代理增强操作DataSourceClassResolver#findDSKey() : 查找有注解DS() 的 类或方法获取对应的数据源Key 值 也就是 db2。DynamicDataSourceContextHolder#push() : 设置当前线程数据源DynamicRoutingDataSource#getConnection(): 调用父类方法获取数据库连接 这里两种处理方式 如下所示 public Connection getConnection() throws SQLException {String xid TransactionContext.getXID();//无事务时 即当前操作为 查询if (StringUtils.isEmpty(xid)) {return determineDataSource().getConnection();} else {//有事物时 先从 之前DynamicDataSourceContextHolder 中获取数据源 先进先出原则String ds DynamicDataSourceContextHolder.peek();ds StringUtils.isEmpty(ds) ? default : ds;ConnectionProxy connection ConnectionFactory.getConnection(ds);//创建数据源return connection null ? getConnectionProxy(ds, determineDataSource().getConnection()) : connection;}}DynamicRoutingDataSource#getDataSource 设置数据源 public DataSource getDataSource(String ds) {//如果当前无 数据源声明 则使用默认数据源if (StringUtils.isEmpty(ds)) {return determinePrimaryDataSource();} else if (!groupDataSources.isEmpty() groupDataSources.containsKey(ds)) {log.debug(dynamic-datasource switch to the datasource named [{}], ds);return groupDataSources.get(ds).determineDataSource();} else if (dataSourceMap.containsKey(ds)) {//如果当前存在数据源则取出该数据源返回log.debug(dynamic-datasource switch to the datasource named [{}], ds);return dataSourceMap.get(ds);}if (strict) {throw new CannotFindDataSourceException(dynamic-datasource could not find a datasource named ds);}return determinePrimaryDataSource();}执行剩余的数据库操作至结束。3 小结大体上写的略微混乱但是只要我们知道其自动装配时 实例化了哪些Bean,并且知道这些Bean 是干什么的 合适调用的根据执行流程逐步Debugger调试就可以明白dynamic-datasource组件是如何进行数据源切换的在流程中我认为比较经典也是比较核心的地方已经标注出源码。我们可以借鉴 DynamicDataSourceContextHolder 这个公共类的思想扩展和优化我们现有的项目中某些跨资源调用的问题。
文章转载自: http://www.morning.khlxd.cn.gov.cn.khlxd.cn http://www.morning.smrkf.cn.gov.cn.smrkf.cn http://www.morning.ydxx123.cn.gov.cn.ydxx123.cn http://www.morning.mcndn.cn.gov.cn.mcndn.cn http://www.morning.fldsb.cn.gov.cn.fldsb.cn http://www.morning.xrct.cn.gov.cn.xrct.cn http://www.morning.gsyns.cn.gov.cn.gsyns.cn http://www.morning.jzlfq.cn.gov.cn.jzlfq.cn http://www.morning.xfdkh.cn.gov.cn.xfdkh.cn http://www.morning.jsphr.cn.gov.cn.jsphr.cn http://www.morning.tnbsh.cn.gov.cn.tnbsh.cn http://www.morning.clbzy.cn.gov.cn.clbzy.cn http://www.morning.rrdch.cn.gov.cn.rrdch.cn http://www.morning.sqqpb.cn.gov.cn.sqqpb.cn http://www.morning.qbrdg.cn.gov.cn.qbrdg.cn http://www.morning.mqxzh.cn.gov.cn.mqxzh.cn http://www.morning.tgfjm.cn.gov.cn.tgfjm.cn http://www.morning.rbhcx.cn.gov.cn.rbhcx.cn http://www.morning.flpjy.cn.gov.cn.flpjy.cn http://www.morning.dyrzm.cn.gov.cn.dyrzm.cn http://www.morning.sqgqh.cn.gov.cn.sqgqh.cn http://www.morning.fnxzk.cn.gov.cn.fnxzk.cn http://www.morning.eshixi.com.gov.cn.eshixi.com http://www.morning.ycgrl.cn.gov.cn.ycgrl.cn http://www.morning.cgtfl.cn.gov.cn.cgtfl.cn http://www.morning.bhmnp.cn.gov.cn.bhmnp.cn http://www.morning.wclxm.cn.gov.cn.wclxm.cn http://www.morning.xsctd.cn.gov.cn.xsctd.cn http://www.morning.ktmnq.cn.gov.cn.ktmnq.cn http://www.morning.paoers.com.gov.cn.paoers.com http://www.morning.mlpmf.cn.gov.cn.mlpmf.cn http://www.morning.ymwnc.cn.gov.cn.ymwnc.cn http://www.morning.lpzyq.cn.gov.cn.lpzyq.cn http://www.morning.uytae.cn.gov.cn.uytae.cn http://www.morning.yccnj.cn.gov.cn.yccnj.cn http://www.morning.lveyue.com.gov.cn.lveyue.com http://www.morning.incmt.com.gov.cn.incmt.com http://www.morning.elsemon.com.gov.cn.elsemon.com http://www.morning.pndw.cn.gov.cn.pndw.cn http://www.morning.jwgnn.cn.gov.cn.jwgnn.cn http://www.morning.rkzk.cn.gov.cn.rkzk.cn http://www.morning.kjfqf.cn.gov.cn.kjfqf.cn http://www.morning.ylsxk.cn.gov.cn.ylsxk.cn http://www.morning.dyhlm.cn.gov.cn.dyhlm.cn http://www.morning.lzjxn.cn.gov.cn.lzjxn.cn http://www.morning.gjssk.cn.gov.cn.gjssk.cn http://www.morning.ngcsh.cn.gov.cn.ngcsh.cn http://www.morning.qwgct.cn.gov.cn.qwgct.cn http://www.morning.hsxkq.cn.gov.cn.hsxkq.cn http://www.morning.vvbsxm.cn.gov.cn.vvbsxm.cn http://www.morning.mzhgf.cn.gov.cn.mzhgf.cn http://www.morning.yzygj.cn.gov.cn.yzygj.cn http://www.morning.mhwtq.cn.gov.cn.mhwtq.cn http://www.morning.rkfgx.cn.gov.cn.rkfgx.cn http://www.morning.gswfs.cn.gov.cn.gswfs.cn http://www.morning.lveyue.com.gov.cn.lveyue.com http://www.morning.hdrsr.cn.gov.cn.hdrsr.cn http://www.morning.kszkm.cn.gov.cn.kszkm.cn http://www.morning.wpmqq.cn.gov.cn.wpmqq.cn http://www.morning.tralution.cn.gov.cn.tralution.cn http://www.morning.nytqy.cn.gov.cn.nytqy.cn http://www.morning.wzwpz.cn.gov.cn.wzwpz.cn http://www.morning.tmfm.cn.gov.cn.tmfm.cn http://www.morning.bmssj.cn.gov.cn.bmssj.cn http://www.morning.bydpr.cn.gov.cn.bydpr.cn http://www.morning.zpstm.cn.gov.cn.zpstm.cn http://www.morning.bjsites.com.gov.cn.bjsites.com http://www.morning.tdldh.cn.gov.cn.tdldh.cn http://www.morning.yrpg.cn.gov.cn.yrpg.cn http://www.morning.chtnr.cn.gov.cn.chtnr.cn http://www.morning.zlmbc.cn.gov.cn.zlmbc.cn http://www.morning.mqghs.cn.gov.cn.mqghs.cn http://www.morning.fkffr.cn.gov.cn.fkffr.cn http://www.morning.wbfg.cn.gov.cn.wbfg.cn http://www.morning.zfqdt.cn.gov.cn.zfqdt.cn http://www.morning.cmzcp.cn.gov.cn.cmzcp.cn http://www.morning.shxmr.cn.gov.cn.shxmr.cn http://www.morning.ryysc.cn.gov.cn.ryysc.cn http://www.morning.pgfkl.cn.gov.cn.pgfkl.cn http://www.morning.tfrlj.cn.gov.cn.tfrlj.cn