一、概述
在上一篇文章中(微服务-网关Spring Gateway进阶-日志跟踪唯一ID)JY @宋志宗 的指导下,发现了 SpringGateway实现日志跟踪唯一ID实现的问题,查找了一些资料,没有找到比较好的解决方案。最终我决定自己基于SpringBoot实现网关的功能。本文章还讲述了请求达到网关之后,对参数进行自定义封装,再传递给微服务,重点在于如何对请求参数进行设计与封装。
二、基于SpringBoot实现的网关
其工程结果如下图:
与SpringGateway实现的方式相比,区别主要有两点:
- Filter改为Interceptor实现,
- 增加了GatewayController作为网关的入口,网关入口的代码片段如下:
@RestController
public class GatewayController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RestTemplate restTemplate;
@Autowired
private IApiCodeService apiCodeService;
@RequestMapping("/api/{apiCode}")
public String entry(@PathVariable("apiCode") String apiCode) {
ApiCodeDVO apiCodeDVO = apiCodeService.getByApiCode(apiCode);
if (apiCodeDVO == null) {
throw new RuntimeException("unknow api");
}
String url = "http://" + apiCodeDVO.getSystemCode() + apiCodeDVO.getUrl();
logger.info("gateway request url: {}", url);
return restTemplate.getForObject(url, String.class);
}
}
其逻辑主要如下:
- 根据apiCode获取对应的服务信息,包含微服务code以及微服务的url;
- 通过restTemplate调用对应的微服务,实现路由功能。
目前这个例子比较简单,只介绍了getForObject方式的调用,正常我们的请求都有参数,如何实现呢,请看网关参数的设计与封装。
三、网关参数的设计与封装
其主要设计思路如下:
其代码实现片段如下,注意调用微服务返回的数据格式是Map类型,这里我没想到理想的数据格式,用比较通用的Map类型来作为网关返回的数据类型:
@PostMapping("/api/{apiCode}")
public Map entry(@PathVariable("apiCode") String apiCode, HttpServletRequest request, @RequestBody Map body) {
ApiCodeDVO apiCodeDVO = apiCodeService.getByApiCode(apiCode);
if (apiCodeDVO == null) {
throw new RuntimeException("unknow api");
}
String url = "http://" + apiCodeDVO.getSystemCode() + apiCodeDVO.getUrl();
logger.info("gateway request url: {}", url);
// 1. 封装Header
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("traceId", GatewayContextHolder.getContext().getTraceId());
headers.set("startTime", GatewayContextHolder.getContext().getStartTime().toString());
headers.set("userAgent", request.getHeader("user-agent"));
headers.set("refer", request.getHeader("refer"));
// 2.封装body参数,转换为json字符串
String httpEntityBody = null;
try {
ObjectMapper objectMapper = new ObjectMapper();
httpEntityBody = objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
logger.error(e.getMessage(), e);
throw new RuntimeException("object write error");
}
HttpEntity<String> httpEntity = new HttpEntity<>(httpEntityBody, headers);
// 3.调用微服务
ResponseEntity<HashMap> responseEntity = restTemplate.postForEntity(url, httpEntity, HashMap.class);
// 4.获取返回的消息
return responseEntity.getBody();
}
这里微服务的实现如下,主要看返回对象BaseResponse的实现,返回给网关的时候,会转换为Map类型。
@RestController
public class NacosHelloController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@GetMapping("/hello")
public String hello() {
logger.info("nacos hello");
return "nacos hello";
}
@PostMapping("/hello")
public BaseResponse<String> hello(@RequestBody HelloDTO request) {
logger.info("nacos hello post");
BaseResponse<String> baseResponse = new BaseResponse<>();
baseResponse.setData(request.toString());
return baseResponse;
}
}
而BaseResponse的实现就比较简单,如下:
public class BaseResponse<T> {
private T data;
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "BaseResponse{" +
"data=" + data +
'}';
}
}
四、总结
在本例的源代码中,还实现了traceId的传递,其原理是通过ThreadLocal沟通了ContextHolder,在同一个线程中进行隐式传递,采用Inteceptor的方式进行实现。
4.1 网关中的实现
对每一个请求设置一个traceId,放在GatewayContext中。
public class TraceInfoInterceptor extends HandlerInterceptorAdapter {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
logger.info("====== TraceInfoFilter start ======");
String traceId = TraceIdUtil.shortUUID();
// 1.将traceId放到上下文中,后续传递给微服务
GatewayContext context = GatewayContextHolder.getContext();
context.setTraceId(traceId);
GatewayContextHolder.setContext(context);
// 2.将traceId设置到slf4j中,日志打印模板配置打印traceId
MDC.put("traceId", traceId);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
super.postHandle(request, response, handler, modelAndView);
}
}
4.2 微服务中的实现
通过网关封装request的对象中获取traceId。
public class RequestInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String traceId = request.getHeader("traceId");
MDC.put("traceId", traceId);
return super.preHandle(request, response, handler);
}
}