SpringBoot集成MybatisPlus解決Mapper文件修改后動態刷新的問題


很多人在使用SpringBoot集成Mybatis或者MybatisPlus的時候在查詢復雜的情況下會寫mapper文件,雖然說MyBatisPlus提供了常用的增刪查改,但還是難以應付復雜的查詢。關於MyBatisPlus是這樣介紹的:

MyBatis-Plus(簡稱 MP)是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。

特性

  • 無侵入:只做增強不做改變,引入它不會對現有工程產生影響,如絲般順滑
  • 損耗小:啟動即會自動注入基本 CURD,性能基本無損耗,直接面向對象操作
  • 強大的 CRUD 操作:內置通用 Mapper、通用 Service,僅僅通過少量配置即可實現單表大部分 CRUD 操作,更有強大的條件構造器,滿足各類使用需求
  • 支持 Lambda 形式調用:通過 Lambda 表達式,方便的編寫各類查詢條件,無需再擔心字段寫錯
  • 支持多種數據庫:支持 MySQL、MariaDB、Oracle、DB2、H2、HSQL、SQLite、Postgre、SQLServer2005、SQLServer 等多種數據庫
  • 支持主鍵自動生成:支持多達 4 種主鍵策略(內含分布式唯一 ID 生成器 - Sequence),可自由配置,完美解決主鍵問題
  • 支持 XML 熱加載:Mapper 對應的 XML 支持熱加載,對於簡單的 CRUD 操作,甚至可以無 XML 啟動
  • 支持 ActiveRecord 模式:支持 ActiveRecord 形式調用,實體類只需繼承 Model 類即可進行強大的 CRUD 操作
  • 支持自定義全局通用操作:支持全局通用方法注入( Write once, use anywhere )
  • 支持關鍵詞自動轉義:支持數據庫關鍵詞(order、key......)自動轉義,還可自定義關鍵詞
  • 內置代碼生成器:采用代碼或者 Maven 插件可快速生成 Mapper 、 Model 、 Service 、 Controller 層代碼,支持模板引擎,更有超多自定義配置等您來使用
  • 內置分頁插件:基於 MyBatis 物理分頁,開發者無需關心具體操作,配置好插件之后,寫分頁等同於普通 List 查詢
  • 內置性能分析插件:可輸出 Sql 語句以及其執行時間,建議開發測試時啟用該功能,能快速揪出慢查詢
  • 內置全局攔截插件:提供全表 delete 、 update 操作智能分析阻斷,也可自定義攔截規則,預防誤操作
  • 內置 Sql 注入剝離器:支持 Sql 注入剝離,有效預防 Sql 注入攻擊

寫的也是非常好,大家如果用到的話可以去官網看看:https://mp.baomidou.com

我們可以看到他的插件也提供了XML熱加載,但是很不幸,3.0.6版本上移除了該功能,不過最新快照版已加回來並打上廢棄標識,3.1.0版本上已完全移除。

所以要想使用還得自己來寫。我們都知道在做項目的時候修改了xml后不重啟是不行的,必須重啟他才能動態的加載。如果項目大,啟動時間慢,那么這樣無疑中就浪費了很多時間,工欲善其事,必先利其器一個好的架構,效率是成倍提升的。

首先這里我通過兩種模式來給大家介紹下:

  • 通過URL請求的形式來人為的刷新
  • 通過定時任務定時去掃描

各有的個利弊,定時任務動態掃描的話如果文件超過1000多,效率不高,如果項目小,建議使用定時任務,如果項目大,建議使用URL的方式去人為刷新,修改后去執行以下刷新的請求就行。下面我們從搭建一個項目開始。

項目目錄如下:

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.1.4.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.mybatis.dynamic</groupId>
	<artifactId>mybatis-dynamic</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>mybatis-dynamic</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.1.1</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-jdbc</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>6.0.6</version>
		</dependency>

	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

application.properties

server.port=8080

spring.datasource.url=jdbc:mysql://localhost:3306/test?autoReconnect=false&useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=

mybatis-plus.mapper-locations= classpath:mapper/**/*.xml
mybatis-plus.type-aliases-package=com.mb.entity


debug = true

User.java

package com.mb.entity;


import java.io.Serializable;

public class User implements Serializable{

    private Integer id;
    private String username;
    private String password;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

UserMapper.java

package com.mb.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mb.entity.User;

import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

//@Repository
public interface UserMapper extends BaseMapper<User>{
	
	
	/**
	 * 根據ID查詢
	 * @param id
	 * @return
	 */
	public User findById(@Param("id")Integer id);
	
}

MybatisDynamicApplication.java

package com.mb;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.mb.common.XMLMapperLoader;
import com.mb.entity.User;
import com.mb.mapper.UserMapper;

@SpringBootApplication
@MapperScan("com.mb.mapper")
@RestController
public class MybatisDynamicApplication {

	@Autowired
	private UserMapper userMapper;
	@Autowired
	private SqlSessionFactory sqlSessionFactory;
	
	
	public static void main(String[] args) {
		SpringApplication.run(MybatisDynamicApplication.class, args);
	}

	
	@RequestMapping("user")
	public User user(Integer id) {
		return userMapper.findById(id);
	}
	
	
	/**
	 * 第一種方式,通過Spring的方式創建Bean來管理,在構造方法中會啟動新的線程
	 * 
	 * @return
	 */
	@Bean
	public XMLMapperLoader xMLMapperLoader() {
		return new XMLMapperLoader(sqlSessionFactory,"classpath:mapper/**/*.xml");
	}
	
	/**
	 * 第二種方式 通過URL來刷新
	 * @return
	 */
	@RequestMapping("refersh")
	public String refershMyBatisXml() {
		return new XMLMapperLoader(sqlSessionFactory,"classpath:mapper/**/*.xml").readMapperXml();
	}
	
}

到這里大家要特別注意,上面的兩種方式最好用一種。

XMLMapperLoader.java(主要干活的)

package com.mb.common;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

/**
 * 
 * @author yuxuan
 *
 */
public class XMLMapperLoader {

	private Logger logger = LoggerFactory.getLogger(XMLMapperLoader.class);

	private SqlSessionFactory sqlSessionFactory;
	private Resource[] mapperLocations;
    private String packageSearchPath = "classpath:mapper/**/*.xml";
    private HashMap<String, Long> fileMapping = new HashMap<String, Long>();
	
	public XMLMapperLoader(SqlSessionFactory sqlSessionFactory,String packageSearchPath) {
		this.sqlSessionFactory = sqlSessionFactory;
		if(packageSearchPath != null && packageSearchPath != "") {
			this.packageSearchPath = packageSearchPath;
		}
		startThreadListener();
	}
	
	public void startThreadListener() {
		ScheduledExecutorService service =	Executors.newScheduledThreadPool(1);
		//每5秒執行一次
		service.scheduleAtFixedRate(new Runnable() {
			@Override
			public void run() {
				readMapperXml();
			}
		}, 0, 5,TimeUnit.SECONDS );
	}

	public  String readMapperXml() {
		try {
			org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
            // step.1 掃描文件
            try {
                this.scanMapperXml();
            } catch (IOException e) {
                return "packageSearchPath掃描包路徑配置錯誤";
            }
            
            for (String name : configuration.getMappedStatementNames()) {
                logger.debug(name);
            }
            
            // step.2 判斷是否有文件發生了變化
            if (this.isChanged()) {
                // step.2.1 清理
                this.removeConfig(configuration);
                // step.2.2 重新加載
                for (Resource configLocation : mapperLocations) {
                    try {
                        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configLocation.getInputStream(), configuration, configLocation.toString(), configuration.getSqlFragments());
                        xmlMapperBuilder.parse();
                        logger.debug("mapper文件[" + configLocation.getFilename() + "]緩存加載成功");
                    } catch (IOException e) {
                    	logger.debug("mapper文件[" + configLocation.getFilename() + "]不存在或內容格式不對");
                        continue;
                    }
                }
            }
            
            for (String name : configuration.getMappedStatementNames()) {
               logger.debug(name);
            }
            return "刷新mybatis xml配置語句成功";
        } catch (Exception e) {
            e.printStackTrace();
            return "刷新mybatis xml配置語句失敗";
        }
	}
	
	
	public void setPackageSearchPath(String packageSearchPath) {
        this.packageSearchPath = packageSearchPath;
    }
    
    public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
        this.sqlSessionFactory = sqlSessionFactory;
    }

    /**
     * 掃描xml文件所在的路徑
     * @throws IOException 
     */
    private void scanMapperXml() throws IOException {
        this.mapperLocations = new PathMatchingResourcePatternResolver().getResources(packageSearchPath);
    }

    /**
     * 清空Configuration中幾個重要的緩存
     * @param configuration
     * @throws Exception
     */
    private void removeConfig(Configuration configuration) throws Exception {
        Class<?> classConfig = configuration.getClass();
        clearMap(classConfig, configuration, "mappedStatements");
        clearMap(classConfig, configuration, "caches");
        clearMap(classConfig, configuration, "resultMaps");
        clearMap(classConfig, configuration, "parameterMaps");
        clearMap(classConfig, configuration, "keyGenerators");
        clearMap(classConfig, configuration, "sqlFragments");

        clearSet(classConfig, configuration, "loadedResources");

    }

    @SuppressWarnings("rawtypes")
    private void clearMap(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
    	Field field = null;
    	if(configuration.getClass().getName().equals("com.baomidou.mybatisplus.core.MybatisConfiguration")) {
    		field = classConfig.getSuperclass().getDeclaredField(fieldName);
    	}else {
    		field = classConfig.getClass().getDeclaredField(fieldName);
    	}
        field.setAccessible(true);
        Map mapConfig = (Map) field.get(configuration);
        mapConfig.clear();
    }

    @SuppressWarnings("rawtypes")
    private void clearSet(Class<?> classConfig, Configuration configuration, String fieldName) throws Exception {
    	Field field = null;
    	if(configuration.getClass().getName().equals("com.baomidou.mybatisplus.core.MybatisConfiguration")) {
    		field = classConfig.getSuperclass().getDeclaredField(fieldName);
    	}else {
    		field = classConfig.getClass().getDeclaredField(fieldName);
    	}
        field.setAccessible(true);
        Set setConfig = (Set) field.get(configuration);
        setConfig.clear();
    }
    
    /**
     * 判斷文件是否發生了變化
     * @param resource
     * @return
     * @throws IOException
     */
    private boolean isChanged() throws IOException {
        boolean flag = false;
        for (Resource resource : mapperLocations) {
            String resourceName = resource.getFilename();
            
            boolean addFlag = !fileMapping.containsKey(resourceName);// 此為新增標識
            
            // 修改文件:判斷文件內容是否有變化
            Long compareFrame = fileMapping.get(resourceName);
            long lastFrame = resource.contentLength() + resource.lastModified();
            boolean modifyFlag = null != compareFrame && compareFrame.longValue() != lastFrame;// 此為修改標識
            
            // 新增或是修改時,存儲文件
            if(addFlag || modifyFlag) {
                fileMapping.put(resourceName, Long.valueOf(lastFrame));// 文件內容幀值
                flag = true;
            }
        }
        return flag;
    }
	
}

這個文件的話大家不需要改,直接拿到項目里邊用就行了,只需要把sqlSessionFactory和mapper文件的路徑傳進去就行了。這里如果大家嫌棄傳路徑麻煩,只傳一個sqlSessionFactory的話也可以,大家可以把配置的mybatisPlus-mapperLcations路徑取出來在里邊去做處理。

userMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mb.mapper.UserMapper">
   
    <select id="findById" parameterType="Integer" resultType="user">
        SELECT * FROM  user WHERE id = 10
    </select>
    
</mapper>

以上就是所有代碼的一個配置,實現功能最主要的還是XMLMapperLoader這個類。

有問題可以在下面評論,技術問題可以私聊我。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM