RestTemplate+Aop实现RPC远程调用,并实现自动配置

前言

上期写了一篇RestTemplate+Aop实现远程调用,其中关于Aop实现上传文件表单和参数解析有些问题,这期主要处理上期代码遗留的bug和结合上期代码实现springboot自动配置,即导入项目坐标即可使用,无需复制代码自行配置。

Aop重构

package org.neil.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.neil.aspect.annotation.RemoteCall;
import org.neil.modules.remote.entity.RemoteServer;
import org.neil.modules.remote.datasource.RemoteServerDataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.io.AbstractResource;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;

/**
 * 远程调用AOP
 *
 * @author zhu.zn 2024/4/30 18:12
 */
@Slf4j
@Aspect
public class RemoteCallAspect {

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private RemoteServerDataSource remoteServerDataSource;

    @Around("@annotation(org.neil.aspect.annotation.RemoteCall)")
    public Object handlerAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        log.info("远程调用开始");
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        // 获取参数名称
        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        String[] parameterNames = discoverer.getParameterNames(method);
        Object[] args = joinPoint.getArgs();

        // 获取注解的参数
        RemoteCall remoteCall = method.getAnnotation(RemoteCall.class);
        String service = remoteCall.service();
        String path = remoteCall.path();
        HttpMethod httpMethod = remoteCall.method();
        String[] headers = remoteCall.headers();
        String[] params = remoteCall.params();
        String body = remoteCall.body();
        String[] variable = remoteCall.variable();
        String file = remoteCall.file();

        // 首先构建请求头
        RemoteServer remoteServer = remoteServerDataSource.loadRemoteServerByServiceName(service);
        HttpHeaders httpHeaders = handlerHeaders(remoteServer.getHeaders(), headers);

        // 构建body和entity
        HttpEntity<?> entity = new HttpEntity<>(handlerBody(args, parameterNames, body, file, httpHeaders), httpHeaders);
        // 构建请求url,BaseUrl按需从配置中心获取,注解添加service方法,根据选择的服务拿到对应的Url
        String url = handlerUrl(args, parameterNames, params, variable, remoteServer.getUrl(), path);
        try {
            log.info("远程调用的url为:{}", url);
            ResponseEntity<?> responseEntity =
                    restTemplate.exchange(url, httpMethod, entity, method.getReturnType());
            Object data = responseEntity.getBody();
            log.info("远程调用响应结果为:{}", data);
            return data;
        } catch (RestClientException e) {
            log.info("远程调用失败:{}", e.getMessage());
            e.printStackTrace();
        } finally {
            log.info("远程调用结束,耗时:{}ms", (System.currentTimeMillis() - start));
        }
        return joinPoint.proceed(args);
    }

    /**
     * 处理请求头
     *
     * @param headersConfig 服务请求头配置
     * @param headers       当前请求接口的额外配置
     * @return 返回HttpHeader配置
     */
    private HttpHeaders handlerHeaders(Map<String, String> headersConfig, String[] headers) {
        HttpHeaders httpHeaders = new HttpHeaders();
        headersConfig.forEach(httpHeaders::add);
        if (headers.length > 0 && headers.length % 2 == 0) {
            for (int i = 0; i < headers.length; i += 2) {
                httpHeaders.add(headers[i], headers[i + 1]);
            }
        }
        return httpHeaders;
    }

    /**
     * 解析配置yrl
     *
     * @param args           方法参数
     * @param parameterNames 参数名称
     * @param params         注解输入的param
     * @param variable       注解输入的variable
     * @param url            服务配置的url
     * @param path           请求路径
     * @return 拼接出的url
     */
    private String handlerUrl(Object[] args, String[] parameterNames,
                              String[] params, String[] variable,
                              String url, String path) {
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url).path(path);
        MultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        for (String param : params) {
            Object paramValue = getTargetArg(args, parameterNames, param);
            paramMap.addAll(resolveArg(paramValue, param));
        }

        paramMap.forEach(builder::queryParam);

        MultiValueMap<String, Object> variableMap = new LinkedMultiValueMap<>();
        for (String var : variable) {
            Object varValue = getTargetArg(args, parameterNames, var);
            variableMap.addAll(resolveArg(varValue, var));
        }
        builder.uriVariables(variableMap.toSingleValueMap());
        return builder.build().toUriString();
    }

    /**
     * 解析请求体
     *
     * @param args           方法参数
     * @param parameterNames 参数名称
     * @param bodyName       注解输入的body参数名
     * @param fileName       注解输入的文件参数名
     * @param httpHeaders    HttpHeaders
     * @return 返回post请求体
     */
    private Object handlerBody(Object[] args, String[] parameterNames, String bodyName, String fileName, HttpHeaders httpHeaders) {
        Object bodyValue = getTargetArg(args, parameterNames, bodyName);
        // 如果fileName为空,则说明是普通POST请求,直接返回参数对象
        if (fileName.length() == 0) {
            return bodyValue;
        }
        // 有文件上传的POST表单,将参数值解析成MultiValueMap表单
        Object fileValue = getTargetArg(args, parameterNames, fileName);
        httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
        MultiValueMap<String, Object> bodyMap = resolveArg(bodyValue, bodyName);
        MultiValueMap<String, Object> fileMap = resolveArg(fileValue, fileName);
        bodyMap.addAll(fileMap);
        return bodyMap;
    }

    /**
     * 筛选出目标参数值
     *
     * @param args           方法参数
     * @param parameterNames 方法参数名
     * @param target         目标参数名
     * @return 目标参数值
     */
    private Object getTargetArg(Object[] args, String[] parameterNames, String target) {
        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i].equals(target)) {
                return args[i];
            }
        }
        return null;
    }

    /**
     * 解析参数(spring只能解析基本类型、基本类型数组和基本类型的List)
     *
     * @param obj       参数值
     * @param paramName 参数名
     * @return 返回多值Map
     */
    private MultiValueMap<String, Object> resolveArg(Object obj, String paramName) {
        MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        if (obj == null) {
            return map;
        }
        // 如果是基本类型
        if (obj instanceof Number
                || obj instanceof Character
                || obj instanceof Boolean
                || obj instanceof String
                || obj instanceof Date
                || obj instanceof AbstractResource) {
            map.add(paramName, obj);
        } else if (obj instanceof List) {
            map.addAll(paramName, ((List<Object>) obj));
        } else if (obj.getClass().isArray()) {
            map.addAll(paramName, Arrays.asList((Object[]) obj));
        } else if (obj instanceof Map) {
            map.setAll(((Map<String, Object>) obj));
        } else {
            map.addAll(resolveObject(obj));
        }
        return map;
    }

    /**
     * 仿照表单解析对象,不深入解析(spring只能解析基本类型、基本类型数组和基本类型的List)
     *
     * @param obj 需要解析的对象
     * @return 返回多值Map
     */
    private MultiValueMap<String, Object> resolveObject(Object obj) {
        MultiValueMap<String, Object> map = new LinkedMultiValueMap<>();
        Class<?> objClass = obj.getClass();
        Field[] fields = objClass.getDeclaredFields();
        for (Field field : fields) {
            String fieldName = field.getName();
            try {
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value == null) {
                    continue;
                }
                if (value instanceof Number
                        || value instanceof Character
                        || value instanceof Boolean
                        || value instanceof String) {
                    map.add(fieldName, value);
                } else if (value instanceof Date) {
                    DateTimeFormat annotation = field.getAnnotation(DateTimeFormat.class);
                    if (annotation != null) {
                        SimpleDateFormat sdf = new SimpleDateFormat(annotation.pattern());
                        map.add(fieldName, sdf.format(value));
                    } else {
                        map.add(fieldName, value);
                    }
                } else if (value instanceof List) {
                    map.addAll(fieldName, ((List<Object>) value));
                } else if (value.getClass().isArray()) {
                    map.addAll(fieldName, Arrays.asList((Object[]) value));
                }
                field.setAccessible(false);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("解析字段:" + fieldName + "异常", e);
            }
        }
        return map;
    }

}

代码说明

上期代码关于解析方法参数部分我想的过于复杂,导致代码看起来比较乱,其实主要关注GET请求和POST请求即可。

GET请求

主要分为查询参数和path参数
1.查询参数指的是拼接在请求url后的参数(例:http://localhost:8080/path?query=queryParam),queryParm只能是基本数据类型(Number,String, Date,基本类型的数组,其他对象类型建议与服务端沟通用json字符串传输),因为SpringMvc默认实现了基本类型的参数解析,在请求前会将输入的参数转换成字符串,服务端会将字符串解析成对应的参数类型对象,所以只需要关注基本类型和数组类型即可。在发起请求前,先进行方法参数解析,拿到注解中的params,找到对应的参数值,解析参数值,封装成MutilValueMap,如果是基本类型,直接根据参数名放入Map即可,如果是对象类型,解析一次,将其中非空的基本类型参数放入map,key为字段名,最后根据map中的数据封装请求url。
2.path参数类型其实就是path中的模板{},类似@PathVariable注解解析的参数。解析方式和GET一样,将{}中从参数替换成Map中对应的value。

POST请求

POST请求主要是普通POST请求和表单POST请求
1.普通POST请求将对应参数用HttpEntity封装即可
2.表单POST请求就是有文件上传的请求,我的做法是表单参数和附件参数分离成两个参数,如果注解内的file参数为空,就是普通POST请求,走第一步即可,否则需要将body参数对应的参数值解析成表单,和GET请求一样,解析成MultiValueMap,将整个MultiValueMap做为Body封装Entity即可,并设置请求头为表单请求httpHeaders.setContentType(MediaType.MULTIPART_FORM_DATA);
文件参数必须是以org.springframework.core.io.AbstractResource为基类的实现,常用的实现类有:
1.org.springframework.core.io.FileSystemResource 用于本地文件上传。
2.org.springframework.core.io.InputStreamResource 用于输入流上传,必须重写getFilename和contentLength方法,上期已经说过补充些会报错
3.org.springframework.core.io.ByteArrayResource 字节数组上传,也必须重写2的方法
4.org.springframework.web.multipart.MultipartFileResource 用于用户上传的文件(没有实验过,如果报错,按照2重写)
按照我的想法,我们基本上会用的到的就是FileSystemResource 和MultipartFileResource,一个本地文件上传一个用户上传的文件上传这两类。如果是其他类型的会导致请求失败!!!

构建成自动配置项目

既然功能完善了,那么就需要拿来实际应用了。但是每换一个项目就得重新复制一次代码,想想就很复杂吧。所以我就想能不能写成自动配置,其他项目引入依赖后只要按照约定的配置格式在application.yml配置文件写好配置,即可使用。

项目结构介绍

---|neil-boot-system			测试模块
---|neil-modules				管理基本模块
	---|neil-remote				远程调用模块
---|neil-starter				starter模块
	---|neil-remote-starter		远程调用自动配置模块

核心的模块就是远程调用模块和自动配置模块了,自动配置模块依赖远程调用模块
在自动配置模块中的org.neil.configuration.NeilRemoteStarterAutoConfiguration声明出需要被springboot自动配置的bean即可
在resource模块下创建META-INF/spring.fatories文件因为springboot启动时会扫描所有jar包内的这个文件配置所有bean

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.neil.configuration.NeilRemoteStarterAutoConfiguration

我还写了了一个RemoteServerDataSource抽象类,需要实现loadRemoteServerByServiceName方法,默认实现是基于yml配置读取service配置,如果你想基于数据库,只需要实现这个类即可,也可以基于yml和数据库配置,怎么用取决你的想法。

我的项目是基于springboot2.7.10构建的,如果你想使用我的源码打包成starter打包成jar包使用请自行创建一个springboot2.7.10的项目引入模块调整后打包。经过我自己引入我们项目使用测试后springboot2.3.10也是可以使用这个远程调用的的。

相关源码下载

源码我创建了一个gitee仓库,有需要的可以自行下载
链接: 源码下载

最后吐槽一下吧

请求参数封装
这是我们组另一个后端发流程创建的一个请求,可以看出,整个实现就是多行代码,全是参数封装,我跟他沟通想用我这个办法实现远程调用,很可惜完全不听,虽然程序员为了偷懒最后把自己都给优化掉了,但是看着自己曾经写出多垃圾的代码总会忍不住骂一下。我也曾写出过很多垃圾代码,后期优化修改总是很麻烦,总想找机会偷懒,但是又总没偷懒的动力。这远程调用是我空闲时间写的,总会难免有bug是我没发现的,如果你使用了我的代码出了bug请在评论区指出,能改我就改,改不了就没办法了,一个人的能力有限,不喜勿喷。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值