# 微服务远程调用

# 声明式服务调用 Feign

Spring Cloud OpenFeign为微服务架构下服务之间的调用提供了解决方案。使用Feign进行微服务间调用非常简单,只需要创建一个接口并用注解方式配置它,即可完成服务提供方的接口绑定,调用方使用该接口即可完成服务间访问。下面以contract合同服务访问user用户服务为例演示服务间的调用过程。

1.数据库中创建合同表

CREATE TABLE `contract` (
  `id` varchar(32) NOT NULL COMMENT 'ID',
  `name` varchar(32) DEFAULT NULL COMMENT '名称',
  `signer` varchar(32) DEFAULT NULL COMMENT '合同签订人',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `is_deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否删除',
  `create_id` varchar(32) DEFAULT NULL COMMENT '创建人ID',
  `create_organization_id` varchar(32) DEFAULT NULL COMMENT '创建人组织ID',
  `update_id` varchar(32) DEFAULT NULL COMMENT '修改人ID',
  `tenant_id` varchar(32) DEFAULT NULL COMMENT '租户ID',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='合同表';

2.按照创建user用户服务的方式在工程中创建contract合同服务。

3.在user用户服务和contract合同服务的启动类com.mediway.UserApplicationcom.mediway.ContractApplication上分别添加@EnableFeignClients注解开启对Feign的支持。

package com.mediway;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@MapperScan(basePackages = {"com.mediway.hos.*.mapper"})
@EnableFeignClients
@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}
package com.mediway;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@MapperScan(basePackages = {"com.mediway.hos.*.mapper"})
@EnableFeignClients
@SpringBootApplication
public class ContractApplication {

    public static void main(String[] args) {
        SpringApplication.run(ContractApplication.class, args);
    }
}

4.在hos-user-service模块的com.mediway.oa.user.controller.StaffController中添加selectStaffListByName方法。

/**
 * 根据名称模糊查询用户
 *
 * @return
 */
@GetMapping("/selectStaffListByName")
public BaseResponse<List<Staff>> selectStaffListByName(String name) {
    return BaseResponse.success(staffService.selectStaffListByName(name));
}

5.在hos-user-api模块的com.mediway.oa.user.api包下创建StaffApi员工信息接口,用于供合同服务调用。添加selectStaffListByName方法,该方法作用是根据姓名模糊查询员工信息。

package com.mediway.oa.user.api;

import com.mediway.hos.base.model.BaseResponse;
import com.mediway.oa.user.model.entity.Staff;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@FeignClient(value = "oa-user-service", path = "/user/staff")
public interface StaffApi {

    @GetMapping("/selectStaffListByName")
    BaseResponse<List<Staff>> selectStaffListByName(@RequestParam String name);

}

6.在hos-contract-controller模块的com.mediway.oa.contract.controller.ContractController中添加selectStaffListByName,该方法作用是根据姓名远程调用用户服务获取员工信息。

package com.mediway.oa.contract.controller;

import com.mediway.hos.database.controller.BaseController;
import com.mediway.hos.base.model.BaseResponse;
import com.mediway.oa.contract.model.entity.Contract;
import com.mediway.oa.user.api.StaffApi;
import com.mediway.oa.user.model.entity.Staff;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/contract")
public class ContractController extends BaseController<Contract>{

    @Autowired
    private StaffApi staffFeignClient;

    /**
     * 根据员工姓名获取员工信息
     *
     * @param name 员工姓名
     * @return
     */
    @GetMapping("/selectLikeList")
    public BaseResponse<List<Staff>> selectStaffListByName(@RequestParam("name") String name) {
        return staffFeignClient.selectStaffListByName(name);
    }

}

7.分别启动user服务(端口8003)、contract服务(端口8007)和hos-managecenter模块下managecenter-gateway服务(端口7100) feign_14

8.我们通过网关请求ContractControllerselectStaffListByName方法,接口的请求方式、请求参数和响应结果如下

请求方式

url:http://localhost:7100/contract/contract/selectLikeList
method:GET

请求参数

&name=张三

响应结果

{
    "code":"200",
    "msg":"success",
    "data":[
        {
            "id":"9e9c735844b841c0a970decdb2b5e182",
            "createTime":"2022-01-1414:57:21",
            "updateTime":"2022-01-1415:25:01",
            "pageIndex":null,
            "pageSize":null,
            "name":"张三",
            "gender":"男",
            "age":28,
            "orgId":"1",
            "email":"zhangsan@qq.com",
            "phone":"13812345678",
            "description":"开发部张三"
        }
    ],
    "success":true
}

Spring Cloud Feign客户端默认开启支持Ribbon,有两个重要的超时时间即连接超时ConnectTimeout和读取超时ReadTimeout,在默认情况下,两个超时时间都是1秒。由于默认超时时间很短,所以在调用过程中可能会出现请求超时的情况。通过下边配置可修改超时时间。

我们可修改nacos上服务调用方即contract服务的配置文件hos-contract,添加ribbon超时设置。

ribbon:
  #建立连接超时时间,单位:ms
  ConnectTimeout: 30000
  #建立连接之后,读取响应资源超时时间,单位:ms
  ReadTimeout: 30000

# 事务代码说明

# 添加事务注解

contract 模块调用 user 模块的 service 方法上加两个注解:
本地注解: @Transactional(rollbackFor = Exception.class)
分布式注解:@GlobalTransactional(rollbackFor = Exception.class)
代码示例如下:

    @GlobalTransactional(rollbackFor = Exception.class)
    @Transactional(rollbackFor = Exception.class)
    public Contract saveContract(String name, String signer) {
        Contract contract = new Contract();
        contract.setName(name);
        contract.setSigner(signer);
        save(contract);
        // 远程调用用户服务
        BaseResponse<Staff> staffBaseResponse = staffFApi.saveUser(signer);
        log.info("调用用户服务结果:{}", staffBaseResponse);
        // 此处需要手动抛出异常,避免异常被全局异常处理拦截导致seata无法获取到异常
        if (!staffBaseResponse.isSuccess()) {
            throw new RuntimeException("调用用户服务发生异常");
        }
        return contract;
    }

# 单体模式使用本地事务

在单体模块中因为不需要使用分布式事务,因此需要在启动类上的@SpringBootApplication注解上通过 exclude 排除分布式事务的相关配置,

具体代码示例如下:

@SpringBootApplication(exclude = {SeataPropertiesAutoConfiguration.class, SeataDataSourceAutoConfiguration.class, SeataAutoConfiguration.class, HttpAutoConfiguration.class})
public class HosAppApplication {
    public static void main(String[] args) {
        SpringApplication.run(HosAppApplication.class, args);
    }
}


# 启动方式说明

# 单体启动方式

右击oa-runnerOAApplication类,选择“Run”方法即可。

# 微服务启动方式

分别选择hos-user-cloud-runnerhos-contract-cloud-runnerApplication类,选择“Run”方法即可。