Mybatis插件

文章目录

  • 1. 如何自定义插件
    • 1.1 创建接口Interceptor的实现类
    • 1.2 配置拦截器
    • 1.3 运行程序
  • 2. 插件原理
    • 2.1 解析过程
    • 2.2 创建代理对象
      • 2.2.1 Executor
      • 2.2.2 StatementHandler
      • 2.2. 3ParameterHandler
      • 2.2.4 ResultSetHandler
    • 2.3 执行流程
    • 2.4 多拦截器的执行顺序
  • 3. PageHelper
    • 3.1 配置和代码
    • 3.2 原理解析
  • 4. 拦截器应用场景

1. 如何自定义插件

1.1 创建接口Interceptor的实现类

/*** @author Clinton Begin*/
public interface Interceptor {// 执行拦截逻辑的方法Object intercept(Invocation invocation) throws Throwable;// 决定是否触发 intercept()方法default Object plugin(Object target) {return Plugin.wrap(target, this);}// 根据配置 初始化 Intercept 对象default void setProperties(Properties properties) {// NOP}}

mybatis运行拦截的内容包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

定义一个实现类。


/*** MyBatis中的自定义的拦截器** @Signature 表示一个方法签名,唯一确定一个方法*/
@Intercepts({@Signature(type = Executor.class, // 拦截类型method = "query", // 拦截方法// args 中指定 被拦截方法的 参数列表args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = Executor.class,method = "close",args = {boolean.class})})
public class MyInterceptor implements Interceptor {private String interceptorName;public String getInterceptorName() {return interceptorName;}/*** 执行拦截的方法*/@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("------MyInterceptor  before---------");Object proceed = invocation.proceed();System.out.println("------MyInterceptor  after---------");return proceed;}@Overridepublic Object plugin(Object target) {return Interceptor.super.plugin(target);}@Overridepublic void setProperties(Properties properties) {System.out.println("setProperties : " + properties.getProperty("interceptorName"));this.interceptorName = properties.getProperty("interceptorName");}
}

1.2 配置拦截器

 <plugins><plugin interceptor="com.boge.interceptor.MyInterceptor"><property name="interceptorName" value="myInterceptor"/></plugin></plugins>

1.3 运行程序

    @Testpublic void test2() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库UserMapper mapper = sqlSession.getMapper(UserMapper.class);Integer param = 1;User user = mapper.selectUserById(param);System.out.println(user);
}

拦截的query方法和close方法的源码位置在如下:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 插件原理

2.1 解析过程

解析全局配置文件过程中,查看XMLConfigBuilder类的方法parseConfiguration。
在这里插入图片描述

private void pluginElement(XNode parent) throws Exception {if (parent != null) {for (XNode child : parent.getChildren()) {// 获取<plugin> 节点的 interceptor 属性的值String interceptor = child.getStringAttribute("interceptor");// 获取<plugin> 下的所有的properties子节点Properties properties = child.getChildrenAsProperties();// 获取 Interceptor 对象Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();// 设置 interceptor的 属性interceptorInstance.setProperties(properties);// Configuration中记录 Interceptorconfiguration.addInterceptor(interceptorInstance);}}}

该方法主要创建Interceptor 对象,并设置属性,最终放在configuration对象的InterceptorChain里面
在这里插入图片描述
在这里插入图片描述

来看InterceptorChain的源码。

/*** InterceptorChain 记录所有的拦截器* @author Clinton Begin*/
public class InterceptorChain {// 保存所有的 Interceptor  也就我所有的插件是保存在 Interceptors 这个List集合中的private final List<Interceptor> interceptors = new ArrayList<>();// 现在我们定义的有一个 Interceptor MyInterceptorpublic Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) { // 获取拦截器链中的所有拦截器target = interceptor.plugin(target); // 创建对应的拦截器的代理对象}return target;}public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}}

可以看到拦截器放在这个list变量interceptors 。

2.2 创建代理对象

2.1步骤创建了拦截器,并且保存在InterceptorChain,**那拦截器如何与目标对象关联?**拦截器拦截对象包括:Executor,ParameterHandler,ResultSetHandler,StatementHandler. 这些对象创建的时候需要注意什么?

2.2.1 Executor

在创建SqlSession的过程中,会创建执行器Executor。可以看到Executor植入插件

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {// 获取执行器的类型executorType = executorType == null ? defaultExecutorType : executorType;executorType = executorType == null ? ExecutorType.SIMPLE : executorType;Executor executor;// 根据对应的类型创建执行器if (ExecutorType.BATCH == executorType) {executor = new BatchExecutor(this, transaction);} else if (ExecutorType.REUSE == executorType) { // 针对 Statement 对象做缓存executor = new ReuseExecutor(this, transaction);} else {// 默认 SimpleExecutor 每一次只是SQL操作都创建一个新的Statement对象executor = new SimpleExecutor(this, transaction);}// 二级缓存开关,settings 中的 cacheEnabled 默认是 true// 映射文件中 <cache> 标签 --> 创建 Cache对象// settings 中的 cacheEnabled = true 真正的对 Executor 做了缓存的增强if (cacheEnabled) {// 穿衣服的事情 --> 装饰器模式executor = new CachingExecutor(executor);}// 植入插件的逻辑,至此,四大对象已经全部拦截完毕executor = (Executor) interceptorChain.pluginAll(executor);return executor;}

在这里插入图片描述

进入pluginAll方法:

 // 现在我们定义的有一个 Interceptor MyInterceptorpublic Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) { // 获取拦截器链中的所有拦截器target = interceptor.plugin(target); // 创建对应的拦截器的代理对象}return target;}

再进入plugin方法

在这里插入图片描述
在这里插入图片描述

再查看Plugin工具类的实现 wrap方法。

/*** 创建目标对象的代理对象*    目标对象 Executor  ParameterHandler  ResultSetHandler StatementHandler* @param target 目标对象* @param interceptor 拦截器* @return*/public static Object wrap(Object target, Interceptor interceptor) {// 获取用户自定义 Interceptor中@Signature注解的信息// getSignatureMap 负责处理@Signature 注解  interceptor 自定义的拦截器Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 获取目标类型Class<?> type = target.getClass();// 获取目标类型 实现的所有的接口Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// 如果目标类型有实现的接口 就创建代理对象if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}// 否则原封不动的返回目标对象return target;}

getSignatureMap方法
在这里插入图片描述
在这里插入图片描述
再来看Plugin的源码。


/*** @author Clinton Begin*/
public class Plugin implements InvocationHandler {private final Object target; // 目标对象private final Interceptor interceptor; // 拦截器private final Map<Class<?>, Set<Method>> signatureMap; // 记录 @Signature 注解的信息private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {this.target = target;this.interceptor = interceptor;this.signatureMap = signatureMap;}/*** 创建目标对象的代理对象*    目标对象 Executor  ParameterHandler  ResultSetHandler StatementHandler* @param target 目标对象* @param interceptor 拦截器* @return*/public static Object wrap(Object target, Interceptor interceptor) {// 获取用户自定义 Interceptor中@Signature注解的信息// getSignatureMap 负责处理@Signature 注解  interceptor 自定义的拦截器Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);// 获取目标类型Class<?> type = target.getClass();// 获取目标类型 实现的所有的接口Class<?>[] interfaces = getAllInterfaces(type, signatureMap);// 如果目标类型有实现的接口 就创建代理对象if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));}// 否则原封不动的返回目标对象return target;}/*** 代理对象方法被调用时执行的代码* @param proxy* @param method* @param args* @return* @throws Throwable*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {try {// 获取当前方法所在类或接口中,可被当前Interceptor拦截的方法 Executor querySet<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 当前调用的方法需要被拦截 执行拦截操作return interceptor.intercept(new Invocation(target, method, args));}// 不需要拦截 则调用 目标对象中的方法return method.invoke(target, args);} catch (Exception e) {throw ExceptionUtil.unwrapThrowable(e);}}/*** 获取拦截器中的 @Intercepts 注解中的相关内容* @param interceptor* @return*/private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {// 获取 @Intercepts 注解Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);// issue #251if (interceptsAnnotation == null) {throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());}// 获取 @Signature 注解中的内Signature[] sigs = interceptsAnnotation.value();Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();for (Signature sig : sigs) {Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());try {Method method = sig.type().getMethod(sig.method(), sig.args());methods.add(method);} catch (NoSuchMethodException e) {throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);}}return signatureMap;}private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {Set<Class<?>> interfaces = new HashSet<>();while (type != null) {for (Class<?> c : type.getInterfaces()) {// 判断 目标对象的 接口类型是否在 @Signature 注解中声明的有if (signatureMap.containsKey(c)) {interfaces.add(c);}}// 继续获取父类type = type.getSuperclass();}return interfaces.toArray(new Class<?>[interfaces.size()]);}}

2.2.2 StatementHandler

在这里插入图片描述
在这里插入图片描述

2.2. 3ParameterHandler

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2.2.4 ResultSetHandler

在这里插入图片描述
在这里插入图片描述

2.3 执行流程

以Executor的query方法为例,实际执行的是代理对象。

在这里插入图片描述

然后会执行Plugin的invoke方法。
在这里插入图片描述
然后进入interceptor.intercept,进入自定义拦截器
在这里插入图片描述

2.4 多拦截器的执行顺序

在这里插入图片描述

总结:

对象作用
Interceptor自定义插件需要实现接口,实现4个方法
InterceptChain配置的插件解析后会保存在Configuration的InterceptChain中
Plugin触发管理类,还可以用来创建代理对象
Invocation对被代理类进行包装,可以调用proceed()调用到被拦截的方法

3. PageHelper

3.1 配置和代码

<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>4.1.6</version>
</dependency>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageHelper"><property name="dialect" value="mysql" /><!-- 该参数默认为false --><!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 --><!-- 和startPage中的pageNum效果一样 --><property name="offsetAsPageNum" value="true" /><!-- 该参数默认为false --><!-- 设置为true时,使用RowBounds分页会进行count查询 --><property name="rowBoundsWithCount" value="true" /><!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 --><!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型) --><property name="pageSizeZero" value="true" /><!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 --><!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 --><!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 --><property name="reasonable" value="false" /><!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 --><!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 --><!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,不配置映射的用默认值 --><!-- 不理解该含义的前提下,不要随便复制该配置 --><property name="params" value="pageNum=start;pageSize=limit;" /><!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page --><property name="returnPageInfo" value="check" />
</plugin>

代码:

 @Testpublic void test5() throws Exception{// 1.获取配置文件InputStream in = Resources.getResourceAsStream("mybatis-config.xml");// 2.加载解析配置文件并获取SqlSessionFactory对象SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(in);// 3.根据SqlSessionFactory对象获取SqlSession对象SqlSession sqlSession = factory.openSession();// 4.通过SqlSession中提供的 API方法来操作数据库UserMapper mapper = sqlSession.getMapper(UserMapper.class);// 分页PageHelper.startPage(1, 10);List<User> users = mapper.selectUserList();System.out.println(users);// 5.关闭会话sqlSession.close();}

执行结果:
在这里插入图片描述

3.2 原理解析

在这里插入图片描述
PageHelper实现了Interceptor接口,拦截接口Executor的query方法。也就是说SqlSession的executor创建之后,经过类Plugin的wrap方法处理之后,变成代理对象。
在这里插入图片描述
接下里的 PageHelper.startPage(1, 10)做了什么? Let’s get into it.
F7跟踪到这里:
在这里插入图片描述这里主要设置页码和分页大小。SqlUtil类来面有个TreadLocal变量LOCAL_PAGE,绑定当前线程的page对象。
在这里插入图片描述
接着往下走:
在这里插入图片描述
在这里插入图片描述
接着到PageHelper的intercept(Invocation invocation)方法:
在这里插入图片描述
在这里插入图片描述
最终在doProcessPage方法里面实现分页查询。

/*** Mybatis拦截器方法** @param invocation 拦截器入参* @return 返回执行结果* @throws Throwable 抛出异常*/private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {//保存RowBounds状态RowBounds rowBounds = (RowBounds) args[2];//获取原始的msMappedStatement ms = (MappedStatement) args[0];//判断并处理为PageSqlSourceif (!isPageSqlSource(ms)) {processMappedStatement(ms);}//设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响((PageSqlSource)ms.getSqlSource()).setParser(parser);try {//忽略RowBounds-否则会进行Mybatis自带的内存分页args[2] = RowBounds.DEFAULT;//如果只进行排序 或 pageSizeZero的判断if (isQueryOnly(page)) {return doQueryOnly(page, invocation);}//简单的通过total的值来判断是否进行count查询if (page.isCount()) {page.setCountSignal(Boolean.TRUE);//替换MSargs[0] = msCountMap.get(ms.getId());//查询总数Object result = invocation.proceed();//还原msargs[0] = ms;//设置总数page.setTotal((Integer) ((List) result).get(0));if (page.getTotal() == 0) {return page;}} else {page.setTotal(-1l);}//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个countif (page.getPageSize() > 0 &&((rowBounds == RowBounds.DEFAULT && page.getPageNum() > 0)|| rowBounds != RowBounds.DEFAULT)) {//将参数中的MappedStatement替换为新的qspage.setCountSignal(null);BoundSql boundSql = ms.getBoundSql(args[1]);args[1] = parser.setPageParameter(ms, args[1], boundSql, page);page.setCountSignal(Boolean.FALSE);//执行分页查询Object result = invocation.proceed();//得到处理结果page.addAll((List) result);}} finally {((PageSqlSource)ms.getSqlSource()).removeParser();}//返回结果return page;}

// todo 待仔细研究

4. 拦截器应用场景

作用描述实现方式
水平分表一张费用表按月度拆分为12张表。fee_202001-202012。当查询条件出现月度(tran_month)时,把select语句中的逻辑表名修改为对应的月份表。对query update方法进行拦截在接口上添加注解,通过反射获取接口注解,根据注解上配置的参数进行分表,修改原SQL,例如id取模,按月分表
数据脱敏手机号和身份证在数据库完整存储。但是返回给用户,屏蔽手机号的中间四位。屏蔽身份证号中的出生日期。query——对结果集脱敏
菜单权限控制不同的用户登录,查询菜单权限表时获得不同的结果,在前端展示不同的菜单对query方法进行拦截在方法上添加注解,根据权限配置,以及用户登录信息,在SQL上加上权限过滤条件
黑白名单有些SQL语句在生产环境中是不允许执行的,比如like %%对Executor的update和query方法进行拦截,将拦截的SQL语句和黑白名单进行比较,控制SQL语句的执行
全局唯一ID在高并发的环境下传统的生成ID的方式不太适用,这时我们就需要考虑其他方式了创建插件拦截Executor的insert方法,通过UUID或者雪花算法来生成ID,并修改SQL中的插入信息

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/19569.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

聊天系统登录后端实现

定义返回的数据格式 # Restful API from flask import jsonifyclass HttpCode(object):# 响应正常ok 200# 没有登陆错误unloginerror 401# 没有权限错误permissionerror 403# 客户端参数错误paramserror 400# 服务器错误servererror 500def _restful_result(code, messa…

Matlab Optimization Toolbox中的遗传算法工具包(GA)

matlab optimization 中使用了GA求解器 默认的是小于等于 找到GA 工具包 找到 APP选择 Optimization Tool 选择Solver ga - Genetic Algorithm 应用GA solver 定义适应度函数(Fitness function)与问题约束(Constraints) example one 优化函数 sin(x) 2 * cos(x)极其重要的…

windows10 设置代理

场景&#xff1a;同一个办公室&#xff0c;只有A的电脑有权限访问网站 http://10.129.129.129:5601&#xff0c; 那办公室其他B,C同学想访问 http://10.129.129.129:5601&#xff0c;需要怎么处理&#xff1f; A 同学电脑安装代理软件&#xff1a; 1. 下载wproxy IMFirewall, …

iMX6ULL驱动开发 | 让imx6ull开发板支持usb接口FC游戏手柄

手边有一闲置的linux开发板iMX6ULL一直在吃灰&#xff0c;不用来搞点事情&#xff0c;总觉得对不住它。业余打发时间就玩起来吧&#xff0c;总比刷某音强。从某多多上8块儿大洋买来一个usb接口的游戏手柄&#xff0c;让开发板支持以下它&#xff0c;后续就可以接着在上面玩童年…

信息学奥赛一本通——1258:【例9.2】数字金字塔

文章目录 题目【题目描述】【输入】【输出】【输入样例】【输出样例】 AC代码 题目 【题目描述】 观察下面的数字金字塔。写一个程序查找从最高点到底部任意处结束的路径&#xff0c;使路径经过数字的和最大。每一步可以从当前点走到左下方的点也可以到达右下方的点。 在上面…

Java-很深我只知其一-泛型

Java-很深我只知其一-泛型 目录 泛型历史 泛型类/泛型接口 泛型方法 泛型属性 泛型约束 泛型历史 JAVA 泛型&#xff08;generics&#xff09;是 JDK 5 中引入的一个新特性, 允许程序员在编程时指定类型参数&#xff0c;使编译器可以在编译代码时检测到非法的类型。泛型的…

小研究 - 主动式微服务细粒度弹性缩放算法研究(四)

微服务架构已成为云数据中心的基本服务架构。但目前关于微服务系统弹性缩放的研究大多是基于服务或实例级别的水平缩放&#xff0c;忽略了能够充分利用单台服务器资源的细粒度垂直缩放&#xff0c;从而导致资源浪费。为此&#xff0c;本文设计了主动式微服务细粒度弹性缩放算法…

Java 版 spring cloud + spring boot 工程系统管理 工程项目管理系统源码 工程项目各模块及其功能点清单

工程项目各模块及其功能点清单 一、系统管理 1、数据字典&#xff1a;实现对数据字典标签的增删改查操作 2、编码管理&#xff1a;实现对系统编码的增删改查操作 3、用户管理&#xff1a;管理和查看用户角色 4、菜单管理&#xff1a;实现对系统菜单的增删改查操…

基于以太坊+IPFS的去中心化数据交易方法及平台

自己的论文&#xff0c;哎费事 目录 基于以太坊IPFS的去中心化数据交易方法及平台 基于以太坊IPFS的去中心化数据交易方法及平台 摘要&#xff1a; 数据交易过程中存在数据权属不明和数据安全问题。本文开发了一种基于以太坊IPFS的去中心化数据交易方法及平台。方法包括&am…

IPv4网络用户访问IPv6网络服务器

NAT64静态映射为一对一的对应关系&#xff0c;通常应用在IPv4网络主动访问IPv6网络的场景中。 要求位于IPv4网络中的PC通过IPv4地址1.1.1.10能够直接访问位于IPv6网络中Server。 操作步骤 配置FW。 # 配置接口GigabitEthernet 0/0/1的IPv4地址。 <FW> system-view [F…

IP 工具

什么是IP 工具 IP 工具是用于轻松扫描和排除网络 IP 地址空间故障的网络工程工具。IP 工具使网络管理员能够审核、跟踪和监视 IP 地址、子网以及使用 IP 的设备和主机的性能。这个全面的网络工程工具集包括高级 IP 工具&#xff0c;如 Ping、系统资源管理器、MAC 地址解析器和…

设计模式行为型——解释器模式

目录 什么是解释器模式 解释器模式的实现 解释器模式角色 解释器模式类图 解释器模式举例 解释器模式代码实现 解释器模式的特点 优点 缺点 使用场景 注意事项 实际应用 什么是解释器模式 解释器模式&#xff08;Interpreter Pattern&#xff09;属于行为型模式&…

《零基础入门学习Python》第075讲:GUI的终极选择:Tkinter12

Tkinter 的基本组件我们已经介绍得七七八八了&#xff0c;剩下的一些我们在这节课全部都会讲解完毕。 &#xff08;一&#xff09;Message组件 Message&#xff08;消息&#xff09;组件是 Label 组件的变体&#xff0c;用于显示多行文本消息。众所周知&#xff0c;我们的Lab…

【腾讯云 Cloud Studio 实战训练营】Redisgo_task 分布式锁实现

文章目录 前言问题场景腾讯云 Cloud Studio Redisgo_task长短类型分布式场景介绍Redisgo_task实现原理SetNx(valueexpire)原子性子协程Done()时间点子协程中的Ticker Redisgo_task唯一外部依赖Redisgo_task Lock结构Redisgo_task架构健壮性设计Redisgo_task可扩展性Redisgo_tas…

MyBatis-Plus 和达梦数据库实现高效数据持久化

一、添加依赖 首先&#xff0c;我们需要在项目的 pom.xml 文件中添加 MyBatis-Plus 和达梦数据库的依赖&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifac…

微信小程序使用 canvas 2d 实现签字板组件

本文是在微信小程序中使用 canvas 2d 来实现签字板功能&#xff1b; 效果图&#xff1a; 代码&#xff1a; 1、wxml <view><canvas id"canvas"type"2d"bindtouchstart"start"bindtouchmove"move"bindtouchend"end&qu…

Scratch Blocks自定义组件之「下拉图标」

一、背景 由于自带的下拉图标是给水平布局的block使用&#xff0c;放在垂直布局下显得别扭&#xff0c;而且下拉选择后回修改image字段的图片&#xff0c;这让我很不爽&#xff0c;所以在原来的基础上稍作修改&#xff0c;效果如下&#xff1a; 二、使用说明 &#xff08;1&am…

【图论】差分约束

一.情景导入 x1-x0<9 ; x2-x0<14 ; x3-x0<15 ; x2-x1<10 ; x3-x2<9; 求x3-x0的最大值&#xff1b; 二.数学解法 联立式子2和5&#xff0c;可得x3-x0<23;但式子3可得x3-x0<15。所以最大值为15&#xff1b; 三.图论 但式子多了我们就不好解了&#xff0…

【MySQL】视图与用户管理

【MySQL】视图 视图视图概念使用基表与视图的相互影响 用户管理新增用户删除修改密码 用户权限授予权限回收权限 视图 视图概念 视图就是一张虚拟表&#xff0c;其内容由查询定义。与真实的表一样&#xff0c;视图包含一系列带有名称的列和行数据。视图的数据变化影响到基表&…

SpringBoot内嵌的Tomcat:

SpringBoot内嵌Tomcat源码&#xff1a; 1、调用启动类SpringbootdemoApplication中的SpringApplication.run()方法。 SpringBootApplication public class SpringbootdemoApplication {public static void main(String[] args) {SpringApplication.run(SpringbootdemoApplicat…