转型Java微服务

最近有项目指定使用Java技术的需求,给团队分享的资料

转型Java微服务

主要内容

  1. 基础(概述,简单了解)
  2. 单体服务开发(重点
  3. 微服务集成(前期了解)
  4. 部署(了解)
  5. 常用功能(了解)
  6. 注释规范(重点
  7. 服务分层规范(重点
  8. 服务拆分(划分)(了解)
  9. Restful-API规范(重点
  10. 其他(了解)
  11. 附录(了解)

(一) 基础(概述)

1. Java 、DotNet Core

对比

  • 相同点:两者都是开源、跨平台的高级语言,都能构建web、移动、桌面、物联网、机器学习、云原生等应用。
  • 差异:开源协议、IDE、服务器、版本(对比部分)转型Java微服务_第1张图片
    版本更新:

.NET Core发布计划: https://dotnet.microsoft.com/platform/support/policy/dotnet-core转型Java微服务_第2张图片
转型Java微服务_第3张图片
Oracle Java SE Product Releases:https://www.oracle.com/java/technologies/java-se-support-roadmap.html
转型Java微服务_第4张图片

Web Framework Benchmarks Round 19:https://www.techempower.com/benchmarks/#section=data-r19&hw=ph&test=composite
转型Java微服务_第5张图片

2. Java环境

  • 生态:Java生态好,大规模微服务集群几乎都有解决方案。
  • 碎片化:越来越碎片化,尤其是Java 8之后,有众多版本的OpenJDK(腾讯、阿里、华为、亚马逊、微软等),虽然促进生态发展,但收敛统一比较困难
  • 开发:社区繁荣(GitHub,Gitee),成功案例多,利于开发、招聘等。

3. 本地开发环境

  • JDK:选用JDK 8,OpenJDK(Sun版本)缺少部分JDK私有内容,可能会用到一部分依赖JDK私有部分的组件,故而选用JDK 8

    jdk:Java SE Development Kit 缩写,java标准版开发工具包
    Java SE:Java Platform Standard Edition ,标准版。JavaEE 企业版、Java ME 微型版

  • IDE:Eclipse(免费)、IDEA(收费),注:以下示例大都使用IDEA

4. 名词解释

  • JavaBean:可重用组件,更多的是一种规范(包含一组set和get方法的java对象),使应用程序更面向对象,封装数据,分类应用业务逻辑和显示逻辑,降低开发复杂度和维护成本。
  • Entity:实体bean,一般用于ORM对象关系映射,实体对应数据库表,一般无业务逻辑。
  • DTO:数据传输对象(Data Transfer Object),也是实体bean,往往用于HTTP等传输
  • EJB:企业Bean(Enterprise JavaBean),JavaEE特有部分,包含回话Bean(Session Bean)、实体Bean(Entity Bean)、和消息驱动Bean(MessageDriven Bean)。一般项目中不使用。
  • POJO:简单Java对象(Plain Ordinary Java Object),也是JavaBean,为避免和EJB混淆创造的,包括get/set,无业务逻辑,DTO、Entity都是可以归属为POJO。
  • Maven:翻译为"专家"、“内行”, 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理,是 Apache 下的一个纯 Java 开发的开源项目。参考资料

(二)单体服务开发

1. 框架

Springboot简介:伴随Spring而生,减少大量配置,集成常用库。大大开发降低入门难度。
结构:工程、配置、启动类、测试类。
内容:详细见代码
官网地址(https://spring.io/projects/spring-boot)

Why Spring?
Spring makes programming Java quicker, easier, and safer for everybody. Spring’s focus on speed, simplicity, and productivity has made it the world's most popular Java framework.

Spring让Java开发更快、更简单、更安全,因速度、简单、高效使得Spring成为世界上最流行的Java框架

2. 技术栈

ⅰ- ORM:MyBatis-Plus
Mybatis的扩展,简化增删查改。
官网地址(https://mp.baomidou.com)

ⅱ- 参数验证:Spring Framework Validation,Spring自带功能
Reference(https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation)
API Doc(https://docs.spring.io/spring-framework/docs/current/javadoc-api/)

ⅲ- Redis: Spring Data Redis
Reference( https://spring.io/projects/spring-data-redis)

ⅳ- json处理:alibaba/fastjson
Reference(https://github.com/alibaba/fastjson)

实践内容
ⅰ- 分层: controller、service、mapper
ⅱ- 业务函数服务: 依赖注入
  声明服务接口、实现服务方法、注册实现服务(添加注解**@Service**)、使用服务(构造器注入、自动注入、指定注入)
ⅲ- API实现: 添加控制类、编写API函数、公开路由
ⅳ- 代码自动生成
  Reference(https://mp.baomidou.com/guide/generator.html)

注:实践视频播放到14:00时可跳至15:50(花了一分多钟找问题)


4. 实践0 - 创建程序

  1. 创建项目
    转型Java微服务_第6张图片
    转型Java微服务_第7张图片
    转型Java微服务_第8张图片
    转型Java微服务_第9张图片
    转型Java微服务_第10张图片

  2. 编写配置
    工程文件和配置文件内容请参考附录

转型Java微服务_第11张图片
转型Java微服务_第12张图片
转型Java微服务_第13张图片

5. 实践ⅰ- 分层

转型Java微服务_第14张图片

mapper:Mybatis概念,请参考官方说明
分层规范:参考服务规范

6. 实践ⅱ- 数据库实体

采用postgresql和mybatis-plus

  1. 添加项目引用(pom.xml)
        
        <dependency>
            <groupId>org.postgresqlgroupId>
            <artifactId>postgresqlartifactId>
            <scope>runtimescope>
        dependency>

        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.2version>
        dependency>
  1. 修改程序配置(application.yml)
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://192.168.1.161:5432/bup_basedata_dev
    username: postgres
    password: *******

mybatis:
  type-aliases-packag: com.biocome.basedata.entity
  mapper-locations: classpath:mapper/*.xml
  1. 编写数据库实体
    详细使用参考mybatis和mybatis-plus
package com.biocome.basedata.entity;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 

* 用户 *

* * @author yiquan * @since 2021-03-05 */
@Data //这个必须写 否则 自行实现每个字段 getter /setter @EqualsAndHashCode(callSuper = false) @TableName("bup_t_user") //这个必须写 数据库名称和类名称不一致 public class User implements Serializable { //Serializable 必须写 序列化用 private static final long serialVersionUID = 1L; /** * 用户ID */ @TableId private String userId; /** * 用户名 */ private String userName; /** * 密码 */ private String password; /** * 单位GUID */ private String orgGuid; // 省略 }

6. 实践ⅱ- 业务服务

//0 添加mapper , 如果不是数据库服务 就不需要
public interface UserMapper extends BaseMapper<User> {

}

//1 声明服务接口,如果不是数据库仓库 就不需要 extends IService 
public interface IUserService extends IService<User> {
    String GenerateToken(User user);
}

//2 实现接口
@Service   //3 注册服务 , 如果不是数据库服务 就不需要 extends ServiceImpl
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override 
    public String GenerateToken(User user) {
        return "token";
    }
}

//4 使用服务
public class UserController{
    /* 自动注入 不推荐
    * @Autowired
    * private IUserService userService;
    */

    private final IUserService userService;

    //构造函数注入
    public UserController(IUserService userService) {
        this.userService = userService;
    }

    @GetMapping("/{uid}")
    public ResponseEntity<User> Get(@PathVariable String uid) {        
        //使用mybatis-plus领域函数
        User user = userService.getById(uid);
        //使用自定义领域函数
        String token = userService.GenerateToken(user);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }

        return ResponseEntity.ok(user);
    }
}


6. 实践ⅲ- API

//添加控制器
@RestController  //添加Rest注解,注:RestController包含Controller、ResponseBody等注解
@RequestMapping("/api/v1/user")  //声明类路由
public class UserController{

    //Get
    @GetMapping("/{uid}")   //声明API路由,Get /api/v1/user/{uid} ,如果没有声明类路由 Get /{uid}
    public ResponseEntity<User> Get(@PathVariable String uid) {  //@PathVariable注解,获取路由数据  
        return ResponseEntity.ok(new User());
    }

    //Post
    @PostMapping
    public ResponseEntity<?> Create(@RequestBody CreateUser user){ //@RequestBody 获取Body数据
        return ResponseEntity.ok(new User());
    }

    //Put
    @PutMapping("/{uid}")
    public ResponseEntity<?> Put(@PathVariable String uid, @RequestBody ChangeUser changeUser) {
        return ResponseEntity.ok(new User());
    }

    //删除
    @DeleteMapping("/{uid}")
    public ResponseEntity<?> Delete(@PathVariable String uid) {
        return ResponseEntity.ok(null);
    }

    //参数验证
     @PostMapping("/login")
    public ResponseEntity<?> Login(@Validated @RequestBody LoginUser loginUser){ //@Validated注解 进行参数验证
        return new ResponseEntity("账号或则密码错误", HttpStatus.BAD_REQUEST);
    }
    /* 参数验证DTO
    @Data
    public class LoginUser {

        @NotBlank(message = "登录名不能为空")
        private String userName;

        @NotBlank(message = "密码不能用空")
        private String password ;
    }
    */
}

7. 实践ⅳ- 代码自动生成

拷贝示例工程中代码CodeGenerator,配置数据库后,运行后输入表名称即可。注:代码生成示例需要修改模块名称,Basedata中不需要。

参考说明:https://baomidou.com/guide/generator.html

示例代码

package com.biocome.generator;

import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * 

* 代码生成器 *

* * @author yiquan * @since 2021-02-22 10:41 **/
public class CodeGenerator { /** * 监听输入 * * @param tip 监听输入项 * @return 输入内容 * @throws MybatisPlusException 输入异常 * @author yiquan * @since 2021-02-22 10:58 **/ public static String scanner(String tip) { Scanner scanner = new Scanner(System.in); StringBuilder help = new StringBuilder(); help.append("请输入" + tip + ":"); System.out.println(help.toString()); if (scanner.hasNext()) { String ipt = scanner.next(); if (StringUtils.isNotBlank(ipt)) { return ipt; } } throw new MybatisPlusException("请输入正确的" + tip + "!"); } /** * 主函数 * * @param args 启动参数 * @return 无 * @author yiquan * @since 2021-02-22 10:58 * @update yiquan 2021-02-22 11:01 修改包生成格式 * @update yiquan 2021-02-22 11:00 更改从mysql改为postgre **/ public static void main(String[] args) { // 代码生成器 AutoGenerator mpg = new AutoGenerator(); // 全局配置 GlobalConfig gc = new GlobalConfig(); String projectPath = System.getProperty("user.dir"); gc.setOutputDir(projectPath + "/src/main/java"); gc.setAuthor("yiquan"); gc.setOpen(false); //gc.setSwagger2(true); //实体属性 Swagger2 注解 mpg.setGlobalConfig(gc); // 数据源配置 DataSourceConfig dsc = new DataSourceConfig(); dsc.setUrl("jdbc:postgresql://192.168.1.161:5432/bup_basedata_dev"); dsc.setDriverName("org.postgresql.Driver"); dsc.setUsername("postgres"); dsc.setPassword("******"); mpg.setDataSource(dsc); // 包配置 PackageConfig pc = new PackageConfig(); pc.setModuleName("basedata");//scanner("模块名")); pc.setParent("com.biocome"); mpg.setPackageInfo(pc); // 自定义配置 InjectionConfig cfg = new InjectionConfig() { @Override public void initMap() { // to do nothing } }; // 如果模板引擎是 freemarker String templatePath = "/templates/mapper.xml.ftl"; // 如果模板引擎是 velocity // String templatePath = "/templates/mapper.xml.vm"; // 自定义输出配置 List<FileOutConfig> focList = new ArrayList<>(); // 自定义配置会被优先输出 focList.add(new FileOutConfig(templatePath) { @Override public String outputFile(TableInfo tableInfo) { // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!! return projectPath + "/src/main/resources/mapper/" + pc.getModuleName() + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML; } }); /* cfg.setFileCreate(new IFileCreate() { @Override public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) { // 判断自定义文件夹是否需要创建 checkDir("调用默认方法创建的目录,自定义目录用"); if (fileType == FileType.MAPPER) { // 已经生成 mapper 文件判断存在,不想重新生成返回 false return !new File(filePath).exists(); } // 允许生成模板文件 return true; } }); */ cfg.setFileOutConfigList(focList); mpg.setCfg(cfg); // 配置模板 TemplateConfig templateConfig = new TemplateConfig(); // 配置自定义输出模板 //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别 // templateConfig.setEntity("templates/entity2.java"); // templateConfig.setService(); // templateConfig.setController(); templateConfig.setXml(null); mpg.setTemplate(templateConfig); // 策略配置 StrategyConfig strategy = new StrategyConfig(); strategy.setNaming(NamingStrategy.underline_to_camel); strategy.setTablePrefix("bup_t"); strategy.setColumnNaming(NamingStrategy.underline_to_camel); //strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!"); strategy.setEntityLombokModel(true); strategy.setRestControllerStyle(true); // 公共父类 strategy.setSuperControllerClass("BaseController"); // 写于父类中的公共字段 strategy.setSuperEntityColumns("id"); strategy.setInclude(scanner("表名,多个英文逗号分割").split(",")); strategy.setControllerMappingHyphenStyle(true); strategy.setTablePrefix(pc.getModuleName() + "_"); mpg.setStrategy(strategy); mpg.setTemplateEngine(new FreemarkerTemplateEngine()); mpg.execute(); } }

(三)微服务集成

1. 总体架构

需要部署注册中心和网关(已经开发完毕,待整理)

实践内容一览
服务注册、服务发现、网关路由转发、服务通信(RPC)

2. 实践ⅰ- 服务注册、发现

  1. 添加注册中心服务发现组件(pom.xml)
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
            <version>2.2.5.RELEASEversion>
        dependency>
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
            <version>2.2.5.RELEASEversion>
        dependency>

  1. 添加配置(application.yml)
spring:
  application:
    name: basedata   # 应用名 - 开发时**需要修改**
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.152:8848   # 注册中心地址 - 开发时**需要修改 **
        metadata:
          routes: "/api/v1/user/**,/api/v1/org/**"  # 注册路由 - 开发时**需要修改**
      router:
        dataid: gateway_dynamic_route # 动态路由配置文件名 - 开发时无(按)需修改
        group: DEFAULT_GROUP # 动态路由配置分组 - 开发时无(按)需修改
  1. 注册动态路由
    我们在通过网关访问服务时,网关是根据服务名字识别服务并进行转发,即/服务名/对应服务的路由
    比如获取xxx用户(xxService服务),API为api/v1/user/xxxx
    直接访问该服务为GET /api/v1/user/xxxx
    通过网关访问则为GET /xxService/api/v1/user/xxxx
    这种方式最突出问题的是访问资源地址不统一。一方面对调用方(Web APP、移动APP等)及其不友好;另一方面如果对进行服务切分,势必会导致资源地址更换,某某资源从xxService更改为yyService,那么所有通过网关的请求方都需要更改资源地址,扩展升级系统成本巨大。
     
    解决办法是使用gateway动态路由方式:
  • 方法一:创建包(init.route),引用gateway(使用断言相关实体,查看下方 补充资料1 ),然后在项目中禁用网关功能,并使用代码RegisterService(查看下方 补充资料2 )注册。
    注:禁用网关,“If you include the starter, but you do not want the gateway to be enabled, set spring.cloud.gateway.enabled=false.”
  • 方法二:创建包(init.route),依次添加类NameUtils、FilterDefinition、PredicateDefinition、RouteDefinition、RegisterService(查看下方 补充资料2

补充资料1 引用gateway方式:

        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>

补充资料2 代码示例:

查看代码-NameUtils

import java.util.regex.Pattern;

/**
 * @author Spencer Gibb
 */
public final class NameUtils {

    private NameUtils() {
        throw new AssertionError("Must not instantiate utility class.");
    }

    /**
     * Generated name prefix.
     */
    public static final String GENERATED_NAME_PREFIX = "_genkey_";

    private static final Pattern NAME_PATTERN = Pattern.compile("([A-Z][a-z0-9]+)");

    public static String generateName(int i) {
        return GENERATED_NAME_PREFIX + i;
    }
}

查看代码-FilterDefinition

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import javax.validation.ValidationException;
import javax.validation.constraints.NotNull;

import org.springframework.validation.annotation.Validated;

import static org.springframework.util.StringUtils.tokenizeToStringArray;

/**
 * @author Spencer Gibb
 */
@Validated
public class PredicateDefinition {

    @NotNull
    private String name;

    private Map<String, String> args = new LinkedHashMap<>();

    public PredicateDefinition() {
    }

    public PredicateDefinition(String text) {
        int eqIdx = text.indexOf('=');
        if (eqIdx <= 0) {
            throw new ValidationException(
                    "Unable to parse PredicateDefinition text '" + text + "'" + ", must be of the form name=value");
        }
        setName(text.substring(0, eqIdx));

        String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

        for (int i = 0; i < args.length; i++) {
            this.args.put(NameUtils.generateName(i), args[i]);
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Map<String, String> getArgs() {
        return args;
    }

    public void setArgs(Map<String, String> args) {
        this.args = args;
    }

    public void addArg(String key, String value) {
        this.args.put(key, value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PredicateDefinition that = (PredicateDefinition) o;
        return Objects.equals(name, that.name) && Objects.equals(args, that.args);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, args);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("PredicateDefinition{");
        sb.append("name='").append(name).append('\'');
        sb.append(", args=").append(args);
        sb.append('}');
        return sb.toString();
    }

}

查看代码-PredicateDefinition

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;

import javax.validation.ValidationException;
import javax.validation.constraints.NotNull;

import org.springframework.validation.annotation.Validated;

import static org.springframework.util.StringUtils.tokenizeToStringArray;

/**
 * @author Spencer Gibb
 */
@Validated
public class PredicateDefinition {

    @NotNull
    private String name;

    private Map<String, String> args = new LinkedHashMap<>();

    public PredicateDefinition() {
    }

    public PredicateDefinition(String text) {
        int eqIdx = text.indexOf('=');
        if (eqIdx <= 0) {
            throw new ValidationException(
                    "Unable to parse PredicateDefinition text '" + text + "'" + ", must be of the form name=value");
        }
        setName(text.substring(0, eqIdx));

        String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

        for (int i = 0; i < args.length; i++) {
            this.args.put(NameUtils.generateName(i), args[i]);
        }
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Map<String, String> getArgs() {
        return args;
    }

    public void setArgs(Map<String, String> args) {
        this.args = args;
    }

    public void addArg(String key, String value) {
        this.args.put(key, value);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        PredicateDefinition that = (PredicateDefinition) o;
        return Objects.equals(name, that.name) && Objects.equals(args, that.args);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, args);
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("PredicateDefinition{");
        sb.append("name='").append(name).append('\'');
        sb.append(", args=").append(args);
        sb.append('}');
        return sb.toString();
    }

}

查看代码-RouteDefinition

import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
import javax.validation.ValidationException;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.net.URI;
import java.util.*;

import static org.springframework.util.StringUtils.tokenizeToStringArray;


@Validated
public class RouteDefinition {

    private String id;

    @NotEmpty
    @Valid
    private List<PredicateDefinition> predicates = new ArrayList<>();

    @Valid
    private List<FilterDefinition> filters = new ArrayList<>();

    @NotNull
    private URI uri;

    private Map<String, Object> metadata = new HashMap<>();

    private int order = 0;

    public RouteDefinition() {
    }

    public RouteDefinition(String text) {
        int eqIdx = text.indexOf('=');
        if (eqIdx <= 0) {
            throw new ValidationException(
                    "Unable to parse RouteDefinition text '" + text + "'" + ", must be of the form name=value");
        }

        setId(text.substring(0, eqIdx));

        String[] args = tokenizeToStringArray(text.substring(eqIdx + 1), ",");

        setUri(URI.create(args[0]));

        for (int i = 1; i < args.length; i++) {
            this.predicates.add(new PredicateDefinition(args[i]));
        }
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public List<PredicateDefinition> getPredicates() {
        return predicates;
    }

    public void setPredicates(List<PredicateDefinition> predicates) {
        this.predicates = predicates;
    }

    public List<FilterDefinition> getFilters() {
        return filters;
    }

    public void setFilters(List<FilterDefinition> filters) {
        this.filters = filters;
    }

    public URI getUri() {
        return uri;
    }

    public void setUri(URI uri) {
        this.uri = uri;
    }

    public int getOrder() {
        return order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    public Map<String, Object> getMetadata() {
        return metadata;
    }

    public void setMetadata(Map<String, Object> metadata) {
        this.metadata = metadata;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        RouteDefinition that = (RouteDefinition) o;
        return this.order == that.order && Objects.equals(this.id, that.id)
                && Objects.equals(this.predicates, that.predicates) && Objects.equals(this.filters, that.filters)
                && Objects.equals(this.uri, that.uri) && Objects.equals(this.metadata, that.metadata);
    }

    @Override
    public int hashCode() {
        return Objects.hash(this.id, this.predicates, this.filters, this.uri, this.metadata, this.order);
    }

    @Override
    public String toString() {
        return "RouteDefinition{" + "id='" + id + '\'' + ", predicates=" + predicates + ", filters=" + filters
                + ", uri=" + uri + ", order=" + order + ", metadata=" + metadata + '}';
    }

}

查看代码-RegisterService

import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

@Slf4j
@Component
public class RegisterService implements ApplicationRunner {

   @Value("${spring.application.name}")
   private String applicationName;

   @Value("${spring.cloud.nacos.discovery.metadata.routes}")
   private String routes;

   @Value("${spring.cloud.nacos.discovery.server-addr}")
   private String nacosaddr;

   @Value("${spring.cloud.nacos.router.dataid}")
   private String dataid;

   @Value("${spring.cloud.nacos.router.group}")
   private String group;

   private String SeriveId;

   @Override
   public void run(ApplicationArguments args) throws Exception {
       SeriveId = "dynamic_" + applicationName;
       ConfigService configService = getConfigService();
       String content = configService.getConfig(dataid, group, 5000);

       List<RouteDefinition> routeDefinitions = new ArrayList<>();
       AtomicBoolean nacosHasValue = new AtomicBoolean(false);
       AtomicReference<RouteDefinition> routeDefinitionRegister = new AtomicReference<>();

       if (StringUtils.isNotBlank(content))
           routeDefinitions = JSONObject.parseArray(content, RouteDefinition.class);


       routeDefinitions.forEach(routeDefinitionItem -> {
           if (routeDefinitionItem.getId().equals(SeriveId)) {
               routeDefinitionRegister.set(routeDefinitionItem);
               nacosHasValue.set(true);
           }
       });

       if (!nacosHasValue.get()) {
           RouteDefinition routeDefinition = getRouteDefinition();
           routeDefinitionRegister.set(routeDefinition);
       }

       List<PredicateDefinition> predicateDefinitions = getPredicateDefinitions();

       RouteDefinition routeDefinition = routeDefinitionRegister.get();
       routeDefinition.setPredicates(predicateDefinitions);

       routeDefinitions.removeIf(routeDefinitionItem -> routeDefinitionItem.getId().equals(SeriveId));
       routeDefinitions.add(routeDefinition);


       configService.publishConfig(dataid, group, JSONObject.toJSONString(routeDefinitions), "json");
   }

   private RouteDefinition getRouteDefinition() throws URISyntaxException {
       RouteDefinition routeDefinition = new RouteDefinition();
       routeDefinition.setId(SeriveId);
       routeDefinition.setUri(new URI("lb://" + applicationName));
       routeDefinition.setOrder(1);
       return routeDefinition;
   }

   private List<PredicateDefinition> getPredicateDefinitions() {
       List<PredicateDefinition> predicateDefinitions = new ArrayList<>();
       PredicateDefinition predicateDefinition = new PredicateDefinition();
       predicateDefinition.setName("Path");
       Map<String, String> predicateArgs = new HashMap<>();
       AtomicInteger i = new AtomicInteger();
       Arrays.asList(routes.split(",")).forEach(route -> {
           predicateArgs.put("pattern" + (i.getAndIncrement()), route);
       });
       predicateDefinition.setArgs(predicateArgs);
       predicateDefinitions.add(predicateDefinition);
       return predicateDefinitions;
   }

   private ConfigService getConfigService() throws NacosException {
       Properties properties = new Properties();
       properties.put("serverAddr", nacosaddr);
       properties.setProperty("", "");
       ConfigService configService = NacosFactory.createConfigService(properties);
       return configService;
   }
}

3. 实践ⅱ- 路由转发

SpringCloud 构成

网关转发路由,需要在API前面添加服务名称
转型Java微服务_第15张图片

如果转发路由需要与服务路由保持一致,则需要使用动态路由,添加路由断言可以根据路由识别服务地址,详细参考2-实践i-服务注册、发现、网关使用方式、路由断言配置

转型Java微服务_第16张图片

4. 实践ⅲ- 通信(RPC)

  1. 添加项目引用 - 服务端和客户端都需要
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-dubboartifactId>
        <version>2.2.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        <version>2.2.5.RELEASEversion>
    dependency>
    <dependency>
        <groupId>com.alibaba.cloudgroupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
        <version>2.2.5.RELEASEversion>
    dependency>
  1. 服务端 - 提供服务
    添加配置
dubbo:
  application:
    name: dubbo-spring-cloud-provider-basedata   # 服务名 - 需更高
  scan:
    base-packages: com.biocome.basedata.service  # 服务实现的路径 - 需更改
  protocol:
    name: dubbo  # 协议 - 无需更改
    port: -1

编写代码

package com.biocome.basedata.service.authorization;

public interface IAuthorizationServerProvider {
    String GenerateToken(User user);
}

@DubboService(version = "1.0.0")
@Service
public class JWTAuthorizationService implements IAuthorizationServerProvider {
    @Override
    public String GenerateToken(User user) {
        return "token";
    }
}
  1. 消费端 - 消费
    添加配置
dubbo:
  protocol:
    name: dubbo
    port: -1
  cloud:
    subscribed-services: dubbo-spring-cloud-provider-basedata,dubbo-spring-cloud-provider-web # 声明依赖的服务
  consumer:
    timeout: 5000
    check: false

编写代码

package com.biocome.basedata.service.authorization;  // 命名空间不能更改

public interface IAuthorizationServerProvider {
    boolean ValidToken(String token);
}

//使用

@RestController
public class UserBasedataController {

    // 类似本地调用服务函数一样
    @DubboReference(version = "1.0.0")
    IAuthorizationServerProvider AuthorizationServerProvider;

    @PostMapping("/token/check1")
    public String IsValidToken1(@RequestBody CheckToken token) {
        return AuthorizationServerProvider.ValidToken(token.getToken()) ? "有效" : "无效";
    }

}

dubbo 官网地址:https://dubbo.apache.org/zh/

添加RPC整体构成(注:RPC不能循环调用):
转型Java微服务_第17张图片

(四)部署

部署nacos

docker-compose -f standalone-derby.yaml up
standalone-derby.yaml

version: "2"
services:
  nacos:
    image: nacos/nacos-server:latest
    container_name: nacos-standalone
    environment:
    - PREFER_HOST_MODE=hostname
    - MODE=standalone
    volumes:
    - ./standalone-logs/:/home/nacos/logs
    - ./init.d/custom.properties:/home/nacos/init.d/custom.properties
    ports:
    - "8848:8848"

/init.d/custom.properties/custom.properties

# metrics for prometheus
management.endpoints.web.exposure.include=*

默认账号密码:nacos/nacos

部署服务 - 版本一

  1. 编译artifacts
  2. 制作image
  3. 启动服务
    转型Java微服务_第18张图片

转型Java微服务_第19张图片
转型Java微服务_第20张图片
转型Java微服务_第21张图片

注意:METE-INF/MANIFEST.MF目录必须放在\src\main\resources下(原因:里面配置有入口类和依赖等)
转型Java微服务_第22张图片
转型Java微服务_第23张图片

转型Java微服务_第24张图片

转型Java微服务_第25张图片

打包为镜像:docker build -t bup/dubbo_basedata:0.0.1 .

转型Java微服务_第26张图片

运行:docker run --name java_basedata -p 30221:30221 bup/dubbo_basedata:0.0.1

转型Java微服务_第27张图片

# 依赖jdk8
FROM java:8
EXPOSE 30221
MAINTAINER James # 作者
WORKDIR /app # 运行路径
COPY /out/artifacts/dubbo_service_jar/ /app   # 写对应的生成jar生成的目录
ENTRYPOINT ["java","-jar","dubbo_service.jar"]

部署服务 - 版本二

  1. 编译artifacts
    编译前添加文件
    Dockerfile:编译文件
    docker-compose.yml:容器启动脚本
    dubbo_basedata_env.env:环境变量文件
    bup_start.sh:启动shell脚本文件
  2. 部署
    使用命令bash bup_start.sh install部署容器

转型Java微服务_第28张图片
转型Java微服务_第29张图片

各文件内容
Dockerfile:

# 依赖jdk8
FROM java:8
EXPOSE 30221
MAINTAINER yiquan
WORKDIR /app
COPY . .
ENTRYPOINT ["java","-jar","dubbo_service.jar"]

docker-compose.yml

version: '3.4'

services:
  services_basedata:                  # 服务名称
    image: bup/dubbo_basedata:0.1     # 使用镜像
    container_name: dubbo_basedata    # 容器名称
    restart: always                   # 保证开机自启
    ports:
      - "30221:30221"                 # http映射端口
    env_file:
      - dubbo_basedata_env.env        # 环境变量文件,参考 配置 管理说明
    volumes:
      - /etc/localtime:/etc/localtime # 容器内部时间与宿主机一致
    logging:
      driver: "json-file"             # 日志格式
      options:
        max-size: "200M"              # 限制日志文件大小
        max-file: "10"                # 限制日志文件个数
    networks:
      - bup-networks-v1               # 网络

networks:
  bup-networks-v1:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 172.30.0.0/16

环境变量文件,配置读取文件配置的环境变量,而不是系统配置的环境。(目的:数据库等配置正式环境和开发环境不一样,需要修改环境变量文件而不是重新制作jar包)
例如application.yml中

spring:
  cloud:
    nacos:
      discovery:
        server-addr: ${RegisterServerAddr:127.0.0.1:8848}  # RegisterServerAddr需要配置正式环境中的地址

那么环境变量文件(dubbo_basedata_env.env)中内容为:

RegisterServerAddr=192.168.1.152:8848

启动文件bup_start.sh

#! /bin/bash

command=$1 #命令
container_name="dubbo_basedata"
image_name="bup/dubbo_basedata:0.1"

if [ "$command" = "" ];then
  docker-compose -p bup-compose -f docker-compose.yml down
  docker-compose -p bup-compose -f docker-compose.yml up -d
else
  docker-compose -p bup-compose -f docker-compose.yml down
  docker rm -f $container_name
  docker rmi $image_name
  docker build -t $image_name .
  docker-compose -p bup-compose -f docker-compose.yml up -d
fi

(五)常用功能

1. 日志

添加项目

         
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

使用

@Slf4j  // 添加注解
@RestController
public class UserBasedataController {

    @PostMapping("/token/check1")
    public String IsValidToken1(@RequestBody CheckToken token) {
        log.debug("验证登录");   //编写日志
        return "";
    }

}

2. 异常

添加全局异常拦截

package com.biocome.basedata.config;

import com.biocome.basedata.dto.*;
import com.sun.org.apache.bcel.internal.generic.ATHROW;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.util.List;

/**
 * 统一异常处理
 *
 * @author yiquan
 * @since 2021-3-2
 */
@Slf4j
@RestControllerAdvice
//public class GlobalExceptionHandlerConfig implements ResponseBodyAdvice {
public class GlobalExceptionHandlerConfig {


    /**
     * 处理全局异常
     *
     * @param request
     * @param exception
     * @return
     */
    @ExceptionHandler(value = Exception.class)
    public ResponseEntity handlerGlobeException(HttpServletRequest request, Exception exception) {
        log.error(exception.getMessage(), exception);
        return new ResponseEntity(new ResponseResult(request, HttpStatus.INTERNAL_SERVER_ERROR, "服务器异常,请稍后重试"), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    /**
     * HTTP方法无法使用
     *
     * @param request
     * @param exception
     * @return
     */
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public String handlerHTTPGlobeException(HttpServletRequest request, HttpRequestMethodNotSupportedException exception) throws HttpRequestMethodNotSupportedException {
        throw exception;
    }

    /**
     * 参数合法性校验异常
     *
     * @param exception
     * @return
     */
    @ResponseBody
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity validationBodyException(HttpServletRequest request, MethodArgumentNotValidException exception) {

        StringBuffer buffer = new StringBuffer();

        BindingResult result = exception.getBindingResult();
        if (result.hasErrors()) {

            List<ObjectError> errors = result.getAllErrors();

            errors.forEach(p -> {

                FieldError fieldError = (FieldError) p;
                log.debug("Data check failure : object{ " + fieldError.getObjectName() + " },field{ " + fieldError.getField() +
                        " },errorMessage{ " + fieldError.getDefaultMessage() + " }");
                buffer.append(fieldError.getDefaultMessage()).append(",");
            });
        }

        String message = buffer.toString().substring(0, buffer.toString().length() - 1);

        ResponseResult responseResult = new ResponseResult(request, HttpStatus.BAD_REQUEST, message);

        return new ResponseEntity(responseResult, HttpStatus.BAD_REQUEST);
    }
}

3. JSON序列化

添加Maven引用

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.75version>
        dependency>
  1. 对象转json
String userJson = JSON.toJSONString(new User());
  1. json转对象
User user = JSON.parseObject("{'userName':'xxxx'}", User.class);
  1. json转集合
List<User> users = JSONObject.parseArray("[{'userName':'xxxx'},{'userName':'yyyy'}]", User.class);

4. 对象转换


User user = userService.Get("xxxxxxxx");
UserInfoDto userInfoDto = new UserInfoDto();

BeanUtils.copyProperties(user, userInfoDto);

    //可以扩展
    
    /**
     * bean 映射扩展
     *
     * @param clazz  类型
     * @param source 转换源
     * @param     类型
     * @return 生成实体 T
     */
    public static <T> T copyProperties(Class<T> clazz, Object source) {
        T target = ClassUtils.newInstance(clazz);
        BeanUtils.copyProperties(source, target);

        return target;
    }

    /**
     * 转换集合
     *
     * @param clazz   需要转换目标对象
     * @param sources 转换数据
     * @param      target
     * @param      source
     * @return
     */
    public static <T, S> Collection<T> copyListProperties(Class<T> clazz, Collection<S> sources) {
        Collection<T> targets = new ArrayList<>();
        sources.forEach((source) -> {
            T target = ClassUtils.newInstance(clazz);
            BeanUtils.copyProperties(source, target);
            targets.add(target);
        });
        return targets;
    }

5. 获取当前登录用户

/**
 * 用户API
 *
 * @author yiquan
 * @since 2021-02-22
 */
@RestController
@RequestMapping("/api/v1/user")
public class UserController {

    @Autowired
    private HttpServletRequest request;

    private final IUserService userService;


    public UserController(IUserService userService) {
        this.userService = userService;
    }

    private User getCurrentUser() {
        String userInfoJson = request.getHeader("userinfo");
        if(StringUtils.isNotBlank(userInfoJson))
            return JSON.parseObject(userInfoJson,User.class);
         return null;
    } 

      /**
     * 创建用户
     *
     * @param user 用户Dto
     * @return 返回添加的用户
     */
    @PostMapping
    public ResponseEntity<?> Create(@RequestBody CreateUser user) {
        String createUserUid = Optional.ofNullable(getCurrentUser()).get().getUserId();

        boolean isSuccess = true;

        return isSuccess ? ResponseEntity.ok(checkResult.Entity) :
                new ResponseEntity("处理完成,没有添加成功,请稍后重试", HttpStatus.NOT_ACCEPTABLE);
    }


原理:通过网关登录,用户服务产生token,网关会保存该值。后续其他请求网关会根据token解析出用户信息userinfo(Json格式,该对象为用户服务生成token所用的用户对象实体),转发请求http header会带上该json值,各服务按需解析,过程如下:

转型Java微服务_第30张图片

6. Redis

详细说明参考官网说明文档

  1. 添加引用
     
    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-data-redisartifactId>
    dependency>
  1. 配置
package com.biocome.nacosgateway.unitity;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * RedisConfig 配置保存对象为二进制
 *
 * @author yiquan
 * @since 2021-3-8
 */
@Configuration
public class RedisConfig {
    /**
     * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
     *
     * @param redisConnectionFactory redis连接工厂类
     * @return RedisTemplate
     */
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // 使用Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);

        // 设置value的序列化规则和 key的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

  1. 使用
public class UserController extends BaseController {

    private final IUserService userService;
    private final IOrgService orgService;
    private final IAuthorizationServerProvider authorizationServerProvider;


    @Resource
    private RedisTemplate<String, User> redisTemplate;
    
    public UserController(IUserService userService,
                        IOrgService orgService,
                        IAuthorizationServerProvider authorizationServerProvider) {
        this.userService = userService;
        this.orgService = orgService;
        this.authorizationServerProvider = authorizationServerProvider;
    }

    @GetMapping("/{uid}")
    public ResponseEntity<User> get(@PathVariable String uid) {
        String key = "user_" + uid;
        ValueOperations<String, User> operations = redisTemplate.opsForValue();
        boolean hasKey = redisTemplate.hasKey(key);
        if (hasKey) {
            //获取缓存
            User user = operations.get(key);
            return ResponseEntity.ok(user);
        }

        User user = userService.getById(uid);
        if (user == null) {
            return ResponseEntity.notFound().build();
        }

        //保存缓存
        operations.set(key, user, 10, TimeUnit.SECONDS);

        return ResponseEntity.ok(user);
    }

    @DeleteMapping("/{uid}")
    public ResponseEntity<?> delete(@PathVariable String uid) {
        User user = userService.getById(uid);
        if (user == null) {
            return new ResponseEntity(HttpStatus.NOT_FOUND);
        }
        if (user.getUserName() == "admin") {
            return createResponseResult("不能删除管理员", HttpStatus.BAD_REQUEST);
        }

        boolean isSuccess = userService.removeById(uid);
        if (isSuccess) {
            String key = "user_" + uid;
            boolean hasKey = redisTemplate.hasKey(key);
            if (hasKey) {
                //删除缓存
                redisTemplate.delete(key);
            }
        }
        return isSuccess ? ResponseEntity.ok(null) :
                createResponseResult("处理完成,没有删除成功,请稍后重试", HttpStatus.NOT_ACCEPTABLE);
    }

7. 作业调度(循环任务)

参考Spring Framework

package com.biocome.basedata.scheduling;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.biocome.basedata.entity.User;
import com.biocome.basedata.service.IUserService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.Optional;

/**
 * Task Scheduling 任务调度示例
 *
 * @author yiquan
 * @since 2021-03-24
 */
@Component
@EnableScheduling
@EnableAsync
public class PrintScheduling {

    private final IUserService userService;

    public PrintScheduling(IUserService userService) {
        this.userService = userService;
    }

    /**
     * 每隔两秒打印一个时间字符串
     */
    @Async
    @Scheduled(fixedDelay = 2000)
    public void printStringDelayTwoSecond() {
        System.out.println("Scheduling示例,每隔两秒打印一个时间字符串:" + new Date());
    }

    /**
     * 每小时0分0秒 打印admin用户的Id
     */
    @Async
    @Scheduled(cron = "0 0 * * * *")
    public void printAdminUserIdByCron() {
        User user = userService.getOne(new QueryWrapper<User>().lambda().eq(User::getUserName, "admin"));
        System.out.println("Scheduling示例, 每小时0分0秒 打印admin用户的Id:" + Optional.ofNullable(user).map(u -> u.getUserId()).orElse("空"));
    }

    /**
     * 使用application配置, 每天零点 打印admin1用户的Id
     * basedata.scheduling.print-userid-cron : 0 0 0 * * *
     */
    @Async
    @Scheduled(cron = "${basedata.scheduling.print-userid-cron}")
    public void printAdminUserUidByConfigCron() {
        User user = userService.getOne(new QueryWrapper<User>().lambda().eq(User::getUserName, "admin1"));
        System.out.println("Scheduling示例, 每天零点 打印admin1用户的Id:" + Optional.ofNullable(user).map(u -> u.getUserId()).orElse("空"));
    }

}

8. 消息总线(message bus - Rabbitmq)

生产者

  1. 添加引用
  2. 配置交换机
  3. 发送消息
添加配置
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
配置交换机
package com.biocome.basedata.config;

import org.springframework.amqp.core.*;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.converter.MappingJackson2MessageConverter;

/**
 * mq配置
 *
 * @author yiquan
 * @since 2021-03-25
 * @version 1.0
 */
@Configuration
public class RabbitmqConfig {

    public static final String EXCHANGE_TOPICS_INFORM="exchange_topics_inform";

    public static final String QUEUE_INFORM_USER = "queue_inform_user";
    public static final String QUEUE_INFORM_ORG = "queue_inform_org";

    public static final String INFORM_USER_CREATE ="inform.user.create";
    public static final String INFORM_USER_GET ="inform.user.get";
    public static final String INFORM_ORG_CREATE ="inform.org.create";

     /**
     * 声明交换机
     * @return
     */
    @Bean(EXCHANGE_TOPICS_INFORM)
    public Exchange EXCHANGE_TOPICS_INFORM(){
        return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(true).build();
    }

    /**
     * 生产者 确保消息实体用json格式
     * @return
     */
    @Bean
    public Jackson2JsonMessageConverter producerJackson2MessageConverter() {
        return new Jackson2JsonMessageConverter();
    }
}
发送消息
public class UserController {

    private final IUserService userService;
    private final RabbitTemplate rabbitTemplate;
    
     public UserController(IUserService userService,
                            RabbitTemplate rabbitTemplate) {
        this.userService = userService;
        this.rabbitTemplate = rabbitTemplate;
    }

    public void get(String uid) {
        User user = userService.getById(uid);
        rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM, RabbitmqConfig.INFORM_USER_GET, user);
    }
}

消费者

  1. 添加引用
  2. 订阅消息
添加配置
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-amqpartifactId>
        dependency>
订阅消息
package com.biocome.basedata.bus;

import com.biocome.basedata.entity.Org;
import com.biocome.basedata.entity.User;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;

@Service
public class UserReceiveHandler {

    public static final String EXCHANGE_TOPICS_INFORM="exchange_topics_inform";

    public static final String QUEUE_INFORM_USER = "queue_inform_user";
    public static final String QUEUE_INFORM_ORG = "queue_inform_org";

    public static final String INFORM_USER_CREATE ="inform.user.create";
    public static final String INFORM_USER_GET ="inform.user.get";
    public static final String INFORM_ORG_CREATE ="inform.org.create";

    /**
     * 交换机EXCHANGE_TOPICS_INFORM上,申明队列 QUEUE_INFORM_USER
     * 订阅INFORM_USER_CREATE, INFORM_USER_GET, "inform.user.change.*" 消息
     *
     * @param user
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(name = EXCHANGE_TOPICS_INFORM, type = ExchangeTypes.TOPIC),
            value = @Queue(name = QUEUE_INFORM_USER, durable = "true"),
            key = {INFORM_USER_CREATE, INFORM_USER_GET, "inform.user.change.*"}))
    public void receiveUser(User user, Message message) {
        System.out.println("收到User消息,key = " + message.getMessageProperties().getReceivedRoutingKey() + " msg = " + user);
    }

    /**
     * 交换机EXCHANGE_TOPICS_INFORM上,申明队列 QUEUE_INFORM_ORG
     * 订阅INFORM_ORG_CREATE 消息
     *
     * @param org
     * @param message
     */
    @RabbitListener(bindings = @QueueBinding(
            exchange = @Exchange(name = EXCHANGE_TOPICS_INFORM, type = ExchangeTypes.TOPIC),
            value = @Queue(name = QUEUE_INFORM_ORG, durable = "true"),
            key = INFORM_ORG_CREATE))
    public void receiveOrgGet(Org org, Message message) {
        System.out.println("收到Org消息 msg = " + org);
    }
}

(六)注释规范

基础注释

  1. 类/接口注释
  2. 函数(构造函数/方法等)注释
  3. 全局变量注释
  4. 字段/属性注释

备注:简单的代码做简单注释,注释内容不大于10个字即可,另外,持久化对象或VO对象的getter、setter方法不需加注释。

/**
 * 验证路径接口
 *
 * @author yiquan
 * @since 2021-3-9
 */
public class PathVerifyService {

    /**
     * 用户ID
     */
    private String userId;

    /**
     * 用户名
     */
    private String userName;


    /**
     * 判断路径是否需要鉴权
     *
     * @param path
     * @return
     */
    Boolean shouldFilter(String path) { return true; }

}

特殊注释

  1. 典型算法
  2. 在代码不明晰处
  3. 在代码修改处加上修改标识的注释
  4. 在循环和逻辑分支组成的代码
  5. 为他人提供的接口加详细注释。

备注:注释格式自行定义,注释内容准确简洁

注释格式

  1. 单行(single-line)注释:“//……”
  2. 块(block)注释:“/……/”
  3. 文档注释:“/**……*/”
  4. javadoc 注释标签语法
@author   对类的说明 标明开发该类模块的作者
@version   对类的说明 标明该类模块的版本
@see     对类、属性、方法的说明 参考转向,也就是相关主题
@param    对方法的说明 对方法中某参数的说明
@return   对方法的说明 对方法返回值的说明
@exception  对方法的说明 对方法可能抛出的异常进行说明

(七)服务分层规范

服务内部分层

使用layered architecture(分层架构,参考文档 Buschmann, F., et al., 1996. Pattern-Oriented Software Architecture: A System ofPatterns. Wiley.C,第31-55页)。

  1. 控制层(controller)
    读取视图表现层的数据,控制用户的输入,并调用业务层的方法,返回输出;
  2. 业务层(service)
    业务需求逻辑的实现
  3. 数据访问层(dao-Data Access Objects)
    负责与数据库的数据交互,将数据进行存储读取操作

示例,银行转账:

微服务分层

防止业务代码分散,造成查看、分析困难等详细介绍请参考《领域驱动设计-软件核心复杂性应对之道》,这里仅列举部分说明:

  1. 用户界面层(或表示层)
    负责向用户显示信息和解释用户指令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人
  2. 应用层
    定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其他系统的应用层进行交互的必要渠道
    应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度
  3. 领域层(或模型层)
    负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心
  4. 基础设施层
    为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持4个层次间的交互模式。

基本原则

  1. 层中任何元素都依赖于本层的其他元素或下层的元素,向上通信必须通过间接方式进行;
  2. 关注分离,设计中每个部分都得到单独的关注,并维系系统内部复杂的交互关系;

(八)服务拆分(划分)

没有唯一、可解决所有服务划分的方法,先看看大佬们的方法。

新浪微博微服务专家xxx

  1. 纵向拆分
    业务维度进行拆分。业务关联密切功能较独立的适合拆分为一个单独的微服务。
  2. 横向拆分
    存在公共的被多个其他服务调用,且以来资源独立不与其他业务耦合,可成一个服务。

简单粗暴,有底气那就拆

阿里巴巴xxx

  1. 要迎合业务的需要
    充分考虑业务独立性和专业性,避免以团队来定义服务边界。
  2. 考虑维护成本
    拆分后维护成本要低于拆分前,需要考虑人、物、时间。不要迷恋技术,领导预算不对,中小团队(<10)尤其注意。
  3. 大型系统,服务架构调整,组织结构要适应性优化
    确保拆分的服务有独立的团队负责维护,把产品、前后端按产品线划分归到同一个产品团队。
  4. 拆分最有价值的结果是提高可扩展性
    把具有不同扩展性的服务拆分出来,分别进行部署,降低成本,提高效率。
  5. 考虑软件发布频率
    把80%不经常变动的单独管理、部署,剩下经常改的20%进行抽离。减少发布带来的风险。注意:20%属于不同的业务层面,应该分优先级和考虑权重等。

资深技术专家xxx

  1. 基于业务逻辑
    按照职责进行识别划分。
  2. 基于稳定性
    稳定的、不经常修改的和不稳定的、经常修改的分开。
  3. 基于可靠性
    按照可靠性排序,将可靠性要求高的和低的分开。避免单个模块影响整个系统。
  4. 基于高性能
    按照对性能的要求排序。将性能高的和地的分开。比如秒杀服务,其他产品服务不要求高性能,但是产品有秒杀的业务,那么就借助秒杀服务实现需求。

实践

从上面不同的拆分方式可以看出,拆分服务没有绝对的标准,只有合理才是标准,业务、人员组织、公司环境、不同的时间段等都可能影响服务的划分。如果硬套可能会遇到矛盾的地方,假使不到10个人的团队,偏偏拆分数十个或则更多,而人员配置一般为3个最优(参考:三个火枪手的观点)。以下提供一种划分思路。

  1. 分析系统,提取领域实体;
  2. 确定服务边界,确定限界上下文(基础类和聚合类);
  3. 拆分、合并限界上下文,确定微服务。

备注说明

  • 业务逻辑组织模式
  1. 事务脚本模式
  2. 领域模型
  • 聚合模式
    将领域模型组织为聚合的集合,即一个边界内领域对象的集群。

  • 识别聚合边界的原则

  1. 客户端只引用聚合根;
  2. 聚合间的引用必须使用主键;
  3. 一个事务只创建或者更新一个聚合。
  • 确认聚合颗粒度
    确认聚合颗粒度和确认领域事件等需要综合分析的事项,均可采用“xx风暴”的形式进行处理,例如确定领域中的事件,采用:事件风暴
  1. 头脑风暴
    标签:会议、讨论、白板、便签表示、按一定规则(时间)摆放
  2. 识别时间触发器
  3. 识别聚合
  4. 确定领域事件
  • 参考书记
  1. 《领域驱动设计-软件核心复杂性应对之道》
  2. 《Microservices Pattern(微服务架构设计模式)
  3. 《实现领域驱动设计》

(九)Restful-API接口规范

Restful指南:
Architectural Styles and
the Design of Network-based Software Architectures

Representational State Transfer (REST)

1. 创建类

URI

Post /api/v1/user

请求Body格式

{
    "userName": "biocome",
    "password": "biocomeOtherWords",
    "orgGuid": "7e7f4ddd-3a51-4af4-8555-ec4d548f",
    "roleId": "2342342234234234"
}

正确响应

Status: 200 OK
Body

{
    "userId": "aaf519e7-7ad7-4eeb-8684-c653d5c57ea0",
    "userName": "6369",
    "password": "7e2fad22e9bde6eb3c1241e59d5a6475410d1c866881850314b72e96ea836788",
    "orgGuid": "7e7f4ddd-3a51-4af4-8555-ec4d548f",
    "sex": null,
    "mobile": null,
    "email": null,
    "lastLoginTime": null,
    "createTime": 1615537423609,
    "updateTime": 1615537423609,
    "enableFlag": 1,
    "lastLoginIp": null,
    "nameCn": null,
    "lastFailTime": null,
    "failCount": null
}

2. 删除类

URI

DELETE /api/v1/user/{uid}

正确响应

Status: 200 OK

错误响应

HTTP Status:404 Not Found

3. 修改类

URI

PUT /api/v1/user/{uid}

4. 获取类

URI

GET /api/v1/user/{uid}

5. 分页类

URI

POST /api/v1/user/page

请求Body格式

{
    "pageIndex": 1,
    "pageSize": 10,
    "userName": "admin"
}

正确响应

{
    "current": 1,
    "pageSize": 10,
    "total": 1,
    "records": [
        {
            "userId": "b4600fb2-61c3-4786-8ba1-9fc5387a8d17",
            "orgGuid": "",
            "orgName": null,
            "sex": "SEX0001",
            "mobile": null,
            "email": null,
            "lastLoginTime": 1615532617422,
            "createTime": null,
            "updateTime": null,
            "lastLoginIp": "192.168.4.239",
            "nameCn": "超级管理员"
        }
    ],
    "totalPage": 1
}

6. 错误响应格式

客户端请求错误

  1. 业务参数格式、内容错误

Status: 401 UNAUTHORIZED

{
    "error": "UNAUTHORIZED",
    "message": "Invalid authentication credentials",
    "path": "/api/v1/user",
    "status": 401,
    "timestamp": 1615537147633
}

Status: 400 Bad Request

{
    "status": 400,
    "error": "BAD_REQUEST",
    "message": "密码不能用空",
    "timestamp": "2021-03-12T08:21:36.327+00:00",
    "path": "/api/v1/user/login"
}

HTTP Status:404 Not Found

服务端错误

  1. 504
{
    "timestamp": "2021-03-12T08:20:46.930+0000",
    "path": "/api/v1/user/login",
    "status": 504,
    "error": "Gateway Timeout",
    "message": "Response took longer than configured timeout",
    "requestId": "9287d396-10"
}
  1. 500
    Status: 500 Internal Server Error
{
    "timestamp": "2021-02-24T01:38:24.706+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "path": "/api/v1/user/search"
}

(十)其他

1. 搭建开发环境

  1. 安装JDK 1.8
  2. 安装IDE IDEA(也可装Eclipse)
  3. 安装项目工具

安装JDK 1.8

https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html

安装IDEA

  1. 官网下载
  2. 解压安装
  3. 激活 花钱or白嫖??

2. IDEA快捷键

  • 文件 -> 设置 -> 快捷键 -> 切换为 Visual Studio

  • 除了更换其他IDE外,自带Window快捷键有

  1. 格式化代码 Ctrl + R
  2. 插入代码块 Alt + Insert
  3. 向前、向后 Ctrl + Alt + (左键、右键)<- 、->
  4. 重命名 shift + F6
  5. 调试 F9 F8 F7 对应vs F5 F10 F11
  6. 查看实现或引用 Ctrl + B 对应vs F12
  7. 智能提示 Alt + Enter

3. Maven加速

转型Java微服务_第31张图片

填入阿里云的镜像


<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
    <mirrors>
        
        <mirror>
            <id>alimavenid>
            <mirrorOf>centralmirrorOf>
            <name>aliyun mavenname>
            <url>http://maven.aliyun.com/nexus/content/repositories/central/url>
        mirror>

        
        <mirror>
            <id>repo1id>
            <mirrorOf>centralmirrorOf>
            <name>Human Readable Name for this Mirror.name>
            <url>http://repo1.maven.org/maven2/url>
        mirror>

        
        <mirror>
            <id>repo2id>
            <mirrorOf>centralmirrorOf>
            <name>Human Readable Name for this Mirror.name>
            <url>http://repo2.maven.org/maven2/url>
        mirror>

        
        <mirror>
            <id>repoid>
            <mirrorOf>centralmirrorOf>
            <name>Human Readable Name for this Mirror.name>
            <url>http://repo.maven.org/maven2/url>
        mirror>
    mirrors>
settings>

4. 常见问题

    1. 引用包之后无法识别或则无法导入
      右键工程 ——> Maven -> 重新加载项目
    1. 程序无法启动(Application Not Start),没有具体类错误信息
      一定是配置和Maven引用错误
    1. 如果程序一直报错,至始至终都无法启动
      请拷贝示例项目,在此基础上改;
      请拷贝示例项目,在此基础上改;
      请拷贝示例项目,在此基础上改;

5. 语言

折叠代码块

C#      #region和#endregion     
Java   //region和//endregion

包名必须用小写

参考Java Package 教程

Package names are written in all lower case to avoid conflict with the names of classes or interfaces.

方法名小写+大写

参考Java method 教程

Naming a Method

Although a method name can be any legal identifier, code conventions restrict method names. By convention, method names should be a verb in lowercase or a multi-word name that begins with a verb in lowercase, followed by adjectives, nouns, etc. In multi-word names, the first letter of each of the second and following words should be capitalized. Here are some examples:

run
runFast
getBackground
getFinalData
compareTo
setX
isEmpty
Typically, a method has a unique name within its class. However, a method might have the same name as other methods due to method overloading.

(十一)附录

参考资料

  1. Spring Boot
    https://spring.io/projects/spring-boot
  2. Spring Cloud Gateway
    https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
  3. MyBatis-Plus
    https://baomidou.com/
  4. 参数验证使用
    https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#validation
  5. 参数验证API
    https://docs.spring.io/spring-framework/docs/current/javadoc-api/
  6. Redis使用
    https://spring.io/projects/spring-data-redis
  7. Json使用
    https://github.com/alibaba/fastjson
  8. 代码生成
    https://mp.baomidou.com/guide/generator.html
  9. 路由断言
    https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories
  10. dubbo参考
    https://dubbo.apache.org/zh/
  11. Java JDK
    https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
  12. 注册中心、配置中心
    https://nacos.io/zh-cn/index.html
  13. Maven
    https://maven.apache.org/
  14. Representational State Transfer (REST)
    https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm
  15. The Java Tutorials
    https://docs.oracle.com/javase/tutorial/java

pom.xml


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>
    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>2.4.3version>
        <relativePath/> 
    parent>
    <groupId>com.biocomegroupId>
    <artifactId>dubbo_serviceartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>dubbo_servicename>
    <description>基础数据管理平台description>
    
    <packaging>jarpackaging>

    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
        <java.version>1.8java.version>
        <spring-cloud.version>2020.0.1spring-cloud.version>
        <spring-cloud-alibaba.version>2.2.5.RELEASEspring-cloud-alibaba.version>
    properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            <dependency>
                <groupId>com.alibaba.cloudgroupId>
                <artifactId>spring-cloud-alibaba-dependenciesartifactId>
                <version>${spring-cloud-alibaba.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
        dependencies>
    dependencyManagement>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-dubboartifactId>
        dependency>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
        dependency>

        
        <dependency>
            <groupId>com.alibaba.cloudgroupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-validationartifactId>
        dependency>

        
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>

        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-boot-starterartifactId>
            <version>3.4.2version>
        dependency>

        
        <dependency>
            <groupId>com.baomidougroupId>
            <artifactId>mybatis-plus-generatorartifactId>
            <version>3.4.1version>
        dependency>

        
        <dependency>
            <groupId>org.freemarkergroupId>
            <artifactId>freemarkerartifactId>
            <version>2.3.31version>
        dependency>

        
        <dependency>
            <groupId>cn.licoygroupId>
            <artifactId>encrypt-body-spring-boot-starterartifactId>
            <version>1.0.4.RELEASEversion>
        dependency>

        
        <dependency>
            <groupId>org.postgresqlgroupId>
            <artifactId>postgresqlartifactId>
            <scope>runtimescope>
        dependency>

        
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>

        
        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>fastjsonartifactId>
            <version>1.2.75version>
        dependency>

        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-webartifactId>
            <version>5.3.4version>
        dependency>


        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <optional>trueoptional>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-testartifactId>
            <scope>testscope>
        dependency>
    dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>

project>

application.yml

server:
  port: 30221
spring:
  application:
    name: basedata
  cloud:
    nacos:
      discovery:
        server-addr: 192.168.1.152:8848
        metadata:
          routes: "/api/v1/user/**,/api/v1/org/**"
      router:
        dataid: gateway_dynamic_route
        group: DEFAULT_GROUP
  main:
    allow-bean-definition-overriding: true
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://192.168.1.161:5432/bup_basedata_dev
    username: postgres
    password: *******
  redis:
    host: 192.168.1.152
    port: 6379
    database: 0
    lettuce:
      shutdown-timeout: 200ms

dubbo:
  application:
    name: dubbo-spring-cloud-provider-basedata
  scan:
    base-packages: com.biocome.basedata.service
  protocol:
    name: dubbo
    port: -1

mybatis:
  type-aliases-packag: com.biocome.basedata.entity
  mapper-locations: classpath:mapper/*.xml

user:
  login:
    access-token-expire-timespan: 3600

示例工程

GIT:http://192.168.4.84:10080/yangyiquan/Java-Demo.git
转型Java微服务_第32张图片

你可能感兴趣的:(.NET,Core,java,spring,boot)