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

通过前面两篇文章我们准备好了微服务的基础环境并运行注册服务到nacos上了

统一接口返回结构

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

  • 在公共模块中添加统一返回类
    在这里插入图片描述

自定义结果码

package com.changan.response;

public interface CustomizeResultCode {
    /**
     * 获取错误状态码
     * @return 错误状态码
     */
    Integer getCode();

    /**
     * 获取错误信息
     * @return 错误信息
     */
    String getMessage();
}

统一结果返回类

package com.changan.response;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * @program: xinguan-parent
 * @description: 统一的返回结果过类
 * @author: 申剑帅哥
 * @create: 2022-05-22 22:52
 **/
@Data
public class Result {
    @ApiModelProperty(value = "是否成功")
    private Boolean success;

    @ApiModelProperty(value = "返回码")
    private Integer code;

    @ApiModelProperty(value = "返回消息")
    private String message;

    @ApiModelProperty(value = "返回数据")
    private Map<String,Object> data = new HashMap<>();

    /**
     * 构造方法私有化,里面的方法都是静态方法
     * 达到保护属性的作用
     */
    private Result(){

    }

    /**
     * 这里是使用链式编程
     */
    public static Result ok(){
        Result result = new Result();
        result.setSuccess(true);
        result.setCode(ResultCode.SUCCESS.getCode());
        result.setMessage(ResultCode.SUCCESS.getMessage());
        return result;
    }

    public static Result error(){
        Result result = new Result();
        result.setSuccess(false);
        result.setCode(ResultCode.COMMON_FAIL.getCode());
        result.setMessage(ResultCode.COMMON_FAIL.getMessage());
        return result;
    }

    public static Result error(ResultCode resultCode){
        Result result = new Result();
        result.setSuccess(false);
        result.setCode(resultCode.getCode());
        result.setMessage(resultCode.getMessage());
        return result;
    }

    /**
     * 自定义返回成功与否
     * @param success
     * @return
     */
    public Result success(Boolean success){
        this.setSuccess(success);
        return this;
    }

    public Result message(String message){
        this.setMessage(message);
        return this;
    }

    public Result code(Integer code){
        this.setCode(code);
        return this;
    }

    public Result data(String key,Object value){
        this.data.put(key,value);
        return this;
    }

    public Result data(Map<String,Object> map){
        this.setData(map);
        return this;
    }
}

返回码自定义

package com.changan.response;

/**
 * @Author: shenjian
 * @Description: 返回码定义
 * 规定:
 * #200表示成功
 * #1001~1999 区间表示参数错误
 * #2001~2999 区间表示用户错误
 * #3001~3999 区间表示接口异常
 * #后面对什么的操作自己在这里注明就行了
 */
public enum ResultCode implements CustomizeResultCode {


        /* 成功 */
        SUCCESS(200, "成功"),

        /* 默认失败 */
        COMMON_FAIL(999, "失败"),

        /* 参数错误:1000~1999 */
        PARAM_NOT_VALID(1001, "参数无效"),
        PARAM_IS_BLANK(1002, "参数为空"),
        PARAM_TYPE_ERROR(1003, "参数类型错误"),
        PARAM_NOT_COMPLETE(1004, "参数缺失"),

        /* 用户错误 */
        USER_NOT_LOGIN(2001, "用户未登录"),
        USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
        USER_CREDENTIALS_ERROR(2003, "密码错误"),
        USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
        USER_ACCOUNT_DISABLE(2005, "账号不可用"),
        USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
        USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
        USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
        USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

        /*部门错误*/
        DEPARTMENT_NOT_EXIST(3007, "部门不存在"),
        DEPARTMENT_ALREADY_EXIST(3008, "部门已存在"),

        /* 业务错误 */
        NO_PERMISSION(3001, "没有权限"),
        BUCKET_IS_EXISTS(3002, "已经存在此文件名!"),

        /*运行时异常*/
        ARITHMETIC_EXCEPTION(9001, "算数异常");

        private Integer code;

        private String message;

        ResultCode(Integer code, String message) {
            this.code = code;
            this.message = message;
        }

        @Override
        public Integer getCode() {
            return code;
        }

        @Override
        public String getMessage() {
            return message;
        }

}

服务调用

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

定义远程调用包
在这里插入图片描述

package com.changan.feign;

import com.changan.response.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @program: 申剑帅哥
 * @description: 用户调用账单的远程服务
 * @author: Mr.shen
 * @create: 2022-06-18 09:52
 **/
@FeignClient("account-service") //指定那个服务
public interface AccountFeign {
    @GetMapping("/account/getAccountByUserid/{id}") //给那个方法传参并获取返回值
    Result getAccountByUserid(@PathVariable("id") Integer id);

}

消费者引入Feign接口层的依赖

 <!--引入远程调用的依赖 openFeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--openfeign默认使用的是loadBalance的负载均衡器  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>

        </dependency>

消费者启动类

@EnableFeignClients 加上这个依赖

/**
 * @program: 申剑帅哥
 * @description: 用户的启动类
 * @author: Mr.shen
 * @create: 2022-06-17 15:33
 **/
@SpringBootApplication
//开启fegin的远程调用服务
@EnableFeignClients
@MapperScan("com.changan.mapper")
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class,args);
    }
}

消费者端跟使用本地service一样使用Feign

package com.changan.controller;

import com.changan.entity.User;
import com.changan.response.Result;
import com.changan.feign.AccountFeign;
import com.changan.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author com.changan.common
 * @since 2022-06-17
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private AccountFeign accountFeign;

    @GetMapping("/getUserList")
    public List<User> getUserList() {
        List<User> userList = userService.list();
        System.out.println(userList.size());
        return userList;
    }

    @GetMapping("/getAccountByUserid/{id}")
    public Result getAccountByUserid(@PathVariable("id") Integer id) {
        Result result = accountFeign.getAccountByUserid(id);
        return result;
    }
}

血与泪

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

  • Feign不支持直接使用对象作为参数请求

接口中如果有多参数需要用实体接收,要么把参数一个一个摆开,要么在对象参数上加上@RequestBody注解,让其以json方式接收,如:

@PostMapping("/account/insert") 
public Result insert(@RequestBody Account account);
  • 消费者模块启动类上使用@EnableFeignClients注解后一定要指明Feign接口所在的包路径

如:@EnableFeignClients(basePackages = “com.changan.feign.*”) 否则你的消费者启动时会报如下的错误:

​ [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gAIrzrIz-1655999155308)(https://img.hacpai.com/file/2019/12/image-0bed9896.png?imageView2/2/w/1280/format/jpg/interlace/1/q/100)]

所以这里推荐你们在开发中所有feign模块最好能统一包名前缀com.changan.feign

  • @RequestParam的坑

在Feign接口层使用@RequestParam注解要注意,一定要加上value属性,如:

@PostMapping("/account/delete")
public Result delete(@RequestParam(value = "accountCode") String accountCode)

否则你会看到类似如下的错误:

Caused by: java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0这个异常

  • @PathVariable的坑

    在Feign接口层使用@PathVariable注解要注意,一定要跟上面一样加上value属性,如:

    @GetMapping("/account/{accountCode}")
    Result getByCode(@PathVariable(value = "accountCode") String accountCode);
    
    • 在消费者配置文件中添加Feign超时时间配置

    在我们的account-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]

这个坑,真的困扰了很久

SpringCloud OpenFeign报错

No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?

问题原因: 今天的疑惑报错,我大意了啊,没有闪。使用Spring Initializr初始化项目引入了openfeign,没有在意版本。直到运行项目进行远程调用时报错

No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?

解决方法:

由于SpringCloud Feign在Hoxton.M2 RELEASED版本之后不再使用Ribbon而是使用spring-cloud-loadbalancer,所以不引入spring-cloud-loadbalancer会报错

加入spring-cloud-loadbalancer依赖 并且在nacos中排除ribbon依赖,不然loadbalancer无效

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

加完之后,确实不报错了,好家伙,直接报黄,对我这种有强迫症的,只允许我的控制台是黑色的,怎么能有其他颜色干扰(虽然报黄不影响运行)

2021-10-10 12:19:51.235  WARN 14504 --- [           main] iguration$LoadBalancerCaffeineWarnLogger : Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
后面在这里找到了解决方案

在application.yml中添加如下代码

spring:
  cloud:
    loadbalancer:
      cache:
        enabled: false
      ribbon:
        enabled: false

添加之后测试负载均衡是否生效

至此我们已经完成了项目公共返回接口的统一并且成功使用Feign调用远程生产者的服务,那么本期的“SpringCloud Alibaba微服务实战三 - 服务调用”篇也就该结束啦
ith the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.
后面在这里找到了解决方案


在application.yml中添加如下代码

```java
spring:
  cloud:
    loadbalancer:
      cache:
        enabled: false
      ribbon:
        enabled: false

添加之后测试负载均衡是否生效

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

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐