责任链模式的高级用法:多级校验、工作流,这样写代码才足够优雅!_spring职责链 动态编排-程序员宅基地

技术标签: 实战设计模式  java实现责任链模式案例  责任链模式  互联网公司责任链模式实战  设计模式  设计模式实战  

在这里插入图片描述

一、开篇

阅读本文可以了解哪些知识?

  • 结合具体案例,领略责任链模式的魅力。
  • 责任链模式实现流程编排动态扩展
  • 使用 Sping @Resource 注解注入的骚操作
  • 使用递归算法设置责任链路。

二、简介

责任链模式,简而言之,就是将多个操作组装成一条链路进行处理。请求在链路上传递,链路上的每一个节点就是一个处理器,每个处理器都可以对请求进行处理,或者传递给链路上的下一个处理器处理。
责任链模式-链路传递



三、应用场景

责任链模式的应用场景,在实际工作中,通常有如下两种应用场景。

  1. 操作需要经过一系列的校验,通过校验后才执行某些操作。
  2. 工作流。企业中通常会制定很多工作流程,一级一级的去处理任务。

下面通过两个案例来学习一下责任链模式。


案例一:创建商品多级校验场景

以创建商品为例,假设商品创建逻辑分为以下三步完成:①创建商品、②校验商品参数、③保存商品。

第②步校验商品又分为多种情况的校验,必填字段校验、规格校验、价格校验、库存校验等等。这些检验逻辑像一个流水线,要想创建出一个商品,必须通过这些校验。如下流程图所示:

创建商品的坏味道

伪代码如下:

创建商品步骤,需要经过一系列的参数校验,如果参数校验失败,直接返回失败的结果;通过所有的参数校验后,最终保存商品信息。
伪代码

如上代码看起来似乎没什么问题,它非常工整,而且代码逻辑很清晰。(PS:我没有把所有的校验代码都罗列在一个方法里,那样更能产生对比性,但我觉得抽象并分离单一职责的函数应该是每个程序员最基本的规范!)

但是随着业务需求不断地叠加,相关的校验逻辑也越来越多,新的功能使代码越来越臃肿可维护性较差。更糟糕的是,这些校验组件不可复用,当你有其他需求也需要用到一些校验时,你又变成了Ctrl+C , Ctrl+V程序员,系统的维护成本也越来越高。如下图所示:
业务逻辑堆叠
伪代码同上,这里就不赘述了。

终于有一天,你忍无可忍了,决定重构这段代码

使用责任链模式优化 :创建商品的每个校验步骤都可以作为一个单独的处理器,抽离为一个单独的,便于复用。这些处理器形成一条链式调用,请求在处理器链上传递,如果校验条件不通过,则处理器不再向下传递请求,直接返回错误信息;若所有的处理器都通过检验,则执行保存商品步骤。

在这里插入图片描述



案例一实战:责任链模式实现创建商品校验


UML图:一览众山小

在这里插入图片描述

AbstractCheckHandler表示处理器抽象类,负责抽象处理器行为。其有3个子类,分别是:

  1. NullValueCheckHandler:空值校验处理器
  2. PriceCheckHandler:价格校验处理
  3. StockCheckHandler:库存校验处理器

AbstractCheckHandler 抽象类中, handle()定义了处理器的抽象方法,其子类需要重写handle()方法以实现特殊的处理器校验逻辑;

protected ProductCheckHandlerConfig config 是处理器的动态配置类,使用protected声明,每个子类处理器都持有该对象。该对象用于声明当前处理器、以及当前处理器的下一个处理器nextHandler,另外也可以配置一些特殊属性,比如说接口降级配置、超时时间配置等。

AbstractCheckHandler nextHandler 是当前处理器持有的下一个处理器的引用,当前处理器执行完毕时,便调用nextHandler执行下一处理器的handle()校验方法;

protected Result next() 是抽象类中定义的,执行下一个处理器的方法,使用protected声明,每个子类处理器都持有该对象。当子类处理器执行完毕(通过)时,调用父类的方法执行下一个处理器nextHandler

HandlerClient 是执行处理器链路的客户端,HandlerClient.executeChain()方法负责发起整个链路调用,并接收处理器链路的返回值。


撸起袖子开始撸代码吧 ~

商品参数对象:保存商品的入参

ProductVO是创建商品的参数对象,包含商品的基础信息。并且其作为责任链模式中多个处理器的入参,多个处理器都以ProductVO为入参进行特定的逻辑处理。实际业务中,商品对象特别复杂。咱们化繁为简,简化商品参数如下:

/**
 * 商品对象
 */
@Data
@Builder
public class ProductVO {
    
    /**
     * 商品SKU,唯一
     */
    private Long skuId;
    /**
     * 商品名称
     */
    private String skuName;
    /**
     * 商品图片路径
     */
    private String imgPath;
    /**
     * 价格
     */
    private BigDecimal price;
    /**
     * 库存
     */
    private Integer stock;
}

抽象类处理器:抽象行为,子类共有属性、方法

AbstractCheckHandler:处理器抽象类,并使用@Component注解注册为由Spring管理的Bean对象,这样做的好处是,我们可以轻松的使用Spring来管理这些处理器Bean

/**
 * 抽象类处理器
 */
@Component
public abstract class AbstractCheckHandler {
    

    /**
     * 当前处理器持有下一个处理器的引用
     */
    @Getter
    @Setter
    protected AbstractCheckHandler nextHandler;


    /**
     * 处理器配置
     */
    @Setter
    @Getter
    protected ProductCheckHandlerConfig config;

    /**
     * 处理器执行方法
     * @param param
     * @return
     */
    public abstract Result handle(ProductVO param);

    /**
     * 链路传递
     * @param param
     * @return
     */
    protected Result next(ProductVO param) {
    
        //下一个链路没有处理器了,直接返回
        if (Objects.isNull(nextHandler)) {
    
            return Result.success();
        }

        //执行下一个处理器
        return nextHandler.handle(param);
    }

}

AbstractCheckHandler抽象类处理器中,使用protected声明子类可见的属性和方法。使用 @Component注解,声明其为SpringBean对象,这样做的好处是可以利用Spring轻松管理所有的子类,下面会看到如何使用。抽象类的属性和方法说明如下:

  • public abstract Result handle():表示抽象的校验方法,每个处理器都应该继承AbstractCheckHandler抽象类处理器,并重写其handle方法,各个处理器从而实现特殊的校验逻辑,实际上就是多态的思想。

  • protected ProductCheckHandlerConfig config:表示每个处理器的动态配置类,可以通过“配置中心”动态修改该配置,实现处理器的“动态编排”和“顺序控制”。配置类中可以配置处理器的名称下一个处理器、以及处理器是否降级等属性。

  • protected AbstractCheckHandler nextHandler:表示当前处理器持有下一个处理器的引用,如果当前处理器handle()校验方法执行完毕,则执行下一个处理器nextHandlerhandle()校验方法执行校验逻辑。

  • protected Result next(ProductVO param):此方法用于处理器链路传递,子类处理器执行完毕后,调用父类的next()方法执行在config 配置的链路上的下一个处理器,如果所有处理器都执行完毕了,就返回结果了。

ProductCheckHandlerConfig配置类 :

/**
 * 处理器配置类
 */
@AllArgsConstructor
@Data
public class ProductCheckHandlerConfig {
    
    /**
     * 处理器Bean名称
     */
    private String handler;
    /**
     * 下一个处理器
     */
    private ProductCheckHandlerConfig next;
    /**
     * 是否降级
     */
    private Boolean down = Boolean.FALSE;
}

子类处理器:处理特有的校验逻辑

AbstractCheckHandler抽象类处理器有3个子类分别是:

  1. NullValueCheckHandler:空值校验处理器
  2. PriceCheckHandler:价格校验处理
  3. StockCheckHandler:库存校验处理器

各个处理器继承AbstractCheckHandler抽象类处理器,并重写其handle()处理方法以实现特有的校验逻辑。

  • NullValueCheckHandler:空值校验处理器。针对性校验创建商品中必填的参数。如果校验未通过,则返回错误码ErrorCode,责任链在此截断(停止),创建商品返回被校验住的错误信息。注意代码中的降级配置super.getConfig().getDown()是获取AbstractCheckHandler处理器对象中保存的配置信息,如果处理器配置了降级,则跳过该处理器,调用super.next()执行下一个处理器逻辑。

同样,使用@Component注册为由Spring管理的Bean对象,

/**
 * 空值校验处理器
 */
@Component
public class NullValueCheckHandler extends AbstractCheckHandler{
    

    @Override
    public Result handle(ProductVO param) {
    
        System.out.println("空值校验 Handler 开始...");
        
        //降级:如果配置了降级,则跳过此处理器,执行下一个处理器
        if (super.getConfig().getDown()) {
    
            System.out.println("空值校验 Handler 已降级,跳过空值校验 Handler...");
            return super.next(param);
        }
        
        //参数必填校验
        if (Objects.isNull(param)) {
    
            return Result.failure(ErrorCode.PARAM_NULL_ERROR);
        }
        //SkuId商品主键参数必填校验
        if (Objects.isNull(param.getSkuId())) {
    
            return Result.failure(ErrorCode.PARAM_SKU_NULL_ERROR);
        }
        //Price价格参数必填校验
        if (Objects.isNull(param.getPrice())) {
    
            return Result.failure(ErrorCode.PARAM_PRICE_NULL_ERROR);
        }
        //Stock库存参数必填校验
        if (Objects.isNull(param.getStock())) {
    
            return Result.failure(ErrorCode.PARAM_STOCK_NULL_ERROR);
        }
        
        System.out.println("空值校验 Handler 通过...");
        
        //执行下一个处理器
        return super.next(param);
    }
}


  • PriceCheckHandler:价格校验处理。针对创建商品的价格参数进行校验。这里只是做了简单的判断价格>0的校验,实际业务中比较复杂,比如“价格门”这些防范措施等。
/**
 * 价格校验处理器
 */
@Component
public class PriceCheckHandler extends AbstractCheckHandler{
    
    @Override
    public Result handle(ProductVO param) {
    
        System.out.println("价格校验 Handler 开始...");

        //非法价格校验
        boolean illegalPrice =  param.getPrice().compareTo(BigDecimal.ZERO) <= 0;
        if (illegalPrice) {
    
            return Result.failure(ErrorCode.PARAM_PRICE_ILLEGAL_ERROR);
        }
        //其他校验逻辑...

        System.out.println("价格校验 Handler 通过...");

        //执行下一个处理器
        return super.next(param);
    }
}

  • StockCheckHandler:库存校验处理器。针对创建商品的库存参数进行校验。
/**
 * 库存校验处理器
 */
@Component
public class StockCheckHandler extends AbstractCheckHandler{
    
    @Override
    public Result handle(ProductVO param) {
    
        System.out.println("库存校验 Handler 开始...");

        //非法库存校验
        boolean illegalStock = param.getStock() < 0;
        if (illegalStock) {
    
            return Result.failure(ErrorCode.PARAM_STOCK_ILLEGAL_ERROR);
        }
        //其他校验逻辑..

        System.out.println("库存校验 Handler 通过...");

        //执行下一个处理器
        return super.next(param);
    }
}

客户端:执行处理器链路

HandlerClient客户端类负责发起整个处理器链路的执行,通过executeChain()方法。如果处理器链路返回错误信息,即校验未通过,则整个链路截断(停止),返回相应的错误信息。

public class HandlerClient {
    

  public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
    
      //执行处理器
      Result handlerResult = handler.handle(param);
      if (!handlerResult.isSuccess()) {
    
          System.out.println("HandlerClient 责任链执行失败返回:" + handlerResult.toString());
          return handlerResult;
      }
      return Result.success();
  }
}

以上,责任链模式相关的类已经创建好了。接下来就可以创建商品了。


创建商品:抽象步骤,化繁为简

createProduct()创建商品方法抽象为2个步骤:①参数校验②创建商品。参数校验使用责任链模式进行校验,包含:空值校验价格校验库存校验等等,只有链上的所有处理器均校验通过,才调用saveProduct()创建商品方法;否则返回校验错误信息。在createProduct()创建商品方法中,通过责任链模式,我们将校验逻辑进行解耦createProduct()创建商品方法中不需要关注都要经过哪些校验处理器,以及校验处理器的细节。

/**
 * 创建商品
 * @return
 */
@Test
public Result createProduct(ProductVO param) {
    

    //参数校验,使用责任链模式
    Result paramCheckResult = this.paramCheck(param);
    if (!paramCheckResult.isSuccess()) {
    
        return paramCheckResult;
    }

    //创建商品
    return this.saveProduct(param);
}

参数校验:责任链模式

参数校验paramCheck()方法使用责任链模式进行参数校验,方法内没有声明具体都有哪些校验,具体有哪些参数校验逻辑是通过多个处理器链传递的。如下:

/**
 * 参数校验:责任链模式
 * @param param
 * @return
 */
private Result paramCheck(ProductVO param) {
    

    //获取处理器配置:通常配置使用统一配置中心存储,支持动态变更
    ProductCheckHandlerConfig handlerConfig = this.getHandlerConfigFile();

    //获取处理器
    AbstractCheckHandler handler = this.getHandler(handlerConfig);

    //责任链:执行处理器链路
    Result executeChainResult = HandlerClient.executeChain(handler, param);
    if (!executeChainResult.isSuccess()) {
    
        System.out.println("创建商品 失败...");
        return executeChainResult;
    }

    //处理器链路全部成功
    return Result.success();
}

paramCheck()方法步骤说明如下:

步骤1:获取处理器配置

通过getHandlerConfigFile()方法获取处理器配置类对象,配置类保存了链上各个处理器的上下级节点配置,支持流程编排动态扩展。通常配置是通过Ducc(京东自研的配置中心)Nacos(阿里开源的配置中心)等配置中心存储的,支持动态变更实时生效。基于此,我们便可以实现校验处理器的编排、以及动态扩展了。我这里没有使用配置中心存储处理器链路的配置,而是使用JSON串的形式去模拟配置,大家感兴趣的可以自行实现。

/**
 * 获取处理器配置:通常配置使用统一配置中心存储,支持动态变更
 * @return
 */
private ProductCheckHandlerConfig getHandlerConfigFile() {
    
    //配置中心存储的配置
    String configJson = "{\"handler\":\"nullValueCheckHandler\",\"down\":true,\"next\":{\"handler\":\"priceCheckHandler\",\"next\":{\"handler\":\"stockCheckHandler\",\"next\":null}}}";
    //转成Config对象
    ProductCheckHandlerConfig handlerConfig = JSON.parseObject(configJson, ProductCheckHandlerConfig.class);
    return handlerConfig;
}

ConfigJson存储的处理器链路配置JSON串,在代码中可能不便于观看,我们可以使用json.cn等格式化看一下,如下,配置的整个调用链路规则特别清晰。

在这里插入图片描述
getHandlerConfigFile()类获到配置类的结构如下,可以看到,就是把在配置中心储存的配置规则,转换成配置类ProductCheckHandlerConfig对象,用于程序处理。注意,此时配置类中存储的仅仅是处理器Spring Beanname而已,并非实际处理器对象

请添加图片描述
接下来,通过配置类获取实际要执行的处理器。

步骤2:根据配置获取处理器

上面步骤1通过getHandlerConfigFile()方法获取到处理器链路配置规则后,再调用getHandler()获取处理器。
getHandler()参数是如上ConfigJson配置的规则,即步骤1转换成的ProductCheckHandlerConfig对象;根据ProductCheckHandlerConfig配置规则转换成处理器链路对象。代码如下:

/**
 * 使用Spring注入:所有继承了AbstractCheckHandler抽象类的Spring Bean都会注入进来。Map的Key对应Bean的name,Value是name对应相应的Bean
 */
@Resource
private Map<String, AbstractCheckHandler> handlerMap;

/**
 * 获取处理器
 * @param config
 * @return
 */
private AbstractCheckHandler getHandler (ProductCheckHandlerConfig config) {
    
    //配置检查:没有配置处理器链路,则不执行校验逻辑
    if (Objects.isNull(config)) {
    
        return null;
    }
    //配置错误
    String handler = config.getHandler();
    if (StringUtils.isBlank(handler)) {
    
        return null;
    }
    //配置了不存在的处理器
    AbstractCheckHandler abstractCheckHandler = handlerMap.get(config.getHandler());
    if (Objects.isNull(abstractCheckHandler)) {
    
        return null;
    }
    
    //处理器设置配置Config
    abstractCheckHandler.setConfig(config);
    
    //递归设置链路处理器
    abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));

    return abstractCheckHandler;
}

步骤2-1:配置检查。

代码14~27行,进行了配置的一些检查操作。如果配置错误,则获取不到对应的处理器。代码23行handlerMap.get(config.getHandler())是从所有处理器映射Map中获取到对应的处理器Spring Bean

注意第5行代码,handlerMap存储了所有的处理器映射,是通过Spring @Resource注解注入进来的。注入的规则是:所有继承了AbstractCheckHandler抽象类(它是Spring管理的Bean)的子类(子类也是Spring管理的Bean)都会注入进来。

注入进来的handlerMapMapKey对应BeannameValuename对应的Bean实例,也就是实际的处理器,这里指空值校验处理器价格校验处理器库存校验处理器。如下:
请添加图片描述
这样根据配置ConfigJson 步骤1:获取处理器配置)中handler:"priceCheckHandler"的配置,使用handlerMap.get(config.getHandler())便可以获取到对应的处理器Spring Bean对象了。

步骤2-2:保存处理器规则。

代码29行,将配置规则保存到对应的处理器中abstractCheckHandler.setConfig(config),子类处理器就持有了配置的规则。

步骤2-3:递归设置处理器链路

代码32行,递归设置链路上的处理器

//递归设置链路处理器 abstractCheckHandler.setNextHandler(this.getHandler(config.getNext()));

这一步可能不太好理解,结合ConfigJson配置的规则来看,似乎就很很容易理解了。

在这里插入图片描述

由上而下,NullValueCheckHandler 空值校验处理器通过setNextHandler()方法设置自己持有的下一节点的处理器,也就是价格处理器PriceCheckHandler

接着,PriceCheckHandler价格处理器,同样需要经过步骤2-1配置检查步骤2-2保存配置规则,并且最重要的是,它也需要设置下一节点的处理器StockCheckHandler库存校验处理器。

StockCheckHandler库存校验处理器也一样,同样需要经过步骤2-1配置检查步骤2-2保存配置规则,但请注意StockCheckHandler的配置,它的next规则配置了null,这表示它下面没有任何处理器要执行了,它就是整个链路上的最后一个处理节点。

通过递归调用getHandler()获取处理器方法,就将整个处理器链路对象串联起来了。如下:
请添加图片描述

友情提示递归虽香,但使用递归一定要注意截断递归的条件处理,否则可能造成死循环哦!

实际上,getHandler()获取处理器对象的代码就是把在配置中心配置的规则ConfigJson,转换成配置类ProductCheckHandlerConfig对象,再根据配置类对象,转换成实际的处理器对象,这个处理器对象持有整个链路的调用顺序


步骤3:客户端执行调用链路

public class HandlerClient {
    

  public static Result executeChain(AbstractCheckHandler handler, ProductVO param) {
    
      //执行处理器
      Result handlerResult = handler.handle(param);
      if (!handlerResult.isSuccess()) {
    
          System.out.println("HandlerClient 责任链执行失败返回:" + handlerResult.toString());
          return handlerResult;
      }
      return Result.success();
  }
}

getHandler()获取完处理器后,整个调用链路的执行顺序也就确定了,此时,客户端该干活了

HandlerClient.executeChain(handler, param)方法是HandlerClient客户端类执行处理器整个调用链路的,并接收处理器链路的返回值。

executeChain()通过AbstractCheckHandler.handle()触发整个链路处理器顺序执行,如果某个处理器校验没有通过!handlerResult.isSuccess(),则返回错误信息;所有处理器都校验通过,则返回正确信息Result.success()


总结:串联方法调用流程

基于以上,再通过流程图来回顾一下整个调用流程。

在这里插入图片描述


测试:代码执行结果

场景1:创建商品参数中有空值(如下skuId参数为null),链路被空值处理器截断,返回错误信息

//创建商品参数
ProductVO param = ProductVO.builder()
      .skuId(null).skuName("华为手机").imgPath("http://...")
      .price(new BigDecimal(1))
      .stock(1)
      .build();

测试结果

在这里插入图片描述

场景2:创建商品价格参数异常(如下price参数),被价格处理器截断,返回错误信息

ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").imgPath("http://...")
      .price(new BigDecimal(-999))
      .stock(1)
      .build();

测试结果

在这里插入图片描述

场景 3:创建商品库存参数异常(如下stock参数),被库存处理器截断,返回错误信息。

//创建商品参数,模拟用户传入
ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").imgPath("http://...")
      .price(new BigDecimal(1))
      .stock(-999)
      .build();

测试结果

在这里插入图片描述

场景4:创建商品所有处理器校验通过,保存商品。

//创建商品参数,模拟用户传入
ProductVO param = ProductVO.builder()
      .skuId(1L).skuName("华为手机").imgPath("http://...")
      .price(new BigDecimal(999))
      .stock(1).build();

测试结果

在这里插入图片描述



案例二:工作流,费用报销审核流程

同事小贾最近刚出差回来,她迫不及待的就提交了费用报销的流程。根据金额不同,分为以下几种审核流程。报销金额低于1000元,三级部门管理者审批即可,1000到5000元除了三级部门管理者审批,还需要二级部门管理者审批,而5000到10000元还需要一级部门管理者审批。即有以下几种情况:

  1. 小贾需报销500元,三级部门管理者审批即可。
  2. 小贾需报销2500元,三级部门管理者审批通过后,还需要二级部门管理者审批,二级部门管理者审批通过后,才完成报销审批流程。
  3. 小贾需报销7500元,三级管理者审批通过后,并且二级管理者审批通过后,流程流转到一级部门管理者进行审批,一级管理者审批通过后,即完成了报销流程。
    在这里插入图片描述

### UML图

AbstractFlowHandler作为处理器抽象类,抽象了approve()审核方法,一级、二级、三级部门管理者处理器继承了抽象类,并重写其approve()审核方法,从而实现特有的审核逻辑。

在这里插入图片描述

配置类如下所示,每层的处理器都要配置审核人、价格审核规则(审核的最大、最小金额)、下一级处理人。配置规则是可以动态变更的,如果三级部门管理者可以审核的金额增加到2000元,修改一下配置即可动态生效。

在这里插入图片描述

代码实现与案例一相似,感兴趣的自己动动小手吧~



四、责任链的优缺点

在这里插入图片描述

在这里插入图片描述

五、源码查看

github:https://github.com/rongtao7/MyNotes


如果喜欢这篇文章,请不要吝啬你的赞哦,创作不易,感谢!

请添加图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/rongtaoup/article/details/122638812

智能推荐

已解决(selenium操作火狐浏览器报错)TypeError: __init__() got an unexpected keyword argument ‘firefox_options‘-程序员宅基地

文章浏览阅读1.6w次,点赞8次,收藏18次。已解决(selenium操作火狐浏览器报错)TypeError: __init__() got an unexpected keyword argument ‘firefox_options‘_typeerror: __init__() got an unexpected keyword argument 'chrome_options

python方差_Python编程:方差、标准差、均方差、均方根值、均方误差、均方根误差...-程序员宅基地

文章浏览阅读315次。python实现代码# -*- coding: utf-8 -*-import mathdef get_average(records):"""平均值"""return sum(records) / len(records)def get_variance(records):"""方差 反映一个数据集的离散程度"""average = get_average(records)return sum(..._pycharm方差计算代码

jmeter接口测试实例(五)对cookies的自动管理,实现登录后修改用户信息_jmeter中post请求如何更改每次的用户名-程序员宅基地

文章浏览阅读902次。一、前提:cookie是保存在本地,用来记录用户信息,最典型的作用是判断用户是否已经登录。如果一个接口,需要用户登录后,才能进行操作,如下,修改用户信息那么这时候,就需要用到cookie去识别这个登录的用户,因为要修改的是这个登录的用户的信息。二、使用1、jmeter.properties 中 将CookieManager.save.cookies 设置为true。完成后重启jmet..._jmeter中post请求如何更改每次的用户名

python新建项目没有venv_pycharm配置venv虚拟环境-程序员宅基地

文章浏览阅读7.5k次,点赞2次,收藏2次。虚拟环境的好处:搭建独立的python运行环境,不与其他产生冲突虚拟环境有助于包的管理和防止版本冲突3.删除卸载方便虚拟环境的搭建:1.进入python的Scripts下,执行:pip3 install virtualenv2.选择建立虚拟环境的文件夹,我这边是直接在D盘software下面创建了一个virtualenv,如图:image.png3.创建虚拟环境:virtualenv --no-s..._pycharm没有venv怎么办

M1芯片的Mac在开发iOS项目时遇到的问题汇总(模拟器无法运行,Cocoapods错误等)_oc cocospods 安装依赖库之后不能使用模拟器了-程序员宅基地

文章浏览阅读2.3k次。--pod install时报错,且错误提示中有“ffi”字眼,提示错误:/Library/Ruby/Gems/2.6.0/gems/ffi-1.15.3/lib/ffi/library.rb:275: [BUG] Bus Error at 0x00000001042fc000 ruby 2.6.3p62 (2019-04-16 revision 67580) [universal.arm64e-darwin20]解决方案:juejin.cn/post/698064…--pod repo upd_oc cocospods 安装依赖库之后不能使用模拟器了

编辑器VSCode使用心得-程序员宅基地

文章浏览阅读540次。工欲善其事必先利其器,趁手的工具会使我们开发事半功倍。市面上的编辑器我用过许多,编辑器使用经历Notepad++,(开源)这个应该是最轻量级的吧,查看代码还好,编辑代码就算了官网地址:https://notepad-plus-plus.org/Brackets,(开源)这个也不错,github-star:30k了,上次发布版本是6月..._vscode nextick

随便推点

西部数码mysql怎么用织梦_[原创]织梦程序(DeDeCms)常见问题集锦-程序员宅基地

文章浏览阅读95次。前言:织梦程序是最知名的cms程序,使用广泛,但很多朋友对织梦还不太熟悉,通过工单分析得知,经常容易出现本文中的问题,本次统一整理出来,希望对新手朋友有帮助,本文写的非常详细,请仔细阅读,一、描述:“dedecms错误警告:连接数据库失败,可能数据库密码不对或数据库服务器出错”如图:分析:用织梦程序制作的站点做迁移服务器后容易出现这个问题,原因是程序中设置的数据库连接信息有误连接不到数据库,解决方..._西部数码支不支持织梦的程序

免安装mysql启动3534错误处理_mysql 免安装版本启动时 3534出错_mysql-程序员宅基地

文章浏览阅读295次。上午在win7下安装MYSQL,只到“net start mysql”这一步报错:3534的错误:于是在百度中搜索关键字“mysql服务无法启动3534”。参考以下两个链接中的方法,解决了3534启动失败的问题:mysql服务无法启动3534错误。http://jingyan.baidu.com/article/219f4bf7e978fcde442d38a9.htmlhttp://blog.cs..._免安装mysql启动失败

评测了10款画流程图软件,这4款最好用!(完全免费)_好用的流程图软件免费-程序员宅基地

文章浏览阅读5.1w次,点赞58次,收藏295次。最近在做项目和复习的时候,用了不少流程图软件给我帮了大忙,所以今天就来分享分享你在网上搜索一下流程图软件,能找到很多很多:但这些软件多数并不是专门绘制流程图的软件,它可能是一些思维导图软件、或者说一些产品交互原型图软件,使用时或多或少有些麻烦。而且,普遍这些软件缺点也很多,比如:只有在线版:ProcessOn(https://www.processon.com)导出功能收费:迅捷流程图软件体积庞大:VISIO就没有一款简单易用、绿色免费的流程图软件吗?阿虚花了不少时间,挨着_好用的流程图软件免费

在CentOS6.9安装Azkaban3.50.0_centos6.9 azkaban-程序员宅基地

文章浏览阅读1.4k次。cd /data/install_pkgwget https://github.com/azkaban/azkaban/archive/3.50.0.tar.gztar -zxvf 3.50.0.tar.gzvi /data/install_pkg/azkaban-3.50.0/azkaban-common/src/main/java/azkaban/utils/EmailMessage..._centos6.9 azkaban

Java项目基于ssm+vue.js的学生宿舍维修服务平台附带文章和源代码设计说明文档ppt-程序员宅基地

文章浏览阅读586次,点赞23次,收藏30次。博主介绍:CSDN特邀作者、985计算机专业毕业、某互联网大厂高级全栈开发程序员、码云/掘金/华为云/阿里云/InfoQ/StackOverflow/github等平台优质作者、专注于Java、小程序、前端、python等技术领域和毕业项目实战,以及程序定制化开发、全栈讲解、就业辅导、面试辅导、简历修改。精彩专栏 推荐订阅2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选题推荐2023-2024年最值得选的Java毕业设计选题大全:500个热门选题推荐。

ultralytics的YOLOv8改为自用版本_from ultralytics.utils.plotting import annotator, -程序员宅基地

文章浏览阅读944次。由于需要用pyqt给yolov8做一个界面,而ultralytics一层嵌一层,不是很好用,所以对它的这个源码进行精简,具体代码我放到了这里,ultralytics使用的版本是8.0.54。具体代码如下,需要根据自己的情况来修改data的配置文件以及权值文件,在代码的49和50行。_from ultralytics.utils.plotting import annotator, colors, save_one_box modul

推荐文章

热门文章

相关标签