JFinal使用技巧-多数据源自动切换和动态管理

计划等项目小结后再来分享项目得, 看到有不少问这个的,就先分享出一文章吧.
@JFinal  老大,帮忙检查一下问题和补充下


自动切换:

Model的切换:

3.0  中 Model.getConfig() 的可见性由 private 改为 protected,
所以在你的 BaseModel 中重写这一个,不动其他代码 就搞定了Model自动切换数据源,

    @Override
    protected Config getConfig() {
        
        String configName = WebsiteInterceptor.getConfigName();
        
        if(configName == null)
            configName = DbKit.MAIN_CONFIG_NAME;
        
        use(configName);
        
        return DbKit.getConfig(configName);
    }


Db的切换:

我用Ctrl + H 进行全局Db.替换为: Db.use(getConfigName()). 
PS:先搜索"Db.use(" 看系统中有这么用的没有,如果有先处理掉

我的各个Base层都有getConfigName()这个方法,
当然都是调的 WebsiteInterceptor.getConfigName();

各个BaseXxx层:

/***
     * @Title: 获取访问者的 ConfigName
     * @return String    本次访问的ConfigName
     */
    public String getConfigName(){
        return WebsiteInterceptor.getConfigName();
    }



拦截器WebsiteInterceptor:

import javax.servlet.http.HttpServletRequest;

import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.PropKit;
import com.jfinal.log.Log;
import com.momathink.common.constants.DictKeys;
import com.momathink.common.service.ActiveRecordPluginService;

/**
 * @ClassName: 访问者的ConfigName管理
 * @author dufuzhong@126.com
 * @date 2016年10月3日 下午2:33:33
 */
public class WebsiteInterceptor implements Interceptor {
    private static final Log log = Log.getLog(WebsiteInterceptor.class);
    
    private static final ThreadLocal<String> ME_CONFIGNAME = new ThreadLocal<String>();
    
    public static final String SERVER_NAME = "serverName";
    public static final String CONFIG_NAME = "configName";
    
    /***
     * @Title: 获取访问者的 ConfigName
     * @return String    本次访问的ConfigName
     */
    public static String getConfigName(){
        return ME_CONFIGNAME.get();
    }
    
    /* nginx 配置 内容:
     
            location xxxx {
                proxy_set_header Host $host:80;
                proxy_set_header X-Forwarded-For $remote_addr;
                proxy_redirect  off;
                proxy_pass http://127.0.0.1:8080;
                # 配置多台tomcat 就 改  端口号等
            }
        
     */
    
    @Override
    public void intercept(Invocation inv) {
        Controller controller = inv.getController();
        HttpServletRequest request = controller.getRequest();
        
        String ipFromNginx = controller.getHeader("X-Forwarded-For");
        String serverName  = request.getServerName();
        /*
         如果你的数据源是已知固定的  在启动JFinal的时候 用 serverName 做数据源的 configName 也就是说他们是相等的, 
         到这里就可以完结了, 如果是动态的,需要看后的动态管理方式
         * */
        // String configName = serverName;  下面的判断也 改成 if(DbKit.getConfig(configName) != null){
        String configName  = ActiveRecordPluginService.me.getConfigName(serverName);
        
        controller.setAttr(SERVER_NAME, serverName);
        controller.setAttr(CONFIG_NAME, configName);
        
        log.debug("访问者:  域名=" + serverName + "  资源K=" + configName + "   IP=" + ipFromNginx);
        
        if(configName != null){
            
            ME_CONFIGNAME.set(configName);
            
            //异常必须在里面的拦截器进行捕捉处理
            inv.invoke();
            
            ME_CONFIGNAME.remove();
            
        } else {
            controller.renderError(403);
        }
    }

}



如果你的数据源是已知固定的  在启动JFinal的时候
 用 serverName 做数据源的 configName 也就是说他们是相等的,
 到这里就可以完结了, 如果是动态的,需要看后的动态管理方式



动态管理:

ActiveRecordPluginService管理控制

import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.jfinal.kit.HttpKit;
import com.jfinal.kit.JsonKit;
import com.jfinal.kit.PropKit;
import com.jfinal.kit.Ret;
import com.jfinal.log.Log;
import com.jfinal.plugin.IPlugin;
import com.jfinal.plugin.activerecord.ActiveRecordPlugin;
import com.jfinal.plugin.activerecord.CaseInsensitiveContainerFactory;
import com.jfinal.plugin.activerecord.DbKit;
import com.jfinal.plugin.activerecord.cache.EhCache;
import com.jfinal.plugin.druid.DruidPlugin;
import com.momathink.api.ApiInterceptor;
import com.momathink.common.constants.DictKeys;
import com.momathink.common.model.Site;

/**(域名白名单) 
 * <br>
 * 多站点配置信息和多数据源 管理
 * @author dufuzhong@126.com
 * @date 2016年11月8日 下午19:16:23
 */
public class ActiveRecordPluginService implements IPlugin {
    
    private static final Log log = Log.getLog(ActiveRecordPluginService.class);
    
    public static final ActiveRecordPluginService me = new ActiveRecordPluginService();

    /***
     * 存储 多站点信息, 域名为 键, 数据源configName 为 值
     */
    private static final Map<String, String> CONFIGNAME_S = new HashMap<String, String>();
    static{//初始化值
        CONFIGNAME_S.put("localhost", DbKit.MAIN_CONFIG_NAME);
        CONFIGNAME_S.put("127.0.0.1", DbKit.MAIN_CONFIG_NAME);
    }
    
    private ActiveRecordPluginService(){}
    public static ActiveRecordPluginService me() {
        return me;
    }
    
    /***
     * @Title: 获取某站点 数据源configName
     * @param key 访问者域名
     * @return String    该站点数据源configName
     */
    public String getConfigName(String key) {
        return CONFIGNAME_S.get(key);
    }
    
    /***
     * 加载 多站点配置信息
     * @return 
     */
    public synchronized boolean load(List<Site> sites){
        log.info("加载 多站点配置信息");
        for (Site site : sites)
            add(site);
        
        //报告记录一下启动情况
        
        String url = PropKit.get(DictKeys.SITE_SETCONFIGSITE).trim() + ApiInterceptor.getMaskKit();
        try {
            String data = URLEncoder.encode(JsonKit.toJson(sites), "UTF-8");
            log.info("报告多站点多数据源配置信息网站地址: "+ url);
            String post = HttpKit.post(url, "&data=" + data);
            log.info("报告完毕:" + post);
        } catch (Exception e) {
            log.info("报告异常,可能是没有启动运维服务器, 异常信息:" + e.getMessage());
            return false;
        }
        return true;
    }
     
    /***
     * 动态 配置 数据库参数 和 加载系统资源
     */
    public synchronized void add(Site site) {
        log.info("解析内容:" +  site);
        //加 try catch 的原因是 不能因为某个 站点 的错误 配置信息, 影响到 其他的站点
        try {
            //配置 数据库参数 和 加载系统资源
            Ret addDb = init(site);
            
            //数据库启动载入, 启动成功则连接成功, 否则会异常
            boolean druidPlugin = ((DruidPlugin) addDb.get("druidPlugin")).start();
            boolean arp = ((ActiveRecordPlugin) addDb.get("arp")).start();
            
            //连接池和管理器都要成功true 才能通过
            if(!(druidPlugin) || !(arp)) throw new Exception("连接池和管理器");
            
            //网址域名
            String website = site.getWebsite().trim();
            
            //该网站的系统资源 KEY 值
            String configname = site.getConfigName().trim();
            
            //存储 该站点资源KEY信息
            CONFIGNAME_S.put(website, configname);
            
            //成功返回码
            site.keep("id");
            site.set(ApiInterceptor.ERRJSON_ERRCODE, 0).set(ApiInterceptor.ERRJSON_ERRMSG, "OK");
            
        } catch (Exception e) {
            //错误码表 后面再写... 先直接 看错误信息吧
            site.set(ApiInterceptor.ERRJSON_ERRCODE, 500).set(ApiInterceptor.ERRJSON_ERRMSG, e.getMessage());
            
            log.info("配置 数据库参数 错误信息:" +  site);
        }
    }
    
    /***
     * 移除 动态 配置 数据库参数 和 加载的系统资源
     */
    public synchronized void del(Site site) {
        CONFIGNAME_S.remove(site.getWebsite());
        DbKit.removeConfig(site.getConfigName());
        //成功返回码
        site.keep("id");
        site.set(ApiInterceptor.ERRJSON_ERRCODE, 0).set(ApiInterceptor.ERRJSON_ERRMSG, "OK");
    }

    /**配置数据库参数 和 加载系统资源
     */
    private Ret init(Site site) {

        //该网站的系统资源 KEY 值
        String configname = site.getConfigName().trim();
        
        String jdbcurl = site.getJdbcUrl().trim();
        String user = site.getUser().trim();
        String password = site.getPassword().trim();
        
        // 配置DruidPlugin数据库连接池插件
        DruidPlugin druidPlugin = new DruidPlugin(jdbcurl, user, password);
        
        // 配置ActiveRecord插件
        ActiveRecordPlugin arp = new ActiveRecordPlugin(configname, druidPlugin);
        
        //false 是大写, true是小写, 不写是区分大小写, 看老项目情况配置
        arp.setContainerFactory(new CaseInsensitiveContainerFactory(false));
        
        //配置缓存类型
        arp.setCache(new EhCache());
        
        return Ret.create("druidPlugin", druidPlugin).set("arp", arp);
    }
    

    @Override
    public boolean start() {
        //TODO 我的测试代码放在    F:\workspace\moma_oa_test
        String url = PropKit.get(DictKeys.SITE_GETCONFIGSITE).trim() + ApiInterceptor.getMaskKit();
        log.info("获取多站点配置信息网站地址: "+ url);
        String jsonStr =  HttpKit.get(url);
        log.debug("获取的多站点配置: "+ jsonStr);
        JSONObject json = JSONObject.parseObject(jsonStr);
        
        if(json.getInteger(ApiInterceptor.ERRJSON_ERRCODE) != 0){
            log.info("错误信息:" + json.getString(ApiInterceptor.ERRJSON_ERRMSG));
            return false;
        }
        
        //解析json
        List<Site> sites = JSONArray.parseArray(json.getString(ApiInterceptor.ERRJSON_DATA), Site.class);
        
        load(sites);
        
        return true;
    }

    @Override
    public boolean stop() {
        //TODO 报告这些站点停了
        return true;
    }

}


想动态那就要接口调用了:

ConfigController:

import com.alibaba.fastjson.JSONObject;
import com.jfinal.aop.Before;
import com.jfinal.aop.Clear;
import com.momathink.common.annotation.controller.Controller;
import com.momathink.common.base.BaseController;
import com.momathink.common.model.Site;
import com.momathink.common.service.ActiveRecordPluginService;

/***多站点 系统配置 操作API
 * @author dufuzhong@126.com
 * @date 2017年2月25日 下午1:59:07
 */
@Before({ApiInterceptor.class })
@Controller(controllerKey = { "/api/config" })
public class ConfigController extends BaseController {

    /** 动态管理数据库和系统资源接口
     */
    public void operation(){
        String data = getPara("data");
        Site site = JSONObject.parseObject(data, Site.class);
        
        if(site.isStateOn())
            ActiveRecordPluginService.me.add(site);
        
        else if(site.isStateOff())
            ActiveRecordPluginService.me.del(site);
        
        renderJson(ApiInterceptor.errJson(site));
    }
    
  
    
    
}


接口 交接数据的 格式 规范:

import com.jfinal.aop.Interceptor;
import com.jfinal.aop.Invocation;
import com.jfinal.core.Controller;
import com.jfinal.kit.HashKit;
import com.jfinal.kit.JsonKit;
import com.jfinal.kit.StrKit;
/**
 * 接口 交接数据的 格式 规范
 * 此拦截器仅做为示例展示,在本 demo 中 临时做一下 校验
 */
public class ApiInterceptor implements Interceptor {
    
    /**接口 返回值  错误码 K */
    public static final String ERRJSON_ERRCODE = "errcode";
    /**接口 返回值  错误信息 K */
    public static final String ERRJSON_ERRMSG  = "errmsg";
    /**接口 返回值  数据 K */
    public static final String ERRJSON_DATA  = "data";
    /**接口 调用数据时 使用的 K */
    public static final String CHECK_MASK  = "mask";
    
    public void intercept(Invocation inv) {
        Controller controller = inv.getController();
        String mask = controller.getPara(CHECK_MASK);
        if(isMask(mask)){
            
            try {
                inv.invoke();
            } catch (Exception e) {
                inv.getController().renderJson(errJson(1, e.getMessage()));
            }
            
        }else inv.getController().renderJson(errJson(401, "mask验证失败"));
    }
    
    /**判断 密钥*/
    private static boolean isMask(String mask){
        if(StrKit.notBlank(mask))
            return getMask().equals(mask.trim());
        return false;
    }
    
    
    /***获取通行码
     * 临时做一下 校验, 自行改造
     */
    public static String getMask(){
        String mask = (new Date().getTime()+"").substring(0, 8) + "_dufuzhong@126.com";
        //1.66665    分(min)
        return HashKit.md5(mask);
    }
    
    /***获取通行码
     */
    public static String getMaskKit(){
        return "&"+ CHECK_MASK +"=" + getMask();
    }
    
    
    //--------------
    
    public static String errJson(Integer errcode, String errmsg) {
        return "{\"errcode\":" + errcode + ",\"errmsg\":\"" + errmsg + "\"}";
    }
    
    public static String errJson(Object data) {
        return "{\"errcode\":0,\"errmsg\":\"OK\",\"data\":" + (JsonKit.toJson(data)) + "}";
    }
    
    public static String errJson() {
        return "{\"errcode\":0,\"errmsg\":\"OK\"}";
    }
}

相关便利类;

Site
/**
 * Generated by JFinal.
 */
@SuppressWarnings("serial")
public class Site extends BaseSite<Site> {
    public static final Site dao = new Site();
    
    /**关机**/
    public static final Integer STATE_off = 0;
    /**开机**/
    public static final Integer STATE_on = 1;
    /**操作中**/
    public static final Integer STATE_out = 2;
    
    
    public boolean isStateOn(){
        return Site.STATE_on.equals(getState());
    }
    
    public boolean isStateOff(){
        return Site.STATE_off.equals(getState());
    }
    
}
/**
 * Generated by JFinal, do not modify this file.
 */
@SuppressWarnings("serial")
public abstract class BaseSite<M extends BaseSite<M>> extends Model<M> implements IBean {

    public void setId(java.lang.Integer id) {
        set("id", id);
    }

    public java.lang.Integer getId() {
        return get("id");
    }

    public void setConfigName(java.lang.String configName) {
        set("configName", configName);
    }

    public java.lang.String getConfigName() {
        return get("configName");
    }

    public void setWebsite(java.lang.String website) {
        set("website", website);
    }

    public java.lang.String getWebsite() {
        return get("website");
    }

    public void setJdbcUrl(java.lang.String jdbcUrl) {
        set("jdbcUrl", jdbcUrl);
    }

    public java.lang.String getJdbcUrl() {
        return get("jdbcUrl");
    }

    public void setUser(java.lang.String user) {
        set("user", user);
    }

    public java.lang.String getUser() {
        return get("user");
    }

    public void setPassword(java.lang.String password) {
        set("password", password);
    }

    public java.lang.String getPassword() {
        return get("password");
    }

    public void setHostId(java.lang.Integer hostId) {
        set("hostId", hostId);
    }

    public java.lang.Integer getHostId() {
        return get("hostId");
    }

    public void setState(java.lang.Integer state) {
        set("state", state);
    }

    public java.lang.Integer getState() {
        return get("state");
    }

    public void setErrcode(java.lang.Integer errcode) {
        set("errcode", errcode);
    }

    public java.lang.Integer getErrcode() {
        return get("errcode");
    }

    public void setErrmsg(java.lang.String errmsg) {
        set("errmsg", errmsg);
    }

    public java.lang.String getErrmsg() {
        return get("errmsg");
    }

}

到此基本完结了
最近有些小忙,小结后会分享 动态部署JFinal项目和升级项目, 也就是控制中心(自动运维)配合项目使用

最后打个广告有需求的可以联系下:

摩码创想(北京)科技有限公司    赵 睿   ZhaoRui   


Tel:186 0067 6071   QQ:28747355   微信:zhaorui93

-------------------------------------------------------------------------------------------

“教育IT化”一站式全套解决方案  {1v1量身设计,满意付费 }  免费升级,终身维护!

学员档案 CRM、市场招生、渠道管理、销售管理、报名交费、排课教务、教学评价、课件管理、通知提醒、同步课表、课酬考勤、成绩管理、财务管理、数据统计、宿舍管理、库存教材、人事行政、学习报告、就业管理、在线支付、网上预约、在线购课、在线考试、网络课堂(移动端、微信端、数据批量导入、导出,打印、会员卡、积分身份证) + 更多自由设计功能


评论区

JFinal

2017-03-24 22:09

先点赞、收藏,再看,哈哈

XiaoFei

2017-03-24 22:10

点赞收藏

JFinal

2017-03-24 22:12

在 getConfig() 中切换数据源,这一招格外妙,我以前给小伙伴们的建议没想到这招,用好这一招结合拦截器简直爽得不要不要的了

xkcoding

2017-03-24 22:17

明天周六好好观摩

liugz

2017-03-25 11:04

非常感谢,回公司再慢慢拜读。

Dreamlu

2017-03-25 13:05

666,涨姿势了,一直没有研究到这个底层,之前是在Service层弄了。

不要香菜

2017-03-25 15:21

6666,老司机,来不及解释了,快上车

ghostsf

2017-03-27 23:10

getConfig()中切换数据源是不错。后面的ActiveRecordPluginService没看懂。置Config类于何地?还有这个只能是一个serverName(域名)对应一个configName(数据源)?要是一个serverName多个数据源的切换呢?

杜福忠

2017-03-28 09:35

@ghostsf 这只是提供一种管理方式

serverName 多对一 configName:---------
ActiveRecordPluginService中是让多域名对应一个数据源的,
如: static map//初始化值

我这边业务场景: 客户会经常换域名, 而且我们这边也提供二,三,级域名, 有的客户自己绑定一级域名, 所以在中间加了个K, 域,名任你随便换我只保证系统这边K唯一就搞定了.

serverName 一对多 configName:---------
一个serverName对多个数据源, 需要根据你业务组装不同的规则就好了, 比如说: 系统的日志数据源吧, 完全可以是 serverName_log 这种形式当K.
还可以是url挂参configName的形式, 还可以是 缓存中 取不同configName. 比如上面ActiveRecordPluginService中static map中值再放configName集合,这也是一种组装方式, 需要看业务场景

serverName 一对一 configName:---------
直接用DbKit就好了

jfinal数据源非常灵活,根据自己需求去组装不同规则就好了

ghostsf

2017-03-28 10:57

@杜福忠 嗯嗯。是的。

giegie

2017-04-03 11:00

(⊙o⊙)… 看来 我用的方式 太瓜皮了

喈喈嘟

2017-09-20 15:06

先点赞、收藏,再看,哈哈

喈喈嘟

2017-09-20 15:10

收藏了

昵称而已

2018-01-26 17:17

在 BaseModel 中重写getConfig,一个项目好多个BaseModel,全部都要加,这个后续怎么维护?

昵称而已

2018-01-26 17:24

在 BaseModel 中重写getConfig,一个项目好多个BaseModel,全部都要加,这个后续怎么维护?@JFinal

杜福忠

2018-01-26 21:42

@昵称而已
好多个BaseModel ???
你指的是那种 get和set的 BaseXxxModel 吗?
这个简单啊, 让哪些BaseXxxModel 去继承你通用公共的BaseModel 就可以了。
比如我的:

public abstract class BaseAccount> extends BaseModel implements IBean {
setXxx
getXxx


值得注意的是 Jfinal 3.3版本 getConfig() 增加了 _getConfig() 下划线前缀的。
同时Model 生成器的模版也需要自定义一下,这样生成的就不用修改了

昵称而已

2018-01-27 11:40

@杜福忠 你的微信多少,能加下吗?

杜福忠

2018-01-27 16:13

1659811173

2019-03-28 13:58

能开源一下?

杜福忠

2019-03-28 14:10

@1659811173 两三个类没必要上传文件吧,代码就是上面文章里面了,复制一下吧