设计模式学习笔记 - 开源实战五(中):如何利用职责链与代理模式实现Mybatis Plugin

概述

上篇文章对 Mybatis 框架做了简单的背景介绍,并通过对比各种 ORM 框架,学习了代码的易用性、性能、灵活性之间的关系。一般来讲,框架提供的高级功能越多,那性能损耗就越大;框架使用起来越简单,那灵活性就越低。

接下来的两篇文章,再学习下 Mybatis 用到的一些经典的设计模式。本章主要讲解 Mybatis Plugin

尽管名字叫 Plugin(插件),但它实际上和 Servlet Filter(过滤器)、Spring Interceptor(拦截器)类似,设计的初衷都是为了框架的扩展性,用到的主要设计模式是职责链模式。

不过相对于 Servlet Filter、Spring Interceptor,Mybatis Plugin 中职责链模式的代码实现稍微有点复杂。它是借助动态代理模式来实现的职责链。本章就带你看下,如何利用这两个模式实现 Mybatis Plugin。


Mybatis Plugin 功能介绍

实际上,Mybatis Plugin 跟 Servlet Filter、Spring Interceptor 的功能是类似的,都是在不修改流程代码的情况下,拦截某些方法调用,在拦截的方法调用后,执行一些额外的代码逻辑。它们的唯一区别在于拦截的位置是不同的。Servlet Filter 主要拦截 Servlet 请求,Spring Interceptor 主要拦截 Spring 管理的 Bean 方法(比如 Controller 类的方法等),而 Mybatis Plugin 主要拦截的是 Mybatis 在执行 SQL 的过程中涉及的一些方法。

Mybatis Plugin 使用起来比较简单,下面通过一个例子来快速看下。

假设我们需要统计应用中每个 SQL 的执行耗时,如果使用 Mybatis Plugin 来实现的话,只需要定义一个 SqlCostTimeInterceptor 类,让他实现 Mybatis 的 Interceptor 接口,并且在 Mybatis 的全局配置文件中,简单声明一下就可以了。具体代码如下所示:

@Intercepts({
        @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),
        @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),
        @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}),

})
public class SqlCostTimeInterceptor implements Interceptor {
    private static Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        long startTime = System.currentTimeMillis();
        StatementHandler statementHandler = (StatementHandler) target;
        try {
            return invocation.proceed();
        } finally {
            long costTime = System.currentTimeMillis() - startTime;
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            logger.info("执行 SQL:[ {} ],执行耗时[ {} ms]", sql, costTime);
        }
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        System.out.println("插件配置信息: " + properties);
    }
}

<!-- Mybatis全局配置文件:mybatis-config.xml -->
<plugins>
	<plugin interceptor="com.example.SqlCostTimeInterceptor">
		<property name="someProperty" value="100"/>
	</plugin>
</plugins>

待会会详细的介绍 Mybatis Plugin 底层的实现原理,所以,这里暂时不对上面的代码做详细地解释。现在,只关注下 @Intercepts 这个注解。

不管是拦截器、过滤器还是插件,都需要明确地标明拦截的目标方法。@Intercepts 注解实际上就是这个作用。其中,@Intercepts 注解又可以嵌套 @Signature 注解。一个 @Signature 注解标明要拦截的方法。如果要拦截多个方法,我们可以像例子中那样,编写多条 @Signature 注解。

@Signature 注解包含三个元素:typemethodargs。其中,type 指明要拦截的类、method 指明方法名、args 指明方法的参数列表。通过这三个元素,就能完全确定要拦截的方法。

默认情况下,Mybatis Plugin 允许拦截的方法有下面这样几个:

方法
Executorupdate、query、flushStatements、commit、rollback、getTransaction、clos、isClosed
ParameterHandlergetParameterObject、setParameters
ResultSetHandlerhandleResultSets、handleOutputParameters
StatementHandlerprepare、parameterize、batch、update、query

为什么默认允许拦截的是这样几个类的方法呢?

Mybatis 底层是通过 Executor 类来执行 SQL 的。Executor 会创建 StatementHandlerResultSetHandlerParameterHandler 三个对象,并且,首先使用 ParameterHandler 设置 SQL 中的占位符参数,然后使用 StatementHandler 执行 SQL 语句,最后使用 ResultSetHandler 封装执行结果。所以,我们需要拦截 ExecutorStatementHandlerResultSetHandlerParameterHandler 这几个类的方法,基本上就能满足我们对整个 SQL 执行流程的拦截了。

实际上,除了统计 SQL 的执行耗时,利用 Mybatis Plugin,还可以做很多事情,比如分库分表、自动分页、数据脱敏、加密解密等等。如果感兴趣,你可以自己实现下。

Mybatis Plugin 的设计与实现

刚刚简单介绍了 Mybatis Plugin 是如何使用的。现在在剖析下源码,看看如此简洁的使用方式,底层是如何实现的,隐藏了哪些复杂的设计。

相对于 Servlet Filter、Spring Interceptor 中职责链的代码实现,Mybatis Plugin 的代码实现还是蛮有技巧的,因为它是借助动态代理来实现职责链的。

在《责任链模式(上)》和《责任链模式(下)》中,我们讲到,职责链模式的实现一般包括处理器(Handler)和处理器链(HandlerChain)两部分。这两个部分对应到 Servlet Filter 源码就是 FilterFilterChain,对应到 Spring Interceptor 源码就是 HandlerInterceptorHandlerExecutionChain对应到 Mybatis Plugin 的源码就是 InterceptorInterceptorChain。此外,Mybatis Plugin 还包含另外一个非常重要的类:PluginPlugin用来生成被拦截对象的动态代理

集成了 Mybatis 的应用在启动时,Mybatis 框架会读取全局配置文件,解析出 Interceptor,并将它们注入到 Configuration 类的 InterceptorChain 对象中。这部分逻辑对应到源码如下所示。

public class XMLConfigBuilder extends BaseBuilder {
	// ...
	
	// 解析配置
	private void parseConfiguration(XNode root) {
        try {
        	// ...
            this.pluginElement(root.evalNode("plugins"));
            // ...
        } catch (Exception var3) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
        }
    }
    
    // ...
	
	// 解析插件
	private void pluginElement(XNode parent) throws Exception {
        if (parent != null) {
            Iterator var2 = parent.getChildren().iterator();

            while(var2.hasNext()) {
                XNode child = (XNode)var2.next();
                String interceptor = child.getStringAttribute("interceptor");
                Properties properties = child.getChildrenAsProperties();
                // 创建Interceptor类对象
                Interceptor interceptorInstance = (Interceptor)this.resolveClass(interceptor).getDeclaredConstructor().newInstance();
                // 调用Interceptor上的setProperties()方法设置properties
                interceptorInstance.setProperties(properties);
                // 下面这行代码会调用InterceptorChain.addInterceptor()方法
                this.configuration.addInterceptor(interceptorInstance);
            }
        }
    }

	// ...
}

public class Configuration {
	// ...
	
	public void addInterceptor(Interceptor interceptor) {
        this.interceptorChain.addInterceptor(interceptor);
    }
	
	// ...
}

再来看下 InterceptorInterceptorChain 这两个类的代码,如下所示。InterceptorsetProperties() 方法就是单纯的一个 setter 方法,主要是为了方便通过配置文件配置 Interceptor 的一些属性值。Interceptor 类中的 intercept()plugin() 函数,以及 InterceptorChainpluginAll() 函数,是最核心的三个函数,待会再详细解释。

public class Invocation {
    private final Object target;
    private final Method method;
    private final Object[] args;
	// 省略构造函数和getter方法...
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return this.method.invoke(this.target, this.args);
    }
}

public interface Interceptor {
    Object intercept(Invocation var1) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList();

    public InterceptorChain() {
    }

    public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }

    public void addInterceptor(Interceptor interceptor) {
        this.interceptors.add(interceptor);
    }

    public List<Interceptor> getInterceptors() {
        return Collections.unmodifiableList(this.interceptors);
    }
}

解析完配置后,所有的 Interceptor 都加载到了 InterceptorChain 中。接下来,再看下,这些拦截器是在什么时候被触发执行的?又是如何被触发执行的?

前面提到,在执行 SQL 的过程中,Mybatis 会创建 ExecutorStatementHandlerResultSetHandlerParameterHandler 这个几个类的对象,对应的创建代码在 Configuration 类中,如下所示:

public class Configuration {
	// ...
	public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }

        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }

        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }
	// ...
	public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        ResultSetHandler resultSetHandler = (ResultSetHandler)this.interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        StatementHandler statementHandler = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }
	// ...
}

从上面的代码可以看出,这几个类对象的创建构成都调用了 InterceptorChainpluginAll() 方法。这个方法的代码前面已经给出了。它的代码实现很简单,嵌套调用 InterceptorChain 上每个 Interceptorplugin() 方法。plugin() 是一个接口方法,需要由用户给出具体的实现代码。在之前的例子中, SQLTimeCostInterceptorplugin() 方法通过直接调用 Pluginwrap() 方法来实现。wrap() 方法的代码实现如下所示:

// 借助Java InvocationHandler实现动态代理模式
public class Plugin implements InvocationHandler {

  private final Object target;
  private final Interceptor interceptor;
  private final Map<Class<?>, Set<Method>> signatureMap;

  private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
    this.target = target;
    this.interceptor = interceptor;
    this.signatureMap = signatureMap;
  }

  // Wrap()静态方法,用来生成 target 对象的动态代理
  // 动态代理对象 = target对象 + interceptor对象
  public static Object wrap(Object target, Interceptor 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;
  }
  
  // 调用target上的f()方法,会触发执行下面这个方法
  // 这个方法阿包含:执行interceptor的intecept()方法 + 执行targetshangf()方法。
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<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);
    }
  }

  private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
    // issue #251
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
    }
    Signature[] sigs = interceptsAnnotation.value();
    Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
    for (Signature sig : sigs) {
      Set<Method> methods = MapUtil.computeIfAbsent(signatureMap, 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()) {
        if (signatureMap.containsKey(c)) {
          interfaces.add(c);
        }
      }
      type = type.getSuperclass();
    }
    return interfaces.toArray(new Class<?>[0]);
  }
}

实际上,Plugin 是借助 Java InvocationHandler 实现的动态代理。用来代理给 target 对象添加 interceptor 功能。其中,要代理的对象就是 ExecutorStatementHandlerResultSetHandlerParameterHandler 这个四个类的对象。wrap() 静态方法是一个工具函数,用来生成 target 对象的动态代理对象。

当然,只有 interceptortarget 互相匹配时,wrap() 方法才会返回代理对象,否则就返回 target 对象本身。怎么才能算匹配呢? 那就是 interceptor 通过 @Signature 朱姐拦截到的类包含 target 对象,具体可以看 wrap() 函数的代码实现。

Mybatis 中的职责链模式的实现方式比较特殊。它对同一个目标对象嵌套多次代理(也就是 InterceptorChainpluginAll() 函数要执行的任务)。每个代理对象(Plugin 对象)代理一个拦截器(Interceptor 对象)功能。为了方便查看,我把 pluginAll() 函数的代码又拷贝到下面。

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  // 上面这行代码等于下面代码,target(代理对象) = target(目标对象) + interceptpor(拦截器功能)
  // target=Plugin.wrap(target, interceptor);

  // ...
}

// mybatis像下面这样创建target(Executor、StatementHandler、ResultSetHandler、ParameterHandler),
// 相当于多次嵌套代理
Object target = interceptor.pluginAll(target);

当执行 ExecutorStatementHandlerResultSetHandlerParameterHandler 这四个类方法时,Mybatis 会嵌套执行每层代理对象(Plugin 对象)上的 invoke() 方法,而 invoke() 方法会先执行对象中的 interceptor.intercept() 函数,然后再执行被代理对象上的方法。就这样,一层一层地把代理对象上的 intercept() 函数执行完后,Mybatis 才最终执行那 4 个原始类对象上的方法。

总结

本章剖析了如何利用职责链模式和动态代理模式来实现 Mybatis Plugin。至此,我们学习了三种职责链常用的应用场景:过滤器(Servlet Filter)、拦截器(Spring Interceptor)、插件(Mybatis Plugin)。

职责链模式一般包含处理器和处理器链两部分。这两个部分对应到 Servlet Filter 源码就是 FilterFilterChain,对应到 Spring Interceptor 源码就是 HandlerInterceptorHandlerExecutionChain,对应到 Mybatis Plugin 的源码就是 InterceptorInterceptorChain。此外,Mybatis Plugin 还包含另外一个非常重要的类:PluginPlugin用来生成被拦截对象的动态代理。

这三种应用场景中,职责链模式的实现思路都不打一样。其中,Servlet Filter 采用递归来实现拦截方法前后添加逻辑。Spring Interceptor 的实现比较简单,把拦截方法前后要添加的逻辑放到两个方法中去实现。Mybatis Plugin 采用嵌套动态代理的方法来实现,实现思路很有技巧。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

新闻 | 电子系协同智能中心与昌平区未来高教园及多所高校开展交流,共话智能无人平台建设

2024年4月8日&#xff0c;清华大学电子工程系在北京昌平两岸共盈科技产业园电子系地空协同智能无人平台基地成功举办“美团杯”智能无人机挑战赛&#xff0c;清华大学电子系党委书记沈渊、昌平区未来城管委会校城融合处处长熊玉川、清华大学团委副书记黄峰等出席。此外来自昌平…

【面经】汇总

面经 Java基础集合都有哪些面向对象的三大特点ArrayList和LinkedList的区别&#xff1f;ArrayList底层扩容是怎么实现的&#xff1f;讲一讲HashMap、以及put方法的过程讲一讲HashMap的扩容过程Hashmap为什么要用红黑树而不用其他的树&#xff1f;Java8新特性有哪些LoadFactor负…

Scala 03 —— Scala OOP Extension

Scala 2.1 —— Scala OOP Extension 一、正则 文章目录 Scala 2.1 —— Scala OOP Extension一、正则1.1 Java正则和Scala正则的区别1.2 Java正则和Scala正则的的基本知识点Java正则Scala正则 1.3 练习练习一&#xff1a;使用正则表达式解析日志方法一&#xff1a;使用findAl…

99AI3.3二开稳定版(NineAi内核升级)免授权无后门AI系统源码部署及详细安装教程

99AIv3.3.0是基于 NineAI 二开的可商业化 AI Web 应用&#xff08;免授权&#xff0c;无后门&#xff0c;非盗版&#xff0c;已整合前后端&#xff0c;支持快速部署&#xff09;。未编译源码暂不开源&#xff0c;相比稳定版&#xff0c;开发版进度更快一些。前端改进&#xff1…

【Python】全面掌握 Collections Deque:队列与栈的高效实现及动态内存管理指南

文章目录 第一章&#xff1a;deque 的定义和特性1. 什么是双端队列&#xff08;deque&#xff09;2. deque 与普通列表&#xff08;list&#xff09;的性能差异 第二章&#xff1a;构造函数1. 如何创建一个 deque2. 可选参数 maxlen 的作用和使用场景 第三章&#xff1a;添加和…

vue3使用echarts做树图tree

vue3使用echarts做树图tree 1.安装echarts npm install echarts --save2.在main.js引入 import * as echarts from echarts // 全局方法 app.config.globalProperties.$echarts echarts3.使用 <div id"myChart" :style"{ width: 1000px, height: 1000px …

如何设置“mumu模拟器”使用fiddler抓取APP包?

1、打开fiddler-->tools-->optinons,设置如下信息https信息和connections 2、下载证书tools-->optinons-->https-->actions->Export Root Certificate to Desktop到桌面 3、mumu模拟器&#xff0c;安装证书 1)mumu进入桌面有个文件共享&#xff0c;打开后将桌…

python—字符串与正则表达式

1、编写程序&#xff0c;生成一个由15个不重复的大小写字母组成的列表。 &#xff08;1&#xff09;源代码&#xff1a; import random import string list1 [] while len(list1) < 15: x random.choice(string.ascii_letters) if x not in list1: list1.append(x) print…

pycharm-ieda-phpstorm超级好用插件,一键解释代码

功能&#xff1a;解释你看不懂的代码 当你在写python和Java代码的时候&#xff0c;总有你看不懂的代码&#xff0c;怎么办&#xff1f;csdn搜&#xff1f;那不麻烦&#xff0c;直接插件解决。 来安装&#xff1a;文件-设置 点击插件-Marketplace-搜索通义灵码 安装完成后&…

Qt Creator中变量与函数的注释 - 鼠标悬浮可显示

Qt Creator中变量与函数的注释 - 鼠标悬浮可显示 引言一、变量注释二、函数注释三、参考链接 引言 代码注释在软件开发中起着至关重要的作用。它们不仅有助于开发者理解和维护代码&#xff0c;还能促进团队协作&#xff0c;提高代码的可读性和可维护性。适当的注释应该是简洁明…

VoxAtnNet:三维点云卷积神经网络

VoxAtnNet:三维点云卷积神经网络 摘要IntroductionProposed VoxAtnNet 3D Face PAD3D face point cloud presentation attack Dataset (3D-PCPA) VoxAtnNet: A 3D Point Clouds Convolutional Neural Network for 摘要 面部生物识别是智能手机确保可靠和可信任认证的重要组件。…

16册 | 移动机器人(自动驾驶)系列

此文档整理推荐了16本移动机器人&#xff08;自动驾驶&#xff09;相关的书籍&#xff0c;内容包括&#xff1a;ROS、机器人基础开发、分布式机器人控制、集群机器人控制、嵌入式机器人、多传感器融合等等。 学习&#xff0c;切勿急于求成&#xff0c;读书自学&#xff0c;需多…

栈和队列总结

文章目录 前言一、栈和队列的实现1.栈的具体实现2.循环顺序队列的具体实现 二、栈和队列总结总结 前言 T_T此专栏用于记录数据结构及算法的&#xff08;痛苦&#xff09;学习历程&#xff0c;便于日后复习&#xff08;这种事情不要啊&#xff09;。所用教材为《数据结构 C语言版…

【启明智显技术分享】ESP32系列WiFi无线空中抓包指南

前言&#xff1a; 本文档旨在介绍 windows10 系统下网卡抓包工具(AC-1200)的驱动安装过程、Omnipeek 软件安装过程及Omnipeek软件与网卡抓包工具配合抓包的演示过程。 1、抓包工具(AC-1200)驱动安装 1.1 准备好抓包工具及厂家提供的抓包工具驱动文件 1.2 插上 USB 网卡&…

Linux——socket套接字与udp通信

目录 一、理解IP地址与端口 二、socket套接字 三、TCP与UDP的关系 四、网络字节序 五、socket编程 1.socket()创建套接字 2.填充sockaddr_in 结构体 3.bind() 绑定信息 4.recvfrom()接收消息 5.sendto()发送消息 六、UdpServer代码 一、理解IP地址与端口 IP地址是In…

Leetcode—1256. 加密数字【中等】Plus(bitset、find_first_not_of、erase)

2024每日刷题&#xff08;120&#xff09; Leetcode—1256. 加密数字 实现代码 class Solution { public:string encode(int num) {string ans;num 1;while(num ! 0) {ans to_string(num & 1);num num >> 1;}if(ans.empty()) {return "";} else {stri…

17 如何设计一锤子买卖的SDK

在前三个模块里&#xff0c;我将微服务根据目的性划分为三大类&#xff1a;读、写与扣减类&#xff0c;并针对每一大类涉及的各项技术问题讲解了应对方案。其实&#xff0c;每一类微服务除了本身业务特点涉及的技术问题外&#xff0c;在纯技术维度也有很多共性问题&#xff0c;…

房产中介小程序高效开发攻略:从模板到上线一站式服务

对于房产中介而言&#xff0c;拥有一个高效且用户友好的小程序是提升业务、增强客户黏性的关键。而采用直接复制模板的开发方式&#xff0c;无疑是实现这一目标的最佳途径&#xff0c;不仅简单快捷&#xff0c;而且性价比极高。 在众多小程序模板开发平台中&#xff0c;乔拓云网…

docker容器通俗理解

前言 如果大家没使用过Docker,就在电脑上下载一个VMware Workstation Pro&#xff0c;创建一个虚拟机安装一个windows操作一下感受一下,为什么我的电脑上还以再安装一台windows主机&#xff1f;其实你可以理解为Docker就是Linux系统的一个虚拟机软件。 我的Windows也可以安装…

WMS仓库库存管理软件如何优化工厂的仓库管理-亿发

如果一家工厂没有专业的WMS仓储软件支撑&#xff0c;管理原材料、辅料、半成品和产成品等环节可能会面临诸多问题。 在仓库管理方面&#xff0c;缺乏安全库存的管理会导致库存不足或过剩&#xff0c;而没有及时的缺货分析可能会导致生产中断。全凭人工核算剩余库存和订单质检的…