注意:本文不想引起任何技术和框架生态层面的争吵,仅分享个人在使用过程中遇到的问题和解决方案,请不要对任何技术框架和使用者有任何不友好的看法,除非你有足够的能力达到对方的层次
涉及到的技术框架
背景
由于solon自身的事务框架对于ktorm目前的支持存在一定的问题,所以我采取了另外的一种方式,利用solon特有的精简化的AOP特性以及注解继承的特性,单独实现了一个ktorm的事务委托方式。
ktorm自身的事务实现
事务管理接口
ktorm 的所有事务实现本质上都可以通过此接口的实现来托管给第三方生态框架的事务,根据ktorm的官网介绍,此接口本身并没有任何的行为动作,仅仅是一个事务管理器的抽象声明,也就是说你可以自己通过这个接口将ktorm的事务委托给任何的第三方生态框架或者自己去实现,在ktorm自身的事务实现中,有两个具体的事务管理器的实现:
由于本文主要记录我再进行第三方生态框架对ktorm的事务进行代理的记录,所以原生事务管理器不做重点介绍,主要说一下Spring事务管理器的参考和实现solon的事务代理的方案
ktorm事务管理器接口 (以下代码来自于ktorm github仓库)
kotlin
public interface TransactionManager {
public val defaultIsolation: TransactionIsolation?
public val currentTransaction: Transaction?
public fun newTransaction(isolation: TransactionIsolation? = defaultIsolation): Transaction
public fun newConnection(): Connection
}
Spring事务管理器 (以下代码来自于ktorm github仓库)
kotlin
/**
* [TransactionManager] implementation that delegates all transactions to the Spring framework.
*
* This class enables the Spring support, and it's used by [Database] instances created
* by [Database.connectWithSpringSupport] function. Once the Spring support enabled, the
* transaction management will be delegated to the Spring framework, so the [Database.useTransaction]
* function is not available anymore, applications should use Spring's [Transactional] annotation instead.
*
* @property dataSource the data source used to obtained connections, typically comes from Spring's application context.
*/
public class SpringManagedTransactionManager(public val dataSource: DataSource) : TransactionManager {
private val proxy = dataSource as? TransactionAwareDataSourceProxy ?: TransactionAwareDataSourceProxy(dataSource)
override val defaultIsolation: TransactionIsolation? = null
override val currentTransaction: Transaction? = null
override fun newTransaction(isolation: TransactionIsolation?): Nothing {
val msg = "Transaction is managed by Spring, please use Spring's @Transactional annotation instead."
throw UnsupportedOperationException(msg)
}
override fun newConnection(): Connection {
return proxy.connection
}
}
Database.connectWithSpringSupport (Spring DataSourc Connection创建函数) (以下代码来自于ktorm github仓库)
kotlin
/**
* Connect to a database using a [DataSource] with the Spring support enabled.
*
* Once the Spring support is enabled, the transaction management will be delegated to the Spring framework,
* so the [useTransaction] function is not available anymore, we need to use Spring's [Transactional]
* annotation instead.
*
* This function also enables the exception translation, which can convert any [SQLException] thrown by JDBC
* to Spring's [DataAccessException] and rethrow it.
*
* @param dataSource the data source used to obtain SQL connections.
* @param dialect the dialect, auto-detects an implementation by default using JDK [ServiceLoader] facility.
* @param logger logger used to output logs, auto-detects an implementation by default.
* @param alwaysQuoteIdentifiers whether we need to always quote SQL identifiers in the generated SQLs.
* @param generateSqlInUpperCase whether we need to output the generated SQLs in upper case.
* @return the new-created database object.
*/
public fun connectWithSpringSupport(
dataSource: DataSource,
dialect: SqlDialect = detectDialectImplementation(),
logger: Logger = detectLoggerImplementation(),
alwaysQuoteIdentifiers: Boolean = false,
generateSqlInUpperCase: Boolean? = null
): Database {
val translator = SQLErrorCodeSQLExceptionTranslator(dataSource)
return Database(
transactionManager = SpringManagedTransactionManager(dataSource),
dialect = dialect,
logger = logger,
exceptionTranslator = { ex -> translator.translate("Ktorm", null, ex) },
alwaysQuoteIdentifiers = alwaysQuoteIdentifiers,
generateSqlInUpperCase = generateSqlInUpperCase
)
}
上面的注释翻译如下
1.TransactionManager 将所有事务委托给 Spring 框架的实现。 此类启用 Spring 支持,并由函数Database.connectWithSpringSupport创建的实例使用它Database。一旦启用了 Spring 支持,事务管理将被委托给 Spring 框架,因此该Database. useTransaction功能不再可用,应用程序应改用 Spring 的事务注释。
2.使用启用了 Spring 支持的 DataSource 连接到数据库。一旦启用了 Spring 支持,事务管理就会委托给 Spring 框架,所以 useTransaction 函数不再可用,我们需要改用 Spring 的事务注解。此函数还启用异常转换,它可以将 JDBC 抛出的任何 SQLException 转换为 Spring 的 DataAccessException 并重新抛出它。
从注释我们可以看到,Spring的事务代理完全是通过实现TransactionManager接口来实现的,但是从代码的角度来看SpringManagedTransactionManager类并没有任何的具体的操作,仅仅是将ktorm的DataSource Connetion委托给了Spring的代理的DataSource Connetion,那理论上我们对于任何的参考SpringManagedTransactionManager来实现对solon的事务代理。 下面是ktorm官网对于TransactionManager接口的一些描述,以及ktorm的事务管理的描述

实现solon的事务代理
如果要实现solon的事务代理,关键在于实现TransactionManager,并且创建新的DataSource Connetion代理,也就是要实现newConnection()这个函数,从上面Spring的事务代理实现也能看出来,只要将ktorm的DataSource Connetion代理给solon框架的DataSource Connetion就可以了。
solon事务工具(2.7.4之前版本)(以下代码来自于solon的gitee仓库)
Java
/**
* 事务工具
*
* @author noear
* @since 1.0
* @since 2.5
* */
public final class TranUtils {
private static TranExecutor executor = TranExecutorDefault.global;
static {
Solon.context().getBeanAsync(TranExecutor.class, bean -> executor = bean);
}
/**
* 执行事务
*/
public static void execute(Tran tran, RunnableEx runnable) throws Throwable {
executor.execute(tran, runnable);
}
/**
* 是否在事务中
*/
public static boolean inTrans() {
return executor.inTrans();
}
/**
* 是否在事务中且只读
*/
public static boolean inTransAndReadOnly() {
return executor.inTransAndReadOnly();
}
/**
* 监听事务
*
* @since 2.5
*/
public static void listen(TranListener listener) throws IllegalStateException {
executor.listen(listener);
}
/**
* 获取链接
*/
public static Connection getConnection(DataSource ds) throws SQLException {
return executor.getConnection(ds);
}
}
从这部分代码我们可以看到,可有通过getConnection这个函数来获取solon的DataSource Connetion
实现代码如下以及事务测试类
kotlin
/**
* solon connection 创建函数
* 参考 [Database.connectWithSpringSupport] 改写
*/
fun Database.Companion.connectWithSolonSupport(
dataSource: DataSource,
dialect: SqlDialect = detectDialectImplementation(),
logger: Logger = detectLoggerImplementation(),
alwaysQuoteIdentifiers: Boolean = false,
generateSqlInUpperCase: Boolean? = null
): Database {
// val translator = SQLErrorCodeSQLExceptionTranslator(dataSource)
return Database(
transactionManager = SolonManagedTransactionManager(dataSource),
dialect = dialect,
logger = logger,
// exceptionTranslator = { ex -> translator.translate("Ktorm", null, ex) },
alwaysQuoteIdentifiers = alwaysQuoteIdentifiers,
generateSqlInUpperCase = generateSqlInUpperCase
)
}
/**
* solon 事务代理器
* 参考 [SpringManagedTransactionManager] 改写
*/
class SolonManagedTransactionManager(private val dataSource: DataSource) :TransactionManager{
override val currentTransaction: Transaction?
get() = null
override val defaultIsolation: TransactionIsolation?
get() = null
override fun newConnection(): Connection {
return TranUtils.getConnection(dataSource);
}
override fun newTransaction(isolation: TransactionIsolation?): Transaction {
val msg = "Transaction is managed by Solon, please use Solon's @Tran annotation instead."
throw UnsupportedOperationException(msg)
}
}
//测试类
@Component
open class DepartmentComponent {
@Inject("dateBase")
lateinit var database: Database
@Tran(policy = TranPolicy.required, isolation = TranIsolation.read_committed)
open fun insertNew(department: Department) {
database.from(DepartmentTable.department).select()
println()
database.insert(DepartmentTable.department) {
set(it.parent_id, 1)
}
}
}
然而,在测试时,我发现数据连接会异常关闭。虽然数据已经成功写入数据库,但由于异常抛出,事务被回滚了。这表明了这种方式虽然能实现Solon对于Ktorm的事务代理,但由于异常的发生,事务仍然会被回滚,导致操作失败。
造成上面问题的原因,因为通过solon的事务工具获取的DataSource Connetion是不被ktorm兼容的,这里的问题是出现在了solon框架的链接获取方式,具体的原因需要深挖,但是很抱歉,我懒,没有深挖。
为了实现Solon的事务代理,我尝试了一些方法。Solon框架本身有一个非常有意思的特性------注解继承。这个特性大大降低了Solon对AOP的操作,使埋点更加简单。因此,我通过这个特性直接封装了Ktorm的事务操作。
实现代码如下
kotlin
/**
* ktrom 事务注解,由于ktorm框架直接委托给solon自身的事务管理器存在链接异常关闭的问题,故此以[KTran]注解为指定埋点,方便使用
* @property isolation 默认为[TransactionIsolation.READ_COMMITTED]
* @property transactionType 默认为[KtormTransactionType.DEFAULT_TRANSACTION_MANAGER]
*/
@Inherited
@Target(AnnotationTarget.CLASS,AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Around(KtormInterceptor::class)
annotation class KTran(
val isolation :TransactionIsolation = TransactionIsolation.READ_COMMITTED,
val transactionType: KtormTransactionType = KtormTransactionType.DEFAULT_TRANSACTION_MANAGER,
val dataBaseName:String = ""
)
/**
* [KTran]的埋点拦截器
*/
open class KtormInterceptor : Interceptor {
/**
* 定义新的拦截器,以实现[KTran]的埋点获取
*
* @param inv
*/
override fun doIntercept(inv: Invocation): Any? {
val kTran = kTran(inv)
val ktormManagers = KtormManagers(kTran.transactionType)
return ktormManagers.ktormTransactionManager(kTran) { inv.invoke() }
}
private fun kTran(inv: Invocation): KTran = runCatching {
inv.method().getAnnotation(KTran::class.java)?:inv.targetClz.getAnnotation(KTran::class.java)
}.onFailure {
logger.severe("kTran exception $it")
it.printStackTrace()
}.getOrDefault(KTran())
}
这部分是充分利用了solon的简化的AOP特性来拦截了函数的提交,结合封装的ktorm的事务函数,实现了注解式的事务管理。
其实实不实现这个注解的事务管理都可以的,使用
ktorm的原生事务也是可以的,而且更加符合kotlin的函数式的风格,不过我个人的强迫症就是必须要有这么个东西,否则不舒服,然后关于上面的这部分具体代码我已经写到了ktorm-for-solon-plugin这个插件里面,其中后续的具体实现以及对于@KTran拦截后的具体处理函数也在其中,并且对ktorm的原有的事务管理进行了二次封装,可以直接通过引入这个插件使用,开发不易期待 star,谢谢
solon 2.7.4之后的实现
上面说的版本是 solon 2.7.4之前的版本,实际上solon 2.7.4的版本已经支持了通过上面说到的模仿ktorm对于Spring的事务代理来实现solon的事务代理, 这里就离不开solon的作者西东大佬对于我提出的issues的支持了。 西东大佬在最新的版本中,封装了一个新的链接获取方式getConnectionProxy,这个获取的是一个经过事务封装代理链接,通过这个函数获取的链接已经封装了对于链接的事务管理,而原来的getConnection是直接获取的链接,并没有对事务进行控制,而ktorm自身也并未实现对于第三方生态框架的DataSource Connetion的事务进行控制,而是完全委托给了第三方生态框架。
后续ktorm-for-solon-plugin会直接集成最新版
solon连接获取方式以实现原生的事务代理方式,@KTran会摆脱solon的AOP耦合,使用原生的AOP,适配给所有的生态框架
Java
/**
* 事务工具
*
* @author noear
* @since 1.0
* @since 2.5
* */
public final class TranUtils {
private static TranExecutor executor = TranExecutorDefault.global;
static {
Solon.context().getBeanAsync(TranExecutor.class, bean -> executor = bean);
}
/**
* 执行事务
*/
public static void execute(Tran tran, RunnableEx runnable) throws Throwable {
executor.execute(tran, runnable);
}
/**
* 是否在事务中
*/
public static boolean inTrans() {
return executor.inTrans();
}
/**
* 是否在事务中且只读
*/
public static boolean inTransAndReadOnly() {
return executor.inTransAndReadOnly();
}
/**
* 监听事务
*
* @since 2.5
*/
public static void listen(TranListener listener) throws IllegalStateException {
executor.listen(listener);
}
/**
* 获取链接
*/
public static Connection getConnection(DataSource ds) throws SQLException {
return executor.getConnection(ds);
}
/**
* 获取链接代理
*/
public static Connection getConnectionProxy(DataSource ds) throws SQLException {
return new ConnectionProxy(executor.getConnection(ds));
}
}
友情链接 Ktorm官网 Solon官网 Ktorm社区仓库 Solon社区仓库 Ktorm作者主页 Solon作者主页 ktorm-for-solon-plugin仓库 测试项目源码包)