[DDD] ValueObject的一种设计落地及应用

目录

  • 前言
  • 一、ValueObject
  • 二、设计
    • 2.1 接口
    • 2.2 单一值ValueObject
    • 2.3 单一字符串ValueObject
  • 三、实现
    • 3.1 示例
      • 3.1.1 PhoneNumber
      • 3.1.2 SocialCreditCode
  • 四、使用
    • 4.1 异常处理
    • 4.2 Json 反/序列化
      • 4.2.1 请求体
      • 4.2.2 HTTP接口
      • 4.2.3 用例
    • 4.3 JPA/MyBatis
      • 4.3.1 Converter或TypeHandler
      • 4.3.2 Entity
      • 4.3.3 Repository
      • 4.3.4 用例
    • 4.4 CACHE
      • 4.4.1 LocalBasedCache
      • 4.4.2 用例


前言

以前在InfoQ看到过这么一个讲座 Value-Objects-Dan-Bergh-Johnsson.

讲座的细节就不赘述了, 其中举例类似“电话号码”, “货币”在业务中的操作, 如果将这类有业务意义的字符串只是简单通过String/Integer等对象传递, 将丢失其业务意义, 最终编码, 测试都变得更繁琐. 同时程序员还需要在业务流程中时刻关心此类对象是否严格符合业务意义, 比如校验格式, 内容有效性等等. 实际工作看过来, 绝大多数人也都是这样做的.

如果使用ValueObject的设计思想, 设计一个包含“值”和其业务意义的对象, 例如“数量”一定非负之类的. 那么在实际使用中将使得校验, 编码, 测试, 甚至最基本的代码可读性都有明显提高.

本文介绍一种落地设计, 实现最常用的单一字符串值对象, 并参考Springboot环境, 实现接口自动化校验, DAO自动转换落库等等操作, 实现面向对象的编码.

Code Env: JDK21 + SpringBoot3+


一、ValueObject

值对象有两个主要特征:

  • 它们没有任何标识。
    • 没有唯一标识, 可以复用
  • 它们是不可变的。
    • Equals的比较是使用其“值”完成的

二、设计

本文仅对单一字符串值对象的设计作出说明, 因为此类值对象在实现接口, 或者落库时比较容易体会使用ValueObject的好处.

2.1 接口

仅分类, 因为不希望再手动调用校验, 这里就不设计校验的接口了

public interface ValueObject {}

定义单一值ValueObject

  • @JsonValue则提供了通过Jackson实现序列化的能力
    此时Jackson将直接序列化“值”而不是这个ValueObject对象
import com.fasterxml.jackson.annotation.JsonValue;

/**
 * @author hp
 */
public interface SingleValueObject<TYPE> extends ValueObject {

    @JsonValue
    TYPE value();
}

2.2 单一值ValueObject

实现ValueObject的基本特征

  • 值不可变, 在构造时需要提供值
  • equals, hashcode 通过其值完成, 而非对象本身.
  • @JsonAutoDetect 提供json序列化时获取非公共属性/方法的能力, 如果不提供公共getter, 则通过此注解获取值
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.hp.common.base.exception.NullValueObjectException;
import jakarta.annotation.PostConstruct;

import java.util.Objects;

/**
 * 配合jackson方便一些
 * <p>
 * 最好不要提供getter, 但是为了日志妥协一下
 *
 * @author hp
 * @see JsonAutoDetect;
 */
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC)
public abstract class AbstractSingleValueObject<TYPE> implements SingleValueObject<TYPE> {

    protected final TYPE value;

    @Override
    public TYPE value() {
        return value;
    }

    protected AbstractSingleValueObject(TYPE value) throws NullValueObjectException {
        if (Objects.isNull(value)) {
            throw new NullValueObjectException();
        }
        this.value = value;
    }

    protected abstract void validate(TYPE value) throws IllegalArgumentException;

    @Override
    public String toString() {
        return this.value.toString();
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        AbstractSingleValueObject<?> that = (AbstractSingleValueObject<?>) o;
        return Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}

2.3 单一字符串ValueObject

空字符串在此场景下理解为无意义的输入, 此时考虑通过直接在构造期间抛出异常的方式中断构造过程, 并返回NULL, 以保证没有合法输入就不构造出值对象的目的.

import cn.hutool.core.util.StrUtil;
import com.hp.common.base.exception.NullValueObjectException;

/**
 * @author hp
 */
public abstract class AbstractStringBasedSingleValueObject extends AbstractSingleValueObject<String> {
    protected AbstractStringBasedSingleValueObject(String value) throws NullValueObjectException {
        super(value);
        if (StrUtil.isEmpty(value)) {
            throw new NullValueObjectException();
        }
        validate(value);
    }
}

三、实现

需要说明的是, 实现类不一定完全实现了此类值在现实生活中包含的所有方面, 可以根据业务场景做简单调整和取舍. 比如下文的电话号码示例就省略了区号的信息.

3.1 示例

  • 私有化构造, 仅通过静态方法创建对象
    • @JsonCreator提供了Jackson在反序列化时指定创建对象方法的入口, 这里指定使用静态方法
  • 当输入NULL或空字符串时, 业务上视为无意义的输入, 将不做实例化
  • 当输入非“空”字符串时, 在构造时将根据子类实现的规则进行校验, 并在校验失败时抛出IllegalArgumentException供捕获

3.1.1 PhoneNumber

import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Preconditions;
import com.hp.common.base.exception.NullValueObjectException;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import com.hp.common.base.valueobject.Patterns;

import java.util.Optional;

/**
 * @author hp
 */
public final class PhoneNumber extends AbstractStringBasedSingleValueObject {

    private PhoneNumber(String phoneNumber) throws NullValueObjectException {
        super(phoneNumber);
    }

    @JsonCreator
    public static PhoneNumber of(String value) {
        try {
            return new PhoneNumber(value);
        } catch (NullValueObjectException ignore) {
            return null;
        }
    }

    @JsonCreator
    public static PhoneNumber of(Long value) {
        return Optional.ofNullable(value)
                .map(String::valueOf)
                .map(PhoneNumber::of)
                .orElse(null);
    }

    @Override
    public void validate(String value) throws IllegalArgumentException {
        Preconditions.checkArgument(Patterns.PHONE_PATTERN.asPredicate().test(value), "手机号码格式错误");
    }
}

3.1.2 SocialCreditCode

import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Preconditions;
import com.hp.common.base.exception.NullValueObjectException;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import com.hp.common.base.valueobject.Patterns;

/**
 * @author hp
 */
public final class SocialCreditCode extends AbstractStringBasedSingleValueObject {

    private SocialCreditCode(String value) throws NullValueObjectException {
        super(value);
    }

    @JsonCreator
    public static SocialCreditCode of(String value){
        try {
            return new SocialCreditCode(value);
        }catch (NullValueObjectException ignore){
            return null;
        }
    }

    @Override
    public void validate(String value) throws IllegalArgumentException {
        Preconditions.checkArgument(Patterns.CREDIT_CODE_PATTERN.asPredicate().test(value), "统一社会信用代码格式错误");
    }
}

四、使用

4.1 异常处理

可以根据公司情况, 自定义参数校验失败的自定义异常. 这里用最简单的IllegalArgumentException作示例

package com.hp.valueobject.exception;

import com.hp.common.base.model.Returns;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @author hp
 */
@Slf4j
@RestControllerAdvice
public class ExceptionControllerAdvice {

    @ExceptionHandler(IllegalArgumentException.class)
    public Returns<?> handleIllegalArgumentsException(IllegalArgumentException e) {
        log.error("请求参数错误", e);
        return Returns.fail().message(e.getMessage());
    }
}

4.2 Json 反/序列化

最常见场景之一, RESTful接口参数的Json序列化场景

4.2.1 请求体

package com.hp.valueobject.request;

import com.hp.common.base.model.Request;
import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.common.base.valueobject.socialcreditcode.SocialCreditCode;
import lombok.Data;

/**
 * @author hp
 */
@Data
public class ValueObjectPostRequest implements Request {

    private PhoneNumber phone;
 
    private SocialCreditCode socialCreditCode;
    
}

4.2.2 HTTP接口

package com.hp.valueobject.controller;

import com.hp.common.base.model.Returns;
import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.valueobject.request.ValueObjectPostRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

/**
 * @author hp
 */
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("valueobject")
public class ValueObjectController {

    @PostMapping("postRequest")
    public Returns<?> postRequest(@RequestBody ValueObjectPostRequest request) {
        return Returns.success().data(request);
    }

    @GetMapping("getRequest")
    public Returns<?> getRequest(@RequestParam PhoneNumber phone) {
        return Returns.success().data(phone);
    }
}

4.2.3 用例

用例格式为Idea http client.

POST Request, phone正确, 信用代码空字符串无意义

# Request
POST http://localhost:9988/valueobject/postRequest
Content-Type: application/json
Content-Length: 54
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Accept-Encoding: br,deflate,gzip,x-gzip

{
  "phone": "18123123123",
  "socialCreditCode": ""
} 

# Response
POST http://localhost:9988/valueobject/postRequest

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:17:13 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "phone": "18123123123",
    "socialCreditCode": null
  }
}
Response code: 200; Time: 37ms (37 ms); Content length: 219 bytes (219 B)

POST phone 参数错误 10 位

POST http://localhost:9988/valueobject/postRequest
Content-Type: application/json
Content-Length: 27
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Accept-Encoding: br,deflate,gzip,x-gzip

{
  "phone": "1812323123"
}
###

POST http://localhost:9988/valueobject/postRequest

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:12:38 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 500,
  "message": "手机号码格式错误",
  "data": null
}

Response code: 200; Time: 118ms (118 ms); Content length: 45 bytes (45 B)

GET phone格式正确

GET http://localhost:9988/valueobject/getRequest?phone=18123123123

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:21:27 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 200,
  "message": "操作成功",
  "data": "18123123123"
}
Response file saved.
> 2024-03-22T142127.200.json

Response code: 200; Time: 10ms (10 ms); Content length: 50 bytes (50 B)

GET phone格式错误

GET http://localhost:9988/valueobject/getRequest?phone=1812312313

HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:22:23 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{
  "code": 500,
  "message": "手机号码格式错误",
  "data": null
}
Response file saved.
> 2024-03-22T142223.200.json

Response code: 200; Time: 25ms (25 ms); Content length: 45 bytes (45 B)

4.3 JPA/MyBatis

4.3.1 Converter或TypeHandler

PhoneNumber示例

JPA converter

package com.hp.jpa.convertor;

import com.hp.common.base.valueobject.AbstractSingleValueObject;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Optional;

@Converter
public abstract class AbstractStringBasedSingleValueObjectConverter<T extends AbstractStringBasedSingleValueObject> implements AttributeConverter<T, String> {
    public AbstractStringBasedSingleValueObjectConverter() {
    }

    public String convertToDatabaseColumn(T attribute) {
        return (String)Optional.ofNullable(attribute).map(AbstractSingleValueObject::value).orElse("");
    }
}

package com.hp.valueobject.converter;

import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.jpa.convertor.AbstractStringBasedSingleValueObjectConverter;
import jakarta.persistence.Converter;

/**
 * @author hp
 */
@Converter
public class PhoneNumberJPAConverter extends AbstractStringBasedSingleValueObjectConverter<PhoneNumber> {
    @Override
    public PhoneNumber convertToEntityAttribute(String dbData) {
        return PhoneNumber.of(dbData);
    }
}

Mybatis-plus typeHandler

package com.hp.mybatisplus.convertor;

import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.apache.ibatis.type.JdbcType;

public abstract class AbstractStringBasedSingleValueObjectConverter<T extends AbstractStringBasedSingleValueObject> implements TypeHandlerCodeGenAdapter<T, String> {
    public AbstractStringBasedSingleValueObjectConverter() {
    }

    public void setParameter(PreparedStatement ps, int i, T t, JdbcType jdbcType) throws SQLException {
        ps.setString(i, (String)t.value());
    }
}
package com.hp.valueobject.converter;

import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.mybatisplus.convertor.AbstractStringBasedSingleValueObjectConverter;

import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * @author hp
 */
public class PhoneNumberMybatisTypeHandler extends AbstractStringBasedSingleValueObjectConverter<PhoneNumber> {
    @Override
    public PhoneNumber getResult(ResultSet rs, String columnName) throws SQLException {
        return PhoneNumber.of(rs.getString(columnName));
    }

    @Override
    public PhoneNumber getResult(ResultSet rs, int columnIndex) throws SQLException {
        return PhoneNumber.of(rs.getString(columnIndex));
    }

    @Override
    public PhoneNumber getResult(CallableStatement cs, int columnIndex) throws SQLException {
        return PhoneNumber.of(cs.getString(columnIndex));
    }
}

4.3.2 Entity

@Entity
@Table(name = "unified_social_credit_code")
@Getter
@Setter
public class UnifiedSocialCreditCode {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Convert(converter = PhoneNumberConverter.class)
    private PhoneNumber username;

    @Convert(converter = SocialCreditCodeConverter.class)
    private SocialCreditCode socialCreditCode;

4.3.3 Repository

直接传递ValueObject类型参数即可, QueryDSL也可以正常使用
注: MyBatis省略, 其低版本无法在自定义查询中自动通过typeHandler提取值, 需要手动 ValueObject.value();

@Repository
public interface JpaBasedUnifiedSocialCreditCodeDao extends BaseRepository<UnifiedSocialCreditCode, Long> {
    List<UnifiedSocialCreditCode> findAllBySocialCreditCodeIn(Collection<SocialCreditCode> codes);
}

4.3.4 用例

JPA

@Test
public void givenUSCC_whenQueryInDB_thenReturnsNonnull() {
    // given
    final String unifiedSocialCreditCode = "91510115MABRCTYM2W";
    final SocialCreditCode socialCreditCode = SocialCreditCode.of(unifiedSocialCreditCode);

    // when
    final List<UnifiedSocialCreditCode> list = unifiedSocialCreditCodeRepository.findAllBySocialCreditCode(Lists.newArrayList(socialCreditCode));

    // then
    assertThat(list).isNotEmpty().size().isGreaterThanOrEqualTo(1);
    final UnifiedSocialCreditCode first = list.getFirst();
    assertThat(first.getSocialCreditCode()).isEqualTo(socialCreditCode);
    assertThat(first.getUsername()).isNotNull();
}

4.4 CACHE

缓存场景, 这里主要是针对服务内缓存的说明, 例如使用Redis等中间件时, 都需要序列化, 此时使用jackson序列化即可

4.4.1 LocalBasedCache

例如使用Map作为容器的场景, 因为在AbstractSingleValueObject中已经重写了hashCode和equals, 使得ValueObject可以直接作为键完成存储和比较

@Slf4j
@Component
public class LocalBasedCache implements USCCCache {

    private final static Map<SocialCreditCode, List<UserCacheModel>> CACHE = Maps.newConcurrentMap();

    @Override
    public boolean exist(SocialCreditCode socialCreditCode) {
        return CACHE.containsKey(socialCreditCode);
    }

    @Override
    public void put(SocialCreditCode socialCreditCode, UserCacheModel model) {
        CACHE.compute(socialCreditCode, (key, value) -> {
            if (Objects.isNull(value)) {
                return Lists.newArrayList(model);
            } else {
                value.add(model);
                return value;
            }
        });
    }

    @Override
    public List<UserCacheModel> get(SocialCreditCode socialCreditCode) {
        return CACHE.getOrDefault(socialCreditCode, Collections.emptyList());
    }

    @Override
    public void remove(SocialCreditCode socialCreditCode) {
        CACHE.remove(socialCreditCode);
    }
}

4.4.2 用例

 @Test
 public void givenSocialCreditCode_whenCallPutAndExist_thenSuccess() {
     // given
     final LocalBasedCache cache = new LocalBasedCache();
     final SocialCreditCode socialCreditCode = SocialCreditCode.of("915101007130091284");
     final SocialCreditCode socialCreditCode2 = SocialCreditCode.of("915101007130091284");
     final SocialCreditCode socialCreditCode3 = SocialCreditCode.of("915101007130091283");

     // when
     cache.put(socialCreditCode, new UserCacheModel(1L,"1"));

     // then
     assertThat(cache.exist(socialCreditCode)).isTrue();
     assertThat(cache.exist(socialCreditCode2)).isTrue();
     assertThat(cache.exist(socialCreditCode3)).isFalse();
 }

测试结果
在这里插入图片描述

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

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

相关文章

Harmony(鸿蒙)Stage模型综述

设计思想 ​Stage模型的设计&#xff0c;是为了提供给开发者一个更好的开发方式&#xff0c;更好的适用于多设备、分布式场景。 ​Stage模型的设计思想如下图所示。 ​Stage模型的设计基于如下三个出发点&#xff1a; 应用进程的有序管理 随着设备的内存越来越大&#xff0…

SM4加密是什么?SM4算法在国密HTTPS协议中的作用

SM4加密算法是一种分组密码标准&#xff0c;由国家密码管理局于2012年3月21日发布&#xff0c;相关标准为“GM/T 0002-2012《SM4分组密码算法》&#xff0c;与国际上广泛使用的AES等算法类似&#xff0c;SM4同算法样用于保护数据的机密性&#xff0c;确保信息在传输过程中不被未…

罗德与施瓦茨 RS®FSV3000 信号与频谱分析仪

R&SFSV3000 信号与频谱分析仪 罗德与施瓦茨 R&SFSV3000 信号与频谱分析仪一键即可测量&#xff0c;可以通过基于事件的操作捕获信号&#xff0c;并使用 SCPI 记录器轻松编写脚本程序&#xff0c;从而快速设置复杂测量。分析仪还具有出色的测量速度&#xff0c;可实…

学习鸿蒙基础(8)

一、BuilderParam装饰器 当开发者创建了自定义组件&#xff0c;并想对该组件添加特定功能时&#xff0c;例如在自定义组件中添加一个点击跳转操作。若直接在组件内嵌入事件方法&#xff0c;将会导致所有引入该自定义组件的地方均增加了该功能。为解决此问题&#xff0c;ArkUI引…

关于「技术开发技能」课程

本课程分为三个部分&#xff0c;带您了解如何使用大模型平台、如何训练与部署大模型及生成式AI产品应用与开发&#xff0c;您将能了解各类服务的优势、功能、典型使用案例、技术概念和成本。 学习任选的两个课程模块&#xff0c;并通过测验者&#xff0c;将授予「技术开发技能…

【C++】哈希应用之布隆过滤器

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》《算法》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.布隆过滤器的提出…

vue基础——java程序员版(vue路由)

1、引入路由 在控制台执行vue ui&#xff0c;在插件市场里可以找到vue-router并导入。 ​ 一般情况下&#xff0c;vue会自动在main,js中引入vue-router&#xff0c;如下&#xff1a; import Vue from vue import App from ./App.vue import ./plugins/element.js import rou…

springboot整合aop实现自定义注解-方法运行异常重试demo

1.依赖引入 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId> </dependency>2.自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retentio…

简易电路设计,PW1605芯片实现24V/30V/48V限流过压保护功能

一般描述 PW1605 是一款电流限制开关&#xff0c;具有可编程输入过压保护和输出电压箝位功能。集成保护 N 沟道 FET 具有极低的 RDS&#xff08;ON&#xff09; 功能&#xff0c;PW1605有助于降低正常工作期间的功率损耗。可编程软启动时间控制启动期间输出电压的压摆率。独立的…

【LV15 day14 中断处理:按键驱动程序编写】

一、什么是中断 一种硬件上的通知机制&#xff0c;用来通知CPU发生了某种需要立即处理的事件 分为&#xff1a; 内部中断 CPU执行程序的过程中&#xff0c;发生的一些硬件出错、运算出错事件&#xff08;如分母为0、溢出等等&#xff09;&#xff0c;不可屏蔽外部中断 外设发…

一、SpringBoot3 介绍

本章概要 SpringBoot3 简介系统要求快速入门入门总结 1.1 SpringBoot3 简介 此处使用 SpringBoot 版本&#xff1a;3.0.5 https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html 无论使用XML、注解、Java配置类还是他们的混合用法&#xff0…

C语言学习--字符串和整型的转换

目录 整型→字符串 方法1&#xff1a;利用‘0’将单个数字转字符 方法2&#xff1a;利用sprintf函数 方法3&#xff1a;利用itoa函数 字符串→整型 方法1&#xff1a;利用-‘0’直接转换 方法2&#xff1a;利用atoi函数 整型→字符串 整形数据变成字符串&#xff0c;最…

数据结构从入门到精通——归并排序

归并排序 前言一、归并排序的基本思想二、归并排序的特性总结三、归并排序的动画展示四、递归实现归并排序的具体代码展示五、非递归实现归并排序 前言 归并排序是一种分治策略的排序算法。它将一个序列分为两个等长&#xff08;几乎等长&#xff09;的子序列&#xff0c;分别…

百度百科审核不通过全攻略,一看就会!

在撰写百度百科词条时&#xff0c;遇到审核不通过的情况可能会让人感到沮丧。然而&#xff0c;我们并不需要灰心&#xff0c;而是要通过一些方法来改善文章质量&#xff0c;使其符合百度百科的要求。腾轩科技传媒分享百度百科审核不通过全攻略&#xff0c;一看就会&#xff01;…

Docker Stack(堆栈) 部署多服务集群,多服务编排

1、Docker Stack简介 Docker Stack(堆栈) 是在 Swarm 上管理服务堆栈的工具。而在以前文章docker swarm集群搭建 介绍的 Docker Swarm 只能实现对单个服务的简单部署&#xff0c;于是就引出了Docker Stack。 上面我们介绍到 docker-compose&#xff1a;可以在一台机器上使用…

出差补助怎么发放更高效省心?这套攻略快看看

交补、餐补、话补等各类补助场景分散&#xff0c;无法实现一站式统筹管理。不仅如此&#xff0c;补贴核算也总是需要员工提供各类凭证&#xff0c;经过财务反复核实才能发放……出差发放补助原本是为了传递企业关怀&#xff0c;鼓励员工积极出差&#xff0c;由于发放和管理不当…

6 Spring-AOP

文章目录 1&#xff0c;AOP简介1.1 什么是AOP?1.2 AOP作用1.3 AOP核心概念 2&#xff0c;AOP入门案例2.1 需求分析2.2 思路分析2.3 环境准备2.4 AOP实现步骤步骤1:添加依赖步骤2:定义接口与实现类步骤3:定义通知类和通知步骤4:定义切入点步骤5:制作切面步骤6:将通知类配给容器…

新能源电车充电桩运营管理分析

摘要&#xff1a;近年来&#xff0c;我国大力推进新能源公共交通的发展&#xff0c;制定了一系列相关政策法规。作为公共充电设施的新能源充电桩也得到了发展和普及&#xff0c;其在新能源领域发挥着重要的保障作用。在当前&#xff0c;充电桩的管理还存在许多短板&#xff0c;…

MySql实战--全局锁和表锁 :给表加个字段怎么有这么多阻碍

今天我要跟你聊聊MySQL的锁。数据库锁设计的初衷是处理并发问题。作为多用户共享的资源&#xff0c;当出现并发访问的时候&#xff0c;数据库需要合理地控制资源的访问规则。而锁就是用来实现这些访问规则的重要数据结构。 根据加锁的范围&#xff0c;MySQL里面的锁大致可以分成…

开放式耳机性价比高的品牌有哪些呢?五大高性价比选购清单

不入耳开放式蓝牙耳机近两年开始火起来了&#xff0c;因为它佩戴的舒适性和安全性两方面受到了很多人的关注。开放式的设计&#xff0c;就算不放进耳朵里也能听歌&#xff0c;同时加上它独特的空气传导的传声途径&#xff0c;整体的音质还是很不错的。不压耳&#xff0c;不涨耳…