SpringBoot or Spring 接入 Jfinal-Db 替换Mybatis和datasource资源池共享

基于Jfinal3.4(老版本也能如此操作,但是需要多一步就是了)

SpringBoot

// ========= SpringBoot ============
@Configuration
public class JDbConfig {
    @Autowired
	// 等待db1Source启动后注入
    @Qualifier("db1Source")
    private DataSource ds;
	
	/**
     * 注入
     * @return JDb (JDb enxtends Db)
     * @throws Exception
     */
    @Bean(name = "jdb")
    @Primary
    public JDb jdb() throws Exception {
    	ActiveRecordPlugin arp = new ActiveRecordPlugin(ds);
    	arp.getEngine().setSourceFactory(new ClassPathSourceFactory());
        arp.addSqlTemplate("/sql/all_sqls.sql");
    	// 启动Record容器
    	arp.start();    	    	
    	System.out.println("===Jfinal - Db启动成功===");
    	// Db初始化
    	JDb db = new JDb();  	
    	return db;
    }	
}


// ===== JDb.java ============
/**
 * 集成jfinal Db
 * @author MrYang
 *
 */
public class JDb extends Db{
}


springBoot到这里的配置就结束了,就是这么简单!!!

下面的部分是 SpringMvc 的部分


========= SpringMvc============

在xml里配置个项目启动类,然后将数据源赋予这个类即可

例如:

<!-- CoralDbc  -->
<bean id="jdb" class="cn.com.???.coral.core.db.JDb" init-method="start">
  <property name="datasource">
     <ref bean="datasource" />
  </property>
  <property name="sqlPath" value="/sql/all_sqls.sql" />
</bean>


       

/**
 * 集成jfinal Db
 * @author MrYang
 *
 */
public class JDb extends Db{
   /**
         * 使用spring的jdbc对象,不然不知道为什么使用引入的 DataSource ds 对象,有时候是无效的
         */
      @Autowired
      JdbcTemplate jdbcTemplate;
   
   private String sqlPath;
   public void start(){
             // 加载springJdbc一致的数据源
        ActiveRecordPlugin arp = new ActiveRecordPlugin(jdbcTemplate.getDataSource());
    	arp.getEngine().setSourceFactory(new ClassPathSourceFactory());
    	//
        arp.addSqlTemplate(sqlPath);
    	// 启动Record容器
    	arp.start();    	    	
    	System.out.println("===Jfinal - Db启动成功===");
   }
   
   public String getSqlPath(){
       return this.sqlPath;
   }
   
    public void getSqlPath(String sqlPath){
       this.sqlPath=sqlPath;
   }
}

      

    =============================================

如果是直接用 jfinal 事务去操作的,而不需要用到spring事务注解标签的,请忽略这部分内容

jfinal事务和spring事务,如何统一整合,直接使用spring的事务标签呢,这个需要单独建一个插件或者直接复写 com.jfinal.plugin.activerecord.Config.getConnection() 和getThreadLocalConnection()  方法
或者切面在 service 层实现(然后要求大家遵守约束规范)

Config.java

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.jfinal.plugin.activerecord;

import com.jfinal.kit.LogKit;
import com.jfinal.kit.StrKit;
import com.jfinal.plugin.activerecord.cache.EhCache;
import com.jfinal.plugin.activerecord.cache.ICache;
import com.jfinal.plugin.activerecord.dialect.Dialect;
import com.jfinal.plugin.activerecord.dialect.MysqlDialect;
import com.jfinal.plugin.activerecord.sql.SqlKit;
import org.springframework.jdbc.datasource.ConnectionHolder;
import org.springframework.jdbc.datasource.DataSourceUtils;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Map;


/**
 * @ClassName Config
 * @Description: 基于spring事务管理改写,兼容 @Transactional 与 Db.use().tx(......) 类型事务混写
 * @Author MrYang
 * @Date 2020/10/13
 * @Version V1.0
 **/
public class Config {

    // 如果这里是静态的话,会导致线程公用同一个连接对象
    private final ThreadLocal<Connection> threadLocal ;

    String name;
    DataSource dataSource;
    Dialect dialect;
    boolean showSql;
    boolean devMode;
    int transactionLevel;
    IContainerFactory containerFactory;
    IDbProFactory dbProFactory;
    ICache cache;
    SqlKit sqlKit;

    Config(String name, DataSource dataSource, int transactionLevel) {
        this.threadLocal = new ThreadLocal();
        this.dbProFactory = IDbProFactory.defaultDbProFactory;
        this.init(name, dataSource, new MysqlDialect(), false, false, transactionLevel, IContainerFactory.defaultContainerFactory, new EhCache());
    }

    public Config(String name, DataSource dataSource, Dialect dialect, boolean showSql, boolean devMode, int transactionLevel, IContainerFactory containerFactory, ICache cache) {
        this.threadLocal = new ThreadLocal();
        this.dbProFactory = IDbProFactory.defaultDbProFactory;
        if (dataSource == null) {
            throw new IllegalArgumentException("DataSource can not be null");
        } else {
            this.init(name, dataSource, dialect, showSql, devMode, transactionLevel, containerFactory, cache);
        }
    }

    private void init(String name, DataSource dataSource, Dialect dialect, boolean showSql, boolean devMode, int transactionLevel, IContainerFactory containerFactory, ICache cache) {
        if (StrKit.isBlank(name)) {
            throw new IllegalArgumentException("Config name can not be blank");
        } else if (dialect == null) {
            throw new IllegalArgumentException("Dialect can not be null");
        } else if (containerFactory == null) {
            throw new IllegalArgumentException("ContainerFactory can not be null");
        } else if (cache == null) {
            throw new IllegalArgumentException("Cache can not be null");
        } else {
            this.name = name.trim();
            this.dataSource = dataSource;
            this.dialect = dialect;
            this.showSql = showSql;
            this.devMode = devMode;
            this.setTransactionLevel(transactionLevel);
            this.containerFactory = containerFactory;
            this.cache = cache;
            this.sqlKit = new SqlKit(this.name, this.devMode);
        }
    }

    public Config(String name, DataSource dataSource) {
        this(name, dataSource, new MysqlDialect());
    }

    public Config(String name, DataSource dataSource, Dialect dialect) {
        this(name, dataSource, dialect, false, false, 4, IContainerFactory.defaultContainerFactory, new EhCache());
    }

    private Config() {
        this.threadLocal = new ThreadLocal();
        this.dbProFactory = IDbProFactory.defaultDbProFactory;
    }

    void setDevMode(boolean devMode) {
        this.devMode = devMode;
        this.sqlKit.setDevMode(devMode);
    }

    void setTransactionLevel(int transactionLevel) {
        if (transactionLevel != 0 && transactionLevel != 1 && transactionLevel != 2 && transactionLevel != 4 && transactionLevel != 8) {
            throw new IllegalArgumentException("The transactionLevel only be 0, 1, 2, 4, 8");
        } else {
            this.transactionLevel = transactionLevel;
        }
    }

    static Config createBrokenConfig() {
        Config ret = new Config();
        ret.dialect = new MysqlDialect();
        ret.showSql = false;
        ret.devMode = false;
        ret.transactionLevel = 4;
        ret.containerFactory = IContainerFactory.defaultContainerFactory;
        ret.cache = new EhCache();
        return ret;
    }

    public String getName() {
        return this.name;
    }

    public SqlKit getSqlKit() {
        return this.sqlKit;
    }

    public Dialect getDialect() {
        return this.dialect;
    }

    public ICache getCache() {
        return this.cache;
    }

    public int getTransactionLevel() {
        return this.transactionLevel;
    }

    public DataSource getDataSource() {
        return this.dataSource;
    }

    public IContainerFactory getContainerFactory() {
        return this.containerFactory;
    }

    public IDbProFactory getDbProFactory() {
        return this.dbProFactory;
    }

    public boolean isShowSql() {
        return this.showSql;
    }

    public boolean isDevMode() {
        return this.devMode;
    }

    /**
     * 设置当前线程连接
     * @param connection
     */
    public void setThreadLocalConnection(Connection connection) {
        this.threadLocal.set(connection);
        //System.out.println("当前线程2-1:"+Thread.currentThread().getId()+ "对象:" + this.threadLocal.get());
    }

    /**
     * 移除当前线程连接
     */
    public void removeThreadLocalConnection() {
        //System.out.println("当前线程2-3:"+Thread.currentThread().getId() + "准备释放对象:" + this.threadLocal.get());
        // 移除事务层
        this.threadLocal.remove();
    }


    /**
     * 获取连接
     * @return
     * @throws SQLException
     */
    public Connection getConnection() throws SQLException {
        //System.out.println("当前线程2-2:"+Thread.currentThread().getId() + "对象:" + this.threadLocal.get());
        Connection conn = null;
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);

        // 不是spring的事务管理,则走原来的逻辑
        if(conHolder == null ){
            conn = (Connection)this.threadLocal.get();
            if (conn != null) {
                return conn;
            } else {
                return this.showSql ? (new SqlReporter(this.dataSource.getConnection())).getConnection() : this.dataSource.getConnection();
            }
        }else{
            //
            conn = (Connection)this.threadLocal.get();
            if (conn != null) {
                return conn;
            } else {
                // 通过spring获取连接对象
                conn = DataSourceUtils.getConnection(this.getDataSource());
            }

        }
        return  conn;
    }




    /**
     * 获取当前连接 -- 如果已经有事务的话,这里是会有对象的
     * @return
     */
    public Connection getThreadLocalConnection() {
        Connection connection =  threadLocal.get();
        if(connection != null){
            return connection;
        }
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        if(conHolder == null){
            return threadLocal.get();
        }else{
            return DataSourceUtils.getConnection(this.getDataSource());
        }
    }

    /**
     * 是否在事务中
     * @return
     */
    public boolean isInTransaction() {
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // 不是spring的事务管理,则走原来的逻辑
        if(conHolder == null){
            return threadLocal.get() != null;
        }
        return conHolder != null && (conHolder.getConnectionHandle()!=null || conHolder.isSynchronizedWithTransaction());
    }

    public void close(ResultSet rs, Statement st, Connection conn) {
        if (rs != null) {
            try {
                rs.close();
            } catch (SQLException var7) {
                LogKit.error(var7.getMessage(), var7);
            }
        }

        if (st != null) {
            try {
                st.close();
            } catch (SQLException var6) {
                LogKit.error(var6.getMessage(), var6);
            }
        }

        // 关闭连接
        close(conn);
    }

    public void close(Statement st, Connection conn) {
        if (st != null) {
            try {
                st.close();
            } catch (SQLException var5) {
                LogKit.error(var5.getMessage(), var5);
            }
        }
        // 关闭连接
        close(conn);
    }

    /**
     * 改造关闭对象
     * @param conn
     */
    public void close(Connection conn) {
        // 是否在事务中
        ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
        // spring事务则走这个方法 && !conHolder.isSynchronizedWithTransaction()
        if (conHolder != null ) {
            // 直接交给spring来关闭 -- 是否释放,在spring里会有判断
            DataSourceUtils.releaseConnection(conn, dataSource);
        }else{
            // 原 jfinal 事务走法
            if (threadLocal.get() == null){
                // 连接
                if (conn != null)
                {
                    try {
                        //conn.close();
                        DataSourceUtils.releaseConnection(conn, dataSource);
                    } catch (Throwable e) {
                        e.printStackTrace();
                    }
                }
            }
        }


    }



}


这样子,大家就可以愉快的用spring的事务玩耍了!!!! 

备注:2020-11-02 修复之前线程池一直占用没有恢复连接的bug(备注:还有一个DbPro对象改造后,才能进行多数据源的切换和联动),如果要让异步事务 txInNewThread 的事务生效,需要改写对应的 tx 方法,便于扩展兼容,不扩展的话,只有当前线程事务可以交互拿到对应的数据

==============================================


项目里使用的时候,直接注入即可

    @Autowired    
    JDb jdb;
    
    public void test(){
        // 接下来,你们懂的
        jdb.find("select count(1) from tb");
        // 读取sql脚本
        jdb.getSqlPara("index.getProjectList",100)
    }


对应的日志:


// jdb.find("select count(1) from tb");
2018-07-05 17:41:12.831|XNIO-2 task-1|DEBUG|{conn-10005, pstmt-20000} created.   select count(1) from tb

// jdb.getSqlPara("index.getProjectList",100)
Sql: 	select p.id,
		substring(p.title, 1, 100) as title,
		substring(p.content, 1, 180) as content,
		a.avatar,
		a.id as accountId
	from project p inner join account a on p.accountId = a.id
	where report < ?
	order by p.id asc limit 10

Para: [100]


事务一致性的一个小测试:

User user = userService.findByUsername("xxx");
user.setNickname("测试名称1"// 执行保存 -- 必须是带有flush的保存,否则只会存在jpa的缓存层,导致同一个事务层的jdbc查询的时候查找不到
userService.update(user);
// 
Record record = jdb.findFirst("select * from sys_user where user_name='xxx'");
// 打印的是 测试名称1
System.out.println("jfinal查询:"+record.getStr("nick_name"));
// 打印的是 测试名称1
System.out.println("spring自带查询:"+jdbcTemplate.queryForMap("select * from sys_user where user_name='xxx'").get("nick_name"));


特殊案例应用,非事务和事务的结合 -- 2020-08-07 -- 兼容处理修正后方可实现:

/**
 * 在普通方法里独立开启事务,与基础操作区分开来
 */
public void test2(){
    Organization organization=new Organization();
    organization.setOrgCode("JG001");
    organization.setOrgName("机构001");
    organization.setPid(0L);
    organizationService.save(organization);

    // 设置为null,准备在插入一个新的对象,然后设置事务回滚 return false ,结果是不会插入到数据库
    organization.setOrgId(null);
    Db.use().tx(Connection.TRANSACTION_SERIALIZABLE, () -> {
        // save 其实默认将主键给剔除了
        JDb.save(organization);
        return false;
    });

}


评论区

小佳

2018-07-05 16:40

因为jfinal在某方面比spring强大多了,包括sql模板的部分,所以用了以上方法,就可以把 Mybatis 踢掉,实现高效的开发

JFinal

2018-07-05 16:52

要是再来一点使用的代码就好了,感谢你的分享

小佳

2018-07-05 17:44

peefau

2018-07-05 18:05

学习一下

晴天009

2018-08-23 17:50

jfinal事务和spring事务,如何统一整合 楼主能再说详细点吗 谢谢

晴天009

2018-08-23 17:53

jdb都是静态方法 注入好像意义不大

小佳

2018-09-08 14:29

@晴天009 嗯,习惯性用法,因为我秉承spirng体系一脉,会比较舒服点,让spring-bean托管容器,是根本性意识

小佳

2018-09-08 14:43

@晴天009 我文章有小改,关于tx事务和jfinal事务重叠的部分,需要替换对应的类和方法,建议做成中间插件覆盖即可

WenJ

2019-09-12 10:45

感谢楼主分享,在实际整合到spring过程中,Config.close(),关闭链接的方法也需要处理,否则当存在事务操作的业务中,第二次操作数据库则会报错,事务操作关闭conn操作应该交给spring自行处理。而且我在查看druid检测页面时发现,逻辑打开连接数与逻辑关闭连接数在druid 1.0.29版本会出现不一致的情况,可能是druid自身统计bug造成,升级至1.1.18后问题解决。我的close代码修改如下:
ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource);
//判断当前是否为事务环境
if(conHolder == null || !conHolder.isSynchronizedWithTransaction()){
try {
//非事务环境释放conn
DataSourceUtils.doReleaseConnection(conn,dataSource);
} catch (SQLException e) {
e.printStackTrace();
}
}

小佳

2019-11-18 16:50

@WenJ 非常感谢,其实我这边也是有做了调整,基本大致流程与你所述一致,看你写的代码和注释,也是老鸟一枚啊

小佳

2019-11-18 17:42

@WenJ 我这里其实是用切面实现,然后在after那里,还会做一次处理,就是每次结束后,统一将就final的connect对象置为null,呢么处理connect的释放,就还是交由spring自己处理,效果与在jfinal的close那里的处理是一致的

balujun

2020-04-11 11:46

方便提供一下源码吗?

小佳

2020-04-15 16:20

@balujun 仔细看,已经是源码了。运行了很久都没啥问题

小佳

2020-08-07 15:10

今天我对jfinal的事务和spring非事务的结合做了一个调整,便于兼容 Db.use().tx() 事务方法的应用,有特殊场景时,还是非常方便的,之前硬编码,改造的位置,导致jfinal的事务控制失效,这个是原来设计中故意留下的一环,目前有需要,所以放开了限制, 主要是 config 这个类部分地方做了调整

fmpoffice

2023-07-03 21:44

别用springboot了,我已经放弃了

小佳

2024-02-21 12:57

@fmpoffice 这个是拯救 springboot 老项目的福星~~~新项目建议用jfinal

小佳

2024-02-21 12:58

@fmpoffice https://jfinal.com/share/2472

热门分享

扫码入社