进学阁

业精于勤荒于嬉,行成于思毁于随

0%

微服务中的ACL与OpenFeign的绝佳配合

ACL的概念

在一些情况下我们需要引入第三方的接口来进行操作,但是当我们引用第三方接口的时候也会有一些隐患,第三方=不可空,没准哪一天对方的接口参数突然就变掉了,如果我们直接在多个地方引用了第三方的接口,我们就需要在不同的地方处理接口方法,这时我们就需要引入防腐层的概念

什么是防腐层

在许多情况下,我们的系统需要依赖其他系统,但被依赖的系统可能具有不合理的数据结构、API、协议或技术实现。如果我们强烈依赖外部系统,就会导致我们的系统受到“腐蚀”。在这种情况下,通过引入防腐层,可以有效地隔离外部依赖和内部逻辑,无论外部如何变化,内部代码尽可能保持不变。

防腐层不仅仅是一层简单的调用封装,在实际开发中,ACL可以提供更多强大的功能:

  1. 适配器: 很多时候外部依赖的数据、接口和协议并不符合内部规范,通过适配器模式,可以将数据转化逻辑封装到ACL内部,降低对业务代码的侵入。
  2. 缓存: 对于频繁调用且数据变更不频繁的外部依赖,通过在ACL里嵌入缓存逻辑,能够有效的降低对于外部依赖的请求压力。同时,很多时候缓存逻辑是写在业务代码里的,通过将缓存逻辑嵌入ACL,能够降低业务代码的复杂度。
  3. 兜底: 如果外部依赖的稳定性较差,提高系统稳定性的策略之一是通过ACL充当兜底,例如在外部依赖出问题时,返回最近一次成功的缓存或业务兜底数据。这种兜底逻辑通常复杂,如果散布在核心业务代码中,会难以维护。通过集中在ACL中,更容易进行测试和修改。
  4. 易于测试: ACL的接口类能够很容易的实现Mock或Stub,以便于单元测试。
  5. 功能开关: 有时候,我们希望在某些场景下启用或禁用某个接口的功能,或者让某个接口返回特定值。我们可以在ACL中配置功能开关,而不会影响真实的业务代码。

如何实现防腐层

实现ACL防腐层的步骤如下:

  1. 对于依赖的外部对象,我们提取所需的字段,并创建一个内部所需的DTO类。
  2. 构建一个新的Facade,在Facade中封装调用链路,将外部类转化为内部类。Facade可以参考Repository的实现模式,将接口定义在领域层,而将实现放在基础设施层。
  3. 在ApplicationService中依赖内部的Facade对象。

具体的实现如下:

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
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomerMenuDTO {
// 菜单ID
private String menuId;
// 父菜单ID,一级菜单为0
private String parentId;
// 菜单名称
private String name;
// 别称
private String anotherName;
// 组件路径
private String component;
// 菜单URL
private String path;
// 排序
private Integer orderNum;
// 菜单标题
private String title;
// 授权(多个用逗号分隔)
private String perms;
// 类型 0:目录 1:菜单 2:按钮
private Integer type;
// 菜单图标
private String icon;
// 是否跳转
private Boolean IsFull;
// 是否外链
private String isLink;
// 是否隐藏
private Boolean isHide;
// 是否固定
private Boolean isAffix;
// 是否长连接
private Boolean isKeepAlive;
}

Facade

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@FeignClient(name = "pomelo-customer")
public interface CustomerRemoteFacade {
//获取用户角色
@GetMapping("/Internal/customer/getUserInfoByUserName/{UserName}")
CustomerUserDTO getUserInfoByUserName(@PathVariable("UserName") String UserName);
//获取用户信息
@GetMapping("/Internal/customer/getUserInfoByUserId/{UserId}")
CustomerUserDTO getUserInfoByUserId(@PathVariable("UserId") String UserId);
//根据手机号获取用户信息
@GetMapping("/Internal/customer/getUserInfoByPhoneNumber/{PhoneNumber}")
CustomerUserDTO getUserInfoByPhoneNumber(@PathVariable("PhoneNumber") String PhoneNumber);
//获取用户角色信息
@GetMapping("/Internal/customer/getRoleByUserId/{userId}")
List<CustomerRoleDto> getRoleByUserId(@PathVariable("userId") String userId);
//获取用户权限
@GetMapping("/Internal/customer/getByRoleId/{roleId}")
List<CustomerMenuDTO> getByPermissions(@PathVariable("roleId") String roleId);
}

在服务中应用

1
2
3
4
5
6
7
private final CustomerRemoteFacade customerRemoteFacade;


CustomerUserDTO customerUser = customerRemoteFacade.getUserInfoByPhoneNumber(phonePasswordLoginDTO.getPhone());
if (customerUser ==null){
throw new BusinessException(ErrorCode.USERNAME_PASSWORD_INCORRECT);
}

这样,经过acl防腐处理,我们的服务中就不需要直接调用第三方接口了,当第三方的接口有过修改,我们只需要在acl中将逻辑更改,这样并不会影响到主体业务

小结

在没有防腐层ACL的情况下,系统需要直接依赖外部对象和外部调用接口,调用逻辑如下:

而有了防腐层ACL后,系统只需要依赖内部的值类和接口,调用逻辑如下:

OpenFeign的应用

微服务中的远程调用

在pomelo中服务间的远程调用,我们单独的定义一个包来完成。同样我们在引用方,也可以以acl的思想来使用openFeign应用。接下来我们看一下用户登录引用用户服务的实现过程

定义服务接口

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
@RestController
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class CustomerUserInternal {
private final CustomerUserService customerUserService;
private final CustomerRoleService customerRoleService;
private final CustomerMenuService customerMenuService;

/**
* 根据用户名获取用户信息
*
* @param UserName
* @return
*/
@GetMapping("/Internal/customer/getUserInfoByUserName/{UserName}")
public CustomerUserDTO getUserInfoByUserName(@PathVariable("UserName") String UserName) {
return customerUserService.selectUserByUserName(UserName);
}
/**
* 根据手机号获取用户信息
*
* @param PhoneNumber
* @return
*/
@GetMapping("/Internal/customer/getUserInfoByPhoneNumber/{PhoneNumber}")
public CustomerUserDTO getUserInfoByPhoneNumber(@PathVariable("PhoneNumber") String PhoneNumber) {
return customerUserService.selectUserByPhoneNumber(PhoneNumber);
}
/**
* 根据id获取用户信息
*
* @param id
* @return
*/
@GetMapping("/Internal/customer/getUserInfoById/{id}")
public CustomerUserListDTO getUserInfoById(@PathVariable("id") String id) {
return customerUserService.selectUserById(id);
}

@GetMapping("/Internal/customer/getRoleByUserId/{userId}")
public List<CustomerRoleDto> getRoleByUserId(@PathVariable("userId") String userId) {
return customerRoleService.selectRoleByUserId(userId);
}

/**
* 根据角色获取权限信息
*/
@GetMapping("/Internal/customer/getByRoleId/{roleId}")
public List<CustomerMenuDTO> getByPermissions(@PathVariable("roleId") String roleId) {
return customerMenuService.getMenuOptionsByRoleId(roleId);
}
}

acl防腐层facade实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@FeignClient(name = "pomelo-customer")
public interface CustomerRemoteFacade {
//获取用户角色
@GetMapping("/Internal/customer/getUserInfoByUserName/{UserName}")
CustomerUserDTO getUserInfoByUserName(@PathVariable("UserName") String UserName);
//获取用户信息
@GetMapping("/Internal/customer/getUserInfoByUserId/{UserId}")
CustomerUserDTO getUserInfoByUserId(@PathVariable("UserId") String UserId);
//根据手机号获取用户信息
@GetMapping("/Internal/customer/getUserInfoByPhoneNumber/{PhoneNumber}")
CustomerUserDTO getUserInfoByPhoneNumber(@PathVariable("PhoneNumber") String PhoneNumber);
//获取用户角色信息
@GetMapping("/Internal/customer/getRoleByUserId/{userId}")
List<CustomerRoleDto> getRoleByUserId(@PathVariable("userId") String userId);
//获取用户权限
@GetMapping("/Internal/customer/getByRoleId/{roleId}")
List<CustomerMenuDTO> getByPermissions(@PathVariable("roleId") String roleId);
}

启动服务增加注解

1
@EnableFeignClients("com.fbb.pomelo.auth.acl")

这样我们就可以在引用方调用接口进行参数处理

自定义微服务解码器

我们在构建统一返回以及全局异常处理中提出过对参数的返回体进行了包装,那我们引用feign的时间就需要对之前的包装进行解码

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class PomeloFeignResponseDecoder implements Decoder {

@Override
public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
// 因为在web中定义过返回自动转换为Result,所以在使用openfeign时为了方便我们需要自定义解码将Result接触
Result<?> result = JsonUtils.inputStream2Obj(response.body().asInputStream(),Result.class);
if (ErrorCode.OK.getCode().equals(result.getCode())) {
Object data = result.getData();
if (ObjectUtil.isEmpty(data)){
return null;
}
JavaType javaType = TypeFactory.defaultInstance().constructType(type);
return JsonUtils.convertValue(data, javaType);
}
// 异常则抛出业务异常
throw new RemoteException(result.getCode(), result.getMessage());
}
}

上游异常的统一处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Slf4j
public class PomeloFeignErrorDecoder implements ErrorDecoder {

@Override
public Exception decode(String s, Response response) {
try {
Reader reader = response.body().asReader(Charset.defaultCharset());
Result<?> result = JsonUtils.reader2Obj(reader, Result.class);
return new RemoteException(result.getCode(), result.getMessage());
} catch (IOException e) {
log.error("Response转换异常", e);
throw new RemoteException(ErrorCode.FEIGN_ERROR);
}
}
}

Feign全局异常处理

feign的全局异常依然是沿用了spring的RestControllerAdvice注解

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
@RestControllerAdvice
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级
@ResponseStatus(code = HttpStatus.BAD_REQUEST) // 统一 HTTP 状态码
public class PomeloFeignExceptionHandler {

@ExceptionHandler(FeignException.class)
public Result<?> handleFeignException(FeignException e) {
// log.error("FeignException: ", e);
return new Result<Void>()
.setCode(ErrorCode.REMOTE_ERROR.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}

@ExceptionHandler(DecodeException.class)
public Result<?> handleDecodeException(DecodeException e) {
log.error("Feign Decode Error: ", e);
Throwable cause = e.getCause();
if (cause instanceof AbstractException) {
RemoteException remoteException = (RemoteException) cause;
// 上游符合全局响应包装约定的再次抛出即可
return new Result<Void>()
.setCode(remoteException.getCode())
.setMessage(remoteException.getMessage())
.setTimestamp(System.currentTimeMillis());
}
// 全部转换成RemoteException
return new Result<Void>()
.setCode(ErrorCode.REMOTE_ERROR.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}
@ExceptionHandler(RemoteException.class)
public Result<?> handleRemoteException(RemoteException e) {
log.error("Feign Remote Error: ", e);
return new Result<Void>()
.setCode(e.getCode())
.setMessage(e.getMessage())
.setTimestamp(System.currentTimeMillis());
}
}

小结:

我们学习了如何使用ACL来隔离外部依赖,降低系统耦合度。在微服务架构中,我们探讨了如何通过OpenFeign来实现跨服务调用,并解决了全局包装和异常处理的问题,希望本文的内容对您在软件开发项目中有所帮助。