barry的异想世界

barry的异想世界 查看完整档案

重庆编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

barry的异想世界 发布了文章 · 9月27日

使用Spring Cloud Stream玩转RabbitMQ,RocketMQ和Kafka

前一章我们讲了《SpringBoot RabbitMQ消息队列的重试、超时、延时、死信队列》,从代码层面引用了非常多的rabbit特征代码,如:rabbitTemplate.convertAndSend() @RabbitListener(queues = "xxx")等,都是很简单的代码看起来一切都是合理的,但隐约感觉代码遭到了入侵。

业务的发展对MQ的依赖越来越重,地位也越来越高,对它的需求也越来越多。比如顺序消费,事务消息,回溯消费等,性能方面也有更高要求。越来越多的趋势提醒我们有更好MQ方案。

假如我们将“MQ从Rabbit替换成Rocket”的方案提上议程,就会发放这是一个非常浩大的工程。以前好多服务都是用的有RabbitMQ的特征代码,如果要替换相当于所有服务的代码都要较大的更新,这带来的运营风险是巨大的,需要非常多的开发测试资源的投入。

那回头来讲,我们最开始使用rabbitmq的时候能不能尽量隐藏特征代码吗,为以后的升级替换保留可能性。

这个时候就需要使用Spring Cloud的子组件Spring Cloud Stream。它是一个构建消息驱动微服务的框架,提供一套消息订阅消费的标准为不同供应商的消息中间件进行集成。目前官方提供KafkaRabbitMQ的集成实现,而阿里也实现对RocketMQ的集成。

一、 Spring Cloud Stream简介

Spring Cloud Stream应用由第三方的中间件组成。应用间的通信通过输入通道(input channel)和输出通道(output channel)完成。这些通道是由Spring Cloud Stream 注入的。而通道与外部的代理的连接又是通过Binder实现的。

二、 RabbitMQ集成

1. 引入包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>

2. 设置消息输入输出通道

public interface Source {
    String OUTPUT = "myOutput";

    @Output(OUTPUT)
    MessageChannel message();
}

public interface Sink {
    String INPUT = "myInput";

    @Input(INPUT)
    SubscribableChannel sub1();
}

输出通道为消息的发送方,输入通道为消息的接收方

myOutputmyInput为通道名,后续通过配置文件进行特性配置,切记两个通道的绑定最好是分开定义,不然有可能产生莫名错误

3. 消息特性配置

spring
  cloud:
    stream:
      bindings:
        myOutput:
          destination: login-user
        myInput: # 通道名,对应代码中的消费监听组
          destination: login-user # exchange
          group: logined-member   # 消费组

      rabbit:
        bindings:
          myOutput:
            producer:
              routing-key-expression: headers.routingKey   # 发送端路由key
              delayed-exchange: true    # 开启延时队列

          myInput:
            consumer:
              binding-routing-key: login.user.succeed   # 消费监听路由表达式
              delayed-exchange: true    # 开启延时队列
              auto-bind-dlq: true   # 绑定死信队列
              republish-to-dlq: true  # 重投到死信队列并带有报错信息

1) destination消息的主题名

在Rabbit中用来定义exchange以及成为queue的一部分

2) group消费组

  • 没有定义消费组时,如果启动多实例则一个消息同时都消费

  • 定义了消费组后,多实例共用一个queue,负载消费。从图可以看出queue名为destination.group组成

  • binding-routing-key:消费路由监听表达式
  • delayed-exchange: 开启延时队列
  • auto-bind-dlq:开启死信队列
  • republish-to-dlq:此设置可以让死信消息带报错信息

4. 消息的发送接收实现

发送消息

@Autowired
private Source source;

@GetMapping("/")
public void sendSucceed() {
    source.message().send(MessageBuilder.withPayload("Hello World...")
            .setHeader("routingKey", "login.user.succeed")
            .setHeader("version", "1.0")
            .setHeader("x-delay", 5000)
            .build());
}

这里可以为消息设置不同header,以现实不同的功能,这部分每种MQ有不同的特性,需要视情况而定

接收消息

@StreamListener(value = Sink.MY_INPUT_1, condition = "headers['version']=='1.0'")
public void receiveSucceed_v1(@Payload String message) {
    String msg = "StreamReceiver v1: " + message;
    log.error(msg);
}

5. 绑定消息通道

@EnableBinding(value = {Source.class, Sink.class})
@SpringBootApplication
public class RabbitApplication {
    public static void main(String[] args) {
        SpringApplication.run(RabbitApplication.class, args);
    }
}

实现这5步就可以正常发送接收消息了,你会发现除了引入不同的包和消息特性配置外,其它的代码都是抽象代码,没有任何rabbitmq的特征代码

三、 RocketMQ集成

根据RabbitMQ的相关代码,只需要修改引入包和特片配置就可以替换成RocketMQ了(一些特性功能除外

1. 引入包

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-stream-rocketmq</artifactId>
</dependency>

2. 消息特征配置

spring
  cloud:
    stream:
      bindings:
        myOutput:
          destination: login-user
          content-type: application/json

        myInput: # 通道名,对应代码中的消费监听组
          destination: login-user # exchange
          group: logined-member   # 消费者组, 同组负载消费

      rocketmq:
        binder:
          name-server: 127.0.0.1:9876

四、 Kafka集成

1. 引入包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

2. 消息特征配置

spring
    cloud:
    stream:
      bindings:
        myOutput:
          destination: login-user
          content-type: application/json

        myInput: # 通道名,对应代码中的消费监听组
          destination: login-user # exchange
          group: logined-member   # 消费者组, 同组负载消费


      kafka:
        binder:
          brokers: localhost:9092         #Kafka的消息中间件服务器
          auto-create-topics: true

五、 总结

由上面三个简单的例子可以看出,Spring Cloud Stream对消息订阅和消费做了高度抽象,用一套代码实现多种消息中间件的支持。同时它也可以非常简单的实现多种消息中间件的混用,大大扩展了消息中间件的玩法。

这里也建议如果没有特殊的特征场景需要实现的话,推荐使用Spring Cloud Stream组件来实现消息的订阅与消费,对中间件进行高度接耦。

六、源代码

文中代码由于篇幅原因有一定省略并不是完整逻辑,如有兴趣请Fork源代码
https://gitee.com/hypier/barry-cloud/tree/master/cloud-stream

七、请关注我的公众号

请关注我的公众号

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 9月13日

企业中台化落地:从战略分析到战术实践及架构演进过程

谈及中台,大都雾里看花,抱有一份敬畏之心,恐误导众人。但愿通过自己的思考与一同思考实践的朋友们一些启发,让中台建设得到它应有的收益,总结出更多的成功经验。

最近接触到一些公司说在做中台,交流之后大都是应该使用什么样的技术,如何解决数据一致性问题等。其中公司发展时间有长有短,有十几二十年的传统企业,也有三四个月才起步的创业团队。交流下来心中不免有些担忧,不太清楚所谓中台是追求一种技术实现还是一个流行噱头。

经过较长时间的思考、学习和实践,我发现了解得越多越不敢讲自己做的称之为中台。它是一种企业级业务构架设计方法论,如何做好还得从企业的愿景发出分析企业发展目标,合理利用资源对系统架构进行持续性的演进

每个企业的愿景和目标都不一样,对信息化诉求不一样,所构建出的中台系统自然也不一样,但是对企业经营有有效的提升是显尔易见的,所以设定好可量化的指标尤为重要。

背景

本文通过一个简单的例子来讲述如何进行中台化落地,企业实际过程远比这复杂得多。这是一家新零售企业,通过数字化转型获得新的业务增长点。“数字钱包”是公司的一个重点产品,项目特点是和其它业务相对独立且前端功能多样化,经商量解决引入中台化的思想来规划这个项目。

一、 战略分析

架构设计就是为未来而设计的,首先要清楚这个产品的愿景是什么,做这个事是目的是什么,要达成什么目标等。
战略分析是至上而下的,经历公司的发展历史,了解公司现在的发展状况,清楚公司未来的发展方向。此过程需要公司领导及各业务负责人参与沟通,达成一致意见。

1. 业务愿景分析

  • 增加企业经营效益:通过钱包预付款功能,增加资金沉淀,增强资金利用效率,同时降低顾客的促销成本
  • 增强顾客粘性:增加顾客复购机会,增强顾客用户体验

2. 业务模式分析

此产品主要用于公司各种类型资金交易的解决方案。目前包括:

  • 会员钱包功能:与会员系统打通,与会员系统强绑定,实现会员专属虚拟账户功能
  • 消费卡功能:主要用于线下实体卡业务,会员和非会员均可使用

    • 记名:用于会员
    • 不记名:用于赠送、福利卡
    • 亲属,用于家庭共用卡
  • 积分功能:用于购物返利,活动奖励等

3. 业务场景分析

业务构架图

主要为线上线下结合,不同的终端不一样的使用场景

  • 线上微信、APP实现可信任的快速支付服务
  • 线下门店、合作商户实现凭密码安全消费服务

4. 业务功能分析

不同的业务模式和业务场景有不一样的业务功能,这里需要去切分和隔离

  • 会员钱包:充值、消费等
  • 消费卡:充值、消费、密码、转账、挂失、开卡、销户等
  • 积分:赠送、消费、兑换等

5. 系统建设目标

  • 为满足销售任务的达成需要快速响应前端各业务场景的需求变化
  • 系统需要易于复制业务模式的创新尝试

二、 战术设计

战术设计就是根据战略目标制定具体的作战步骤。

  • 作战步骤需要紧贴公司战略步骤制定,根据当下的实际的资源情况进行合理的配置
  • 战略目标较为宽大,且较为耗时耗力,需要先选择一个较为容易实现的目标,取得阶段性的成果
  • 设定目标后还需要设计一个可量化的指标,得以评估中台化改造的收益,是否带来正向结果

本项目所处在钱包功能急将上线解决业务功能闭环的阶段,需要快速出成果故在后续系统结构不做大的改变的情况下,考虑到线下操作都是由公司员工完成,实施风险相对可控,故先完成线下基础版本。

由于第一阶段功能较简单,架构关键点在于如何保持系统的灵活扩展性,故前期的架构设计是重点,而后的功能实现就能顺理成章了。
可量化指标是实现新老功能的迁移,实现多端操作的整合。

三、 战术落地

1. 逻辑结构分析

领域结构图

(1)领域驱动设计

根据战略愿景的诉求,系统设计上要求保持灵活性,易于功能扩展和业务形态快速复制性。
我们采用DDD(领域驱动设计)思想来分析业务:

  • 将系统中的钱包账户交易流水划分为两个领域实体,形成聚合
  • 使用命令模式驱动业务操作,以交易流水实体为聚合根驱动钱包账户实体的变化
  • 使用领域事件来联动系统内与系统外相关功能

此阶段可以以事件风暴的形式,与领域专家一起使用通用语言来展开讨论,以达到业务、技术认识一致性

(2)抽象能力

系统需要保持满足的复用能力,可以方便快速的迭代出新的业务功能、业务规则和业务场景。故需要识别出这其中的业务共性和可变性,通过多种程序设计模式保持系统的灵活性

业务抽象逻辑

这个项目的业务共性就是所有的业务操作都是可以以交易流水为驱动,引发一个业务变化
可变性就是不同的业务变化,如金额增加、金额减少、账户锁定、密码变动等
可变的内容抽象为业务行为业务规则,不可变的就是交易处理交易完成交易事件发布

(3)系统扩展性

系统结构图
这里主要指系统间的扩展性。需要定义好相互通讯的协议和标准,通过定义好的流程将数字钱包系统与其它系统融合成一个整体。

2. 逻辑结构设计

程序结构图

上图为系统架构的核心逻辑,主要有3大部分组成

(1)WalletService 核心交易服务接口

所有交易操作的执行器

public interface WalletService {
    void done();
}
DefaultService 默认的抽象类,主要实现CheckPolicyBehavior接口的主线流程调用

现实类调用:

public class ConsumeService extends DefaultService {
    public ConsumeService(Wallet wallet, BigDecimal tradeAmount) {
        super(TradeRecord.builder().wallet(wallet).tradeAmount(tradeAmount).build());
    }
}

(2)Behavior 交易行为接口

public interface Behavior {
    void doAction();
    InOutFlag getInOutFlag();
}

public class CreditBehavior extends DefaultBehavior {
    private final BigDecimal tradeAmount;

    public CreditBehavior(Wallet wallet, BigDecimal tradeAmount) {
        super(wallet);
        this.tradeAmount = tradeAmount;
    }

    @Override
    public void doAction() {
        super.doAction();
        wallet.setBalance(wallet.getBalance().add(tradeAmount));
    }

    @Override
    public InOutFlag getInOutFlag() {
        return InOutFlag.IN;
    }
}

根据设计方案将所有钱包账户操作都定义为行为,此处实现的是具体的账户操作逻辑,实现类继承至抽象类进行简单的封闭。
getInOutFlag()是对行为产生的资金进出结果的配置

(3)CheckPolicy 交易规则接口

public interface CheckPolicy {
    void check();
}

public class NoOverdraftAllowed implements CheckPolicy {
    private final Wallet wallet;
    private final BigDecimal tradeAmount;

    public NoOverdraftAllowed(Wallet wallet, BigDecimal tradeAmount) {
        this.wallet = wallet;
        this.tradeAmount = tradeAmount;
    }

    @Override
    public void check() {
        if (wallet.getBalance().compareTo(tradeAmount) < 0){
            throw new BizException("余额不足");
        }
    }
}

实现类用于判断相关操作是否存在余额不足(透支)情况,如果有则中止执行

3. 设计模式的运用

(1) 模板模式

public abstract class DefaultService implements WalletService {

    protected abstract static class TradeConfig {
        public abstract TradeType tradeType();
        public abstract Behavior behavior();
        public abstract List<CheckPolicy> checkPolicies();
    }

    protected abstract TradeConfig tradeConfig();

    @Override
    public void done() {
        check();
        tradeConfig().behavior().doAction();
        tradeRecord.setBalance(tradeRecord.getWallet().getBalance());
        tradeRecord.setTradeStatus(TradeStatus.SUCCEED);
    }
}

public class LockService extends DefaultService {

    public LockService(Wallet wallet) {
        super(TradeRecord.builder().wallet(wallet).build());
    }

    @Override
    protected TradeConfig tradeConfig() {
        return new TradeConfig() {
            @Override
            public TradeType tradeType() {
                return TradeType.LOCK;
            }

            @Override
            public Behavior behavior() {
                return new LockBehavior(getWallet());
            }

            @Override
            public List<CheckPolicy> checkPolicies() {
                return CheckPolicyBuilder.builder()
                        .add(new NoAvailableStatusAllowed(getWallet()))
                        .build();
            }
        };
    }
}

在主交易流程中,将共有的流程放在done()中执行,将可变的部分抽象成配置模板供现实类现实

模板模式的优点:

  • 扩展性好,对不变的代码进行封装,对可变的进行扩展;
  • 可维护性好,因为将公共代码进行了提取,使用的时候直接调用即可;
  • 现实类在有限的空间扩展,不影响主流程的实现;

(2) 策略模式

public class RechargeRollbackService extends DefaultService {
    private final TradeRecord sourceTrade;

    public RechargeRollbackService(TradeRecord sourceTrade) {
        // ...
    }

    @Override
    protected DefaultService.TradeConfig tradeConfig() {
        return new TradeConfig() {
            @Override
            public TradeType tradeType() {
                return TradeType.RECHARGE_ROLLBACK;
            }

            @Override
            public Behavior behavior() {
                return new DebitBehavior(getWallet(), getTradeAmount());
            }

            @Override
            public List<CheckPolicy> checkPolicies() {
                return CheckPolicyBuilder.builder()
                        .add(new NoRechargeTypeAllowed(sourceTrade))
                        .add(new NoTimeoutAllowed(sourceTrade))
                        .add(new NoOverdraftAllowed(getWallet(), getTradeAmount()))
                        .add(new NoAvailableStatusAllowed(getWallet()))
                        .build();
            }
        };
    }
}

在交易行为和交易规则的设计中使用了策略模式,可根据不同业务操作设计不同的策略

策略模式的优点

  • 扩展性好,可以在不修改对象结构的情况下,为新的算法进行添加新的类进行实现;
  • 灵活性好,可以对算法进行自由切换;
  • 结构清晰,代码可读性高;

(3) 组合模式

public abstract class DefaultTowPCService extends DefaultService implements TwoPCWalletService {

    public DefaultTowPCService(TradeRecord tradeRecord) {
        super(tradeRecord);
    }

    @Override
    public void process() {
        check();
        getTradeRecord().setTradeStatus(TradeStatus.PROCESSING);
    }
}

public class RechargeService extends DefaultTowPCService {
    public RechargeService(Wallet wallet, BigDecimal tradeAmount){
        super(TradeRecord.builder().wallet(wallet).tradeAmount(tradeAmount).build());
    }
}

TwoPCWalletServiceDefaultTowPCService 用于在原有接口基础上扩展的二阶段提交功能,此处为了保持DefaultService功能的单一性,并没有在原有类上进行功能扩展,而是使用组合模式进行功能扩展
此现实类就现实了WalletServiceTwoPCWalletService两个接口

策略模式的优点

  • 扩展性好,可以在不修改对象结构的情况下,为新的功能增加新的现实;
  • 变动性小,不需要现实的类,不修改代码

四、 系统架构演进

1. 结构说明

程序层次结构图

  • wallet-common为内部各层的共用实体对象
  • wallet-domain为抽象的业务基础逻辑与规则,一般不具体直接业务场景支持。需要与wallet-service结合实现完成的业务逻辑。在团队开发层面,这一个层次的独立可以有效控制基础规则的代码稳定性
  • wallet-repository专注数据对象的持久化,与业务逻辑进行隔离
  • wallet-service为符合业务场景的业务功能实现,主要依靠wallet-domainwallet-repository的相互组合来完成
  • wallet-client用于对外的前端接口层
  • wallet-provider用于对内部微服务的接口层

2. 第一阶段 单一结构模式

单一运行结构图

这一阶段,业务模式单一,业务功能单一,业务量较少,开发人员也较少,将所有模块打包运行在一个jvm中

3. 第二阶段 多业务结构模式

多业务运行结构图

这一阶段,随着业务模式和业务量的增加,模式间的业务功能也不相同或有互斥性,单一结构已不能满足,故将基础模块封闭为SDK,每种业务模式单独一套系统独立维护

4. 第三阶段 业务中台化

业务中台结构图

随着业务模式、业务量、业务功能需求持续增加。由于各业务线独立运营,导致SDK版本不一致,增加了维护成本。多团队维护架构导致功能重复且实现过程参差不齐,带来一定维护成本且系统间无法实现复用,同时每条业务线独立运营也带来用人成本的增加。

这一阶段的目标是将大部分的共用功能下沉形成标准化逻辑,统一维护版本,减少人力成本,故架构演进采用中台化的思想

  • 业务能力(wallet-domain):将业务高度抽象形成一个一个基础能力
  • 业务域(wallet-service):将业务能力根据业务场景进行组合编排形成功能域
  • 持久层(wallet-repository):根据不同的情况,将数据持久化到中台或前台
  • 前台触点(wallet-client):根据不同的场景定制不用的前台API

五、综述

本文通过一个较简单的例子讲述中台架构演进的过程,实际场景远比此复杂。但最重要的不是最终的系统架构,而是对系统演进的思考和实施过程,因为中台的形态也是随时间不断变化的。

六、源代码

文中代码由于篇幅原因有一定省略并不是完整逻辑,如有兴趣请Fork源代码
https://gitee.com/hypier/barry-wallet

七、请关注我的公众号

请关注我的公众号

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 9月6日

SpringCloud轻松集成Dubbo实现RPC调用

很久之前在做微服务架构选型的时候就听说阿里的微服务RPC框架dubbo,当时与Spring Cloud以http协议调用的架构做对比。发现dubbo的rpc框架学习成本较高,代码入侵性强,本身生态不完整,需要整合多个外部组件,故选择了相对性能弱一点的Spring Cloud全家桶。

直到Spring Cloud Alibaba的出现,使用Nacos作为服务发现与注册,同时兼容使用Feignhttp方式和使用Dubborpc方式调用。

Spring Cloud 为什么需要RPC

在Spring Cloud构建的微服务系统中,大多数的开发者使用都是官方提供的Feign组件来进行内部服务通信,这种声明式的HTTP客户端使用起来非常的简洁、方便、优雅,并且和开发平台、语言无关,但是通常情况下,HTTP并不会开启KeepAlive功能,即当前连接为短连接,短连接的缺点是每次请求都需要建立TCP连接,这使得其效率变的相当低下。

对外部提供REST API服务是一件非常好的事情,但是如果内部调用也是使用HTTP调用方式,就会显得显得性能低下,Spring Cloud默认使用的Feign组件进行内部服务调用就是使用的HTTP协议进行调用,这时,我们如果内部服务使用RPC调用,对外使用REST API,将会是一个非常不错的选择。

引用至:Dubbo 与 Spring Cloud 完美结合

使用Dubbo Spring Cloud使用内部的RPC协议调用几乎是零成本的改造。

一、系统结构

  • cloud-gateway 作为cloud集群的网关,外部的路由转发使用http协议,内部的服务调用使用dubbo协议
  • cloud-usercloud-mq之间的远程调用使用dubbo协议
  • 使用Nacos作为服务注册与发现配置中心的服务
  • 使用Sentinel作为服务间httpdubbo调用的流量控制服务

目录结构

├── cloud-admin         # 服务监控
├── cloud-gateway       # 服务网关
├── cloud-mq            # mq服务
├── cloud-provider      # 服务接口
└── cloud-user          # user服务

二、服务接口提供方实现

1. 服务接口定义

public interface UserProvider {
    UserDTO checkUser(String userName, String password);
    UserDTO findByUserName(String userName);
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserDTO implements Serializable {
    String userName;
    String realName;
    String password;
}
  • 服务接口是服务提供方和消费方的契约,包含服务的方法传输对象DTO。由于涉及多个应用服务的引入,最好是将其独立成Module
  • DTO对象必须实现Serializable接口

2. 引入dubbo

POM

 <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

spring-boot-starter-actuator 也是必须的

3. 服务接口实现

import org.apache.dubbo.config.annotation.Service;

@Service
public class UserProviderImpl implements UserProvider {

    @Autowired
    private UserService userService;

    @Override
    public UserDTO checkUser(String userName, String password) {

        User user = userService.checkUser(userName, password);
        return UserConvertor.toDTO(user);
    }

}

@Service必须是org.apache.dubbo.config.annotation.Service

4. 配置Dubbo服务相关的信息

spring:
  main:
    allow-bean-definition-overriding: true
    
dubbo:
  scan:
    base-packages: fun.barryhome.cloud.provider   #指定 Dubbo 服务实现类的扫描基准包
  protocols:
    dubbo:
      name: dubbo   # Dubbo 的协议名称
      port: -1      # port 为协议端口( -1 表示自增端口,从 20880 开始)

  registry:
    address: spring-cloud://localhost       # 挂载到 Spring Cloud 注册中心

启动后有可能出现连接失败,不影响使用

java.net.ConnectException: Connection refused (Connection refused)
    at java.net.PlainSocketImpl.socketConnect(Native Method) ~[na:1.8.0_111]
    at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) ~[na:1.8.0_111]
    at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) ~[na:1.8.0_111]

三、服务调用方实现

1. 引入依赖包

<!--服务接口-->
<dependency>
    <groupId>fun.barryhome</groupId>
    <artifactId>cloud-provider</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
 <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

2. 调用服务接口

import org.apache.dubbo.config.annotation.Reference;

public class UserController {

    @Reference
    private UserProvider userProvider;

    @GetMapping(value = "/sessionUser")
    public UserDTO sessionUser(HttpServletRequest request) {

        String userName = request.getHeader("X-User-Name");
        if (Strings.isEmpty(userName)) {
            throw new RuntimeException("没有找到用户");
        }

        return userProvider.findByUserName(userName);
    }
}

3. 配置Dubbo服务相关的信息

dubbo:
  cloud:
    subscribed-services: cloud-user   # 服务提供方的服务名
  consumer:
    check: false
    loadbalance: "leastactive"    # 最小活跃数负载均衡

  registry:
    # 挂载到 Spring Cloud 注册中心
    address: spring-cloud://localhost

dubbo.consumer.check:用于启动时是否检查服务提供方是否运行正常,如果不正常将不能启动调用方

dubbo.consumer.loadbalance:负载均衡策略

  • RandomLoadBalance:随机,按权重设置随机概率
  • ConsistentHashLoadBalance:一致性哈希算法
  • LeastActiveLoadBalance:最小活跃数负载均衡
  • RoundRobinLoadBalance:根据权重进轮训

四、总结

  1. 服务间使用了长连接,在正常运行时,提供方某个节点断掉后会需要一段时间来切换,可使用sentinel来控制快速切换可用节点
  2. 使用dubbo进行远程调用,内部调用性能上有所提供,调用方式上也相对简单
  3. sentinel配合,合理使用负载策略,可实现更多功能,如灰度发布,版本控制等
  4. 性能的提升让调用链增加成为可能性,可实现更小粒度的微服务拆分与组合

五、源代码

https://gitee.com/hypier/barry-cloud

六、请关注我的公众号

请关注我的公众号

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 8月31日

SpringBoot RabbitMQ消息队列的重试、超时、延时、死信队列

今天介绍使用SpringBoot实现RabbitMQ消息队列的高级用法。

  • MQ安装
  • 自动创建
  • 消息重试
  • 消息超时
  • 死信队列
  • 延时队列

一、RabbitMQ的安装

众所周知,RabbitMQ的安装相对复杂,需要先安装Erlang,再按着对应版本的RabbitMQ的服务端,最后为了方便管理还需要安装rabbitmq_management管理端插件,偶尔还会出现一些安装配置问题,故十分复杂。
在开发测试环境下使用docker来安装就方便多了,省去了环境和配置的麻烦。

1. 拉取官方image

docker pull rabbitmq:management

2. 启动RabbitMQ

docker run -dit --name MyRabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin -p 15672:15672 -p 5672:5672 rabbitmq:management
rabbitmq:management: image:tag
--name:指定容器名;
-d:后台运行容器;
-t:在新容器内指定一个伪终端或终端;
-i:允许你对容器内的标准输入 (STDIN) 进行交互;
-p:指定服务运行的端口(5672:应用访问端口;15672:控制台Web端口号);
-e:指定环境变量;(RABBITMQ_DEFAULT_USER:默认的用户名;RABBITMQ_DEFAULT_PASS:默认用户名的密码);

至此RabbitMQ就安装启动完成了,可以通过http://localhost:15672 登陆管理后台,用户名密码就是上面配置的admin/admin

二、使用SpringBoot自动创建队列

1. 引入amqp包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2. MQ配置

bootstrap.yml 配置

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: /
    username: admin
    password: admin
    listener:
      simple:
        concurrency: 5
      direct:
        prefetch: 10

concurrency:每个listener在初始化的时候设置的并发消费者的个数
prefetch:每次从一次性从broker里面取的待消费的消息的个数

rabbitmq-spring.xml配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">

    <!--接收消息的队列名-->
    <rabbit:queue name="login-user-logined" />
    <!--声明exchange的名称与类型-->
    <rabbit:topic-exchange name="login_barryhome_fun">
        <rabbit:bindings>
            <!--queue与exchange的绑定和匹配路由-->
            <rabbit:binding queue="login-user-logined" pattern="login.user.logined"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
</beans>

rabbit:topic-exchange:声明为topic消息类型
pattern="login.user.logined":此处是一个表达式,可使用“*”表示一个词,“#”表示一个或多个词

3. 消息生产端

@Autowired
RabbitTemplate rabbitTemplate;

@GetMapping("/send")
public LoginUser SendLoginSucceedMessage(){
    LoginUser loginUser = getLoginUser("succeed");
    // 发送消息
    rabbitTemplate.convertAndSend(MessageConstant.MESSAGE_EXCHANGE,
            MessageConstant.LOGIN_ROUTING_KEY, loginUser);
    return loginUser;
}

@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements Serializable {
    String userName;
    String realName;
    String userToken;
    Date loginTime;
    String status;
}

这里需要注意的是默认情况下消息的转换器为SimpleMessageConverter只能解析stringbyte,故传递的消息对象必须是可序列化的,实现Serializable接口

SimpleMessageConverter only supports String, byte[] and Serializable payloads, received: fun.barryhome.cloud.dto.LoginUser

4. 消息消费端

@Component
public class ReceiverMessage {

    @RabbitListener(queues = "login-user-logined")
    public void receiveLoginMessage(LoginUser loginUser) {
        System.err.println(loginUser);
    }
}

@RabbitListener(queues = "login-user-logined"):用于监听名为login-user-logined 队列中的消息

5. 自动创建Queue

@SpringBootApplication
@ImportResource(value = "classpath:rabbitmq-spring.xml")
public class MQApplication {
    public static void main(String[] args) {
        SpringApplication.run(MQApplication.class, args);
    }
}

在没有导入xml且MQ服务器上没有列队的情况下,会导致找不到相关queue的错误

channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no queue 'login-user-logined' in vhost '/', class-id=50, method-id=10)

而导入之后将自动创建exchangequeue

三、消息重试

默认情况下如果有消息消费出错后会一直重试,造成消息堵塞

如图可观察unackedtotal一直是1,但deliver/get飙升

消息堵塞之后也影响到后续消息的消费,时间越长越来越多的消息将无法及时消费处理。
如果是单条或极少量的消息有问题可通过多开节点concurrency将正常的消息消息掉,但如果较多则全部节点都将堵塞。

如果想遇到消息消费报错重试几次就舍弃,从而不影响后续消息的消费,如何实现呢?

spring:
  rabbitmq:
    host: localhost
    port: 5672
    virtual-host: /
    username: admin
    password: admin
    listener:
      simple:
        concurrency: 5
        prefetch: 10
        retry:
          enabled: true   # 允许消息消费失败的重试
          max-attempts: 3   # 消息最多消费次数3次
          initial-interval: 2000    # 消息多次消费的间隔2秒

以上配置允许消息消费失败后重试3次,每次间隔2秒,如果还是失败则直接舍弃掉本条消息。
重试可解决因非消息体本身处理问题产生的临时性的故障,而将处理失败的消息直接舍弃掉只是为其它消息正常处理的权益之计而以,将业务操作降到相对低的影响。

四、消息超时

消息重试可解决因消息处理报错引起的问题。如果是消息处理过慢导致错过时效,除了可在处理逻辑中进行处理外,也可以通过消息的超时机制来处理,设定超时时间后将消息直接舍弃。

修改rabbitmq-spring.xml

<rabbit:queue name="login-user-logined">
    <rabbit:queue-arguments>
    <entry key="x-message-ttl" value="10000" value-type="java.lang.Long" />
    </rabbit:queue-arguments>
</rabbit:queue>

x-message-ttl:在消息服务器停留的时间(ms)


如果配置前已存在queue将不能被修改,需要删除原有queue后自动创建
创建成功后会在Features中有TTL标识

五、死信队列

死信队列就是当业务队列处理失败后,将消息根据routingKey转投到另一队列,这样的情况有:

  • 消息被拒绝 (basic.reject or basic.nack) 且带 requeue=false不重新入队参数或达到的retry重新入队的上限次数
  • 消息的TTL(Time To Live)-存活时间已经过期
  • 队列长度限制被超越(队列满,queue的"x-max-length"参数)

1. 修改rabbitmq-spring.xml

<!--接收消息的队列名-->
<rabbit:queue name="login-user-logined">
    <rabbit:queue-arguments>
        <entry key="x-message-ttl" value="10000" value-type="java.lang.Long"/>
        <!--死信的交换机-->
        <entry key="x-dead-letter-exchange" value="login_barryhome_fun"/>
        <!--死信发送的路由-->
        <entry key="x-dead-letter-routing-key" value="login.user.login.dlq"/>
    </rabbit:queue-arguments>
</rabbit:queue>
<rabbit:queue name="login-user-logined-dlq"/>

<!--申明exchange的名称与类型-->
<rabbit:topic-exchange name="login_barryhome_fun">
    <rabbit:bindings>
        <!--queue与exchange的绑定和匹配路由-->
        <rabbit:binding queue="login-user-logined" pattern="login.user.logined"/>
        <rabbit:binding queue="login-user-logined-dlq" pattern="login.user.login.dlq"/>
    </rabbit:bindings>
</rabbit:topic-exchange>

通过对死信发送的交换机和路由的的设置,可将消息转向具体的queue中。这里交换机可以和原业务队列不是一个。
login-user-logined中的消息处理失败后将直接转投向login-user-logined-dlq队列中。
当程序逻辑修复后可再将消息再移回业务队列中move messages

2. 安装插件


如图提示需要先安装插件

3. 移动消息


安装成功后就可以输入业务队列名再转投

六、延时队列

延时队列除了可以做一般的延时处理外,还可以当作单个job的定时任务处理,比起一般通过定时器去轮询的方式更优雅。

1. 修改rabbitmq-spring.xml

<rabbit:topic-exchange name="login_barryhome_fun" delayed="true">

初次配置时,如果报以下错误,则是服务器不支持此命令,需要安装插件

Channel shutdown: connection error; protocol method: #method<connection.close>(reply-code=503, reply-text=COMMAND_INVALID - unknown exchange type 'x-delayed-message', class-id=40, method-id=10)

2. 安装插件

1) 下载插件:https://github.com/rabbitmq/r...

2) 上传插件到docker容器中/plugins
docker ps 查询rabbitmq的 CONTAINER ID

docker cp rabbitmq_delayed_message_exchange-3.8.0.ez 2c248563a2b0:/plugins

3) 进入docker容器内部

docker exec -it 2c248563a2b0 /bin/bash

4) 安装插件

cd /plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
具体安装教程可参考:https://blog.csdn.net/magic_1...

安装成功后重启程序,观察mq管理端的exchange可发现

3. 发送延时消息

@GetMapping("/sendDelay")
public LoginUser SendDelayLoginSucceedMessage() {
    LoginUser loginUser = getLoginUser("succeed");

    MessagePostProcessor messagePostProcessor = message -> {
        // 延时10s
        message.getMessageProperties().setHeader("x-delay", 10000);
        return message;
    };

    // 发送消息
    rabbitTemplate.convertAndSend(MessageConstant.MESSAGE_EXCHANGE,
            MessageConstant.LOGIN_ROUTING_KEY, loginUser, messagePostProcessor);
    return loginUser;
}
需要注意的是消息的发送是实时的,消息服务器接收到消息待延时时间后再投到对应的queue中

七、完整代码

https://gitee.com/hypier/barr...

八、请关注我的公众号

请关注我的公众号

查看原文

赞 1 收藏 1 评论 0

barry的异想世界 发布了文章 · 8月27日

微服务下的用户鉴权方案

上一章讲了微服务下的用户身份认证《SpringCloud Gateway 身份认证》,这次主要讲如何进行鉴权。
相对上一章的身份认证代码略有改动

Java下常用的安全框架主要有Spring Securityshiro,都可提供非常强大的功能,但学习成本较高。在微服务下鉴权多多少少都会对服务有一定的入侵性。
为了降低依赖,减少入侵,让鉴权功能相对应用服务透明,我们采用网关拦截资源请求的方式进行鉴权。

一、整体架构

整体结构

用户鉴权模块位于API GateWay服务中,所有的API资源请求都需要从此通过。

  1. 身份认证,通过则缓存用户权限数据,不通过返回401
  2. 用户鉴权,比对当前访问资源(URI和Method)是否在已缓存的用户权限数据中,在则转发请求给对应应用服务,不在则返回403

二、实现步骤

登陆鉴权流程

1. 用户登陆

public LoginUser login(String userName, String password){
    // 检查密码
    User user = userService.checkUser(userName, password);

    LoginUser loginUser = LoginUser.builder()
            .userName(userName)
            .realName(user.getRealName())
            .userToken(UUID.randomUUID().toString())
            .loginTime(new Date())
            .build();

    // 保存session
    session.saveSession(loginUser);

    // 查询权限
    List<Permission> permissions = permissionRepository.findByUserName(userName);
    // 保存用户权限到缓存中
    session.saveUserPermissions(userName, permissions);

    return loginUser;
}

// ...
// 缓存用户权限到Redis
public void saveUserPermissions(String userName, List<Permission> permissions) {
    String key = String.format("login:permission:%s", userName);

    HashOperations<String, String, Object> hashOperations = redisTemplate.opsForHash();
    hashOperations.putAll(key, permissions.stream().collect(
            Collectors.toMap(p -> p.getMethod().concat(":").concat(p.getUri()),
                    Permission::getName, (k1, k2) -> k2)));

    if (expireTime != null) {
        redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
    }
}
  • 用户验证通过后,下发userToken,保存当前登陆信息,缓存用户授权列表
  • 缓存授权列表时,为了方便读取使用hash方式保存为list,切勿直接将数组对象保存为一个object

2. 拦截请求

@Slf4j
@Component
public class AuthorizationFilter extends AbstractGatewayFilterFactory {

    @Autowired
    private Session session;

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {

            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            String uri = request.getURI().getPath();
            String method = request.getMethodValue();

            // 1.从AuthenticationFilter中获取userName
            String key = "X-User-Name";
            if (!request.getHeaders().containsKey(key)) {
                response.setStatusCode(HttpStatus.FORBIDDEN);
                return response.setComplete();
            }

            String userName = Objects.requireNonNull(request.getHeaders().get(key)).get(0);

            // 2.验证权限
            if (!session.checkPermissions(userName, uri, method)) {
                log.info("用户:{}, 没有权限", userName);
                response.setStatusCode(HttpStatus.FORBIDDEN);
                return response.setComplete();
            }

            return chain.filter(exchange);
        };
    }
}
  • 第一步从取出身份认证模块传递的X-User-Name
  • 第二步去缓存中检查是否有相应的权限
public boolean checkPermissions(String userName, String uri, String method) {
    String key = String.format("login:permission:%s", userName);
    String hashKey = String.format("%s:%s", method, uri);

    if (redisTemplate.opsForHash().hasKey(key, hashKey)){
        return  true;
    }

    String allKey = "login:permission:all";
    // 权限列表中没有则通过
    return !redisTemplate.opsForHash().hasKey(allKey, hashKey);
}
  • 权限列表中没有则通过 主要是放过一些没有必要配置的公共资源,默认都可以访问的资源
  • login:permission:all 所有配置过的权限列表需要在程序启动时放入缓存,并需要保持数据的更新

3. 鉴权Filter配置

spring:
  cloud:
    gateway:
      routes:
        - id: cloud-user
          uri: lb://cloud-user  # 后端服务名
          predicates:
            - Path=/user/**   # 路由地址
          filters:
            - name: AuthenticationFilter  # 身份认证
            - name: AuthorizationFilter   # 用户鉴权
            - StripPrefix=1 # 去掉前缀
  • 特别注意filter的顺序,必须先做身份认证后再进行鉴权
  • 如果有较多的路由都需要配置,可使用default-filters默认Filter配置

三、其它问题

在做单元测试时,如遇到如下错误

nested exception is java.lang.NoClassDefFoundError: javax/validation/ValidationException

请升级依赖包版本:

<!--升级validation-api的版本-->
<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.5.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

四、完整代码

https://gitee.com/hypier/barr...

五、请关注我的公众号

请关注我的公众号

查看原文

赞 2 收藏 2 评论 0

barry的异想世界 发布了文章 · 8月19日

SpringCloud Gateway 身份认证

使用SpringCloud技术栈搭建微服务集群,可以选择的组件比较多,由于有些组件已经闭源或停更,这里主要选用spring-cloud-alibaba作为我们的技术栈。

  • 服务注册与发现: nacos-discovery
  • 统一配置管理:nacos-config
  • 微服务网关:spring cloud gateway

由于nacos本身就已经是完备的服务,故参考官方文档直接安装使用就可以,这里重点介绍如何使用SpringCloud Gateway实现路由转发和身份认证。

一、微服务架构

微服务架构

  1. 所有的请求先通过nginx进行负载和转发
  2. API Gateway负责进行微服务内的路由转发和身份认证

二、实现路由转发

1. 引入gateway包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

需要注意的是:如果启动时报错,提示在依赖中发现的springMvc与gateway不能兼容,需要删除spring-boot-starter-web相关引用

**********************************************************

Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway at this time. Please remove spring-boot-starter-web dependency.

**********************************************************

2. 添加启动类

@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
  • @EnableDiscoveryClient 用于集群下的服务注册与发现

3. 配置路由表

配置文件最好选用YAML,结构清晰易读

spring:
  application:
    name: cloud-api #服务名
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848 # nacos服务器地址
    gateway:
      routes:
        - id: cloud-user
          uri: lb://cloud-user  # 后端服务名
          predicates:
            - Path=/user/**   # 路由地址
          filters:
            - StripPrefix=1 # 去掉前缀

server:
  port: 8000

# 用于actuator暴露监控指标
management:
  endpoints:
    web:
      exposure:
        include: "*"
  • StripPrefix=1 用于在路由转发时去掉前缀地址,若无则将前缀一起转发给后端服务,比如:
请求地址为:http://localhost:8000/user/home
在没有加StripPrefix时,转发给后端服务地址为:http://{cloud-user}/user/home,否则为http://{cloud-user}/home
  • management 配置用于暴露监控指标,可请求 http://localhost:8000/actuator/gateway/routes 获取所有的映射路由

三、实现身份认证

在分布式系统中有三种常用的身份认证方式:

1.使用Session,可使用spring security来实现Session的管理 ,使用redis来存储会话状态,客户端的sessionID需要cookie来存储

Session时序图

优点

  • 使用方便,客户端无感知
  • 安全性高
  • 会话管理支持较好

缺点

  • 对客户端应用支持不友好
  • 无法实现跨站跨端共享
  • 实现方式相对复杂
  • 需要客户端Cookie支持

2.使用Token,由服务端签发,并将用户信息存储在redis中,客户端每次请求都带上进行验证

token时序图

优点

  • 对多端共享支持友好
  • 对多端共享会话支持友好
  • 实现方式相对简单
  • 安全性高
  • 无须Cookie支持

缺点

  • 会话过期时间维护较复杂
  • 服务端需要维持会话状态

3.使用JWT,由服务端签发且不保存会话状态,客户端每次请求都需要验证合法性

jwt时序

优点

  • 对多端共享支持友好
  • 对多端共享会话支持友好
  • 服务端无会话状态
  • 无须Cookie支持
  • 可携带载荷数据

缺点

  • 会话过期时间维护较复杂
  • 默认情况下,安全性较低
  • 一旦签发无法撤销,或撤销较复杂

简单token验证

本例子的token是uuid生成随机码的方式,没有使用算法做验证,这样有可能导致客户端穷举token,不断查询redis造成风险。在生产环境中可使用一定算法进行token签发(如加密解密,有效时间戳等),保证伪造token对服务器的影响降到最低。

1. 用户登陆保存session状态

@Service
public class Session {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    Long expireTime = 10800L;

    /**
     * 保存session
     * @param loginUser
     */
    public void saveSession(LoginUser loginUser) {
        String key = String.format("login:user:%s", loginUser.userToken);

        redisTemplate.opsForValue().set(key, JSON.toJSONString(loginUser),
                expireTime, TimeUnit.SECONDS);
    }

    /**
     * 获取session
     * @param token
     * @return
     */
    public LoginUser getSession(String token){
        String key = String.format("login:user:%s", token);

        String s = redisTemplate.opsForValue().get(key);
        if (Strings.isEmpty(s)){
            return null;
        }

        return JSON.parseObject(s, LoginUser.class);
    }
}

保存会话状态时,需要设置过期时间,且不宜过长或过短。如进一步思考如何刷新会话过期时间。

2. 增加AuthCheckFilter,拦截路由请求

@Slf4j
@Component
public class AuthCheckFilter extends AbstractGatewayFilterFactory {

    @Autowired
    private Session session;

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {

            ServerHttpRequest request = exchange.getRequest();
            ServerHttpResponse response = exchange.getResponse();

            // 1. 获取token
            String token = request.getHeaders().getFirst("token");

            log.info("当前请求的url:{}, method:{}", request.getURI().getPath(), request.getMethodValue());

            if (Strings.isEmpty(token)) {
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            // 2. 验证用户是否已登陆
            LoginUser loginUser = this.session.getSession(token);
            if (loginUser == null) {
                response.setStatusCode(HttpStatus.UNAUTHORIZED);
                return response.setComplete();
            }

            // 3. 将用户名传递给后端服务
            ServerWebExchange build;
            try {
                ServerHttpRequest host = exchange.getRequest().mutate()
                        .header("X-User-Name", loginUser.userName)
                        // 中文字符需要编码
                        .header("X-Real-Name", URLEncoder.encode(loginUser.realName, "utf-8"))
                        .build();
                build = exchange.mutate().request(host).build();
            } catch (UnsupportedEncodingException e) {
                build = exchange;
            }

            return chain.filter(build);
        };

    }
}

此拦截器作用为验证请求是否已登陆,否则返回401状态,并将用户会话信息传递给后端服务。

3. 配置Filter

在gateway项目的yml配置文件中配置需要进行验证的路由filters: AuthCheckFilter

spring:
    gateway:
      routes:
        - id: cloud-user
          uri: lb://cloud-user  # 后端服务名
          predicates:
            - Path=/user/**   # 路由地址
          filters:
            - name: AuthCheckFilter     #会话验证
            - StripPrefix=1 # 去掉前缀

由此就实现了对后端路由地址的身份验证功能

三、完整代码

https://gitee.com/hypier/barr...

请关注我的公众号

请关注我的公众号

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 8月3日

领域建模的贫血模型与充血模型

领域建模是通过识别领域对象与行为来连接与现实世界业务主体与操作的映射关系。对象与行为的组织设计原则更体现面向对象设计的思想,通过聚合、解耦、抽象、组合等多种设计方式达到系统可复用,可维护,易扩展的能力。

在实际程序代码设计中,由于语言、结构、技术的不一样对领域建模代码落地也有所不同,且各有优缺点。

一、贫血模型

此种模型下领域对象的作用很简单,只有所有属性的get/set方式,以及少量简单的属性值转换,不包含任何业务逻辑,不关系对象持久化,只是用来做为数据对象的承载和传递的介质。

@Entity
@Data
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class User {

    @Id
    private String userId;
    private String userName;
    private String password;
    private boolean isLock;
}

而真正的业务逻辑则由领域服务负责实现,此服务引入持久化仓库,在业务逻辑完成之后持久化到仓库中,并在此可以发布领域事件(Domain Event)

public interface UserService {

    void create(User user);
    void edit(User user);
    void changePassword(String userId, String newPassword);
    void lock(String userId);
    void unlock(String userId);

}

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository repo;

    @Override
    public void edit(User user) {
        User dbUser = repo.findById(user.getUserId()).get();
        dbUser.setUserName(user.getUserName());
        repo.save(dbUser);
        // 发布领域事件 ...
    }

    @Override
    public void lock(String userId) {
        User dbUser = repo.findById(userId).get();
        dbUser.setLock(true);
        repo.save(dbUser);
        // 发布领域事件 ...
    }
    
    // ... 省略完整代码
}

优点: 结构简单,职责单一,相互隔离性好,使用单例模型提高运行性能

缺点: 对象状态与行为分离,不能直观地描述领域对象。行为的设计主要考虑参数的输入和输出而非行为本身,不太具有面向对象设计的思考方式。行为间关联性较小,更像是面向过程式的方法,可复用性也较小。

SpringBoot 采用单例模式,尽量不手动创建对象,对象无状态化,故较推荐使用贫血模型

二、 充血模型

此种模型下领域对象作用此领域相关行为,包含此领域相关的业务逻辑,同时也包含对领域对象的持久化操作。

@Entity
@Data
@Builder
@AllArgsConstructor
public class User implements UserService {

    @Id
    private String userId;
    private String userName;
    private String password;
    private boolean isLock;

    // 持久化仓库
    @Transient
    private UserRepository repo;

    // 是否是持久化对象
    @Transient
    private boolean isRepository;

    @PostLoad
    public void per() {
        isRepository = true;
    }

    public User() {
    }

    public User(UserRepository repo) {
        this.repo = repo;
    }

     @Override
    public void create(User user) {
        repo.save(user);
    }

    @Override
    public void edit(User user) {
        if (!isRepository) {
            throw new RuntimeException("用户不存在");
        }

        userName = user.userName;
        repo.save(this);
        // 发布领域事件 ...
    }


    @Override
    public void lock() {
        if (!isRepository) {
            throw new RuntimeException("用户不存在");
        }

        isLock = true;
        repo.save(this);
        // 发布领域事件 ...
    }

}

优点: 对象自洽程度很高,表达能力很强,因此非常适合于复杂的企业业务逻辑的实现,以及可复用程度比较高,更符合面向对象设计思想

缺点: 对象属性中掺杂持久化仓库,不够纯粹,持久化操作是否属于业务逻辑有待求证。但由于持久化仅需暴露接口,对业务逻辑与持久化操作的耦合度有一定降低。

说明: 有人认为对象中的Create(),是新建对象方法不应该属于对象本身,应由其它对象产生或static方法产生。我的理解是不能把业务对象中的新建和程序对象上的新建混淆。业务对象的新建是指的是业务行为操作得出的结果,理应属于对象本身行为。而程序里的新建则是对象初始化过程New(),这是程序构建逻辑不是业务概念,不能相等对待。

在领域对象行为逻辑较复杂的情况下,需要多个行为共享对象状态的时候,充血模型表现力更强,个人比较推荐此种模型

三、充血模型2

为了解决业务逻辑不纯粹问题,也有将持久化操作移出业务逻辑的作法。

@Entity
@Data
@Builder
@AllArgsConstructor
public class User implements UserService {

    @Id
    private String userId;
    private String userName;
    private String password;
    private boolean isLock;

    // 是否是持久化对象
    @Transient
    private boolean isRepository;

    @Override
    public void create(User user) {
        user.userId = UUID.randomUUID().toString();
    }

    @Override
    public void edit(User user) {
        userName = user.userName;
    }


    @Override
    public void lock() {
        isLock = true;
    }

}


@Service
public class UserManager {

    @Autowired
    private UserRepository repo;

    public User findOne(String userId){
        return repo.findById(userId).get();
    }
    
    public void edit(User u) {
        User user = findOne(u.getUserId());
        user.edit(u);

        repo.save(user);
        // 发布领域事件 ...
    }

    public void lock(String userId) {
        User user = findOne(userId);
        user.lock();
        repo.save(user);
        // 发布领域事件 ...
    }
}

优点: 保持了业务逻辑的纯粹性,去掉了持久化的入侵

缺点: 降低了领域服务的自治性,破坏了行为逻辑的完整性,部分逻辑混入了application层,尤其是领域事件的发布

此种方式是前两种方式的折中,充分地做到了解耦,但也牺牲了部分内聚

四、总结

架构设计是一项持续性演进性的工作,不是一成不变的。架构的选择并没有好坏只有适合,每一种都有自己的使用场景。如何选择需要自身理论支持,保持相对方向性统一,并持续审视是否符合预期目标。

五、 源码

https://gitee.com/hypier/barr...

此处输入图片的描述

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 6月21日

使用领域驱动设计分析业务

开发人员在需求拿到之后,写代码前需要对项目进行需求分析和系统设计。一些业务逻辑和流程较复杂的项目,如何即能快速地抓住重点,清晰理解需求,又能设计出具体扩展性的系统架构呢?答案是使用领域驱动设计,以下是引用网上对此的一段描述:

领域驱动设计是一种针对大型复杂系统的领域建模与分析方法。它完全改变了传统软件开发工程师针对数据库进行的建模方法,从而将要解决的业务概念和业务规则转换为软件系统中的对象以及对象的属性与行为,通过合理运用面向对象的封装、继承和多态等设计要素,降低或隐藏整体系统的业务复杂性,并使得系统具有更好的扩展性,应对纷繁多变的现实业务问题。

重点:

  • 一种解决复杂系统的建模与分析方法
  • 将业务概念和规则转为对象和对象的属性行为
  • 通过封装、继承和多态降低业务复杂性

我们引入网上购物的例子来具体讲解如何使用DDD来指导分析业务和架构设计。

一. 需求梳理

需求梳理是第一步,主要的目的是理解业务需求,明确业务流程。采用的方式是通过画流程图去解构业务流程。

1. 去掉干扰项

为了更清晰了解业务功能,先去掉干扰项

  • 需求描述中的场外信息,如拿出手机,打开APP,连入网络等
  • 查询或不影响数据的操作,如浏览页面,打开商品页,点击查询
  • 外部系统操作,非当前系统功能,如用户注册,用户登陆,物流运输等

2. 抓住业务关键行为

解构业务关键行为,用简单的动名词句子来描述业务需求,这里可以是业务的大节点作为,但切记须是对数据产生影响的行为,如:

业务流程图

二. 领域结构设计

主要的目的是明确对象,对象服务能力以及对象的结构关系

1. 明确领域对象

通过主要行为确定领域对象,通常为业务描述中的名词,这种方式是以对象驱动行为的发生,这种是最常见的一种方式。对象是行为能力的拥有者,也是行为能力的参与者。如下订单,支付订单,确认收货等都是以订单作为对象的行为能力。
而另一种则是以行为驱动对象发生变化,对象只是行为能力的参与者,不是行为能力的拥有者。这是一个命令驱动模式,设计时将记录所有行为的发生,更强调可回溯性。如以交易记录做为对象去驱动账户余额或状态的变化,充值、消费等场景使用较多。

2. 识别领域服务

领域服务是领域对象拥有的行为能力,它必须是以现实业务的角度去识别,切勿以技术或数据的角度去分析。现实业务反映的是事物的规律变化,从而提取的规则天然会适配未来的变化,这也是领域驱动设计的精髓所在。实体是业务操作的承载者,行为命名代表着很强的领域概念,需要使用通用语言中的动词,应极力避免无法表达业务逻辑的命名。
服务的识别应保持服务功能的完整性,且职责的单一性,切分好服务的粒度,是可以对外使用的。

3. 划分领域边界

领域边界的划分就是划分功能的实现范围,行为能力的归属,对象间的解耦,明确系统结构关系。划分的标准也是以现实业务对象为参考点。
每个领域相对独立,各自的作用、结构与地位也不一样,有核心域,支撑域,通用域等。各域间明确通讯方式,根据不同的使用场景,有同步的也有异步的,并确认通讯的标准。

领域图

三. 领域模型设计

主要将上一图进行细化,明确各对象之间的关系和服务接口的设计,可以直接实现代码来呈现设计结果。这一步和以往的设计有所不同在于,它摒弃了针对数据库的建模设计,将开发设计人员的关注点从数据库层面转向了业务本身,将业务概念与规则直接转为系统对象与对象属性及行为上。这部分最大的不同点在于思考问题不再是按流程一个一个设计数据对象,等数据对象基本上设计完成后才能进行编码,而是从一个核心点开始,一步一步扩散,设计即编码,再逐步完善且不会影响结构,最终形成一个网。

1. 整理对象及关系属性

整理核心对象实体,完善重要属性。属性的数据类型可以是普通类型,也可以是自定义类型,一切以业务需求出发,这里需要厘清主对象与子对象的强弱关系。

2. 整理对象服务接口

服务接口的设计应该按第一节的分析,构建对象的行为能力,输入输出参数尽量以实体对象的形式出现。还需要考虑通用性和扩展性,共同的部分可提取统一处理,并对功能进行抽象封装。而不同的部分可根据实际情况进行合理设计。

  • 变化小的细节可通过使用属性标识区别,方便变化和扩展,切勿就具体对象使用硬编码的方式不易扩展
  • 大逻辑的大变化可使用多态的形式扩展,易于维护方便扩展

这一步完成之后,系统的整个大结构就出来了,再下来就是完善细化各部分逻辑了。

请关注我的公众号

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 6月6日

中台改造,我之拙见

中台是个啥

中台是个啥,感觉是一个很复杂的东西。网上很多相关的文章,但好像越说越复杂,并没有一个统一的定义。
我从18年一个阿里背景的到企业来做项目推广时第一次听到中台一词。“中台?”我以为听错了,我只听到“前台”“后台”,“中台”又是个啥?后来在网上了解了一下,感觉挺高大上的,由此对它越来越有兴趣。

经过一些人的不断鼓动,领导也觉得好像是可以做的事情,好像收益也不错,便决定我们来做中台吧。我们没有经验得请一个第三方公司来做,期间我们见了好几种类型的外包公司,有理念很强想咨询的,有理念一般想推产品的,还有更实在有很强的执行力且价格还能接受的,显然我们选择了最后一个。经过对方一小段时间对业务的了解,再经过一小段时间的开发,“中台出来了!”。然后把代码交接给我们一看,“嗯...这个系统有些实现好像有点复杂,先理解一下,再得改改”,于是经过一小段时间的理解,再经过一大段时间的修改,系统终于上线了。“感觉和我们做的差不多嘛,中台那啥,不就是一个独立一点的系统吗?”。大家可能看出来了,一个失败的例子,外包了一个项目,怎么看也没有网上说的高大上且复杂的中台的感觉。

那就继续做吧,再拿一个系统来自己做中台,这个系统要替换现有系统得快点上,而且你们要考虑以后的扩展性问题。

中台不是平台

平台是要求功能集成化,一旦有需求变化便需要修改,着眼于集成现有功能,有变更帮我做。而中台要求能力集成,一旦有需求变化便调整组合配置,着眼于可组合能力,有变更自己拿。

中台的愿景

中台的愿景不是着眼于解决现有问题,而是为企业长期的企业发展提供解决方案。这一点也是很多企业没有想清楚的一点。眼前的需求要求具体且快速,要求一个设计或开发人员在满足现有需求的同时自行考虑为以后功能变化留下扩展性这不现实。

我们经常会听到产品或需求说一句话“这次的功能就这么做吧,你保留好接口以后方便扩展吧”,这句话乍一听来很贴心,即为开发减少了工作量,又为以后的需求变更留有余地。但开发仔细想一下留好接口,需求是什么,留成什么标准,有具体的确认过吗,难道就只是我认为吗?有经验的同学可能考虑比较成熟,对未来的变化了如指掌,这很好,但没有经验的同学呢?保不齐后面需求说不是叫你留好接口,怎么不得行了,这就不好说了。

还有一种情况就是直接把某某系统做成中台替换现有系统,这通常是业务领导方的想法,有了它以后什么样的需求都能接入进来。如遇到接入有问题,就变更需求。这种想法很明显是一个需求式的想法,先满足现有业务,以后的问题以后再解决。在此需求的重要述求是替换现有系统,中台只是一个名头。而架构师虽然有中台的想法,想把以后更新的可能性考虑到其中,但碍于有限的人手和时间的问题,也只能妥协把系统变成了一个烟囱式的系统。在这种情况下,需求和架构对系统的目标是不一致的,所以做出来的东西事与愿违。

中台的重要性

中台的重要性是不言而喻的,它是关系到企业的长期发展的。而中台的实施也是一个极复杂的过程,涉及多个部门的协作支持,需要对企业业务流程进行梳理,它不光是哪一个部门为主其它部门配合的事情,它必须是一把手负责的项目。

另外中台化项目必须是自己的项目,只有企业里的人才对企业有一个全面的了解,了解企业的过去,现在和将来,想让一个外包团队来给企业做中台化是非常困难的。第一你们的着眼点不一样,一个是看着企业的长期发展,一个是想如何完成这个项目;第二外包的企业的了解是很难快速达到企业员工的水平的。这样外包的项目很难达到期望的收益。所以中化建设必须是以自身为主导,外包可以为辅助。

组织的共识性

中台项目和普通项目的目标不一样,需要项目实施人员长期而持续的投入。因此成员不应该只是从其它组里借调使用而以,而是需要是一个固定的组织形式。临时的组织只是解决临时紧急的任务,完成后就解散,组员也会是这样的思考方式;而固定的组织是有共同的目标的,是需要长期持续地关注组织职责的。什么样的组织做什么样的事,中台项目实施必须是一个固定的组织。

中台的实施是贯穿企业多个业务链的,带来组织、业务技术的变革,自然会涉及到多个部门。每个部门对中台的认识和述求都不一样,这样他们在配合上自然也会不一样。这样必须找到各自的价值结合点,让每个部门都清楚中台会给他们带来多大的收益,只有这样才能得到各方的积极配合。

中台的持续性

中台建设是一个长期持续的过程,投入也非常大。一开始不宜铺得太大,风险直线上升。应该找到合适的切入点,设立里程碑,设计考核点,循序渐进。这样所有人都清楚目标,才能持续完成目标。

为什么要做中台

中台建设这么复杂,投入这么大,那为什么还要做中台呢?

  1. 效能提升
    端到端的效能提升,用户接入变得简单,不需要再考虑复杂的技术实现,只需要把关注点放在业务本身。以前需要一两个月完成的东西,现在可能只需要一两周。时间就是金钱,需要快速迭代开发,这正是中台支撑业务的一个方面。
  2. 能力沉淀
    将系统的核心,关键,通用的能力沉淀下来。为用户提供简单多样的服务清单,把复杂的实现留在中台。能力的沉淀带来基础架构的稳定,同时带来整个稳定。技术输出,积累的中台子系统可以为更多业务使用,变成一个企业应用平台业务线。
  3. 快速创新
    市场不断变化,为了达到业务目标必须要保持业务的创新,业务的创新也带来应用的改变。应用就是能力的编排,这样就可以快速开发应用。

此处输入图片的描述

查看原文

赞 0 收藏 0 评论 0

barry的异想世界 发布了文章 · 5月31日

架构师如何对待不会的技术问题

对于从小厂基层一步一步成长起来的架构师,技术经验不够全面,如何面对面试大厂时对方提出的自己不会的技术问题呢?

从小厂基层成长起来的架构师,技术经验基本上都是从实践中获得。而主动学习,翻阅文献吸收回来的东西,在公司体量和环境的实际限制下大多也停留在理论基础上,没有实践不一定深刻,如何改变这样的状态呢?

架构师涉略的知识面可为非常全面的,现在获取知识也越来越便捷,博客、公众号
比比皆是,有写面试题的,有写经验分享的,有写技术分析的等等。作为一个稍普通的架构师,技术开发从业者来讲自然觉得什么都有用,哪篇都是学习,何奈没有超高的天赋,一点就通、超强的学习理解能力,每个人的时间都是有限的,如何去做有效的筛选呢?

认清自己

首先需要正确认清自己的学习和理解能力水平,承认自己的不足。一个人不可能了解到所有的知识面,找到对自己最重要的知识面进行加强,了解周围的知识面,知道更广阔的面。同时正视自己的不足,虚心以待,大胆求证,这就是面对知识的态度。

学习是一种习惯

你需要有一个长期终身学习的态度,让它成为一种习惯。用学习去改善自己的不足,增强自己的自信。
学习也是需要有方法的,很多知识都是相通的,多思考多动手,找到关键点成为自己快速有效学习的法宝。

兴趣是最好的驱动力

兴趣是学习最大的动力,学任何东西都需要有兴趣,而不是被迫去学习。如果你找不到兴趣点,宁愿搁置,不然你无法运用自如,事倍功半。有了兴趣你就会学以致用,举一反三。有了兴趣你将忘掉时间,忘掉加班,忘掉996,把它当作自己的事,你将体会到快乐。

在实践中加强学习

实践是学习最有效的方法。如果只是学习文章或书本上的东西,那只是代表你知道而并非掌握。很多人要学习一本新的技术,大部分的获取方式都是在工作实践中,面对实际的问题你必须在有限的时间内找到可行的方案,这就形成了自己的经验。任何的学习和技术研究,最有成效的方式都是站在现有问题之上做适当的提升。凭空的学习研究,得不到有效实践的东西都不一定能经得起考验。

能力比知识点更重要

分析问题的思路和解决问题的方法形成自身能力素养,知识如繁星点点,能力才是通往知识殿堂的钥匙。知识你无法穷尽,但有了能力你将对知识运用自如。有些知识可能暂且不知道,但你有能力想得到知识也只是花一点时间的事。
除此,你还是需要找到在你的知识图谱上最重要的关键知识点,学习、加强、扩充,这样才是有能力有经验的人才。

提升知识的眼界

架构师是需要有知识的眼界的,设计的的架构必须要保持它的先进性和可持续性。对当下的东西需要全面的掌握,对即将发生的东西需要有所了解,对未来可能发生的东西需要有所设想。架构不是静态的,一旦设计出来之后需要去维护它,需要全面掌握当下有问题,清楚如何去完善;也需要了解业务的变化架构如何去支撑;还需要去畅想未来如何通过架构的演进去支持业务的发展。
另一方面也需要时刻关注行业的发展,留意新的技术如何可以带来业务新的增长点。

最后

世间事没有绝对的对错,本文仅代表笔者在特定环境特定情况下的经验分享,如果大家能从中获得一两点则倍感荣誉。

此处输入图片的描述

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 4 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 3月2日
个人主页被 210 人浏览