SpringCloud Alibaba 微服务实战三 - 服务调用

本贴最后更新于 1811 天前,其中的信息可能已经沧海桑田

导读:通过前面两篇文章我们准备好了微服务的基础环境并让 accout-service 和 product-service 对外提供了增删改查的能力,本篇我们的内容是让 order-service 作为消费者远程调用 accout-service 和 product-service 的服务接口。

统一接口返回结构

在开始今天的正餐之前我们先把上篇文章中那个丑陋的接口返回给优化掉,让所有的接口都有统一的返回结构。

  • 建立公共模块 cloud-common
    image.png

  • 其他模块都引入 cloud-common,修改 pom 文件,加入依赖

<dependency>
	<groupId>com.jianzh5.cloud</groupId>
	<artifactId>cloud-common</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>
  • 建立接口返回的数据结构,这个数据结构大家可以根据自身项目情况统一即可
@Data
public class ResultData<T> {
    /** 结果状态 ,正常响应200,其他状态码都为失败*/
    private int status;
    private String message;
    private T data;
    private boolean success;
    private long timestamp ;
	
    ... 提供一些静态方法 ...
}
  • 改造 accout-serviceproduct-service 模块中 controller 层的返回结构,改造完的代码如下:
@RestController
@Log4j2
public class AccountController {
    @Autowired
    private AccountService accountService;
    @PostMapping("/account/insert")
    public ResultData<String> insert(@RequestBody AccountDTO accountDTO){
        log.info("insert account:{}",accountDTO);
        accountService.insertAccount(accountDTO);
        return ResultData.success("insert account succeed");
    }
    @PostMapping("/account/delete")
    public ResultData<String> delete(@RequestParam String accountCode){
        log.info("delete account,accountCode is {}",accountCode);
        accountService.deleteAccount(accountCode);
        return ResultData.success("delete account succeed");
    }
    @PostMapping("/account/update")
    public  ResultData<String> update(@RequestBody AccountDTO accountDTO){
        log.info("update account:{}",accountDTO);
        accountService.updateAccount(accountDTO);
        return ResultData.success("update account succeed");
    }
    @GetMapping("/account/getByCode/{accountCode}")
    public ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode){
        log.info("get account detail,accountCode is :{}",accountCode);
        AccountDTO accountDTO = accountService.selectByCode(accountCode);
        return ResultData.success(accountDTO);
    }
}

服务调用

SpringCloud 体系中,所有微服务间的通信都是通过 Feign 进行调用,Feign 是一个 http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像使用 HttpClientOKHttp3 等组件通过封装 HTTP 请求报文的方式调用。Feign 通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。而且 Feign 默认集成了负载均衡器 Ribbon,不需要自己实现负载均衡逻辑。

FeignSpringCloud 的组件,在引入 Feign 之前我们先看看 Spring BootSpring CloudSpring Cloud Alibaba 三者之间的关系,防止在业务中引入了错误的版本。

Spring Boot Spring Cloud Spring Cloud Alibaba
2.1.x Greenwich 0.9.x
2.0.x Finchley 0.2.x
1.5.x Edgware 0.1.x
1.5.x Dalston 0.1.x
很显然,我们引用的是 SpringCloud Alibab 0.9.0,所以我们需要引入 SpringCloud Greenwich
  • 引入 SpringCloud 版本依赖
    在项目主 pom <dependencyManagement> 中引入 SpringCloud 依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-dependencies</artifactId>
	<version>Greenwich.SR2</version>
	<type>pom</type>
	<scope>import</scope>
</dependency>
  • 在所有需要用到 Feign 的模块中引入 openfeign 依赖
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 抽取公共的接口层,建立 Feign 接口,接口的定义和返回值需要跟 Controller 层保持一致
@FeignClient(name = "account-service")
public interface AccountFeign {
    @PostMapping("/account/insert")
    ResultData<String> insert(@RequestBody AccountDTO accountDTO);

    @PostMapping("/account/delete")
    ResultData<String> delete(@RequestParam("accountCode") String accountCode);

    @PostMapping("/account/update")
    ResultData<String> update(@RequestBody AccountDTO accountDTO);

    @GetMapping("/account/getByCode/{accountCode}")
    ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode);
}

在接口上添加注解 @FeignClient(name = "account-service"),表明这是一个 Feign 客户端,name 属性的配置表示我这个接口最终会转发到 accout-service 上。

正如回字有多种写法,这里 Feign 也有多种使用方式。
第一种就是我们这里介绍的,Feign 和生产者的 RequestMapping 保持一致,大家可以看看上面改造后的 AccountControllerAccountFeign 一模一样有米有。

第二种方式就是让我们的 Controller 直接实现 Feign 接口,不再需要写 RequestMapping,如:

@RestController
@Log4j2
public class ProductController implements ProductFeign {
    @Autowired
    private ProductService productService;

    @Override
    public ResultData<String> insert(@RequestBody ProductDTO productDTO){
        log.info("insert product:{}",productDTO);
        productService.insertProduct(productDTO);
        return ResultData.success("insert product succeed");
    }
}
  • 消费者模块引入 Feign 接口层的依赖
<dependency>
	<groupId>com.jianzh5.cloud</groupId>
	<artifactId>account-feign</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>
  • 在消费者 product-service 启动类上添加 @EnableFeignClients 注解
@SpringBootApplication
@RestController
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.javadaily.feign.*")
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}
  • 消费者端跟使用本地 service 一样使用 Feign
@RestController
public class OrderController {
    @Autowired
    private AccountFeign accountFeign;

    @Autowired
    private ProductFeign productFeign;

    @PostMapping("/order/getAccount/{accountCode}")
    public ResultData<AccountDTO> getAccount(@PathVariable String accountCode){
        return accountFeign.getByCode(accountCode);
    }

    @PostMapping("/order/insertAccount")
    public ResultData<String> insertAccount(AccountDTO accountDTO){
        return accountFeign.insert(accountDTO);
    }

    @PostMapping("/order/updateAccount")
    public ResultData<String> updateAccount(AccountDTO accountDTO){
        return accountFeign.update(accountDTO);
    }

    @PostMapping("/order/deleteAccount/{accountCode}")
    public ResultData<String> deleteAccount(@PathVariable String accountCode){
        return accountFeign.delete(accountCode);
    }
}
  • 项目模块截图
    image.png

  • 联调测试
    我们请求 OrderController 中的接口,验证下接口请求结果:
    image.png
    image.png
    联调成功,搞定收工!

血与泪

使用 feign 过程中有以下几点需要注意,否则一不小心你就会掉进坑里。(我不会告诉你我当时在坑里踩了多久才爬上来)
image.png

  • Feign 不支持直接使用对象作为参数请求
    接口中如果有多参数需要用实体接收,要么把参数一个一个摆开,要么在对象参数上加上 @RequestBody 注解,让其以 json 方式接收,如:
@PostMapping("/account/insert")ResultData<String> insert(@RequestBody AccountDTO accountDTO);
  • 消费者模块启动类上使用 @EnableFeignClients 注解后一定要指明 Feign 接口所在的包路径
    如:@EnableFeignClients(basePackages = "com.javadaily.feign.*")
    否则你的消费者启动时会报如下的错误:
    image.png
    所以这里推荐你们在开发中所有 feign 模块最好能统一包名前缀 com.javadaily.feign

  • @RequestParam 的坑
    在 Feign 接口层使用 @RequestParam 注解要注意,一定要加上 value 属性,如:
    ResultData<String> delete(@RequestParam("accountCode") String accountCode);
    否则你会看到类似如下的错误:
    Caused by: java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0 这个异常

  • @PathVariable 的坑
    在 Feign 接口层使用 @PathVariable 注解要注意,一定要跟上面一样加上 value 属性,如:
    ResultData<AccountDTO> getByCode(@PathVariable(value = "accountCode") String accountCode);
    否则你也会看到类似如下的错误 @PathVariable(value = "accountCode") String accountCode

  • 在消费者配置文件中添加 Feign 超时时间配置
    在我们的 order-service 配置文件中增加 feign 超时时间配置

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000

否则你会经常看到如下所示的错误:

java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method) ~[?:1.8.0_112]

至此我们已经完成了项目公共返回接口的统一并且成功使用 Feign 调用远程生产者的服务,那么本期的“SpringCloud Alibaba 微服务实战三 - 服务调用”篇也就该结束啦,咱们下期有缘再见!
image.png

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    944 引用 • 1459 回帖 • 17 关注
  • 云计算
    78 引用 • 91 回帖 • 1 关注
  • 微服务

    微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务。服务之间互相协调,互相配合,为用户提供最终价值。每个服务运行在独立的进程中。服务于服务之间才用轻量级的通信机制互相沟通。每个服务都围绕着具体业务构建,能够被独立的部署。

    96 引用 • 155 回帖 • 1 关注
  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • jianzh5
    作者

    可以关注我公众号啊,javadaily

  • 其他回帖
  • sgkt

    没有源码可以看看吗?

    怎么感觉 dto 在每个工程都有依赖

    1 回复