mybatis插件机制源码解析
引言
本篇源码解析基于MyBatis3.5.8版本。源码
首先需要说明的封装是,本篇文章不是插件mybatis插件开发的教程,而是编程从源码层面分析mybatis是如何支持用户自定义插件开发的。
mybatis的教学插件机制,让其扩展能力大大增加。源码造梦西游源码比如我们项目中经常用到的封装PageHelper,这就是插件一款基于mybatis插件能力开发的产品,它的编程功能是让基于mybatis的数据库分页查询更容易使用。
当然基于插件我们还可以开发其它功能,教学比如在执行sql前打印日志、源码做权限控制等。封装
正文mybatis插件也叫mybatis拦截器,插件它支持从方法级别对mybatis进行拦截。编程整体架构图如下:
解释下几个相关概念:
Interceptor拦截器接口,教学用户自定义的拦截器就是实现该接口。
InterceptorChain拦截器链,其内部维护一个interceptorslist,表示拦截器链中所有的拦截器,并提供增加或获取拦截器链的方法。比如有个核心的方法是pluginAll。该方法用来生成代理对象。
Invocation拦截器执行时的上下文环境,其实就是geotools源码目标方法的调用信息,包含目标对象、调用的方法信息、参数信息。核心方法是proceed。该方法的主要目的就是进行处理链的传播,执行完拦截器的方法后,最终需要调用目标方法的invoke方法。
mybatis支持在哪些地方进行拦截呢?你只需要在代码里搜索interceptorChain.pluginAll的使用位置就可以获取答案,一共有四处:
parameterHandler=(ParameterHandler)interceptorChain.pluginAll(parameterHandler);resultSetHandler=(ResultSetHandler)interceptorChain.pluginAll(resultSetHandler);statementHandler=(StatementHandler)interceptorChain.pluginAll(statementHandler);executor=(Executor)interceptorChain.pluginAll(executor);这四处实现的原理都是一样的,我们只需要选择一个进行分析就可以了。
我们先来看下自定义的插件是如何加载进来的,比如我们使用PageHelper插件,通常会在mybatis-config.xml中加入如下的配置:
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>mybatis在创建SqlSessionFactory的时候会加载配置文件,
publicConfigurationparse(){ if(parsed){ thrownewBuilderException("EachXMLConfigBuildercanonlybeusedonce.");}parsed=true;parseConfiguration(parser.evalNode("/configuration"));returnconfiguration;}parseConfiguration方法会加载包括plugins在内的很多配置,
privatevoidparseConfiguration(XNoderoot){ try{ ...pluginElement(root.evalNode("plugins"));...}catch(Exceptione){ thrownewBuilderException("ErrorparsingSQLMapperConfiguration.Cause:"+e,e);}}privatevoidpluginElement(XNodeparent)throwsException{ if(parent!=null){ for(XNodechild:parent.getChildren()){ Stringinterceptor=child.getStringAttribute("interceptor");Propertiesproperties=child.getChildrenAsProperties();InterceptorinterceptorInstance=(Interceptor)resolveClass(interceptor).getDeclaredConstructor().newInstance();interceptorInstance.setProperties(properties);configuration.addInterceptor(interceptorInstance);}}}pluginElement干了几件事情:
创建Interceptor实例
设置实例的属性变量
添加到Configuration的interceptorChain拦截器链中
mybatis的插件是通过动态代理实现的,那肯定要生成代理对象,生成的逻辑就是前面提到的pluginAll方法,比如对于Executor生成代理对象就是,
executor=(Executor)interceptorChain.pluginAll(executor);接着看pluginAll方法,
/***该方法会遍历用户定义的插件实现类(Interceptor),并调用Interceptor的plugin方法,对target进行插件化处理,backtrace 源码*即我们在实现自定义的Interceptor方法时,在plugin中需要根据自己的逻辑,对目标对象进行包装(代理),创建代理对象,*那我们就可以在该方法中使用Plugin#wrap来创建代理类。*/publicObjectpluginAll(Objecttarget){ for(Interceptorinterceptor:interceptors){ target=interceptor.plugin(target);}returntarget;}这里遍历所有我们定义的拦截器,调用拦截器的plugin方法生成代理对象。有人可能有疑问:如果有多个拦截器,target不是被覆盖了吗?
其实不会,所以如果有多个拦截器的话,生成的代理对象会被另一个代理对象代理,从而形成一个代理链条,执行的时候,依次执行所有拦截器的拦截逻辑代码。
plugin方法是接口Interceptor的默认实现类,
defaultObjectplugin(Objecttarget){ returnPlugin.wrap(target,this);}然后进入org.apache.ibatis.plugin.Plugin#wrap,
publicstaticObjectwrap(Objecttarget,Interceptorinterceptor){ Map<Class<?>,Set<Method>>signatureMap=getSignatureMap(interceptor);Class<?>type=target.getClass();Class<?>[]interfaces=getAllInterfaces(type,signatureMap);if(interfaces.length>0){ returnProxy.newProxyInstance(type.getClassLoader(),interfaces,newPlugin(target,interceptor,signatureMap));}returntarget;}首先是获取我们自己实现的Interceptor的方法签名映射表。然后获取需要代理的对象的Class上声明的所有接口。比如如果我们wrap的是Executor,就是Executor的所有接口。然后就是最关键的一步,用Proxy类创建一个代理对象(newProxyInstance)。vmware 源码
注意,newProxyInstance方法的第三个参数,接收的是一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。
我们这里传入的是Plugin类,故在动态运行过程中会执行Plugin的invoker方法。
如果对这一段不是很理解,建议先了解下java动态代理的原理。java动态代理机制中有两个重要的角色:InvocationHandler(接口)和Proxy(类),这个是背景知识需要掌握的。
我们在深入看下上面的getSignatureMap方法,
privatestaticMap<Class<?>,Set<Method>>getSignatureMap(Interceptorinterceptor){ //从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解InterceptsinterceptsAnnotation=interceptor.getClass().getAnnotation(Intercepts.class);//issue#if(interceptsAnnotation==null){ thrownewPluginException("No@Interceptsannotationwasfoundininterceptor"+interceptor.getClass().getName());}Signature[]sigs=interceptsAnnotation.value();Map<Class<?>,Set<Method>>signatureMap=newHashMap<>();//解析Interceptor的values属性(Signature[])数组,存入HashMap,Set<Method>>for(Signaturesig:sigs){ Set<Method>methods=MapUtil.computeIfAbsent(signatureMap,sig.type(),k->newHashSet<>());try{ Methodmethod=sig.type().getMethod(sig.method(),sig.args());methods.add(method);}catch(NoSuchMethodExceptione){ thrownewPluginException("Couldnotfindmethodon"+sig.type()+"named"+sig.method()+".Cause:"+e,e);}}returnsignatureMap;}首先需要从Interceptor的类上获取Intercepts注解,说明我们自定义拦截器需要带注解,比如PageHelper插件的定义如下:
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>0所以我们可以知道,getSignatureMap其实就是拿到我们自定义拦截器声明需要拦截的类以及类对应的方法。
前面说过,当我们调用代理对象时,最终会执行Plugin类的invoker方法,我们看下Plugin的webgis源码invoker方法,
<plugins><plugininterceptor="com.github.pagehelper.PageInterceptor"><!--configparamsasthefollowing--><propertyname="param1"value="value1"/></plugin></plugins>1Interceptor接口的intercept方法就是我们自定义拦截器需要实现的逻辑,其参数为Invocation,可从Invocation参数中拿到执行方法的对象,方法,方法参数,比如我们可以从statementHandler拿到SQL语句,实现自己的特殊逻辑。
在该方法的结束需要调用invocation#proceed()方法,进行拦截器链的传播。
参考:
blogs.com/chenpi/p/.html
pytest系列——pluggy插件源码解读(二)PluginManager类实例化
插件管理器类简介
在插件系统中,插件管理器类扮演着核心角色。通过装饰器,我们定义了`HookspecMarker`和`HookimplMarker`,为函数添加了`{ project_name}_spec`和`{ project_name}_impl`属性,从而在实际应用中,函数将被当作接口和实现。
`HookspecMarker`类定义了接口(方法),而`HookimplMarker`类实现了这些接口。`Spec类`类似于定义接口,它允许我们预设一系列方法,并通过`@hookspec`注解将其指定为插件接口。
定义插件类时,我们将实现`Spec类`中定义的方法,具体功能由插件实现。通过`@hookimpl`装饰器,接口的实现与接口相绑定。
插件管理器类(`PluginManager`)是插件系统中的中枢大脑,负责管理插件的加载、执行等操作。在`manager.py`文件中定义。
初始化函数的源码揭示了`PluginManager`类的核心初始化逻辑,包括创建变量和实例化其他类。其中,`_HookRelay`和`_multicall`类的代码用于处理钩子调用和执行,`_multicall`中的`_inner_hookexec`属性是关键。
至此,`PluginManager`类的实例化过程完成。这一过程是插件系统中至关重要的一步,它确保了插件的正确加载和执行。
kettle 源码 怎么运行repositories插件
1.2. 编译源码
项目加载eclipse
kettle项目拷贝eclipseworkspace目录eclipse新建java project项目名称拷贝kettle文件夹名称致
项目导入eclipse现错误图文件源码全部注释掉
编译
打build.xml, 右边Outline 点击kettle-run as -ant build
第编译候需要网载几文件放C:\Documents and Settings\Administrator\.subfloor网络载比较慢直接文件放C:\Documents and Settings\Administrator\编译完bin目录.bat文件拷贝Kettle目录点击Spoon.bat运行运行功代表编译已近通
用源码运行Spoon
Kettle源码工程本身能linux位机器调试swt配置linux库所运行源码前需要修改winswt步骤:工程à属性àJava Build Pathàlibrariesàadd jars
linuxSWT库删除
打src-uiàorg.pentaho.di.ui.spoonàSpoon.java Run As àjava application
二.源码析
2.1. 修改kettle界面
修改初始化界面
打package org.pentaho.di.ui.spoonSpoon.Java找main函数该main函数Spoon工具入口找语句
Splash splash = new Splash(display);
该语句spoon初始化显示界面跳定义Splash.java面函数
canvas.addPaintListener(new PaintListener() {
publicvoid paintControl(PaintEvent e) {
String versionText = BaseMessages.getString(PKG, "SplashDialog.Version") + " " + Const.VERSION; //$NON-NLS-1$ //$NON-NLS-2$
StringBuilder sb = new StringBuilder();
String line = null;
try {
BufferedReader reader = new BufferedReader(newInputStreamReader(Splash.class.getClassLoader().getResourceAsStream("org/pentaho/di/ui/core/dialog/license/license.txt")));//$NON-NLS-1$
while((line = reader.readLine()) != null) {
sb.append(line + System.getProperty("line.separator")); //$NON-NLS-1$
}
} catch (Exception ex) {
sb.append(""); //$NON-NLS-1$
Log.warn(BaseMessages.getString(PKG, "SplashDialog.LicenseTextNotFound")); //$NON-NLS-1$
}
String licenseText = sb.toString();
e.gc.drawImage(kettle_image, 0, 0);
// If this is a Milestone or RC release, warn the user
if (Const.RELEASE.equals(Const.ReleaseType.MILESTONE)) {
versionText = BaseMessages.getString(PKG, "SplashDialog.DeveloperRelease") + " - " + versionText; //$NON-NLS-1$ //$NON-NLS-2$
drawVersionWarning(e);
} elseif (Const.RELEASE.equals(Const.ReleaseType.RELEASE_CANDIDATE)) {
versionText = BaseMessages.getString(PKG, "SplashDialog.ReleaseCandidate") + " - " + versionText; //$NON-NLS-1$//$NON-NLS-2$
}
elseif (Const.RELEASE.equals(Const.ReleaseType.PREVIEW)) {
versionText = BaseMessages.getString(PKG, "SplashDialog.PreviewRelease") + " - " + versionText; //$NON-NLS-1$//$NON-NLS-2$
}
elseif (Const.RELEASE.equals(Const.ReleaseType.GA)) {
versionText = BaseMessages.getString(PKG, "SplashDialog.GA") + " - " + versionText; //$NON-NLS-1$//$NON-NLS-2$
}
Font verFont = new Font(e.display, "Helvetica", , SWT.BOLD); //$NON-NLS-1$
e.gc.setFont(verFont);
e.gc.drawText(versionText, , , true);
// try using the desired font size for the license text
int fontSize = 8;
Font licFont = new Font(e.display, "Helvetica", fontSize, SWT.NORMAL); //$NON-NLS-1$
e.gc.setFont(licFont);
// if the text will not fit the allowed space
while (!willLicenseTextFit(licenseText, e.gc)) {
fontSize--;
licFont = new Font(e.display, "Helvetica", fontSize, SWT.NORMAL); //$NON-NLS-1$
e.gc.setFont(licFont);
}
e.gc.drawText(licenseText, , , true);
}
});
1. 修改背景
找ui/image/面kettle_splash.png替换该
2. 修改版本信息
找e.gc.drawText(versionText, , , true); 改e.gc.drawText("海康威视数据交换平台V1.0", , , true);
3. 修改面描述性文字
找e.gc.drawText(licenseText, , , true);改e.gc.drawText("作者:海康", , , true);
4. 预览效
Vue封装组件并发布到npm仓库
使用Vue框架进行开发时,组件封装是一个非常常规的操作。封装好的组件可以在项目的任意地方使用,甚至可以直接从npm仓库下载别人封装好的组件进行使用,比如iview、element-ui等组件库。然而,每个公司的业务场景可能不同,开发人员仍需要封装自己的组件。如果换了新项目,只能复制组件代码到新项目中,这样略显繁琐。其实可以将组件上传到npm仓库,需要时可以直接从npm安装使用。 封装Vue组件的好处主要有两点:环境准备
由于此次封装的是Vue组件,因此直接在Vue脚手架项目中进行封装即可。初始化Vue项目
运行项目
组件封装
步骤1:新建package文件夹在src下面新建一个package文件夹,存放所有需要上传的组件。
打算封装两个组件:pig-button、pig-input,分别在package文件夹下新建存放两个组件代码的文件夹。
步骤2:编写组件代码以pig-button组件为例,编写代码,然后引用到App.vue组件验证组件可用性。
最终效果显示组件可用。
步骤3:使用Vue插件模式封装组件的关键步骤,使用Vue提供的install方法,使得插件在使用Vue.use(plugin)时被调用,实现全局注册。
在package目录下新建index.js文件,配置组件的全局注册。
组件打包
完成组件封装后,需要进行打包。
修改package.json文件,配置打包命令。
执行打包命令,生成打包后的文件。
发布到npm
初始化package.json文件。
注册npm账号,切换npm源。
添加npm用户。
在pig-ui目录下执行发布npm命令。
发布成功后,可在npm官网查看发布的npm包。
从npm安装使用
执行安装命令。
在main.js中引用注册。
修改App.vue文件,直接使用组件。
总结起来,Vue组件封装发布到npm仓库的整体难度不大,关键在于理解Vue的install方法以及打包相关知识。最重要的是如何封装一个适用范围广、扩展性高的公用组件。此项目源码已公开。UMI3源码解析系列之构建原理
基于前面umi插件机制的原理可以了解到,umi是一个插件化的企业级前端框架,它配备了完善的插件体系,这也使得umi具有很好的可扩展性。umi的全部功能都是由插件完成的,构建功能同样是以插件的形式完成的。下面将从以下两个方面来了解umi的构建原理。UMI命令注册想了解umi命令的注册流程,咱们就从umi生成的项目入手。
从umi初始化的项目package.json文件看,umi执行dev命令,实际执行的是start:dev,而start:dev最终执行的是umidev。
"scripts":{ "dev":"npmrunstart:dev","start:dev":"cross-envREACT_APP_ENV=devMOCK=noneUMI_ENV=devumidev"}根据这里的umi命令,我们找到node_modules里的umi文件夹,看下umi文件夹下的package.json文件:
"name":"umi","bin":{ "umi":"bin/umi.js"}可以看到,这里就是定义umi命令的地方,而umi命令执行的脚本就在bin/umi.js里。接下来咱们看看bin/umi.js都做了什么。
#!/usr/bin/envnoderequire('v8-compile-cache');constresolveCwd=require('@umijs/deps/compiled/resolve-cwd');const{ name,bin}=require('../package.json');constlocalCLI=resolveCwd.silent(`${ name}/${ bin['umi']}`);if(!process.env.USE_GLOBAL_UMI&&localCLI&&localCLI!==__filename){ constdebug=require('@umijs/utils').createDebug('umi:cli');debug('Usinglocalinstallofumi');require(localCLI);}else{ require('../lib/cli');}判断当前是否执行的是本地脚手架,若是,则引入本地脚手架文件,否则引入lib/cli。在这里,我们未开启本地脚手架指令,所以是引用的lib/cli。
//获取进程的版本号constv=process.version;//通过yParser工具对命令行参数进行处理,此处是将version和help进行了简写constargs=yParser(process.argv.slice(2),{ alias:{ version:['v'],help:['h'],},boolean:['version'],});//若参数中有version值,并且args._[0]为空,此时将version字段赋值给args._[0]if(args.version&&!args._[0]){ args._[0]='version';constlocal=existsSync(join(__dirname,'../.local'))?chalk.cyan('@local'):'';console.log(`umi@${ require('../package.json').version}${ local}`);//若参数中无version值,并且args._[0]为空,此时将help字段复制给args._[0]}elseif(!args._[0]){ args._[0]='help';}处理完version和help后,紧接着会执行一段自执行代码:
(async()=>{ try{ //读取args._中第一个参数值switch(args._[0]){ case'dev'://若当前运行环境是dev,则调用Node.js的核心模块child_process的fork方法衍生一个新的Node.js进程。scriptPath表示要在子进程中运行的模块,这里引用的是forkedDev.ts文件。constchild=fork({ scriptPath:require.resolve('./forkedDev'),});//ref:///api/process/signal_events.html///post/2024-12-23 06:55
2024-12-23 06:35
2024-12-23 06:31
2024-12-23 06:19
2024-12-23 06:05