Mybatis經過Interceptor來簡單實現影子表進行動態sql讀取和寫入 續

    繼上一篇Mybatis經過Interceptor來簡單實現影子表進行動態sql讀取和寫入 地址:http://www.javashuo.com/article/p-vlncabhl-gs.html前端

    以後留了一個小坑,那就是但願可以根據控制層傳輸過來的是否採用影子表標識來動態的進行影子表的讀取和寫入,而不是寫死在代碼中java

    這次的目的就是解決這個問題:結合以前寫的一篇文章:ThreadLocal實現線程安全 地址:http://www.javashuo.com/article/p-dotdjpbx-bs.htmlweb

    此次解決的方案就是結合ThreadLocal進行解決的,首先對ThredLocal進行一個簡單的總結:spring

    引用自:http://www.iteye.com/topic/103804sql

    首先,ThreadLocal 不是用來解決共享對象的多線程訪問問題的,通常狀況下,經過ThreadLocal.set() 到線程中的對象是該線程本身使用的對象,其餘線程是不須要訪問的,也訪問不到的。各個線程中訪問的是不一樣的對象。 數據庫

    另外,說ThreadLocal使得各線程可以保持各自獨立的一個對象,並非經過ThreadLocal.set()來實現的,而是經過每一個線程中的new 對象 的操做來建立的對象,每一個線程建立一個,不是什麼對象的拷貝或副本。經過ThreadLocal.set()將這個新建立的對象的引用保存到各線程的本身的一個map中,每一個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從本身的map中取出放進去的對象,所以取出來的是各自本身線程中的對象,ThreadLocal實例是做爲map的key來使用的。 apache

    若是ThreadLocal.set()進去的東西原本就是多個線程共享的同一個對象,那麼多個線程的ThreadLocal.get()取得的仍是這個共享對象自己,仍是有併發訪問問題。json


    下面來看一個hibernate中典型的ThreadLocal的應用: 安全

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {  
    Session s = (Session) threadSession.get();  
    try {  
        if (s == null) {  
            s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {  
        throw new InfrastructureException(ex);  
    }  
    return s;  
}  

    能夠看到在getSession()方法中,首先判斷當前線程中有沒有放進去session,若是尚未,那麼經過sessionFactory().openSession()來建立一個session,再將session set到線程中,實際是放到當前線程的ThreadLocalMap這個map中,這時,對於這個session的惟一引用就是當前線程中的那個ThreadLocalMap(下面會講到),而threadSession做爲這個值的key,要取得這個session能夠經過threadSession.get()來獲得,裏面執行的操做實際是先取得當前線程中的ThreadLocalMap,而後將threadSession做爲key將對應的值取出。這個session至關於線程的私有變量,而不是public的。 
    顯然,其餘線程中是取不到這個session的,他們也只能取到本身的ThreadLocalMap中的東西。要是session是多個線程共享使用的,那還不亂套了。 
    試想若是不用ThreadLocal怎麼來實現呢?可能就要在action中建立session,而後把session一個個傳到service和dao中,這可夠麻煩的。或者能夠本身定義一個靜態的map,將當前thread做爲key,建立的session做爲值,put到map中,應該也行,這也是通常人的想法,但事實上,ThreadLocal的實現恰好相反,它是在每一個線程中有一個map,而將ThreadLocal實例做爲key,這樣每一個map中的項數不多,並且當線程銷燬時相應的東西也一塊兒銷燬了,不知道除了這些還有什麼其餘的好處。 session

    總之,ThreadLocal不是用來解決對象共享訪問問題的,而主要是提供了保持對象的方法和避免參數傳遞的方便的對象訪問方式。概括了兩點: 
1。每一個線程中都有一個本身的ThreadLocalMap類對象,能夠將線程本身的對象保持到其中,各管各的,線程能夠正確的訪問到本身的對象。 
2。將一個共用的ThreadLocal靜態實例做爲key,將不一樣對象的引用保存到不一樣線程的ThreadLocalMap中,而後在線程執行的各處經過這個靜態ThreadLocal實例的get()方法取得本身線程保存的那個對象,避免了將這個對象做爲參數傳遞的麻煩。 

    固然若是要把原本線程共享的對象經過ThreadLocal.set()放到線程中也能夠,能夠實現避免參數傳遞的訪問方式,可是要注意get()到的是那同一個共享對象,併發訪問問題要靠其餘手段來解決。但通常來講線程共享的對象經過設置爲某類的靜態變量就能夠實現方便的訪問了,彷佛不必放到線程中。
    ThreadLocal的應用場合,我以爲最適合的是按線程多實例(每一個線程對應一個實例)的對象的訪問,而且這個對象不少地方都要用到。 此次就很合適

    首先須要定義一個靜態全局變量,類型是ThredLocal<Boolean>類型的,用來判斷是否須要進行測試,若是是測試的話,則進行影子表的讀寫

package cn.chinotan.dto.request;

import java.io.Serializable;

/**
 * @program: test
 * @description: 公共請求
 * @author: xingcheng
 * @create: 2019-03-02 17:18
 **/
public class CommonRequest implements Serializable {

    private static final long serialVersionUID = -2617189175983301155L;

    public static ThreadLocal<Boolean> isTest = new ThreadLocal<Boolean>() {
        @Override
        protected Boolean initialValue() {
            return false;
        }
    };

    public static Boolean isTest() {
        return CommonRequest.isTest.get();
    }

    public static void setTest(Boolean test) {
        CommonRequest.isTest.set(test);
    }
}

    接下來定義一個controller切面,對請求進行攔截,將測試變量記錄在當前的線程的ThreadLocalMap中,以後mybatis的Interceptor從當前線程無需參數進行拿取,以後即可以進行判斷是否須要進行影子表的操做

    

package cn.chinotan.interceptor;

import cn.chinotan.dto.request.CommonRequest;
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Objects;

/**
 * @program: test
 * @description:
 * @author: xingcheng
 * @create: 2019-03-02 18:29
 **/
public class TestInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String isTestHeader = httpServletRequest.getHeader("isTest");
        // 先從header中去,取不到就從queryParam中取
        if (StringUtils.isNotBlank(isTestHeader)) {
            Boolean isTestBoolean = Objects.equals(isTestHeader, "true");
            CommonRequest.setTest(isTestBoolean);
        } else {
            String isTestParam = httpServletRequest.getParameter("isTest");
            if (StringUtils.isNotBlank(isTestParam)) {
                Boolean isTestBoolean = Objects.equals(isTestParam, "true");
                CommonRequest.setTest(isTestBoolean);
            }
        }
        return true;
    }
    
}
package cn.chinotan.config;

import cn.chinotan.interceptor.TestInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * @program: test
 * @description:
 * @author: xingcheng
 * @create: 2019-03-02 18:33
 **/
@Configuration
public class WebAppConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry){
        registry.addInterceptor(new TestInterceptor()).addPathPatterns("/**");
    }
    
}
package cn.chinotan.interceptor;

import cn.chinotan.aop.TableConfig;
import cn.chinotan.controller.UserController;
import cn.chinotan.dto.request.CommonRequest;
import cn.chinotan.service.Strategy;
import cn.chinotan.service.impl.BakStrategy;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.sql.Connection;
import java.util.Map;
import java.util.Properties;

/**
 * 完成插件簽名:
 * 告訴MyBatis當前插件用來攔截哪一個對象的哪一個方法
 * type  指四大對象攔截哪一個對象,
 * method : 表明攔截哪一個方法  ,在StatementHandler 中查看,須要攔截的方法
 * args  :表明參數
 */
@Component
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {
                Connection.class, Integer.class})})
public class ShareStatementPlugin implements Interceptor {

    private static final Logger LOG = LoggerFactory.getLogger(ShareStatementPlugin.class);

    @Autowired
    private Map<String, Strategy> strategyMap;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        doTable(statementHandler, metaObject);
        return invocation.proceed();
    }

    private void doTable(StatementHandler handler, MetaObject metaStatementHandler) throws ClassNotFoundException {
        BoundSql boundSql = handler.getBoundSql();
        String originalSql = boundSql.getSql();

        if (originalSql != null && !originalSql.equals("")) {
            LOG.info("分表前的SQL:{}", originalSql);
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler
                    .getValue("delegate.mappedStatement");
            String id = mappedStatement.getId();
            String className = id.substring(0, id.lastIndexOf("."));
            Class<?> classObj = Class.forName(className);
            Class baseEntity = null;
            Type[] interfacesTypes = classObj.getGenericInterfaces();
            for (Type type : interfacesTypes) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType interfacesType = (ParameterizedType) interfacesTypes[0];
                    Type t = interfacesType.getActualTypeArguments()[0];
                    baseEntity = (Class) t;
                }
            }
            // 根據配置自動生成分表SQL
            TableConfig tableConfig = classObj.getAnnotation(TableConfig.class);
            // 獲取表名 並進行相應轉化
            String tableName = baseEntity.getSimpleName().toLowerCase();
            if (StringUtils.isNotBlank(tableConfig.value())) {
                tableName = tableConfig.value();
            }

            if (tableConfig != null && tableConfig.isTest()) {
                // 獲取策略來處理
                Strategy strategy = strategyMap.get(tableConfig.strategy());
                if (strategy instanceof BakStrategy) {
                    ThreadLocal<Boolean> isTest = CommonRequest.isTest;
                    Boolean aBoolean = isTest.get();
                    LOG.info("是否測試:{}", aBoolean);
                    if (aBoolean) {
                        String convertedSql = originalSql.replaceAll(tableName, strategy.convert(tableName));
                        metaStatementHandler.setValue("delegate.boundSql.sql", convertedSql);
                        LOG.info("分表後的SQL:{}", convertedSql);
                    }
                }
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
    }

    /**
     * 得到真正的處理對象,可能多層代理
     *
     * @param target
     * @param <T>
     * @return
     */
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }
}

請求Controller:

package cn.chinotan.controller;


import cn.chinotan.entity.User;
import cn.chinotan.service.UserService;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;

/**
 * <p>
 * 用戶表 前端控制器
 * </p>
 *
 * @author xingcheng
 * @since 2019-02-16
 */
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @GetMapping("/list")
    public Object list() {
        List<User> list = userService.list();
        return list;
    }

    @GetMapping("/page/list/{current}/{size}")
    public Object page(@PathVariable("current") Long current, @PathVariable("size") Long size) {
        Page<User> objectPage = new Page<>(current, size);
        IPage<User> page = userService.page(objectPage);
        return page;
    }

    @GetMapping("/save/{name}")
    public Object save(@PathVariable("name") String name) {
        User user = new User();
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        user.setName(name);
        boolean save = userService.save(user);
        return save;
    }

    @GetMapping("/update/{name}")
    public Object update(@PathVariable("name") String name) {
        User user = new User();
        user.setId(1L);
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        user.setName(name);
        boolean update = userService.updateById(user);
        return update;
    }
}

下面是測試過程:

    

 

接下來,進行寫入操做:

分別插入測試和非測試數據參數,看看數據庫的狀況:

大公告成

相關文章
相關標籤/搜索