Bootstrap

Srping Cloud - @Async注解+Feign手动设置token

在Spring cloud项目开发中,经常需要通过feign远程调用其他的服务的方法。有时因为跨服务调用的时间较长,就会存在需要异步跨服务调用的场景,此时就会出现请求头(主要是token)丢失的问题。

报错:
Full authentication is required to access this resource

一、项目场景

1 异步生成Excel文件
2 跨服务调用上传文件
3 返回文件id
4 回更业务表

思路:从request中获取token;再将token设置到上下文工具中

二、代码示例

1.工具类

基于ThreadLocal类封装的工具类,用于在同一线程中的封装数据和获取数据。

/**
* 上下文工具类
*/
public class BaseContextHandler {
    private static final ThreadLocal<Map<String, String>> THREAD_LOCAL = new ThreadLocal();

    public BaseContextHandler() {
    }
    public static void set(String key, Object value) {
        Map<String, String> map = getLocalMap();
        map.put(key, value == null ? "" : value.toString());
    }

    public static <T> T get(String key, Class<T> type) {
        Map<String, String> map = getLocalMap();
        return Convert.convert(type, map.get(key));
    }

    public static <T> T get(String key, Class<T> type, Object def) {
        Map<String, String> map = getLocalMap();
        return Convert.convert(type, map.getOrDefault(key, String.valueOf(def == null ? "" : def)));
    }

    public static String get(String key) {
        Map<String, String> map = getLocalMap();
        return (String)map.getOrDefault(key, "");
    }

    public static Map<String, String> getLocalMap() {
        Map<String, String> map = (Map)THREAD_LOCAL.get();
        if (map == null) {
            map = new HashMap(10);
            THREAD_LOCAL.set(map);
        }

        return (Map)map;
    }

    public static void setLocalMap(Map<String, String> threadLocalMap) {
        THREAD_LOCAL.set(threadLocalMap);
    }

    public static String getToken() {
        return (String)get("Authorization", String.class);
    }

    public static void setToken(String token) {
        set("Authorization", token);
    }

    public static void remove() {
        if (THREAD_LOCAL != null) {
            THREAD_LOCAL.remove();
        }

    }
}

2.Feign配置

在请求头中获取不到数据时,从BaseContextHandler里面取ThreadLocal存起来的请求头。

/**
 * feign添加请求头拦截器
 */
public class FeignAddHeaderRequestInterceptor implements RequestInterceptor {
    private static final Logger log = LoggerFactory.getLogger(FeignAddHeaderRequestInterceptor.class);
    private static final List<String> HEADER_NAME_LIST = Arrays.asList("Authorization", "x-trace-id", "X-Real-IP", "x-forwarded-for");

    public FeignAddHeaderRequestInterceptor() {
    }

    public void apply(RequestTemplate template) {
        if (TracingContext.tracing().hasGroup()) {
            template.header("X-FEIGN-ID", new String[]{TracingContext.tracing().groupId()});
        }

        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes == null) {
            HEADER_NAME_LIST.forEach((headerName) -> {
                template.header(headerName, new String[]{BaseContextHandler.get(headerName)});
            });
        } else {
            HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
            if (request == null) {
                log.warn("path={}, 在FeignClient API接口未配置FeignConfiguration类, 故而无法在远程调用时获取请求头中的参数!", template.path());
            } else {
                HEADER_NAME_LIST.forEach((headerName) -> {
                    template.header(headerName, new String[]{(String)StringUtils.defaultIfEmpty(request.getHeader(headerName), BaseContextHandler.get(headerName))});
                });
            }
        }

    }

3.业务代码示例

1)控制器层


    @GetMapping("/asyncFeignTest")
    public R asyncFeignTest() {
        try {
			// 第一步:在主线程中, 通过请求头获取token
            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
            HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
            String token = request.getHeader("Authorization");
			
			// 调用异步方法时,将token传入
			asyncFeignTestService.asyncFeignTest(token);
            return R.success("成功");
        } catch (Exception e) {
            log.error("异常>>>>>>>>>>>>>>>>>>", e);
            return R.fail("失败");
        }
    }

2)服务层

public interface AsyncFeignTestService{

 void asyncFeignTest(String token);
 
 Long feignFileUpload(String token);
}

3)服务实现层

/**
     * 调用生成文件的方法,并获取返回的数据
     * @return
     * @throws IOException
     */
    @Async
    @Override
    public void asyncFeignTest(String token) {
        try {
            // 调用含feign调用的方法,并获取返回的数据
            Long resultId = this.feignFileUpload(token);
            if (resultId != null) {
                log.info("feign调用服务返回id成功,resultId:{}", resultId);
                // 回更业务表代码
                // ...
            }
        } catch (Exception e) {
            log.error("feign调用服务返回id方法异常:>>>>>>>>>>>>{}", e);
        }
    }
/**
     * 生成文件并保存到文件服务
     * @return
     * @throws IOException
     */
    @Override
    public Long feignFileUpload(String token) throws IOException {

        // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 文件生成准备 >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        // 获取模版文件
        ClassPathResource classPathResource = new ClassPathResource("xls_template/file_template_fixed.xlsx");
        InputStream fis = classPathResource.getInputStream();

        // 保存写入的数据流
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {

            // 设置excel输出策略
            ExcelWriter excelWriter = EasyExcel
                    .write(outputStream, InternalSolutionFillDTO.class)
                    .withTemplate(fis)
                    .build();

            // 转为文件上传
            String fileName = "文件"+System.currentTimeMillis()+".xlsx";
            String originName = "file_"+System.currentTimeMillis()+".xlsx";
            MockMultipartFile customerFile = new MockMultipartFile(fileName,originName
                    , "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
                    , outputStream.toByteArray());

            // 第二部:在调用feign方法前,设置token
            BaseContextHandler.setToken(token);
            // 上传文件并获取主键
            R uploadFileVo = fileApi.upload(customerFile, "bucket");
            if (uploadFileVo.getData() != null) {
                FileResultVO customerFileVo = JSONUtil.toBean(JSONUtil.toJsonStr(uploadFileVo.getData()), FileResultVO.class);
                if (customerFileVo != null && customerFileVo.getId() != null) {
                    return customerFileVo.getId();
                }
            }

        } catch (Exception e) {
            log.error("文件生成,方法异常>>>>>>>>>>>>>>", e);
        } finally {
            fis.close();
        }
        return null;
    }
;