什么JRebel热部署失效?多数据源分页失效?

什么 JRebel 热部署失效? 多数据源分页失效?

  • 上一次有讲到配置多数据源,但就在此时我的JRebel热部署失效了,但也不是完全失效,更改代码可以实现热部署,可是编写xml改变sql 就不会生效,不能热部署就要重启项目,由于项目太大启动就要2分钟,这效率可太慢了!

怎么办?,那就自己写个xml 等资源文件的热部署吧.

1. 编写代码
package com.ym.web.config.mybatis;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.boot.autoconfigure.MybatisProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * mybatis热部署插件
 * 设置mybatis配置文件cache-enabled: false不缓存,开启热部署xml
 *
 * @author: 
 * @version: 2021年03月28日 14:10
 */
@Component
public class MapperHotDeployPlugin implements InitializingBean, ApplicationContextAware {

	private final static Logger logger = LoggerFactory.getLogger(MapperHotDeployPlugin.class);

	@Autowired
	private MybatisProperties mybatisProperties;

	private Configuration configuration;


	@Override
	public void afterPropertiesSet() {
		// 设置mybatis配置文件cache-enabled: false不缓存,开启热部署xml
		if (!mybatisProperties.getConfiguration().isCacheEnabled()) {
			new WatchThread().start();
			logger.info("热部署开启{}",mybatisProperties);
		}
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean("ds1SqlSessionFactory");
		configuration = sqlSessionFactory.getConfiguration();
		logger.info("setApplicationContext{},{}",sqlSessionFactory,configuration);
		System.out.println();
	}

	class WatchThread extends Thread {
		private final Logger logger = LoggerFactory.getLogger(WatchThread.class);

		@Override
		public void run() {
			startWatch();
		}


		/**
		 * 启动监听
		 */
		private void startWatch() {
			try {
				WatchService watcher = FileSystems.getDefault().newWatchService();
				getWatchPaths().forEach(p -> {
					try {
						Paths.get(p).register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
					} catch (Exception e) {
						logger.error("ERROR: 注册xml监听事件", e);
						throw new RuntimeException("ERROR: 注册xml监听事件", e);
					}
				});
				while (true) {
					WatchKey watchKey = watcher.take();
					Set<String> set = new HashSet<>();
					for (WatchEvent<?> event : watchKey.pollEvents()) {
						set.add(event.context().toString());
					}
					// 重新加载xml
					reloadXml(set);
					boolean valid = watchKey.reset();
					if (!valid) {
						break;
					}
				}
			} catch (Exception e) {
				System.out.println("Mybatis的xml监控失败!");
				logger.info("Mybatis的xml监控失败!", e);
			}
		}

		/**
		 * 加载需要监控的文件父路径
		 */
		private Set<String> getWatchPaths() {
			Set<String> set = new HashSet<>();
			Arrays.stream(getResource()).forEach(r -> {
				try {
					logger.info("资源路径:{}", r.toString());
					set.add(r.getFile().getParentFile().getAbsolutePath());
				} catch (Exception e) {
					logger.info("获取资源路径失败", e);
					throw new RuntimeException("获取资源路径失败");
				}
			});
			logger.info("需要监听的xml资源: {}", set);
			return set;
		}

		/**
		 * 获取配置的mapperLocations
		 */
		private Resource[] getResource() {
			return mybatisProperties.resolveMapperLocations();
		}

		/**
		 * 删除xml元素的节点缓存
		 * "mappedStatements",
		 */
		private void clearMap(String nameSpace) {
			logger.info("清理Mybatis的namespace={}在mappedStatements、caches、resultMaps、parameterMaps、keyGenerators、sqlFragments中的缓存");
			Arrays.asList("mappedStatements" ,"caches", "resultMaps", "parameterMaps", "keyGenerators", "sqlFragments").forEach(fieldName -> {
				Object value = getFieldValue(configuration, fieldName);
				if (value instanceof Map) {
					Map<?, ?> map = (Map) value;
					List<Object> list = map.keySet().stream().filter(o -> o.toString().startsWith(nameSpace + ".")).collect(Collectors.toList());
					logger.info("需要清理的元素: {}", list);
					list.forEach(k -> map.remove((Object) k));
				}
			});
		}

		/**
		 * 清除文件记录缓存
		 */
		private void clearSet(String resource) {
			logger.info("清理mybatis的资源{}在容器中的缓存", resource);
			Object value = getFieldValue(configuration, "loadedResources");
			if (value instanceof Set) {
				Set<?> set = (Set) value;
				set.remove(resource);
				set.remove("namespace:" + resource);
			}
		}

		/**
		 * 获取对象指定属性
		 *
		 * @param obj       对象信息
		 * @param fieldName 属性名称
		 */
		private Object getFieldValue(Object obj, String fieldName) {
			logger.info("从{}中加载{}属性", obj, fieldName);
			try {
				Field field = obj.getClass().getDeclaredField(fieldName);
				boolean accessible = field.isAccessible();
				field.setAccessible(true);
				Object value = field.get(obj);
				field.setAccessible(accessible);
				return value;
			} catch (Exception e) {
				logger.info("ERROR: 加载对象中[{}]", fieldName, e);
				throw new RuntimeException("ERROR: 加载对象中[" + fieldName + "]", e);
			}
		}

		/**
		 * 重新加载set中xml
		 *
		 * @param set 修改的xml资源
		 */
		private void reloadXml(Set<String> set) {
			logger.info("需要重新加载的文件列表: {}", set);
			List<Resource> list = Arrays.stream(getResource())
					.filter(p -> set.contains(p.getFilename()))
					.collect(Collectors.toList());
			logger.info("需要处理的资源路径:{}", list);
			list.forEach(r -> {
				try {
					clearMap(getNamespace(r));
					clearSet(r.toString());
					XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(r.getInputStream(), configuration,
							r.toString(), configuration.getSqlFragments());
					xmlMapperBuilder.parse();
				} catch (Exception e) {
					logger.info("ERROR: 重新加载[{}]失败", r.toString(), e);
					throw new RuntimeException("ERROR: 重新加载[" + r.toString() + "]失败", e);
				} finally {
					ErrorContext.instance().reset();
				}
			});
			logger.info("成功热部署文件列表: {}", set);
		}

		/**
		 * 获取xml的namespace
		 *
		 * @param resource xml资源
		 */
		private String getNamespace(Resource resource) {
			logger.info("从{}获取namespace", resource.toString());
			try {
				XPathParser parser = new XPathParser(resource.getInputStream(), true, null, new XMLMapperEntityResolver());
				return parser.evalNode("/mapper").getStringAttribute("namespace");
			} catch (Exception e) {
				logger.info("ERROR: 解析xml中namespace失败", e);
				throw new RuntimeException("ERROR: 解析xml中namespace失败", e);
			}
		}
	}
}

实话说 网上百度也有源代码 但是对于我配置的多数据源来说 些许不同

问题点1. 我的多数据源原来的是MybatisSqlSessionFactory 把这里改成 SqlSessionFactory 后问题就来了

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		SqlSessionFactory sqlSessionFactory = (SqlSessionFactory) applicationContext.getBean("ds1SqlSessionFactory");
		configuration = sqlSessionFactory.getConfiguration();
		logger.info("setApplicationContext{},{}",sqlSessionFactory,configuration);
		System.out.println();
	}

报错

java.lang.RuntimeException: 在系统中发现了多个分页插件,请检查系统配置!

原因: SqlSessionFactory 自动注入了PageHelper 也就是分页插件

分析源码:
/**
 * 自定注入分页插件
 *
 * @author liuzh
 */
@Configuration
@ConditionalOnBean(SqlSessionFactory.class)
@EnableConfigurationProperties(PageHelperProperties.class)
@AutoConfigureAfter(MybatisAutoConfiguration.class)
public class PageHelperAutoConfiguration {

//这里是所有的数据源 并都注入分页插件 问题点就在这

    @Autowired
    private List<SqlSessionFactory> sqlSessionFactoryList;

    @Autowired
    private PageHelperProperties properties;

    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    @PostConstruct
    public void addPageInterceptor() {
        PageInterceptor interceptor = new PageInterceptor();
        Properties properties = new Properties();
        //先把一般方式配置的属性放进去
        properties.putAll(pageHelperProperties());
        //在把特殊配置放进去,由于close-conn 利用上面方式时,属性名就是 close-conn 而不是 closeConn,所以需要额外的一步
        properties.putAll(this.properties.getProperties());
        interceptor.setProperties(properties);
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            sqlSessionFactory.getConfiguration().addInterceptor(interceptor);
        }
    }

}

@Configuration 配置类
@ConditionalOnBean(SqlSessionFactory.class) 装配条件该类装配完成
@EnableConfigurationProperties(PageHelperProperties.class) 自动装配
@AutoConfigureAfter(MybatisAutoConfiguration.class) ==最后装配 ==

配置类无疑

//获取了所有sqlSessionFactoryList,多数据源就注入了两次
private List sqlSessionFactoryList;

问题找到了 ,既然多次注入 那就不要让他注入我们手动注入

1. 在SpringApplication 入口取消分页自动注入
@EnableAutoConfiguration(exclude= PageHelperAutoConfiguration.class)
2.编写手动注入分页bean交给spring
package com.ym.web.config.datasource;

import com.github.pagehelper.PageInterceptor;
import com.github.pagehelper.autoconfigure.PageHelperProperties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Properties;

/**
 * @Author wenbo
 * @Date 2021/3/25 19:05
 **/
@Component
public class MybatisPageInterceptor {


    /**
     * 接受分页插件额外的属性
     *
     * @return
     */
    @Bean
    @ConfigurationProperties(prefix = PageHelperProperties.PAGEHELPER_PREFIX)
    public Properties pageHelperProperties() {
        return new Properties();
    }

    /**
     * 配置插件
     *
     * @return bean
     */
    @Bean(name = "pagePlugin")
    public PageInterceptor pageInterceptor() {
        //org.apache.ibatis.plugin.Interceptor
        PageInterceptor interceptor = new PageInterceptor();
        //java.util.Properties
        Properties properties = new Properties();
        properties.putAll(pageHelperProperties());
        //properties.putAll(this.properties.getProperties());
        // 是否返回行数,相当于MySQL的count(*)
        //properties.setProperty("rowBoundsWithCount", "true");
        interceptor.setProperties(properties);
        return interceptor;
    }
}
3.在每个数据源中手动加入到 mybatis的拦截器
configuration.addInterceptor(pageInterceptor.pageInterceptor());
数据源1
   @Autowired
    private MybatisPageInterceptor pageInterceptor;
    
        /**
     * 主数据源 ds1数据源
     * @return
     * @throws Exception
     */
    @Primary
    @Bean("ds1SqlSessionFactory")
    @DependsOn("pagePlugin")
    public SqlSessionFactory ds1SqlSessionFactory(@Qualifier("ds1SqlSessionFactoryBean") SqlSessionFactoryBean sqlSessionFactoryBean) throws Exception {
        //sqlSessionFactoryBean.setPlugins(plugins ());
        SqlSessionFactory sqlSessionFactor = sqlSessionFactoryBean.getObject();
        org.apache.ibatis.session.Configuration configuration =  sqlSessionFactor.getConfiguration();
        configuration.addInterceptor(pageInterceptor.pageInterceptor());
        configuration.setMapUnderscoreToCamelCase(true);
        return sqlSessionFactoryBean.getObject();
    }
4.数据源2相同操作

然后测试分页两个数据源都生效了 热部署也生效了

重点: 如果使用 MybatisSqlSessionFactory 就不会有多个分页这种错误 这无疑是一种快捷方式!!


如果喜欢我微信扫码关注

end

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值