SpringBoot应用如何使用切面+注解的形式优雅的实现Http通信

114 阅读5分钟

引言. 很多小伙伴在进行功能设计或者系统研发的时候都会遇到需要对接第三方Api的场景,目前大部分第三方Api都是以Restful接口形式进行访问. 当然, 使用Http工具类进行接口对接也没什么问题. 但是当需要使用大量的Http请求去对接的场景不可避免的就会出现重复编码的情况. 相信很多大佬会将这部分代码进行提取,封装成一个公共的静态方法来减少重复编码的情况,其实这样做也很优雅.但是如果想更优雅的话为何不考虑使用Aop+注解的形式来实现呢? 那么,接下来我们就来使用这种方式来实现优雅的Http通信. 废话不多说, 这就开始

Step-1 定义一个名字好听的注解

  • 以我的实现为例,新建一个名为@Request的注解类, 需要在该注解上添加元注解. 因为我们是以一个方法为载体, 所以此处标注@Target({ElementType.METHOD})注解
/**
 * Http请求注解
 *
 * @author Anker
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Request {

}

  • 接下来应该考虑我们注解中都需要哪些元素, 比如: 接口地址, 请求方式, 域名.....
    • doMain (域名地址)
    • url (接口地址)
    • requestMethod(请求方式)

当然, 你也可以直接一个字段来标识完整接口地址

/**
 * 请求注解
 *
 * @author Anker
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Request {

    String domain();

    /**
     * 请求接口地址
     *
     * @return 接口地址
     */
    String url();
    /**
     * 请求方式, 只支持POST 和 GET 请求
     *
     * @return {@link RequestMethod}
     */
    RequestMethod method() default RequestMethod.POST;
}

那么, 我们的注解基本就算是定义好了.

Step-2 创建切面

Spring 不仅支持以包,具体方法为切点,还支持以注解为切点, 代码如下. 此处为了能让我们的方法直接可以返回请求结果, 使用了@Around 注解,即环绕通知!

@Slf4j
@Aspect
@Component
public class RequestAop {


    /**
     * 注解切点
     */
    @Pointcut("@annotation(saas.annotation.Request)")
    public void pointCut() {
    }

    @Around("pointCut()")
    public Object doRequest(ProceedingJoinPoint joinPoint) throws Throwable {
    }
  }

Step-3 切面的具体实现

接下来,就可以在切面方法中进行具体实现了, 使用过切面的同学肯定都知道其强大的特性, 底层使用动态代理实现, 至于什么是动态代理, 我会专门写一篇文章来赘述. 通过动态代理技术我们可以轻松的获取到该方法的入参, 返回值类型, 方法签名,以及方法的执行器等等...., 可以搭配Java的反射特性完成一系列高端操作.重新关心回本次的业务实现, 其具体的实现思路和步骤如下

  1. 开始
  2. 获取切面的目标方法对象
  3. 获取目标方法的入参
  4. 获取目标方法的返回值类型
  5. 获取标注在方法上的注解参数
  6. 获取方法的入参的参数注解
  7. 拼接请求地址
  8. 根据注解的请求方式构建请求
  9. 根据参数注解确定使用Json还是表单还是拼接路径参数
  10. 使用Http工具类发起Http请求
  11. 拿到返回值后进行解析
  12. 解析的结果
  13. 使用工具类将请求结果的Json对象转换为对应返回值类型的对象然后返回结果
  14. 结束

大致的一个实现思路就是这样的, 上代码

/**
 *  请求实现
 
 * @author YBin
 */
@Slf4j
@Aspect
@Component
public class RequestAop {

    /**
     * 注解切点
     */
    @Pointcut("@annotation(saas.annotation.Request)")
    public void pointCut() {
    }


    @Around("pointCut()")
    public Object doRequest(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取目标方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 形参列表
        Object[] args = joinPoint.getArgs();
        // 获取参数元信息
        Parameter[] parameters = method.getParameters();
        // 获取返回值类型
        Class<?> returnType = method.getReturnType();
        // 获取注解
        Request metaInfo = method.getAnnotation(Request.class);
        // 请求方式
        RequestMethod requestMethod = metaInfo.method();
        // 请求地址
        String url = metaInfo.url();
        // 域名或服务地址
        String domain = metaInfo.domain();
        String requestUrl = domain.concat("/").concat(url);
        // 构建请求地址
        log.info("requestUrl:{}", requestUrl);
        // 构建请求
        HttpRequest httpRequest;
        switch (requestMethod) {
            case POST:
                httpRequest = this.createPostRequest(requestUrl, args, parameters);
                break;
            case GET:
            	// Get 请求自行实现
                break;
            default:
                throw new RuntimeException("unSupport this request type");
        }
        if (ObjectUtil.isNull(httpRequest)) {
            throw new RuntimeException("build request failed");
        }
        String respResult = null;
        try {
            log.info("RequestCloudCallCenter build success, the request info :{}", JsonUtil.toJson(httpRequest));
            respResult = httpRequest.setConnectionTimeout(10000).setReadTimeout(10000).execute().body();
            log.info("RequestCloudCallCenter http request is executed , response result is:{}", respResult);
        } catch (Exception ex) {
            // 异常则执行实现方法
            joinPoint.proceed();
        }
        if (CharSequenceUtil.isBlank(respResult) || !JSONUtil.isJson(respResult)) {
            // 请求内容非预想结果,执行实现方法
            joinPoint.proceed();
        }
        JSONObject jsonObject = JsonUtil.toBean(respResult, JSONObject.class);
        Integer respCode = jsonObject.getInt(StrConstant.CODE);
        if (!respCode.equals(NumConstant.ONE_THOUSAND)) {
            throw new ApplicationException(jsonObject.getStr(StrConstant.MSG));
        }
        String data = jsonObject.getStr(StrConstant.DATA);
        return JsonUtil.toBean(data, returnType);
    }

    /**
     * 构建post请求
     *
     * @param url         请求地址
     * @param args        参数
     * @param parameters  参数反射对象
     * @param takeLicense 是否携带机构凭证
     * @return {@link HttpRequest}
     */
    private HttpRequest createPostRequest(String url, Object[] args, Parameter[] parameters) {
        String jsonRequestParam = null;
        if (args.length == 0) {
            throw new IllegalArgumentException("RequestBody is missing");
        } else if (args.length == 1) {
            Object arg = args[0];
            Parameter parameter = parameters[0];
            RequestBody annotation = parameter.getAnnotation(RequestBody.class);
            if (ObjectUtil.isNull(annotation)) {
                throw new IllegalArgumentException("Please annotate the parameters");
            }
            if (annotation.required() && ObjectUtil.isNull(arg)) {
                throw new IllegalArgumentException("request body is missing");
            }

            jsonRequestParam = JsonUtil.toJson(arg);
        } else {
            throw new IllegalArgumentException("the request param is too long");
        }
        HttpRequest post = HttpRequest.post(url).contentType("application/json");
        if (CharSequenceUtil.isNotBlank(jsonRequestParam)) {
            post.body(jsonRequestParam);
        }
        return post;
    }

}

通过以上操作基本实现了注解式的Http请求

Step-4 测试

  • 新建一个接口类
/**
 * saas公有云 呼叫中心交互服务
 *
 * @author YBin
 */
public interface RequestService {

    /**
     * 查询机构坐席有效性
     *
     * @param request   请求参数
     * @return
     */
    R<User> getUserInfo(User user);

}
  • 创建接口实现类
/**
 * 请求呼叫中心服务回调
 *
 * @author YBIn
 */
@Service
public class RequestFallbackAndServiceImpl implements RequestService {


    @Override
    @RequestCloudCallCenter(domain = "https://xxx.xxx.com/",url = "getUserInfo", method = RequestMethod.POST)
    public R<User> getUserInfo(@RequestBody User user) {
        return R.error("获取用户信息调用失败");
    }
    
}
  • 单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestRequest {


    @Resource
    private RequestService requestService;


    @Test
    public void doRequest(){
        R<UserInfo> resp = requestService.getUserInfo(UserInfo.build(300));
        System.out.println(resp);
    }

}

最终打印出了本次请求的请求结果

以上内容本人原创,部分逻辑使用伪代码方法标识, 如有问题可以私信我,免费帮忙解决. 如果觉得不错, 求个赞不过分吧 作者: 夜航猩