JavaWeb-Mybatis动态刷新XML

mybatis-1

使用Mybatis过程中,很多时候修改了XML文件需要整个项目重新启动,比较耗时,如果没什么业务数据状态还好,有数据状态可就惨啦,所以XML自动线下更新就很有必要。手写一个简单实现,大家参考下。

我的实现思路就是利用一个额外线程扫描mybatis XML文件,更新到 Spring中的 上下文ApplicationContext中。

1. 配置文件

我们定义一套刷新时间和周期频次的配置文件在路径 persistence-mybatis\mybatis-base\src\main\resources\conf\mybatis-refresh.properties 中,里面内容如下:

enabled=true
delaySeconds=30
sleepSeconds=10
mappingPath=mapper
  • enabled:是否开启自动刷新
  • delaySeconds: 间隔时间
  • sleepSeconds: 休眠时间
  • mappingPath:XML的路径

核心类需要实现上下文接口 ApplicationContextAware

2. 关键步骤

  • @Override重写setApplicationContext 方法
  • 用静态语句块,初始化配置文件中的相关参数
  • @PostConstruct:在构造函数之后对SqlSessionFactory进行额外配置
  • 启用线程按照频次间隔重复执行上述操作

关键性步骤如下:

// 1、从上下文容器获取 SqlSessionFactory
SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
// 2、获取Configuration
Configuration configuration = sessionFactory.getConfiguration();
this.configuration = configuration;
// 3、扫描Locations
mapperLocations = getResource(basePackage,XML_RESOURCE_PATTERN);
// 4、启动线程执行
exeTask();

核心类在akkad-base\persistence-mybatis\mybatis-base\src\main\java\xyz\wongs\drunkard\base\persistence\mybatis\loader\MapperAutoRefresh.java 下,而且行数太长,代码就不贴。



/**
 * Mybatis的mapper文件中的sql语句被修改后, 只能重启服务器才能被加载, 非常耗时,所以就写了一个自动加载的类,
 * 配置后检查xml文件更改,如果发生变化,重新加载xml里面的内容.
 *
 * @author <a href="https://github.com/rothschil">Sam</a>
 * @date 20/11/17 10:29
 * @since 1.0.0
 */
@Slf4j
@Component
@SuppressWarnings("unused")
public class MapperAutoRefresh implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    private static final Properties prop = new Properties();
    /**
     * 是否启用Mapper刷新线程功能
     */
    private static final boolean enabled;
    /**
     * 刷新启用后,是否启动了刷新线程
     */
    private static boolean refresh;

    /**
     * Mapper实际资源路径
     */
    private Set<String> location;
    /**
     * Mapper资源路径
     */
    private Resource[] mapperLocations;
    /**
     * MyBatis配置对象
     */
    private Configuration configuration;
    /**
     * 上一次刷新时间
     */
    private Long beforeTime = 0L;
    /**
     * 延迟刷新秒数
     */
    private static int delaySeconds;
    /**
     * 休眠时间
     */
    private static int sleepSeconds;
    /**
     * xml文件夹匹配字符串,需要根据需要修改
     */
    private static String mappingPath;

    private static final String XML_RESOURCE_PATTERN = "**/*.xml";

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    static {
        String FILE_NAME = "/conf/mybatis-refresh.properties";
        try {
            prop.load(MapperAutoRefresh.class.getResourceAsStream(FILE_NAME));
        } catch (Exception e) {
            log.error("Load mybatis-refresh “" + FILE_NAME + "” file error.");
        }

        enabled = ConstMapper.ENABLED_TRUE.equalsIgnoreCase(getPropString(ConstMapper.ENABLED));

        delaySeconds = getPropInt(ConstMapper.DELAY_SECONDS);
        sleepSeconds = getPropInt(ConstMapper.SLEEP_SECONDS);
        mappingPath = getPropString(ConstMapper.MAPPING_PATH);

        delaySeconds = delaySeconds == 0 ? 50 : delaySeconds;
        sleepSeconds = sleepSeconds == 0 ? 3 : sleepSeconds;
        mappingPath = StringUtils.isBlank(mappingPath) ? "mappings" : mappingPath;

        if(log.isDebugEnabled()){
            log.debug("[enabled] " + enabled);
            log.debug("[delaySeconds] " + delaySeconds);
            log.debug("[sleepSeconds] " + sleepSeconds);
            log.debug("[mappingPath] " + mappingPath);
        }
    }


    @PostConstruct
    public void start() throws IOException {
        SqlSessionFactory sessionFactory = applicationContext.getBean(SqlSessionFactory.class);
        this.configuration = sessionFactory.getConfiguration();
        String basePackage = "/mapper";
        mapperLocations = getResource(basePackage, XML_RESOURCE_PATTERN);
        exeTask();
    }

    /** 根据路径获取XML 的Resource
     * @param basePackage 给定包
     * @param pattern 正则表达式
     * @return javax.annotation.Resource[]
     * @date 20/11/17 10:48
     */
    public Resource[] getResource(String basePackage, String pattern) throws IOException {
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + ClassUtils.convertClassNameToResourcePath(applicationContext.getEnvironment().resolveRequiredPlaceholders(
                basePackage)) + "/" + pattern;
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        return resourcePatternResolver.getResources(packageSearchPath);
    }

    class MyBatisThreadRefresh implements Runnable {

        private final MapperAutoRefresh mapperAutoRefresh;

        MyBatisThreadRefresh(MapperAutoRefresh mapperAutoRefresh) {
            this.mapperAutoRefresh = mapperAutoRefresh;
        }

        @Override
        public void run() {
            // 解析资源
            if (null == location) {
                location = Sets.newHashSet();
                log.debug("MapperLocation's length:" + mapperLocations.length);
                for (Resource mapperLocation : mapperLocations) {
                    String s = mapperLocation.toString().replaceAll("\\\\", "/");
                    s = s.substring("file [".length(), s.lastIndexOf(mappingPath) + mappingPath.length());
                    if (!location.contains(s)) {
                        location.add(s);
                        log.info("Location:" + s);
                    }
                }
                log.info("Locarion's size:" + location.size());
            }

            // 暂定时间
            try {
                TimeUnit.SECONDS.sleep(delaySeconds);
            } catch (InterruptedException e2) {
                e2.printStackTrace();
            }

            refresh = true;
            log.info("========= Enabled refresh mybatis mapper =========");

            // 开始执行刷新操作
            while (true) {
                try {
                    for (String s : location) {
                        mapperAutoRefresh.refresh(s, beforeTime);
                    }
                    TimeUnit.SECONDS.sleep(sleepSeconds);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 执行资源刷新任务
     *
     * @date 20/11/17 11:04
     */
    public void exeTask() {
        if (null == mapperLocations || mapperLocations.length == 0) {
            return;
        }
        beforeTime = System.currentTimeMillis();
        if (enabled) {
            // 启动刷新线程
            final MapperAutoRefresh runnable = this;

            ExecutorService es = ThreadPoolsUtil.doCreate(1, 1, "Mybatis-Refresh");
            MyBatisThreadRefresh mtr = new MyBatisThreadRefresh(this);
            es.execute(mtr);
        }
    }

    /**
     * 刷新资源的操作
     *
     * @param filePath   资源的路径
     * @param beforeTime 开始时间
     * @date 20/11/17 11:06
     */
    public void refresh(String filePath, long beforeTime) {
        // 本次刷新时间
        long refrehTime = System.currentTimeMillis();
        // 获取需要刷新的Mapper文件列表
        List<File> fileList = this.getRefreshFile(new File(filePath), beforeTime);

        if (fileList.isEmpty()) {
            return;
        }
        log.info("Refresh file: " + fileList.size());

        for (File file : fileList) {
            try {
                InputStream inputStream = new FileInputStream(file);
                String resource = file.getAbsolutePath();

                // 清理原有资源,更新为自己的StrictMap方便,增量重新加载
                String[] mapFieldNames = new String[]{
                        "mappedStatements", "caches",
                        "resultMaps", "parameterMaps",
                        "keyGenerators", "sqlFragments"
                };

                for (String fieldName : mapFieldNames) {
                    Field field = configuration.getClass().getDeclaredField(fieldName);
                    field.setAccessible(true);
                    Map map = ((Map) field.get(configuration));
                    if (!(map instanceof StrictMap)) {
                        Map newMap = new StrictMap(StringUtils.capitalize(fieldName) + "collection");
                        for (Object key : map.keySet()) {
                            try {
                                newMap.put(key, map.get(key));
                            } catch (IllegalArgumentException ex) {
                                newMap.put(key, ex.getMessage());
                            }
                        }
                        field.set(configuration, newMap);
                    }
                }

                // 清理已加载的资源标识,方便让它重新加载。
                Field loadedResourcesField = configuration.getClass().getDeclaredField("loadedResources");
                loadedResourcesField.setAccessible(true);
                Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
                loadedResourcesSet.remove(resource);

                //重新编译加载资源文件。
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(inputStream, configuration,
                        resource, configuration.getSqlFragments());
                xmlMapperBuilder.parse();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                ErrorContext.instance().reset();
            }
            if (log.isDebugEnabled()) {
                log.info("Refresh file: " + file.getAbsolutePath());
                log.info("Refresh filename: " + file.getName());
            }

            if (!fileList.isEmpty()) {
                this.beforeTime = refrehTime;
            }
        }
    }

    /**
     * 获取需要刷新的文件列表,返回 刷新文件列表
     *
     * @param dir        目录
     * @param beforeTime 上次刷新时间
     * @return java.util.List<java.io.File>
     * @date 20/11/17 11:18
     */
    private List<File> getRefreshFile(File dir, Long beforeTime) {

        List<File> fileList = new ArrayList<>();

        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    fileList.addAll(this.getRefreshFile(file, beforeTime));
                } else if (file.isFile()) {
                    if (this.checkFile(file, beforeTime)) {
                        fileList.add(file);
                    }
                } else {
                    log.error("Error file." + file.getName());
                }
            }
        }
        return fileList;
    }


    /**
     * 重写 org.apache.ibatis.session.Configuration.StrictMap 类
     * 来自 MyBatis3.4.0版本,修改 put 方法,允许反复 put更新。
     */
    public static class StrictMap<V> extends HashMap<String, V> {

        private static final long serialVersionUID = -4950446264854982944L;
        private final String name;

        public StrictMap(String name, int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            this.name = name;
        }

        public StrictMap(String name, int initialCapacity) {
            super(initialCapacity);
            this.name = name;
        }

        public StrictMap(String name) {
            super();
            this.name = name;
        }

        public StrictMap(String name, Map<String, ? extends V> m) {
            super(m);
            this.name = name;
        }

        @SuppressWarnings("unchecked")
        @Override
        public V put(String key, V value) {
            // ThinkGem 如果现在状态为刷新,则刷新(先删除后添加)
            if (MapperAutoRefresh.isRefresh()) {
                remove(key);
            }
            // ThinkGem end
            if (containsKey(key)) {
                throw new IllegalArgumentException(name + " already contains value for " + key);
            }
            if (key.contains(Constants.POINT)) {
                final String shortKey = getShortName(key);
                if (super.get(shortKey) == null) {
                    super.put(shortKey, value);
                } else {
                    super.put(shortKey, (V) new Ambiguity(shortKey));
                }
            }
            return super.put(key, value);
        }

        @Override
        public V get(Object key) {
            V value = super.get(key);
            if (value == null) {
                throw new IllegalArgumentException(name + " does not contain value for " + key);
            }
            if (value instanceof Ambiguity) {
                throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
                        + " (try using the full name including the namespace, or rename one of the entries)");
            }
            return value;
        }

        private String getShortName(String key) {
            final String[] keyparts = key.split("\\.");
            return keyparts[keyparts.length - 1];
        }

        protected static class Ambiguity {
            private final String subject;

            public Ambiguity(String subject) {
                this.subject = subject;
            }

            public String getSubject() {
                return subject;
            }
        }

    }

    public static boolean isRefresh() {
        return refresh;
    }

    /**
     * 判断文件是否需要刷新,需要刷新返回true,否则返回false
     *
     * @param file       文件
     * @param beforeTime 上次刷新时间
     * @return boolean
     * @date 20/11/17 11:17
     */
    private boolean checkFile(File file, Long beforeTime) {
        return file.lastModified() > beforeTime;
    }


    /**
     * 获取整数属性
     *
     * @param key KEY
     * @return int
     * @date 20/11/17 10:37
     */
    private static int getPropInt(String key) {
        return Integer.parseInt(Objects.requireNonNull(getPropString(key)));
    }

    /**
     * 获取字符串属性
     *
     * @param key KEY
     * @return int
     * @date 20/11/17 10:37
     */
    private static String getPropString(String key) {
        return prop.getProperty(key);
    }

}


在多线程处理这块有需要注意有一定的线程使用基础,看官自行学习。

3. 源码地址,如果觉得对你有帮助,请Star

觉得对你有帮助,请Star

Github源码地址

Gitee源码地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java EE 项目的目录结构可以根据具体的需求进行灵活设计,但一般情况下,推荐使用以下的标准目录结构: ``` project ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ └── example │ │ │ ├── controller │ │ │ ├── dao │ │ │ ├── entity │ │ │ ├── service │ │ │ └── util │ │ ├── resources │ │ │ ├── mapper │ │ │ └── db.properties │ │ └── webapp │ │ ├── WEB-INF │ │ │ ├── classes │ │ │ ├── lib │ │ │ └── web.xml │ │ ├── css │ │ ├── js │ │ ├── images │ │ └── index.jsp │ └── test │ ├── java │ └── resources ├── target ├── pom.xml └── README.md ``` 其中,各个目录的作用如下: - `src/main/java`:存放项目的 Java 源代码,按照包名分层,一般包括 `controller`、`dao`、`entity`、`service` 和 `util` 等包; - `src/main/resources`:存放项目的配置文件和资源文件,一般包括数据库连接配置文件 `db.properties`、MyBatis 的 mapper 文件等; - `src/main/webapp`:存放 Web 应用的 Web 资源,包括 JSP 页面、CSS 样式表、JavaScript 脚本等; - `src/test/java`:存放项目的测试代码; - `src/test/resources`:存放测试代码所需要的资源文件; - `target`:存放编译后的 .class 文件、打包后的 .war 文件等; - `pom.xml`:Maven 项目管理工具的配置文件; - `README.md`:项目说明文件。 以上是一种常见的 Java EE 项目目录结构,但并不是唯一的标准。在实际开发中,可以根据项目的具体需求进行合理的调整和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王老邪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值