diff --git a/mybatis-flex-solon-plugin/README.md b/mybatis-flex-solon-plugin/README.md index fca44ecd..ee151791 100644 --- a/mybatis-flex-solon-plugin/README.md +++ b/mybatis-flex-solon-plugin/README.md @@ -1,3 +1,5 @@ + + ```xml com.mybatis-flex @@ -10,57 +12,48 @@ 数据扩展插件,为 Solon Data 提供基于 mybatis-flex([代码仓库](https://gitee.com/mybatis-flex/mybatis-flex))的框架适配,以提供ORM支持。 -#### 2、强调多数据源支持 +可注入类型: -> Solon 的 ORM 框架都是基于多数据源理念进行适配的。关于 Solon 数据源概念的描述,可参考:[多数据源与动态数据源](https://solon.noear.org/article/353) - -* 强调多数据源的配置。例:demo.db1...,demo.db2... -* 强调带 name 的 DataSource Bean -* 强调使用 @Db("name") 的数据源注解 - - -@Db 可注入类型: - -| 支持类型 | 说明 | -| -------- | -------- | -| Mapper.class | 注入 Mapper。例:`@Db("db1") UserMapper userMapper` | -| FlexConfiguration | 注入 FlexConfiguration,一般仅用于配置。例:`@Db("db1") FlexConfiguration db1Cfg` | -| FlexGlobalConfig | 注入 FlexGlobalConfig,一般仅用于配置。例:`@Db("db1") FlexGlobalConfig db1Gc` | -| SqlSessionFactory | 注入 SqlSessionFactory。例:`@Db("db1") SqlSessionFactory db1` (不推荐直接使用) | -| RowMapperInvoker | 注入 RowMapperInvoker。例:`@Db("db1") RowMapperInvoker rowMapper` | +| 支持类型 | 说明 | +| -------- |----------------------------------------------------------------------| +| Mapper.class | 注入 Mapper。例:`@Inject UserMapper userMapper` | +| FlexConfiguration | 注入 FlexConfiguration,一般仅用于配置。例:`@Inject FlexConfiguration db1Cfg` | +| FlexGlobalConfig | 注入 FlexGlobalConfig,一般仅用于配置。例:`@Inject FlexGlobalConfig db1Gc` | +| SqlSessionFactory | 注入 SqlSessionFactory。例:`@Inject SqlSessionFactory db1` (不推荐直接使用) | +| RowMapperInvoker | 注入 RowMapperInvoker。例:`@Inject RowMapperInvoker rowMapper` | #### 3、数据源配置 ```yml -# 配置数据源 -demo.db1: - schema: rock - jdbcUrl: jdbc:mysql://localhost:3306/rock?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true - driverClassName: com.mysql.cj.jdbc.Driver - username: root - password: 123456 - +# 配置数据源(或者使用 solon.dataSources 配置数据源,效果一样) +mybatis-flex.datasource: + db1: + jdbcUrl: jdbc:mysql://localhost:3306/rock?useUnicode=true&characterEncoding=utf8&autoReconnect=true&rewriteBatchedStatements=true + driverClassName: com.mysql.cj.jdbc.Driver + username: root + password: 123456 + # 配置数据源对应的 mybatis 信息(要与 DataSource bean 的名字对上) -mybatis.db1: - typeAliases: #支持包名 或 类名(大写开头 或 *)//支持 ** 或 * 占位符 +mybatis-flex: + type-aliases-package: #支持包名 或 类名(大写开头 或 *)//支持 ** 或 * 占位符 - "demo4021.model" - "demo4021.model.*" #这个表达式同上效果 - typeHandlers: #支持包名 或 类名(大写开头 或 *)//支持 ** 或 * 占位符 + type-handlers-package: #支持包名 或 类名(大写开头 或 *)//支持 ** 或 * 占位符 - "demo4021.dso.mybaits.handler" - "demo4021.dso.mybaits.handler.*" #这个表达式同上效果 - mappers: #支持包名 或 类名(大写开头 或 *)或 xml(.xml结尾)//支持 ** 或 * 占位符 + mapper-locations: #支持包名 或 类名(大写开头 或 *)或 xml(.xml结尾)//支持 ** 或 * 占位符 - "demo4021.**.mapper" - "demo4021.**.mapper.*" #这个表达式同上效果 - "classpath:demo4035/**/mapper.xml" - - "classpath:demo4035/**/mapping/*.xml" + - "classpath:demo4035/**/mapping/*.xml" configuration: #扩展配置(要与 FlexConfiguration 类的属性一一对应) cacheEnabled: false mapUnderscoreToCamelCase: true - globalConfig: #全局配置(要与 FlexGlobalConfig 类的属性一一对应)//只是示例,别照抄 + global-config: #全局配置(要与 FlexGlobalConfig 类的属性一一对应)//只是示例,别照抄 printBanner: false - keyConfig: + keyConfig: keyType: "Generator" value: "snowFlakeId" @@ -70,66 +63,77 @@ mybatis.db1: # ``` -> configuration、globalConfig 没有对应属性时,可用代码处理 +主要支持的属性说明: + +| 支持属性 | 别名(保持与之前的兼容) | 说明 | +|-------------------------|----------------------------|----------------------------------| +| type-aliases-package | typeAliases | 类型别名 | +| type-aliases-super-type | typeAliasesSuperType | 类型别名的父类(用于过滤) | +| type-handlers-package | typeHandlers | 类型处理器 | +| mapper-locations | mappers | mapper 类或xml文件 | +| configuration | configuration | mybatis 配置。对应类:FlexConfiguration | +| global-config | globalConfig | 全局部置。对应类:FlexGlobalConfig | + + ##### Mapper 配置注意事项: * 通过 mapper 类包名配置。 xml 与 mapper 需同包同名 ```yml -mybatis.db1.mappers: "demo4035.dso.mapper" +mybatis-flex.mapper-locations: "demo4035.dso.mapper" ``` * 通过 xml 目录进行配置。xml 可以固定在一个资源目录下 ```yml -mybatis.db1.mappers: "classpath:mybatis/db1/*.xml" +mybatis-flex.mapper-locations: "classpath:mybatis/db1/*.xml" ``` #### 4、代码应用 ```java -//配置数据源 +//配置 mf (如果配置不能满足需求,可以进一步代助代码) @Configuration public class Config { - //此下的 db1 与 mybatis.db1 将对应在起来 //可以用 @Db("db1") 注入mapper - //typed=true,表示默认数据源。@Db 可不带名字注入 - @Bean(value = "db1", typed = true) - public DataSource db1(@Inject("${demo.db1}") HikariDataSource ds) { - return ds; + @Bean + public void ormConfig(@Inject FlexConfiguration cfg, + @Inject FlexGlobalConfig globalConfig) { + + cfg.setCacheEnabled(false); + } - - //@Bean(value = "db2", typed = true) - //public DataSource db2(@Inject("${demo.db2}") HikariDataSource ds) { - // return ds; - //} - - //调整 db1 的配置(如:增加插件)// (配置可以解决的,不需要这块代码) - //@Bean - //public void db1_cfg(@Db("db1") FlexConfiguration cfg, - // @Db("db1") FlexGlobalConfig globalConfig) { - - // cfg.setCacheEnabled(false); - - //} } //应用 -@ProxyComponent -public class AppService{ - //可用 @Db 或 @Db("db1") 注入 - @Db +@Component +public class AppService { + @Inject AppMapper appMapper; //xml sql mapper - //可用 @Db 或 @Db("db1") - @Db + @Inject BaseMapper appBaseMapper; //base mapper - - public void test(){ + + public void test0() { App app1 = appMapper.getAppById(12); App app2 = appBaseMapper.selectOneById(12); } + + @UseDataSource("db1") + public void test1() { + App app1 = appMapper.getAppById(12); + App app2 = appBaseMapper.selectOneById(12); + } + + public void test2() { + try { + DataSourceKey.use("db1"); + App app1 = appMapper.getAppById(12); + App app2 = appBaseMapper.selectOneById(12); + } finally { + DataSourceKey.clear(); + } + } } ``` - diff --git a/mybatis-flex-solon-plugin/pom.xml b/mybatis-flex-solon-plugin/pom.xml index c0aeb516..fa7a8610 100644 --- a/mybatis-flex-solon-plugin/pom.xml +++ b/mybatis-flex-solon-plugin/pom.xml @@ -21,7 +21,7 @@ org.noear - mybatis-solon-plugin + solon-data ${solon.version} @@ -30,6 +30,13 @@ mybatis-flex-core ${mybatis-flex.version} + + + org.noear + solon-aot + ${solon.version} + provided + diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/core/datasource/FlexDataSourceRouting.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/core/datasource/FlexDataSourceRouting.java new file mode 100644 index 00000000..29c32ec3 --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/core/datasource/FlexDataSourceRouting.java @@ -0,0 +1,22 @@ +package com.mybatisflex.core.datasource; + +import org.noear.solon.data.datasource.RoutingDataSource; + +import javax.sql.DataSource; + +/** + * @author noear 2024/12/3 created + */ +public class FlexDataSourceRouting { + public static DataSource determineCurrentTarget(DataSource original) { + if (original instanceof FlexDataSource) { + return ((FlexDataSource) original).getDataSource(); + } + + if (original instanceof RoutingDataSource) { + return ((RoutingDataSource) original).determineCurrentTarget(); + } + + return original; + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/aot/MybatisRuntimeNativeRegistrar.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/aot/MybatisRuntimeNativeRegistrar.java new file mode 100644 index 00000000..47f81990 --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/aot/MybatisRuntimeNativeRegistrar.java @@ -0,0 +1,137 @@ +package com.mybatisflex.solon.aot; + +import com.mybatisflex.solon.integration.XPluginImpl; +import com.mybatisflex.solon.mybtais.MybatisAdapterDefault; +import org.apache.ibatis.cache.decorators.FifoCache; +import org.apache.ibatis.cache.decorators.LruCache; +import org.apache.ibatis.cache.decorators.SoftCache; +import org.apache.ibatis.cache.decorators.WeakCache; +import org.apache.ibatis.cache.impl.PerpetualCache; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.io.DefaultVFS; +import org.apache.ibatis.io.JBoss6VFS; +import org.apache.ibatis.javassist.util.proxy.ProxyFactory; +import org.apache.ibatis.javassist.util.proxy.RuntimeSupport; +import org.apache.ibatis.logging.Log; +import org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl; +import org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl; +import org.apache.ibatis.logging.log4j2.Log4j2Impl; +import org.apache.ibatis.logging.nologging.NoLoggingImpl; +import org.apache.ibatis.logging.slf4j.Slf4jImpl; +import org.apache.ibatis.logging.stdout.StdOutImpl; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.scripting.defaults.RawLanguageDriver; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.noear.solon.aot.NativeMetadataUtils; +import org.noear.solon.aot.RuntimeNativeMetadata; +import org.noear.solon.aot.RuntimeNativeRegistrar; +import org.noear.solon.aot.hint.ExecutableMode; +import org.noear.solon.aot.hint.MemberCategory; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.util.ResourceUtil; +import org.noear.solon.core.wrap.MethodWrap; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.TreeSet; +import java.util.stream.Stream; + +/** + * mybatis aot 注册 native 元数据 + * + * @author songyinyin + * @since 2.3 + * @link MyBatisNativeConfiguration + */ +public class MybatisRuntimeNativeRegistrar implements RuntimeNativeRegistrar { + + @Override + public void register(AppContext context, RuntimeNativeMetadata metadata) { + Stream.of(RawLanguageDriver.class, + XMLLanguageDriver.class, + RuntimeSupport.class, + ProxyFactory.class, + Slf4jImpl.class, + Log.class, + JakartaCommonsLoggingImpl.class, + Log4j2Impl.class, + Jdk14LoggingImpl.class, + StdOutImpl.class, + NoLoggingImpl.class, + SqlSessionFactory.class, + PerpetualCache.class, + FifoCache.class, + LruCache.class, + SoftCache.class, + WeakCache.class, + ArrayList.class, + HashMap.class, + TreeSet.class, + HashSet.class + ).forEach(x -> metadata.registerReflection(x, MemberCategory.values())); + + Stream.of( + "org/apache/ibatis/builder/xml/.*.dtd", + "org/apache/ibatis/builder/xml/.*.xsd" + ).forEach(metadata::registerResourceInclude); + + metadata.registerJdkProxy(Executor.class); + metadata.registerReflection(Executor.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + metadata.registerAllDeclaredMethod(Executor.class, ExecutableMode.INVOKE); + + metadata.registerJdkProxy(StatementHandler.class); + metadata.registerReflection(StatementHandler.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + metadata.registerAllDeclaredMethod(StatementHandler.class, ExecutableMode.INVOKE); + + metadata.registerReflection(BoundSql.class, MemberCategory.DECLARED_FIELDS, MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); + metadata.registerAllDeclaredMethod(BoundSql.class, ExecutableMode.INVOKE); + + metadata.registerReflection(Configuration.class, MemberCategory.DECLARED_FIELDS); + metadata.registerAllDeclaredMethod(Configuration.class, ExecutableMode.INVOKE); + + metadata.registerReflection(JBoss6VFS.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + metadata.registerReflection(DefaultVFS.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + + + registerMybatisAdapter(context, metadata, XPluginImpl.getAdapterFlex()); + + } + + protected void registerMybatisAdapter(AppContext context, RuntimeNativeMetadata metadata, MybatisAdapterDefault bean) { + if (bean == null) { + return; + } + + //注册 xml 资源 + for (String res : bean.getMappers()) { + if (res.startsWith(ResourceUtil.TAG_classpath)) { + res = res.substring(ResourceUtil.TAG_classpath.length()); + res = res.replace("**", "*"); + res = res.replace("*", ".*"); + metadata.registerResourceInclude(res); + } + } + + //注册 mapper 代理 + for (Class clz : bean.getConfiguration().getMapperRegistry().getMappers()) { + metadata.registerJdkProxy(clz); + metadata.registerReflection(clz, MemberCategory.INTROSPECT_PUBLIC_METHODS); + Method[] declaredMethods = clz.getDeclaredMethods(); + for (Method method : declaredMethods) { + MethodWrap methodWrap = context.methodGet(method); + NativeMetadataUtils.registerMethodAndParamAndReturnType(metadata, methodWrap); + } + } + + // 注册 entity + for (Class clz : bean.getConfiguration().getTypeAliasRegistry().getTypeAliases().values()) { + metadata.registerReflection(clz, MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + metadata.registerDefaultConstructor(clz); + } + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFactoryFlex.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFactoryFlex.java deleted file mode 100644 index 3b13caac..00000000 --- a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFactoryFlex.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) 2022-2025, Mybatis-Flex (fuhai999@gmail.com). - *

- * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - *

- * http://www.apache.org/licenses/LICENSE-2.0 - *

- * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.mybatisflex.solon.integration; - -import org.apache.ibatis.solon.MybatisAdapter; -import org.apache.ibatis.solon.MybatisAdapterFactory; -import org.noear.solon.core.BeanWrap; -import org.noear.solon.core.Props; - -/** - * MyBatis-Flex 适配器工厂。 - * - * @author noear - * @since 2.2 - */ -public class MybatisAdapterFactoryFlex implements MybatisAdapterFactory { - - @Override - public MybatisAdapter create(BeanWrap dsWrap) { - return new MybatisAdapterFlex(dsWrap); - } - - @Override - public MybatisAdapter create(BeanWrap dsWrap, Props dsProps) { - return new MybatisAdapterFlex(dsWrap, dsProps); - } - -} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFlex.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFlex.java index 03fa3a69..22d2c1a2 100644 --- a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFlex.java +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/MybatisAdapterFlex.java @@ -20,14 +20,15 @@ import com.mybatisflex.core.FlexGlobalConfig; import com.mybatisflex.core.mybatis.FlexConfiguration; import com.mybatisflex.core.mybatis.FlexSqlSessionFactoryBuilder; import com.mybatisflex.core.row.RowMapperInvoker; +import com.mybatisflex.solon.mybtais.MybatisAdapterDefault; import org.apache.ibatis.mapping.Environment; import org.apache.ibatis.session.SqlSessionFactory; -import org.apache.ibatis.solon.integration.MybatisAdapterDefault; import org.noear.solon.Utils; import org.noear.solon.core.BeanWrap; import org.noear.solon.core.Props; import org.noear.solon.core.VarHolder; import org.noear.solon.core.event.EventBus; +import org.noear.solon.core.util.ClassUtil; import javax.sql.DataSource; @@ -41,6 +42,7 @@ public class MybatisAdapterFlex extends MybatisAdapterDefault { private FlexSqlSessionFactoryBuilder factoryBuilderPlus; private FlexGlobalConfig globalConfig; private RowMapperInvoker rowMapperInvoker; + private Class typeAliasesBaseType; protected MybatisAdapterFlex(BeanWrap dsWrap) { super(dsWrap); @@ -70,6 +72,11 @@ public class MybatisAdapterFlex extends MybatisAdapterDefault { //for configuration section config = new FlexConfiguration(environment); + String typeAliasesBaseTypeStr = dsProps.get("typeAliasesSuperType"); + if (Utils.isNotEmpty(typeAliasesBaseTypeStr)) { + typeAliasesBaseType = ClassUtil.loadClass(typeAliasesBaseTypeStr); + } + Props cfgProps = dsProps.getProp("configuration"); if (cfgProps.size() > 0) { Utils.injectProperties(config, cfgProps); @@ -134,4 +141,29 @@ public class MybatisAdapterFlex extends MybatisAdapterDefault { varH.setValue(rowMapperInvoker); } } + + @Override + protected boolean isTypeAliasesType(Class type) { + //typeAliasesSuperType + if (typeAliasesBaseType == null) { + return true; + } else { + return typeAliasesBaseType.isAssignableFrom(type); + } + } + + @Override + protected boolean isTypeAliasesKey(String key) { + return super.isTypeAliasesKey(key) || key.startsWith("typeAliasesPackage["); + } + + @Override + protected boolean isTypeHandlersKey(String key) { + return super.isTypeHandlersKey(key) || key.startsWith("typeHandlersPackage["); + } + + @Override + protected boolean isMappersKey(String key) { + return super.isMappersKey(key) || key.startsWith("mapperLocations["); + } } diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/XPluginImpl.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/XPluginImpl.java index 6cebe6a4..2f339bde 100644 --- a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/XPluginImpl.java +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/integration/XPluginImpl.java @@ -16,9 +16,28 @@ package com.mybatisflex.solon.integration; -import org.apache.ibatis.solon.integration.MybatisAdapterManager; +import com.mybatisflex.annotation.UseDataSource; +import com.mybatisflex.core.FlexConsts; +import com.mybatisflex.core.FlexGlobalConfig; +import com.mybatisflex.core.MybatisFlexBootstrap; +import com.mybatisflex.core.datasource.DataSourceKey; +import com.mybatisflex.core.mybatis.FlexConfiguration; +import com.mybatisflex.core.row.RowMapperInvoker; +import com.mybatisflex.solon.aot.MybatisRuntimeNativeRegistrar; +import org.apache.ibatis.session.SqlSessionFactory; +import org.noear.solon.annotation.Inject; +import org.noear.solon.aot.RuntimeNativeRegistrar; import org.noear.solon.core.AppContext; +import org.noear.solon.core.BeanWrap; import org.noear.solon.core.Plugin; +import org.noear.solon.core.Props; +import org.noear.solon.core.runtime.NativeDetector; +import org.noear.solon.core.util.ClassUtil; +import org.noear.solon.core.util.TmplUtil; +import org.noear.solon.data.datasource.DsUtils; + +import javax.sql.DataSource; +import java.util.Map; /** * 配置 MyBatis-Flex 插件。 @@ -27,9 +46,105 @@ import org.noear.solon.core.Plugin; * @since 2.2 */ public class XPluginImpl implements Plugin { + private static final String CONFIG_PREFIX = "mybatisFlex"; + private static final String CONFIG_DS_PREFIX = "mybatisFlex.datasource"; + + private static MybatisAdapterFlex adapterFlex; + + public static MybatisAdapterFlex getAdapterFlex() { + return adapterFlex; + } + @Override public void start(AppContext context) throws Throwable { - // 此插件的 solon.plugin.priority 会大于 mybatis-solon-plugin 的值 - MybatisAdapterManager.setAdapterFactory(new MybatisAdapterFactoryFlex()); + // 注册动态数据源的事务路由 + //TranManager.routing(FlexDataSource.class, new FlexDataSourceRouting()); + + // 订阅数据源 + context.subWrapsOfType(DataSource.class, bw -> { + loadDs(context, bw); + }); + + // aot + if (NativeDetector.isAotRuntime() && ClassUtil.hasClass(() -> RuntimeNativeRegistrar.class)) { + context.wrapAndPut(MybatisRuntimeNativeRegistrar.class); + } + + + // 构建 mf 配置的数据源 + Class dsDefClz = ClassUtil.loadClass("com.zaxxer.hikari.HikariDataSource"); + Props dsProps = context.cfg().getProp(CONFIG_DS_PREFIX); + if (dsProps.size() > 0) { + Map dsMap = DsUtils.buildDsMap(dsProps, dsDefClz); + + for (Map.Entry entry : dsMap.entrySet()) { + String dsName = entry.getKey(); + DataSource ds = entry.getValue(); + BeanWrap bw = context.wrap(dsName, ds); + loadDs(context, bw); + } + } + } + + private void loadDs(AppContext context, BeanWrap bw) { + boolean isInit = MybatisFlexBootstrap.getInstance().getDataSource() == null; + MybatisFlexBootstrap.getInstance().addDataSource(bw.name(), bw.raw()); + + if (isInit) { + initDo(context); + } + } + + private void initDo(AppContext context) { + BeanWrap dsBw = context.wrap(FlexConsts.NAME, MybatisFlexBootstrap.getInstance().getDataSource(), true); + MybatisAdapterFlex dsFlex = new MybatisAdapterFlex(dsBw, context.cfg().getProp(CONFIG_PREFIX)); + dsFlex.mapperPublish(); + adapterFlex = dsFlex; + + // 注册到管理器(aot 时会用到) + //MybatisAdapterManager.register(dsBw, dsFlex); + + //绑定到容器 + context.beanInjectorAdd(Inject.class, FlexGlobalConfig.class, ((vh, anno) -> { + dsFlex.injectTo(vh); + })); + + + context.beanInjectorAdd(Inject.class, FlexConfiguration.class, ((vh, anno) -> { + dsFlex.injectTo(vh); + })); + + context.beanInjectorAdd(Inject.class, RowMapperInvoker.class, ((vh, anno) -> { + dsFlex.injectTo(vh); + })); + + context.beanInjectorAdd(Inject.class, SqlSessionFactory.class, ((vh, anno) -> { + dsFlex.injectTo(vh); + })); + + context.beanInterceptorAdd(UseDataSource.class, inv -> { + UseDataSource anno = inv.getMethodAnnotation(UseDataSource.class); + + if (anno == null) { + anno = inv.getTargetAnnotation(UseDataSource.class); + } + + if (anno == null) { + return inv.invoke(); + } else { + //备份 + String backup = DataSourceKey.get(); + + try { + String dsName = TmplUtil.parse(anno.value(), inv); + + DataSourceKey.use(dsName); + return inv.invoke(); + } finally { + //还原 + DataSourceKey.use(backup); + } + } + }); } } diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisAdapterDefault.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisAdapterDefault.java new file mode 100644 index 00000000..c5a13f6a --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisAdapterDefault.java @@ -0,0 +1,358 @@ +package com.mybatisflex.solon.mybtais; + +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.builder.xml.XMLMapperBuilder; +import org.apache.ibatis.executor.ErrorContext; +import org.apache.ibatis.io.Resources; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.type.TypeHandler; +import org.noear.solon.Solon; +import org.noear.solon.Utils; +import org.noear.solon.core.BeanWrap; +import org.noear.solon.core.LifecycleIndex; +import org.noear.solon.core.Props; +import org.noear.solon.core.VarHolder; +import org.noear.solon.core.event.EventBus; +import org.noear.solon.core.util.ResourceUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.InputStream; +import java.lang.reflect.Proxy; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Mybatis 适配器默认实现 + * + * @author noear + * @since 1.1 + */ +public class MybatisAdapterDefault { + protected static final Logger log = LoggerFactory.getLogger(MybatisAdapterDefault.class); + + protected final BeanWrap dsWrap; + protected final Props dsProps; + + //mapper 注解验证启用? + protected final boolean mapperVerifyEnabled; + + protected Configuration config; + protected SqlSessionFactory factory; + protected List mappers = new ArrayList<>(); + protected SqlSessionFactoryBuilder factoryBuilder; + + /** + * 构建Sql工厂适配器,使用默认的 typeAliases 和 mappers 配置 + */ + protected MybatisAdapterDefault(BeanWrap dsWrap) { + this(dsWrap, Solon.cfg().getProp("mybatis")); + } + + /** + * 构建Sql工厂适配器,使用属性配置 + */ + protected MybatisAdapterDefault(BeanWrap dsWrap, Props dsProps) { + this.dsWrap = dsWrap; + if (dsProps == null) { + this.dsProps = new Props(); + } else { + this.dsProps = dsProps; + } + + this.mapperVerifyEnabled = dsProps.getBool("configuration.mapperVerifyEnabled", false); + this.factoryBuilder = new SqlSessionFactoryBuilder(); + + DataSource dataSource = getDataSource(); + String dataSourceId = dsWrap.name(); + if (Utils.isEmpty(dataSourceId)) { + dataSourceId = "_main"; + } + + TransactionFactory tf = new SolonManagedTransactionFactory(); + Environment environment = new Environment(dataSourceId, tf, dataSource); + + initConfiguration(environment); + + //加载插件(通过Bean) + dsWrap.context().lifecycle(LifecycleIndex.PLUGIN_BEAN_USES, () -> { + dsWrap.context().beanForeach(bw -> { + if (bw.raw() instanceof Interceptor) { + config.addInterceptor(bw.raw()); + } + }); + }); + + //1.分发事件,推给扩展处理 + EventBus.publish(config); + + //2.初始化(顺序不能乱) + initDo(); + + dsWrap.context().getBeanAsync(SqlSessionFactoryBuilder.class, bean -> { + factoryBuilder = bean; + }); + } + + public List getMappers() { + return mappers; + } + + protected DataSource getDataSource() { + return dsWrap.raw(); + } + + protected void initConfiguration(Environment environment) { + config = new Configuration(environment); + + //for configuration section + Props cfgProps = dsProps.getProp("configuration"); + if (cfgProps.size() > 0) { + Utils.injectProperties(config, cfgProps); + } + } + + protected boolean isTypeAliasesType(Class type) { + return true; + } + + protected boolean isTypeAliasesKey(String key){ + return key.startsWith("typeAliases[") || key.equals("typeAliases"); + } + + protected boolean isTypeHandlersKey(String key){ + return key.startsWith("typeHandlers[") || key.equals("typeHandlers"); + } + + protected boolean isMappersKey(String key){ + return key.startsWith("mappers[") || key.equals("mappers"); + } + + protected void initDo() { + //for typeAliases & typeHandlers section + dsProps.forEach((k, v) -> { + if (k instanceof String && v instanceof String) { + String key = (String) k; + String valStr = (String) v; + + if (isTypeAliasesKey(key)) { + for (String val : valStr.split(",")) { + val = val.trim(); + if (val.length() == 0) { + continue; + } + + //package || type class,转为类表达式 + for (Class clz : ResourceUtil.scanClasses(dsWrap.context().getClassLoader(), val)) { + if (clz.isInterface() == false) { + if (isTypeAliasesType(clz)) { + getConfiguration().getTypeAliasRegistry().registerAlias(clz); + } + } + } + } + } + + if (isTypeHandlersKey(key)) { + for (String val : valStr.split(",")) { + val = val.trim(); + if (val.length() == 0) { + continue; + } + + //package || type class,转为类表达式 + for (Class clz : ResourceUtil.scanClasses(dsWrap.context().getClassLoader(), val)) { + if (TypeHandler.class.isAssignableFrom(clz)) { + getConfiguration().getTypeHandlerRegistry().register(clz); + } + } + } + } + } + }); + + //todo: 上面的完成后,才能做下面这个 + + //for mappers section + dsProps.forEach((k, v) -> { + if (k instanceof String && v instanceof String) { + String key = (String) k; + String valStr = (String) v; + + if (isMappersKey(key)) { + for (String val : valStr.split(",")) { + val = val.trim(); + if (val.length() == 0) { + continue; + } + + mappers.add(val); + + if (ResourceUtil.hasClasspath(val)) { + //mapper xml, 新方法,替代旧的 *.xml (基于表达式;更自由,更语义化) + for (String uri : ResourceUtil.scanResources(val)) { + addMapperByXml(uri); + } + + //todo: 兼容提醒: + compatibilityTipsOfXml(val); + } else { + //package || type class,转为类表达式 + for (Class clz : ResourceUtil.scanClasses(dsWrap.context().getClassLoader(), val)) { + if (clz.isInterface()) { + if (mapperVerifyEnabled) { + if (isMapper(clz)) { + getConfiguration().addMapper(clz); + } + } else { + getConfiguration().addMapper(clz); + } + } + } + } + } + } + } + }); + + if (mappers.size() == 0) { + if (Utils.isEmpty(dsWrap.name())) { + log.warn("Mybatis: Missing mappers configuration!"); + } else { + log.warn("Mybatis: Missing mappers configuration. name='{}'", dsWrap.name()); + } + //throw new IllegalStateException("Please add the mappers configuration!"); + } else { + //如果有配置,但是没有 mapper 注册成功;说明有问题了 + if (config.getMapperRegistry().getMappers().size() == 0) { + //log.warn("Mybatis: Missing mapper registration, please check the mappers configuration!"); + if (Utils.isEmpty(dsWrap.name())) { + throw new IllegalStateException("Missing mapper registration, please check the mappers configuration!"); + } else { + throw new IllegalStateException("Missing mapper registration, please check the mappers configuration. name='" + dsWrap.name() + "'"); + } + } + } + + //for plugins section + List interceptors = MybatisPluginUtils.resolve(dsProps, "plugins"); + for (Interceptor itp : interceptors) { + getConfiguration().addInterceptor(itp); + } + } + + protected boolean isMapper(Class clz) { + return clz.isAnnotationPresent(Mapper.class); + } + + /** + * 获取配置器 + */ + public Configuration getConfiguration() { + return config; + } + + /** + * 获取会话工厂 + */ + public SqlSessionFactory getFactory() { + if (factory == null) { + factory = factoryBuilder.build(getConfiguration());//new SqlSessionFactoryProxy(factoryBuilder.build(config)); + } + + return factory; + } + + Map, Object> mapperCached = new HashMap<>(); + + public T getMapper(Class mapperClz) { + Object mapper = mapperCached.get(mapperClz); + + if (mapper == null) { + synchronized (mapperClz) { + mapper = mapperCached.get(mapperClz); + if (mapper == null) { + MybatisMapperInterceptor handler = new MybatisMapperInterceptor(getFactory(), mapperClz); + + mapper = Proxy.newProxyInstance( + mapperClz.getClassLoader(), + new Class[]{mapperClz}, + handler); + mapperCached.put(mapperClz, mapper); + } + } + } + + return (T) mapper; + } + + public void injectTo(VarHolder vh) { + //@Db("db1") SqlSessionFactory factory; + if (SqlSessionFactory.class.isAssignableFrom(vh.getType())) { + vh.setValue(this.getFactory()); + return; + } + + //@Db("db1") Configuration cfg; + if (Configuration.class.isAssignableFrom(vh.getType())) { + vh.setValue(this.getConfiguration()); + return; + } + + //@Db("db1") UserMapper userMapper; + if (vh.getType().isInterface()) { + Object mapper = this.getMapper(vh.getType()); + + vh.setValue(mapper); + return; + } + } + + protected void addMapperByXml(String uri) { + try { + // resource 配置方式 + ErrorContext.instance().resource(uri); + + //读取mapper文件 + InputStream stream = Resources.getResourceAsStream(uri); + + //mapper映射文件都是通过XMLMapperBuilder解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(stream, getConfiguration(), uri, getConfiguration().getSqlFragments()); + mapperParser.parse(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void compatibilityTipsOfXml(String val) { + //todo: 兼容提醒: + //if (val.endsWith("*.xml") && val.indexOf("*") == val.indexOf("*.xml")) { + //@Deprecated //弃用提示 + // log.warn("Mybatis-新文件表达式提示:'" + val + "' 不包括深度子目录;如有需要可增加'/**/'段"); + //} + } + + + public void mapperPublish() { + for (Class clz : getConfiguration().getMapperRegistry().getMappers()) { + mapperPublishDo(clz); + } + } + + private void mapperPublishDo(Class clz) { + if (clz != null && clz.isInterface()) { + Object mapper = getMapper(clz); + + //进入容器,用于 @Inject 注入 + dsWrap.context().wrapAndPut(clz, mapper); + } + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisMapperInterceptor.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisMapperInterceptor.java new file mode 100644 index 00000000..1daad1cb --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisMapperInterceptor.java @@ -0,0 +1,31 @@ +package com.mybatisflex.solon.mybtais; + +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +/** + * Mybatis Mapper Interceptor + * + * @author noear + * @since 1.6 + */ +public class MybatisMapperInterceptor implements InvocationHandler { + private SqlSessionFactory factory; + private Class mapperClz; + + public MybatisMapperInterceptor(SqlSessionFactory factory, Class mapperClz) { + this.factory = factory; + this.mapperClz = mapperClz; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try (SqlSession session = factory.openSession(true)) { + Object mapper = session.getMapper(mapperClz); + return method.invoke(mapper, args); + } + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisPluginUtils.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisPluginUtils.java new file mode 100644 index 00000000..72999635 --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/MybatisPluginUtils.java @@ -0,0 +1,60 @@ +package com.mybatisflex.solon.mybtais; + +import org.apache.ibatis.plugin.Interceptor; +import org.noear.solon.core.Props; +import org.noear.solon.core.util.ClassUtil; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * 插件解析工具 + * + * @author noear + * @since 1.10 + */ +public class MybatisPluginUtils { + /** + * 解析 + * + * @param prefix 配置前缀 + */ + public static List resolve(Props configRoot, String prefix) { + List interceptors = new ArrayList<>(); + + int index = 0; + while (true) { + Props props = configRoot.getProp(prefix + "[" + index + "]"); + if (props.size() == 0) { + break; + } else { + index++; + + String name = null; + for (Map.Entry kv : props.entrySet()) { + if (kv.getKey() instanceof String) { + String key = (String) kv.getKey(); + if (key.endsWith(".class")) { + name = key.split("\\.")[0]; + } + } + } + + if (name != null) { + props = props.getProp(name); + Interceptor plugin = ClassUtil.tryInstance(props.get("class")); + if (plugin == null) { + throw new IllegalArgumentException("Mybatis plugin [" + name + "].class load failed"); + } + props.remove("class"); + + plugin.setProperties(props); + interceptors.add(plugin); + } + } + } + + return interceptors; + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransaction.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransaction.java new file mode 100644 index 00000000..ba41edc0 --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransaction.java @@ -0,0 +1,62 @@ +package com.mybatisflex.solon.mybtais; + +import com.mybatisflex.core.datasource.FlexDataSourceRouting; +import org.apache.ibatis.transaction.Transaction; +import org.noear.solon.data.tran.TranUtils; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.sql.SQLException; + +/** + * @author noear + * @since 1.6 + */ +public class SolonManagedTransaction implements Transaction { + private DataSource dataSource; + private Connection connection; + + public SolonManagedTransaction(DataSource dataSource) { + this.dataSource = dataSource; + } + + @Override + public Connection getConnection() throws SQLException { + if (connection == null) { + DataSource selected = FlexDataSourceRouting.determineCurrentTarget(dataSource); + connection = TranUtils.getConnectionProxy(selected); + } + + return connection; + } + + @Override + public void commit() throws SQLException { + if (connection != null) { + connection.commit(); + } + } + + @Override + public void rollback() throws SQLException { + if (connection != null) { + connection.rollback(); + } + } + + @Override + public void close() throws SQLException { + if (connection != null) { + connection.close(); + } + } + + @Override + public Integer getTimeout() throws SQLException { + if (connection != null) { + return connection.getNetworkTimeout(); + } else { + return null; + } + } +} diff --git a/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransactionFactory.java b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransactionFactory.java new file mode 100644 index 00000000..6a37b3c9 --- /dev/null +++ b/mybatis-flex-solon-plugin/src/main/java/com/mybatisflex/solon/mybtais/SolonManagedTransactionFactory.java @@ -0,0 +1,30 @@ +package com.mybatisflex.solon.mybtais; + +import org.apache.ibatis.session.TransactionIsolationLevel; +import org.apache.ibatis.transaction.Transaction; +import org.apache.ibatis.transaction.TransactionFactory; + +import javax.sql.DataSource; +import java.sql.Connection; +import java.util.Properties; + +/** + * @author noear + * @since 1.6 + */ +public class SolonManagedTransactionFactory implements TransactionFactory { + @Override + public void setProperties(Properties properties) { + + } + + @Override + public Transaction newTransaction(Connection connection) { + throw new UnsupportedOperationException("New Solon transactions require a DataSource"); + } + + @Override + public Transaction newTransaction(DataSource dataSource, TransactionIsolationLevel transactionIsolationLevel, boolean b) { + return new SolonManagedTransaction(dataSource); + } +} diff --git a/pom.xml b/pom.xml index 0a7deaf7..311fd2f3 100644 --- a/pom.xml +++ b/pom.xml @@ -76,7 +76,7 @@ 5.3.27 2.7.11 - 2.9.3 + 3.0.1 4.13.2 1.19.3