DDD系列 - 第2讲 从贫血模型、事务脚本到面向对象(富血模型)、DDD领域模型的跨越

目录

    • 一、灵魂拷问
    • 二、CRUD Boy现状
    • 三、贫血模型
    • 四、事务脚本
    • 五、从贫血模型演变到面向对象(富血模型)
    • 六、借助DDD领域模型摆脱事务脚本
    • 七、更多

一、灵魂拷问

Java作为面向对象的编程语言,使用Java编程的你面向对象了吗?

二、CRUD Boy现状

起初学习Java时,老师给我们介绍了Java语言各种面向对象的特性:封装(属性、方法)、继承、多态、关联关系(关联、依赖、组合、聚合)等等,当时觉得Java很牛B。工作了以后,写着写着代码连我们自己都没有觉察到我们写下Java代码已经变味了,好像丢失了面向对象分析与设计的精髓,似乎跟面不面向对象没啥关系了。作为一名 资深后端CRUD Boy,对于SpringMvc代码信手拈来,随便啥功能上来都这么整:

  • 数据库设计(实体表、关系表)
  • 设计接口、定义Controller
  • 使用Mybatis定义DAO接口、映射DB实体关系为Java POJO(实体对象、关系对象、贫血模型
  • 编写Service写业务逻辑(事务脚本)、调用DAO操作POJO

细心点就会发现,这里哪有面向对象啊?

  • POJO实体对象,仅有属性和getter/setter方法,对象的业务操作根本没有,严格来说不算是面向对象建模
  • Service有面向对象吗?完全就是面向过程编程,用方法(事务脚本)怼操作。
  • Controller为接口层,处理用户请求,纯技术维度的, 跟对象建模没关系。

三、贫血模型

假设存在实体:商品、商品标签,
且商品支持绑定多个商品标签。

数据库设计:

-- 商品表
CREATE TABLE goods (
	id bigint(20) NOT NULL COMMENT '主键ID',
	category_id bigint(20) NOT NULL COMMENT '所属分类ID',
	goods_name varchar(120) NOT NULL COMMENT '商品名称',
	manufacture_date date NOT NULL COMMENT '生成日期',
	expiration_date date NOT NULL COMMENT '过期日期',
	goods_weight decimal(10, 2) NOT NULL COMMENT '商品重量',
	goods_weight_unit varchar(16) NOT NULL COMMENT '重量单位',
	goods_desc varchar(1024) NOT NULL COMMENT '商品描述',
	goods_price decimal(20, 2) NOT NULL COMMENT '商品价格',
	goods_status int(11) NOT NULL COMMENT '商品状态(10已上架, 20已下架)',
	create_time datetime NOT NULL COMMENT '创建时间',
	update_time datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB;

-- 商品标签表
CREATE TABLE goods_tag (
	id bigint(20) NOT NULL COMMENT '主键ID',
	tag_name varchar(64) NOT NULL COMMENT '标签名称',
	tag_desc varchar(512) DEFAULT NULL COMMENT '标签描述',
	create_time datetime NOT NULL COMMENT '创建时间',
	update_time datetime DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB;

--  商品标签绑定表
CREATE TABLE goods_tag_binding (
	goods_id bigint(20) NOT NULL COMMENT '商品ID',
	tag_id bigint(20) NOT NULL COMMENT '商品标签ID'
) ENGINE = InnoDB;

Java POJO:

/**
 * 基础数据对象
 */
@Data
public class BaseEntity {
    @TableId(type = IdType.ASSIGN_ID)
    private Long id;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
}


/**
 * Goods数据对象
 */
@Data
@TableName("goods")
public class Goods extends BaseEntity {
    private Long categoryId;
    private String goodsName;
    private LocalDate manufactureDate;
    private LocalDate expirationDate;
    private BigDecimal goodsWeight;
    private String goodsWeightUnit;
    private String goodsDesc;
    private BigDecimal goodsPrice;
    private Integer goodsStatus;
}

/**
 * 商品标签
 */
@Data
@TableName("goods_tag")
public class GoodsTag extends BaseEntity {
    private String tagName;
    private String tagDesc;
}


/**
 * 商品与标签绑定关系 - 数据对象
 */
@Data
@TableName("goods_tag_binding")
public class GoodsTagBinding {
    private Long goodsId;
    private Long tagId;
}

通过以上代码,不难发现Goods、GoodsTag、GoodsTagBinding作为POJO,仅是对数据库表的映射,仅保留有getter/setter方法作为操作属性的手段,其中并没有任何实际的业务处理逻辑,实体对象中仅有属性没有操作,仅是作为数据的载体,如此即为贫血模型,而大量的业务操作都写到了Service实现中,导致Service过于臃肿。

四、事务脚本

由于DAO层采用了Mybatis,相较于完整的 ORM - 对象(Object)关系(Relation)映射,面向对象间的关联关系是无法直接通过Mybatis进行映射的,如以面向对象的方式建模商品和商品关联的标签,如下仅包含2个对象:Goods, GoodsTag,而商品与标签的关联关系可以通过Goods.goodsTags属性进行建模:

class Goods {
	private Long id;
	private String goodsName;
	//...
	//商品关联的标签集合
	private Set<GoodsTag> goodsTags;	
}

而在Mybatis中,除了Goods、GoodsTag对象,还需要格外引入数据库中的关系表GoodsTagBinding这第3个对象来表示商品和标签的关联关系,正是由于这种我们习以为常的映射,导致我们只能面向数据库编程,我们上层的业务逻辑只能针对Mybatis中的实体进行操作,除了需要操作业务中的对象实体Goods、GoodsTag,还不得不操作数据库中的关系对象GoodsTagBinding,使得本来的面向对象编程不得不变为面向数据库编程,如此即为事务脚本模式

事务脚本(Transaction Script)是一种应用程序架构模式,主要用于处理简单的业务场景。它将业务逻辑和数据库访问紧密耦合在一起,以便实现对数据的操作。
简单描述就是将“脚本”(SQL)进行打包,然后放在一个“事务”中运行。这也就是“事务脚本”命名的由来。

例如Service中新建商品的代码如下:

/**
 * 商品服务实现类
 */
@Service
public class GoodsServiceImpl implements GoodsService {

    @Resource
    private GoodsMapper goodsMapper;

    @Resource
    private CategoryMapper categoryMapper;

    @Resource
    private GoodsTagMapper goodsTagMapper;

    @Resource
    private GoodsTagBindingMapper goodsTagBindingMapper;

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SingleResponse<GoodsVo> createGoods(GoodsCreateDto goodsCreateDto) {
        //验证新增商品分类、标签是否存在
        this.validateGoods(goodsCreateDto.getCategoryId(), goodsCreateDto.getTagIds());
        
        //创建商品实体
        Goods goods = BaseAssembler.convert(goodsCreateDto, Goods.class);
        //设置商品状态
        goods.setGoodsStatus(GoodsStatus.UNSHELVED.getValue());
        //保存商品
        this.goodsMapper.insert(goods);
  
        //批量保存新的标签绑定关系
        if (!CollectionUtils.isEmpty(goodsCreateDto.getTagIds())) {
            for (Long tagId : goodsCreateDto.getTagIds()) {
                GoodsTagBinding goodsTagBinding = new GoodsTagBinding(goods.getId(), tagId);
                this.goodsTagBindingMapper.insert(goodsTagBinding);
            }
        }
        //转换GoodsDto
        return SingleResponse.of(BaseAssembler.convert(goods, GoodsVo.class));
    }

    /**
     * 验证商品分类、标签是否存在
     *
     * @param categoryId  分类ID
     * @param goodsTagIds 标签ID集合
     */
    private void validateGoods(Long categoryId, Collection<Long> goodsTagIds) {
        //验证分类ID是否存在
        Boolean existCategory = this.categoryMapper.existCategoryId(categoryId);
        Validates.isTrue(existCategory, "category id does not exist");

        //若标签ID集合为空,则直接验证通过
        if (Objects.isNull(goodsTagIds) || goodsTagIds.isEmpty()) {
            return;
        }
        //查询存在的标签数量
        Integer existGoodsTagCount = this.goodsTagMapper.countByIdIn(goodsTagIds);
        Validates.isTrue(existGoodsTagCount.equals(goodsTagIds.size()), "goods tags don't exist");
    }
}

受限于使用Mybatis仅对数据表进行了直接映射:Goods、GoodsTag、GoodsTagBinding,
且使用了贫血模型,我们不得不在上层的Service中编写大量的业务逻辑,直接面向数据库进行编程,
在面向对象的维度,商品和其关联的标签是作为一个完整的对象模型的,但是在面向数据库层面就变成了3个对象,所以Service中新增商品的逻辑就要额外操作GoodsTagBinding这个对象,可参见如上代码中createGoods方法就是典型的事务脚本模式:

  • 验证商品标签、分类是否存在
  • 转换商品、通过setter设置商品状态、保存商品Goods
  • 转换商品标签绑定关系列表、批量保存绑定关系GoodsTagBinding

至此,CURD Boy的三宗罪 就都清晰了,你占了哪几样:

  • Mybatis
  • 贫血模型
  • 事务脚本

还有一种事务脚本思维,例如修改订单的状态,面向数据库的思维就会想着执行SQL,且仅去修改相关的属性,而面向对象思维则把订单看做一个整体,对订单状态的修改也是对订单的修改,不去关心底层是修改一个属性还是修改全部属性,如此也即引出了DDD中的仓库Repository模式

事务脚本模式:

//OrderServcieImpl.updateOrderStatus中核心代码
this.orderMapper.updateStatus(orderId, newOrderStatus);

//OrderMapper.updateStatus方法
@Update("update orders set order_status = #{newOrderStatus} where id = #{orderId}")
Integer updateStatus(Long orderId, Integer newOrderStatus);

仓库模式:

//OrderServcieImpl.updateOrderStatus中核心代码
Order order = this.orderRepository.findById(orderId);
order.modifyStatus(newOrderStatus);
this.orderRepository.save(order);

五、从贫血模型演变到面向对象(富血模型)

贫血模型并没有对业务操作进行建模,仅有属性没有操作,
对象变成了对数据表的映射,进而导致想当然的面向对象编程变成了面向数据库编程,
Service承担了几乎全部的业务逻辑,最终呈现出的就是Servcie中的事务脚本模式。
模式没有好坏之分,只要适合就好。
但如果想要真正体验到面向对象建模的优势,将大量的业务逻辑都封装到对应的实体对象中,则可以借鉴DDD所倡导的富血模型、领域建模等思想。

所谓富血模型,就是对象中包含了大量的业务操作,不仅仅是数据属性的载体。
例如修改订单发货状态时,贫血模型中OrderServcieImpl.deliverOrder方法定义如下:

/**
 * 订单服务实现类
 */
public class OrderServiceImpl implements OrderService {
    //......
    
  	@Transactional(rollbackFor = Throwable.class)
	@Override
	public Response deliverOrder(OrderDeliverDto orderDeliverDto) {
	    Order order = this.orderRepository.selectById(orderDeliverDto.getOrderId());
	    Validates.notNull(order, "order not exist!");
	    
	    //验证订单状态
	    Validates.isTrue(OrderStatus.PAID.getValue().equals(order.getOrderStatus()), "order status is not PAID");
	    //修改订单已发货状态
	    order.setOrderStatus(OrderStatus.DELIVERED.getValue());
	    order.setExpressCode(orderDeliverDto.getExpressCode());
	    order.setDeliverTime(orderDeliverDto.getDeliveryTime());
	    order.setUpdateTime(LocalDateTime.now());
	    //更新订单
	    this.orderRepository.updateById(order);
	    
	    return Response.buildSuccess();
	}
	
}

以上代码中,充斥着大量的order.set方法,代码可读性差,代码意图不明确。
使用富血模型进行建模,则可将大量的set方法都统一封装到Order对象中的deliverOrder方法中,由Order对象完成它本该完成的业务职责,提升了业务的内聚性以及代码的可读性、可维护性,调整后代码如下:

/**
 * Order实体对象
 */
@Getter
@TableName("orders")
public class Order {
    private Long id;
    private BigDecimal orderPrice;
    private String receiveAddress;
    private OrderStatus orderStatus;
    private LocalDateTime payTime;
    private String expressCode;
    private LocalDateTime deliverTime;
    private LocalDateTime completeTime;
    private LocalDateTime cancelTime;
    
    /**
     * 发货订单
     *
     * @param expressCode 快递单号
     * @param deliverTime 发货时间
     */
    public void deliverOrder(String expressCode, LocalDateTime deliverTime) {
        //验证订单状态
        Validates.isTrue(OrderStatus.PAID.equals(this.getOrderStatus()), "order status is not PAID");
        //修改订单状态
        this.orderStatus = OrderStatus.DELIVERED;
        this.expressCode = expressCode;
        this.deliverTime = deliverTime;
        super.updateTime = LocalDateTime.now();
    }
}

/**
 * 订单服务实现类
 */
public class OrderServiceImpl implements OrderService {
  //......
  
  @Transactional(rollbackFor = Throwable.class)
  @Override
  public Response deliverOrder(OrderDeliverCommand orderDeliverCommand) {
      Order order = this.orderRepository.findById(orderDeliverCommand.getOrderId());
      Validates.notNull(order, "order not exist!");
      
      //订单已发货
      order.deliverOrder(orderDeliverCommand.getExpressCode(), orderDeliverCommand.getDeliveryTime());
      //保存订单信息
      this.orderRepository.save(order);
      
      return Response.buildSuccess();
  }
}

六、借助DDD领域模型摆脱事务脚本

在DDD中,应用架构主要分为4层:接口层、应用层、领域层、基础设施层,具体层次划分可见下图:

注:
D3S(DDD with SpringBoot)为本作者使用DDD过程中开发的框架及示例,
源码地址:https://gitee.com/luoex/d3s

在这里插入图片描述

在DDD中,应用层、领域层为其核心,领域层更是核心中的核心,领域层中的领域对象通常包括:

  • 聚合Aggregate,可以理解为对象建模的边界,落到代码层面可对应一个包,包中包含高内聚的实体、值对象等,其中仅有一个实体作为聚合根AggregateRoot只有聚合根才是访问聚合的唯一入口,聚合外部的对象不能引用除聚合根实体之外的任何内部对象
  • 实体Entity(可作为聚合根),即对应业务对象及操作的建模(富血模型),通常可通过ID唯一标识。
  • 值对象ValueObject,通常作为实体的属性(不具备ID),亦可包含该值对象相关的业务操作。
  • 仓库Repository(接口形式),负责聚合根(实体)的持久化,此层无需考虑底层数据库的实体、关系表,仅将聚合根作为一个整体进行持久化,具体底层的实现可在基础设施层Infrastructure实现Respository接口 又或者 直接通过ORM框架实现(如JPA、Hibernate)。
  • 领域服务DomainService,不适合Entity、ValueObject中的逻辑均可放在DomainServcie,通常负责协调多个聚合根(实体)间的交互业务编排、大段复杂业务的编排等。
  • 防腐层ACL(接口形式),负责与外部系统的交互,之所以叫做防腐层意为隔离外部的变化,不受外部变化的腐蚀,比如HttpClient接口、调用第三方服务等接口通过ACL进行定义,具体底层的实现可在基础设施层Infrastructure实现ACL接口 又或者 直接通过诸如OpenFeign等接口定义形式。

DDD中提倡的便是 富血模型,即由领域层的领域对象承担业务逻辑(面向对象建模),上层的应用层(应用服务Application Service)仅是对领域对象的流程编排,特别轻量的一层,将所有的业务逻辑都下沉到领域层。也就是说应用服务Service操作的都是领域对象,大量的业务逻辑都是由领域对象来完成的,Service仅是负责协调、调用领域对象来完成业务,并将领域对象(聚合根) 作为一个整体交由 仓库Respository 进行持久化,如此便规避了贫血模式、事务脚本的编程方式,直接面向领域对象进行编程。

以最初的新建商品为例,使用DDD的面向对象模型如下:

/**
 * Goods数据对象(聚合根)
 */
@Data
@TableName("goods")
public class Goods extends BaseEntity {
    private Long categoryId;
    private String goodsName;
    private LocalDate manufactureDate;
    private LocalDate expirationDate;
    private BigDecimal goodsWeight;
    private String goodsWeightUnit;
    private String goodsDesc;
    private BigDecimal goodsPrice;
    private Integer goodsStatus;
    
    //商品绑定的标签ID集合
    private Set<Long> tags;

	/**
     * 修改商品标签
     *
     * @param tags 商品标签ID集合
     */
    public void modifyTags(Set<Long> tags) {
        this.tags = tags;
    }

    /**
     * 上架商品
     */
    public void shelve() {
        super.updateTime = LocalDateTime.now();
        this.goodsStatus = GoodsStatus.SHELVED;
    }

    /**
     * 下架商品
     */
    public void unshelve() {
        super.updateTime = LocalDateTime.now();
        this.goodsStatus = GoodsStatus.UNSHELVED;
    }
}


/**
 * 商品标签(聚合根)
 */
@Data
@TableName("goods_tag")
public class GoodsTag extends BaseEntity {
    private String tagName;
    private String tagDesc;
}

注:
此处示例代码中的商品和标签的关联关系定义如下:
Set<Long> tags(实为标签ID集合)
而之前介绍面向对象建模时商品和标签的关联关系定义如下:
Set<GoodsTag> tags

此处的细微差别即揭示了DDD有别于面向对象建模的不同之处,
在DDD中强调聚合边界,聚合根作为持久化的整体,对聚合根的持久化便要对其包含的实体、值对象均进行持久化,加载查询时也要全部查询,所以聚合粒度的控制尤为重要,此处使用ID集合表示关联关系,也是为了更好的控制聚合的粒度,后续会再写一篇关于聚合的文章重点介绍此处。

如上代码中,原来的Goods、GoodsTag、GoodsTagBinding在使用DDD建模后仅保留了Goods、GoodsTag这2个实体,而Goods和GoodsTag间的关联关系则使用Goods.tags这个对象建模维度中的属性来表示(区别于数据库建模的关系表GoodsTagBinding),如此隔离了底层数据库思维(事务脚本模式),以面向对象的思维来对操作Goods,Goods作为一个整体(实为DDD中的聚合根)交由GoodsRespority(仓库)完成持久化,对于商品标签的操作即变成对商品中的标签ID集合属性Set<Long> tags进行的操作,如此使用DDD建模后的Service代码调整如下:

/**
 * 商品服务实现类
 */
public class GoodsServiceImpl implements GoodsService {
    //......

    @Transactional(rollbackFor = Exception.class)
    @Override
    public SingleResponse<GoodsVo> createGoods(GoodsCreateCommand goodsCreateCommand) {
        //创建商品实体
        Goods goods = GoodsAssembler.createGoods(goodsCreateCommand);
        
        //注:此处使用的Specification模式,负责规则验证,后续会在系列文章中单独介绍此模式
        //验证新增商品分类是否存在
        this.categoryExistSpecification.isSatisfiedBy(goods.getCategoryId());
        //验证商品标签是否存在
        this.goodsTagExistSpecification.isSatisfiedBy(goods.getTags());
        
        //保存商品
        goods = this.goodsRepository.save(goods);
        
        //转换GoodsDto
        return SingleResponse.of(BaseAssembler.convert(goods, GoodsVo.class));
    }
}

其中GoodsAssember.createGoods代码如下:

/**
 * Goods转换器
 */
public interface GoodsAssembler {

    static Goods createGoods(GoodsCreateCommand goodsCreateCommand) {
        return new Goods(
                goodsCreateCommand.getCategoryId(),
                goodsCreateCommand.getGoodsName(),
                goodsCreateCommand.getManufactureDate(), goodsCreateCommand.getExpirationDate(),
                goodsCreateCommand.getGoodsWeight(), WeightUnit.of(goodsCreateCommand.getGoodsWeightUnit()),
                goodsCreateCommand.getGoodsDesc(),
                goodsCreateCommand.getGoodsPrice(),
                GoodsStatus.UNSHELVED,
                goodsCreateCommand.getTagIds()
        );
    }
}

此处GoodsAssembler充当了工厂的角色,负责创建领域对象,此处的创建区别于对象构造函数的创建,DDD中通过工厂创建领域对象指的是初始创建,创建成功后会通过仓库持久化,强调领域对象的生命周期从此开始。而面向对象中的构造函数,可以在初始创建时使用,也可以在数据库中查询出来后被重新构造时使用,又或者对象转换时使用,又或者通过无参构造函数创建然后一堆setter设置等等。相较于工厂,构造函数的使用并没有显式的生命周期阶段标识意义,故在DDD中创建领域对象还是首推工厂模式,工厂的实现可以通过:

  • 领域层的工厂Factory
  • 领域层的Entity自身的创建方法
  • 应用层的转换器Assembler
  • 应用层的Command参数创建方法

本例中使用了应用层的转换器GoodsAssembler来实现工厂模式。

关于事务脚本、DDD建模的代码编排总结如下图:
在这里插入图片描述

七、更多

关于DDD的更多实战代码可参见:
D3S (DDD with SpringBoot) - https://gitee.com/luoex/d3s
D3S为本作者使用DDD过程中开发的框架及示例,旨在打造一款可落地的DDD(领域驱动设计)实现框架,提供了统一的DDD模型抽象,并扩展实现了DDD架构的一些关键特性,同时给出了截取自电商案例的基于D3S的示例实现(反模式、完整版、轻量版),力求打造一款即能发挥DDD的优势又能降低上手难度的DDD实现框架,也希望本示例能帮你迈出拥抱DDD的第一步。

后续会持续更新关于DDD的更多的知识、使用心得…敬请期待:

  • 聚合
  • 实体、值对象
  • 仓库
  • 工厂

参考:
https://www.martinfowler.com/eaaCatalog/transactionScript.html
https://www.martinfowler.com/bliki/AnemicDomainModel.html
https://mp.weixin.qq.com/s/7kYGnp0KJGozQZDH1ohnNg

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

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

相关文章

css实现div倾斜效果

效果如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head> <style> *{margin:0;padding: 0;} .box1{margin:30px 100px;width:100px;height:200px;background:blueviolet;} …

Android环境安装

一、环境 安装OS&#xff1a;Windows10 IDE: Android Studio Giraffe | 2022.3.1 Patch 2 Build #AI-223.8836.35.2231.10811636, built on September 15, 2023 JDK:Java8 二、安装Android Studio IDE和JDK Windows下构建安卓开发环境一点也不难就是有点麻烦。 第一、下载…

你的代码有bug

作为程序员&#xff0c;我们时常会收到这样的反馈&#xff1a;“你的代码有bug”。当面临这种情况时&#xff0c;我们可能会感到受伤和失落。然而&#xff0c;我们应该认识到&#xff0c;代码问题是一种常见现象&#xff0c;无论是谁都可能遇到。通过接受批评和建议&#xff0c…

代码随想录训练营Day2:1.有序数组的平方 2.长度最小的子数组3,螺旋矩阵

本专栏内容为&#xff1a;代码随想录训练营学习专栏&#xff0c;用于记录训练营的学习经验分享与总结。 文档讲解&#xff1a;代码随想录 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓…

JRebel热部署——效率提升100倍(程序员工具必备)

1. 下载JRebel 2.激活程序 这里推荐一个免费获取jrebel激活服务器地址和激活邮箱的地址:点击进入 进入网站之后就可以获取到激活链接和邮箱 点击进入激活 复制过去激活就可以 然后就可以看到激活成功了 3.如何使用 代码修改后&#xff0c;直接CtrlShitF9 即可重新启动 4…

Cross-Origin跨站问题详解(跨站请求、跨站cookie)

背景&#xff1a;我部署frontend和backend到两个不同的docker容器&#xff0c;前端路径为http://localhost:3000&#xff0c;后端路径为http://localhost:4000。我设置了用户登录功能&#xff0c;并使用cookie进行session管理。当我的前端登录时&#xff0c;创建了一个session&…

bat脚本设置变量有空格踩到的坑

SET PATH c:\xxx;%PATH% 我想把一个路径作为path环境变量最前面的一个&#xff0c;所以使用了上面的语句。 但是没有生效&#xff0c;我还以为是其他什么原因&#xff0c;后来又有一个类似的需求&#xff1a; set output output\x64 结果在使用 %output% 的时候是一个空格&…

2024最新fl studio 21.2.0.3842中文版完整下载

FL Studio 21.2.0.3842中文版完整下载是最好的音乐开发和制作软件也称为水果音乐软件。它是最受欢迎的工作室&#xff0c;因为它包含了一个主要的听觉工作场所。2024最新fl studioFL Studio 21版有不同的功能&#xff0c;如它包含图形和音乐音序器&#xff0c;帮助您使完美的配…

Android---MVP 中 presenter 声明周期的管理

我们经常在 Android MVP 架构中的 Presenter 层做一些耗时操作&#xff0c;比如请求网络数据&#xff0c;然后根据请求后的结果刷新 View。但是&#xff0c;如果按返回结束 Activity&#xff0c;而 Presenter 依然在执行耗时操作。那么就有可能造成内存泄漏&#xff0c;严重时甚…

什么是记忆能力与泛化能力

更多NLP文章在这里&#xff1a; https://github.com/DA-southampton/NLP_ability 谈到WDL&#xff0c;一个经常看到的总结是&#xff1a;Wide and Deep 模型融合 wide 模型的记忆能力和 Deep 模型的泛化能力&#xff0c;进行两个模型的联合训练&#xff0c;从而兼顾推荐的准确…

IPV6网络技术详细介绍

无状态和有状态并不是相互对立的&#xff0c;他们可以同时存在&#xff0c;也就是一张网卡上可以同时出现通过RA生成的IP以及通过DHCPv6获得的IP。 从图中可以看到&#xff0c;顺序为&#xff1a; 1、Stateless自动配置“链路本地地址”2、Stateless自动配置“全球地址”&…

解决Scrapy爬虫多线程导致抓取错乱的问题

目录 一、概述 二、问题分析 三、解决方案 四、案例分析 五、总结 一、概述 Scrapy是一个流行的Python爬虫框架&#xff0c;可以轻松地抓取网页数据并对其进行解析。然而&#xff0c;在抓取过程中&#xff0c;如果使用多线程进行并发处理&#xff0c;可能会遇到数据抓取错…

基于SSM的学生就业管理系统设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

【安卓13】谷歌原生桌面launcher3源码修改,修改桌面布局(首屏应用、小部件、导航栏、大屏设备任务栏)

前言 近期接到一个关于谷歌EDLA认证的需求&#xff0c;我负责的是谷歌原生桌面布局的修改&#xff0c;通过研究源码&#xff0c;将涉及到了一些修改思路发出来&#xff0c;大家可以参考一下有没有对你有用的信息。主要修改内容有&#xff1a; 1、搜索栏、底部导航栏未居中 2、…

RISC-V处理器设计(五)—— 在 RISC-V 处理器上运行 C 程序

目录 一、前言 二、从 C 程序到机器指令 三、实验 3.1 实验环境 3.11 Windows 平台下环境搭建 3.12 Ubuntu 平台下环境搭建 3.13 实验涉及到的代码或目录 3.2 各文件作用介绍 3.2.1 link.lds 3.2.2 start.S 3.2.3 lib 和 include 目录 3.2.4 common.mk 3.2.5 demo …

数据库安全:InfluxDB 未授权访问-Jwt验证不当 漏洞.

数据库安全&#xff1a;InfluxDB 未授权访问-Jwt验证不当 漏洞. InfluxDB 是一个开源分布式时序&#xff0c;时间和指标数据库。其数据库是使用 Jwt 作为鉴权方式&#xff0c;在用户开启认证时&#xff0c;如果在设置参数 shared-secret 的情况下&#xff0c;Jwt 认证密钥为空…

普华永道于进博会首发“企业数据资源会计处理一体化平台”

11月6日&#xff0c;在第六届中国国际进口博览会上&#xff0c;普华永道发布企业数据资源会计处理一体化平台&#xff08;英文名为Data Accounting Platform&#xff0c;简称DAP&#xff09;。该产品以普华永道“五步法”数据资源入表路径为理论依据&#xff0c;依托多年来普华…

QGIS导出Geoserver样式加载

1.在QGIS中加载并设计样式 加载数据之后按F7键即可打开样式编辑器 可以右键图层&#xff0c;点击属性中的符号化&#xff0c;有一个“基于规则”&#xff0c;可以设定规则或者比例尺范围。可以实现一定比例尺缩放可见或不可见的效果。 2.设计完样式之后右键图层导出 选择保…

【MATLAB源码-第75期】基于模拟退火算法(SA)的栅格路径规划,输出做短路径图和适应度曲线。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 模拟退火算法是一种启发式优化算法&#xff0c;通常用于解决组合优化问题&#xff0c;例如旅行商问题和图着色问题。它模拟了固体材料在退火过程中逐渐冷却达到稳定状态的行为&#xff0c;以寻找问题的全局最优解。 以下是模…

幸运素数(找出给定区间的所有幸运素数)

从键盘输入一个区间&#xff0c;程序判定输出区间的所有幸运素数。 (笔记模板由python脚本于2023年11月11日 12:44:43创建&#xff0c;本篇笔记适合熟悉python整型数据类型和基本编程技巧的coder翻阅) 【学习的细节是欢悦的历程】 Python 官网&#xff1a;https://www.python.o…