dubbo学习

微服务入门

传统应用

多个模块集成在一起,集中部署

  • 带来得问题:
    • 单一业务开发和迭代困难
    • 扩容困难
    • 部署和回滚困难

微服务发展历程

  • 面向服务开发 -SOA(多个服务拆分开来)
  • 微服务开发(“微”)

SOA概述

多个模块通过WebService进行通信

image-20200615205901660

微服务概述

  • 是一种将业务系统进一步拆分得架构风格(非技术,具体实现由dubbo、Spring Cloud……)
  • 每个单一业务独立运行(每个模块占用一个JVM)
  • 每个单一服务都应该使用更轻量得机制保持通信(HTTP、TCP)
  • 服务不强调环境,可以不同语言或数据源

微服务选择

  • Dubbo(Alibaba开源)
  • Spring Cloud
  • Zero ICE

基础环境构建

dubbo文档

课程环境

  • 基于Guns+Spring Boot+Dubbo构建院线平台
  • SpringBoot + Dubbo得教学环境

微服务基本概念

  • Provider:服务提供者,提供服务实现
  • Consumer:服务调用者,调用Provider提供的服务实现
  • 同一个服务可以既是Provider也是Consumer

SpringBoot实现直连提供者

https://github.com/alibaba/dubbo-spring-boot-starter/blob/master/README_zh.md

首先通过IDEA生成一个SpringBoot项目,并在内部生成两个SpringBoot的Module项目Provider和Consumer

Provider

  • 配置

    1. pom.xml

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.alibaba.spring.boot</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.0.0</version>
      </dependency>
    2. application.properties

      1
      2
      3
      spring.application.name=dubbo-spring-boot-starter
      spring.dubbo.server=true
      spring.dubbo.registry=N/A
  • 代码

    1. 生成一个接口

      1
      2
      3
      public interface ServiceAPI {
      String sendMessage(String message);
      }
    2. 实现类

      1
      2
      3
      4
      5
      6
      7
      8
      @Component  //注册成bean
      @Service(interfaceClass = ServiceAPI.class) //dubboservice,对外暴露服务
      public class QuickStartServiceImpl implements ServiceAPI {
      @Override
      public String sendMessage(String message) {
      return "quickstart-provider-message=" + message;
      }
      }
    3. 启动类

      1
      2
      3
      4
      5
      6
      7
      @SpringBootApplication
      @EnableDubboConfiguration //启动Dubbo功能
      public class ProviderApplication {
      public static void main(String[] args) {
      SpringApplication.run(ProviderApplication.class, args);
      }
      }

Consumer

  • 配置

    1. pom.xml

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.alibaba.spring.boot</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.0.0</version>
      </dependency>
    2. application.properties

      1
      spring.application.name=dubbo-spring-boot-starter
  • 代码

    1. 接口类

      1
      2
      3
      public interface ServiceAPI {
      String sendMessage(String message);
      }
    2. 实现类(调用Provider接口)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Component
      public class QuickstartConsumer {
      @Reference(url = "dubbo://localhost:20880") //导入另一个某块的实例,通过dubbo的url
      ServiceAPI serviceAPI;

      public void sendMessage(String message) {
      System.out.println(serviceAPI.sendMessage(message));
      }
      }
    3. 启动类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @SpringBootApplication
      @EnableDubboConfiguration
      public class ConsumerApplication {

      public static void main(String[] args) {
      ConfigurableApplicationContext run = SpringApplication.run(ConsumerApplication.class, args); //获取容器对象
      QuickstartConsumer quickstartConsumer = (QuickstartConsumer)run.getBean("quickstartConsumer"); //由于不是web项目,所以通过IOC容器获取Consumer对象,注册的对象名称为驼峰标志
      quickstartConsumer.sendMessage("Hello Dubbo");
      }

      }

直连提供者

  • 消费端知道服务提供者的地址,直接进行连接
  • 该种方式一般只在测试环境中使用(由于集群比较麻烦,可以在测试时这样进行测试)
  • 直连提供者限制了分布式的易扩展性(不利于集群的搭建,因为每个接口的url是写死的,因此需要注册中心)

dubbo流程

Provider将服务注册到注册中心,Consumer通过订阅获取服务的表,并进行相关的invoke调用

img

角色名称 角色说明
Provider 暴露服务的服务提供方
Consumer 调用远程服务的服务消费方
Registry 服务注册与发现的注册中心,通常使用Zookeeper
Monitor 统计服务的调用次数与调用时间的监控中心
Container 服务运行容器

zookeeper安装

zookeeper官网

  1. 下载一个版本的gz包后,进行解压

  2. 将config中的zoo_xxx.cfg更改成zoo.cfg

  3. 在window环境下,打开bin中的zkserver.cmd,开启注册中心,成功标志

    1
    binding to port 0.0. 0.0/0.0. 0.0:2181

SpringBoot集成Zookeeper

Provider

  • 配置

    1. pom.xml

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.9</version>
      </dependency>
    2. application.properties

      1
      2
      3
      4
      spring.application.name=dubbo-spring-boot-starter
      spring.dubbo.server=true
      #将服务注册到这个ip中
      spring.dubbo.registry=zookeeper://localhost:2181

Consumer

  • 配置

    1. pom.xml

      1
      2
      3
      4
      5
      <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.9</version>
      </dependency>
    2. application.properties

      1
      2
      spring.application.name=dubbo-spring-boot-starter
      spring.dubbo.registry=zookeeper://localhost:2181
  • 代码

    • 实体类

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Component
      public class QuickstartConsumer {
      @Reference(interfaceClass = ServiceAPI.class) //将之前的url覆盖掉,声明接口类型
      ServiceAPI serviceAPI;

      public void sendMessage(String message) {
      System.out.println(serviceAPI.sendMessage(message));
      }
      }

业务基础环境构建

构建基于Guns+Springboot+Dubbo的框架

步骤

  1. 通过IDEA导入整个guns项目

  2. 将rest中的yml配置文件的datasource更改为自己的数据库

  3. 生成guns_rest数据库

  4. 通过SpringBoot的启动类来启动

  5. 访问,进行权限认证

    1
    http://localhost/auth?userName=admin&password=admin

抽离业务接口

因为在每个模块调用另一个模块的API时,都需要生成一个接口,保证这个模块可以识别这个类,这样就会造成每个模块在调用同一个接口时,都要声明接口;我们将所有业务的接口抽离到一个模块当中,这样只需要继承这个模块,那么就不用声明接口了

步骤

  1. 将guns-core复制,并作为guns-api模块

  2. 删除其中不需要的结构(target…..),将其中的依赖全部删除(只起接口作用,无需依赖)

  3. 通过maven的install进行打包(这样才可以被引用)

  4. 在父类模块先导入api依赖(使用dependency,子类模块自动继承,无需再次声明;使用dependencyManagement则需要在子模块进行导入,避免造成子模块依赖臃肿,需要才导入)

    1
    2
    3
    4
    5
    <dependency>
    <groupId>com.stylefeng</groupId>
    <artifactId>guns-api</artifactId>
    <version>${guns.version}</version>
    </dependency>

    参考资料:

    Maven依赖关系

API网关变形应用

API网关

  • 概念:
    1. API网关有点类似于设计模式中的Facade模式
    2. AP|网关一般都是微服务系统中的门面
    3. API网关是微服务的重要组成部分
  • 作用:
    1. 身份验证和安全
    2. 审查和监测
    3. 动态路由(dubbo不需要,spring cloud需要,将请求与服务进行映射)
    4. 压力测试
    5. 负载均衡(通过网关来调用压力较小的服务)
    6. 静态相应处理(将静态的页面缓存起来,直接响应,动静分离)

API网关模块构建

  • 步骤
    1. 复制Guns的guns-rest模块,改名为guns-gateway
    2. 增加parent的pom,修改guns-gateway的pom
    3. Project Struct结构,将guns-rest的其余多余信息删除

API网关集成Dubbo

  • 步骤

    1. 往guns-gateway导入Dubbo、Zookeeper相关依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      <dependency>
      <groupId>com.alibaba.spring.boot</groupId>
      <artifactId>dubbo-spring-boot-starter</artifactId>
      <version>2.0.0</version>
      </dependency>
      <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.9</version>
      </dependency>
    2. 修改yml文件,配饰dubbo、zookeeper信息

      1
      2
      3
      4
      5
      6
      spring:
      application:
      name: Meeting-gateway
      dubbo:
      server: true
      registry: zookeeper://localhost:2181
    3. 书写接口,实现类,然后将实现类注册到Zookeeper

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      public interface UserAPI {
      boolean login(String userName, String password);
      }

      @Component
      @Service(interfaceClass = UserAPI.class) //暴露接口,注册到Zookeeper
      public class UserImpl implements UserAPI {
      @Override
      public boolean login(String userName, String password) {
      return true;
      }
      }
    4. 启动项目,进行网关测试

      1
      http://localhost/auth?userName=admin&password=admin

模块开发

用户模块开发

  • 学会AP|网关权限验证和其他服务交互
  • 学会开发Springboot的自定义配置
  • 学会Dubbo负载均衡策略选择和使用

修改Guns中的JWT模块

  1. 增加忽略验证URL配置
  2. 修改返回内容匹配业务 (修改JWT申请的返回报文)
  3. 增加Threadlocal的用户信息保存

基于SpringBoot配置忽略列表

  • 步骤

    1. 在gateway模块的yml文件中配置jwt的ignoreurl

      1
      2
      3
      4
      5
      6
      7
      jwt:
      header: Authorization #http请求头所需要的字段
      secret: mySecret #jwt秘钥
      expiration: 604800 #7天 单位:秒
      auth-path: auth #认证请求的路径
      md5-key: randomKey #md5加密混淆key
      ignore-url: /user/,/film #忽略列表,不进行jwt检测
    2. 找到config中的JwtProperties,这是基于SpringBoot的特性,将yml中的属性映射到类中

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @Configuration
      @ConfigurationProperties(prefix = JwtProperties.JWT_PREFIX) //将yml中前缀为jwt的与这个类进行映射
      public class JwtProperties {

      public static final String JWT_PREFIX = "jwt";

      private String ignoreUrl = "";//忽略url

      public String getIgnoreUrl() {
      return ignoreUrl;
      }

      public void setIgnoreUrl(String ignoreUrl) {
      this.ignoreUrl = ignoreUrl;
      }
      }
    3. 在filter中对忽略的url进行过滤操作

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //配置忽略列表
      String ignoreUrl = jwtProperties.getIgnoreUrl();
      String[] ignoreUrls = ignoreUrl.split(",");
      for (String url : ignoreUrls) {
      if (request.getServletPath().equals(url)) {
      chain.doFilter(request, response); //过滤器,满足条件的可以继续请求,获取响应
      return;
      }
      }

修改JWT申请的返回报文

  • 步骤

    1. 将登陆后返回的userId封装在token中,避免用户反复登陆(类似于Session)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @RequestMapping(value = "${jwt.auth-path}")
      public ResponseVO createAuthenticationToken(AuthRequest authRequest) {
      boolean validate = true;

      int userId = userAPI.login(authRequest.getUserName(), authRequest.getPassword());
      if (userId == 0) {
      validate = false;
      }

      if (validate) {
      //生成随机字符串,起混淆作用,实现原理是将一个随机字符串,根据固定的长度n,随机取其中的n个字符
      final String randomKey = jwtTokenUtil.getRandomKey();
      //将userId封装到token之中,存储token在缓存中,方便用户再次登陆时,直接获取token""加int转换为String,直接使用toString,Integer为null则报错
      final String token = jwtTokenUtil.generateToken("" + userId, randomKey);
      //返回vo对象(value object),将返回的信息封装起来,其中返回token和randomkey
      return ResponseVO.success(new AuthResponse(token, randomKey));
      } else {
      return ResponseVO.serviceFail("用户名或密码错误");

      }
      }
    2. VO对象

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      public class ResponseVO<M> {
      //返回状态【0-成功 1-业务失败 999-系统异常】
      private int status;
      //返回信息
      private String msg;
      //返回数据实体
      private M data;

      private ResponseVO() { //避免外界创建实体,只能内部创建,空参构造器设置成private

      }

      //成功响应
      public static <M> ResponseVO success(M m) {
      ResponseVO responseVO = new ResponseVO();
      responseVO.setStatus(0);
      responseVO.setData(m);
      return responseVO;
      }

      //业务失败
      public static <M> ResponseVO serviceFail(String msg) {
      ResponseVO responseVO = new ResponseVO();
      responseVO.setStatus(1);
      responseVO.setMsg(msg);
      return responseVO;
      }

      //系统异常
      public static <M> ResponseVO appFail(String msg) {
      ResponseVO responseVO = new ResponseVO();
      responseVO.setStatus(999);
      responseVO.setMsg(msg);
      return responseVO;
      }

      public int getStatus() {
      return status;
      }

      public void setStatus(int status) {
      this.status = status;
      }

      public String getMsg() {
      return msg;
      }

      public void setMsg(String msg) {
      this.msg = msg;
      }

      public M getData() {
      return data;
      }

      public void setData(M data) {
      this.data = data;
      }

      }

ThreadLocal保存用户信息

  • 步骤

    1. 创建CurrentUser,用来存储和获取UserID

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      public class CurrentUser {
      //线程绑定的存储空间
      private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

      //将用户ID存入存储空间
      public static void saveUserId(String userId) {
      threadLocal.set(userId);
      }

      public static String getCurrentUser() {
      return threadLocal.get();
      }

      }
    2. 在Filter存储用户Token

      1
      2
      3
      4
      5
      6
      7
      //通过Token获取userID,并且将之存入ThreadLocal,以便后续业务调用
      String userId = jwtTokenUtil.getUsernameFromToken(authToken);
      if(userId == null){
      return ;
      }else{
      CurrentUser.saveUserId(userId);
      }

JWT修改测试

  • 步骤

    1. 通过postman工具发送两个请求

      登陆获取token:http://localhost/auth?userName=admin&password=admin123

      1
      2
      3
      4
      5
      6
      7
      8
      {
      "data": {
      "randomKey": "dqi3u1",
      "token": "eyJhbGciOiJIUzUxMiJ9.eyJyYW5kb21LZXkiOiJkcWkzdTEiLCJzdWIiOiIzIiwiZXhwIjoxNTkzMDA2NjU3LCJpYXQiOjE1OTI0MDE4NTd9.-t2idrpFLttX7cUqeW6QbuNXXt7wCUX0qrpBFjbxKctSGpc75an-35j0FpYEDPGKFqp2qp-Mk46ol0DkoSrl8Q"
      },
      "msg": "",
      "status": 0
      }

      测试token是否记录userID

      http://localhost/hello

      HEADER:Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJyYW5kb21LZXkiOiJkcWkzdTEiLCJzdWIiOiIzIiwiZXhwIjoxNTkzMDA2NjU3LCJpYXQiOjE1OTI0MDE4NTd9.-t2idrpFLttX7cUqeW6QbuNXXt7wCUX0qrpBFjbxKctSGpc75an-35j0FpYEDPGKFqp2qp-Mk46ol0DkoSrl8Q

      1
      "请求成功!"
    2. 测试Controller

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Controller
      @RequestMapping("/hello")
      public class ExampleController {

      @RequestMapping("")
      public ResponseEntity hello() {
      //在filter中,通过token获取userId后,存储在threadLocal,在这里进行获取
      System.out.println(CurrentUser.getCurrentUser());

      return ResponseEntity.ok("请求成功!");
      }
      }

总结:

  1. 首次登陆时,会将userID封装在Token中,且token的过期时间设置为7天,并响应token给用户
  2. 用户再次请求时,将Token存储在请求头的Authorization中,服务端可以对其进行解析,获取userID,并存储在ThreadLocal,确保UserID局部化到每一个用户
  3. 利用第二步存储在ThreadLocal的UserID来进行业务操作,比如获取用户信息等;(常见作用是,以UserID为key,UserInfo为value存储在redis中,设置一个过期时间,若redis不存在相应信息,则需要进行重新登陆等操作)userID -> key -> redis[userInfo] ->30分钟

业务功能开发

  1. 增加用户服务并提供接口
  2. 初步了解AP|网关与服务之间交互的过程
  3. 根据接口文档开发用户接口

用户服务与网关交互

  • 步骤

    1. 复制Gateway模块,修改为User模块

    2. 在User模块实现UserAPI,并书写login方法,并启动,注册到zookeeper

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Component
      @Service(interfaceClass = UserAPI.class) //暴露接e口,注册到Zookeeper
      public class UserImpl implements UserAPI {
      @Override
      public boolean login(String userName, String password) {
      System.out.println("this is user service!!"+userName+","+password);
      return false;
      }
      }
    3. 网关模块Gateway调用用户模块的login

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      @RestController
      public class AuthController {

      @Autowired
      private JwtTokenUtil jwtTokenUtil;

      @Resource(name = "simpleValidator")
      private IReqValidator reqValidator;
      //注入远程对象
      @Reference(interfaceClass = UserAPI.class)
      UserAPI userAPI;

      @RequestMapping(value = "${jwt.auth-path}") //url注入
      public ResponseEntity<?> createAuthenticationToken(AuthRequest authRequest) {//通过url传入参数,dto对象
      //远程调用dubbo
      userAPI.login(authRequest.getUserName(), authRequest.getPassword());

      boolean validate = reqValidator.validate(authRequest);

      if (validate) {
      final String randomKey = jwtTokenUtil.getRandomKey();
      final String token = jwtTokenUtil.generateToken(authRequest.getUserName(), randomKey);
      return ResponseEntity.ok(new AuthResponse(token, randomKey));
      } else {
      throw new GunsException(BizExceptionEnum.AUTH_REQUEST_ERROR);
      }
      }
      }
    4. 启动Gateway模块,并访问url

      1
      http://localhost/auth?userName=admin&password=admin
    5. 在User模块控制台查看参数

基于用户业务的API修改

注册业务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public boolean register(UserModel userModel) {
//将注册信息实体转换为数据实体
MoocUserT moocUserT = new MoocUserT();
moocUserT.setUserName(userModel.getUserName());
moocUserT.setEmail(userModel.getEmail());
moocUserT.setUserPhone(userModel.getPhone());
moocUserT.setAddress(userModel.getAddress());
//创建时间和修改时间 -> current_timestamp,自动生成

// 密码加密 -> 【MD5混淆加密 + 盐值】 -> shiro
String md5Password = MD5Util.encrypt(userModel.getPassword());
moocUserT.setUserPwd(md5Password);

//将数据实体存入数据库
Integer insert = moocUserTMapper.insert(moocUserT);
if (insert > 0) {
return true;
} else {
return false;
}
}

Guns框架学习

Guns源码

快速构建应用系统,在此基础上进行系统高并发、高可用的研究

基本概念

  1. 快速构建后台管理系统的开源框架
  2. Guns默认提供诸多业务系统的基本功能
  3. Guns集成诸多优秀开源框架

作用

搭建Guns环境

步骤

  1. 导入admin的数据库
  2. 通过IDEA导入整个guns项目
  3. 将admin中的yml配置文件的datasource更改为自己的数据库
  4. 通过SpringBoot的启动类来启动
  5. 访问后台系统(admin—-111111)

结构组成

核心技术

  • Spring Boot
  • Mybatis-plus
  • Swagger(注释生成)
  • Shiro
  • SpringMVC

模块介绍

  • guns-parent : guns的maven父工程
  • guns-admin : guns的业务子模块
  • guns-rest : guns的Restful支持模块
  • guns-core
  • guns-generator:生成代码

生成代码

  • 步骤:
    1. 在数据库创建表
    2. 启动项目,通过生成代码模块生成业务代码并生成相应的表SQL语句,将新的表增加到系统目录内部
    3. 根据需求,调整并实现基本的CRUD操作

作用

  • 使用Guns进行基本业务功能实现
  • 分布式系统搭建

问题

JWT概念、原理

RPC中,实体类需要序列化

总结

细节注意:

  1. 一般类的属性都设置成私有的,这样防止属性直接被访问和修改,需要调用get、set方法

  2. builder设计模式(网关的用户模块Token的生成)

    builder

  3. 微服务中,对外暴露接口时,一般命名以Service

    BO\PO\POJO

0%