api接口动态增加字段

需求场景

附件内容都存在华为云OBS中,数据库中只保存了OBS文件的原始url和文件名,业务接口需要额外返回签名后的可访问url以供前端进行下载/显示文件内容,而在所有业务接口都自行做这个url签名的处理又比较麻烦,于是希望通过在VO类中,给需要签名的url字段添加一个自定义注解,扫描到这个注解之后,自动添加一个新字段,新字段内容为签名后的url地址

 实现方式:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于指定api接口是否需要进行url地址签名(避免不需要处理url签名的api接口进入AOP)
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlNeedSign {

}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlSign {
    /**
     * 文件名的属性名,从这个属性中获取值设置为下载的文件名
     * @return
     */
    String fileNameProp() default "";
}

 

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.cglib.beans.BeanGenerator;
import org.springframework.cglib.beans.BeanMap;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public final class PropertyAppendUtil {

    private static final class DynamicBean {

        private Object target;

        private BeanMap beanMap;

        private DynamicBean(Class superclass, Map<String, Class> propertyMap) {
            this.target = generateBean(superclass, propertyMap);
            this.beanMap = BeanMap.create(this.target);
        }

        private void setValue(String property, Object value) {
            beanMap.put(property, value);
        }

        private Object getValue(String property) {
            return beanMap.get(property);
        }

        private Object getTarget() {
            return this.target;
        }

        /**
         * 根据属性生成对象
         */
        private Object generateBean(Class superclass, Map<String, Class> propertyMap) {
            BeanGenerator generator = new BeanGenerator();
            if (null != superclass) {
                generator.setSuperclass(superclass);
            }
            BeanGenerator.addProperties(generator, propertyMap);
            return generator.create();
        }
    }

    public static Object generate(Object dest, Map<String, Object> newValueMap) throws InvocationTargetException, IllegalAccessException {

        //1.获取原对象的字段数组
        PropertyDescriptor[] descriptorArr = BeanUtils.getPropertyDescriptors(dest.getClass());

        //2.遍历原对象的字段数组,并将其封装到Map
        Map<String, Class> oldKeyMap = new HashMap<>();
        for (PropertyDescriptor it : descriptorArr) {
            //排除掉.getClass方法
            if (!"class".equalsIgnoreCase(it.getName())) {
                oldKeyMap.put(it.getName(), it.getPropertyType());
                newValueMap.put(it.getName(), it.getReadMethod().invoke(dest));
            }
        }

        //3.将扩展字段Map合并到原字段Map中
        newValueMap.forEach((k, v) -> {
            if(v!=null){
                oldKeyMap.putIfAbsent(k, v.getClass());
            }else{
                oldKeyMap.putIfAbsent(k,Object.class);
            }
        });

        //4.根据新的字段组合生成子类对象
        DynamicBean dynamicBean = new DynamicBean(dest.getClass(), oldKeyMap);

        //5.放回合并后的属性集合
        newValueMap.forEach((k, v) -> {
            try {
                dynamicBean.setValue(k, v);
            } catch (Throwable e) {
                log.error("动态添加字段【"+k+"】出错", e);
            }
        });
        return dynamicBean.getTarget();
    }
}
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.test.domain.BaseResult;
import com.test.domain.PageResult;
import com.test.utils.HuaweiObsUtil;
import com.test.utils.JSONUtil;
import com.test.annotation.UrlSign;
import com.test.utils.PageUtil;
import com.test.utils.PropertyAppendUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.*;

/**
 * url鉴权切面
 * 1:支持单个实体:属性上加@UrlAuthentication注解,返回值中包含url
 * 2:支持list:list和属性值都需要加@UrlAuthentication注解,返回值中包含url
 * 3:支持嵌套类:同list
 * 4:支持嵌套list类:同list
 */
@Slf4j
@Aspect
@Component
public class UrlAuthenticationAspect {

    private static final String EXT_URL_FIELD_NAME = "SignUrl";
    private static final String EXT_URL_FIELD_NAME_LIST = "SignUrlList";
    private static final long expireSecond = 3600L;

    @Around("@annotation(com.test.annotation.NeedAuthentication)")
    public Object addUrlSignature(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("进入切面");
        Object result = joinPoint.proceed();
        if (result instanceof BaseResult == false) {
            log.warn("接口返回值不是BaseResult类型,无法进行url鉴权处理");
            return result;
        }
        BaseResult baseResult = (BaseResult) result;
        try {
            Object data = baseResult.getData();
            if (data instanceof PageResult) {
                PageResult<?> pr = ((PageResult<?>) data);
                List<Object> newRecords = new ArrayList<>();
                for (Object o : pr.getList()) {
                    try {
                        newRecords.add(processObjectFields(o));
                    } catch (Exception e) {
                        log.error(StrUtil.format("解析异常:{}", JSONUtil.toJsonStr(baseResult)), e);
                        throw e;
                    }
                }
                baseResult.setData(PageUtil.convertPageResultForList(pr, sourceList -> newRecords));
            } else if (data instanceof Collection) {
                Collection<?> cdata = (Collection<?>) data;
                Collection<Object> newData = new ArrayList<>();
                for (Object item : cdata) {
                    newData.add(processObjectFields(item));
                }
                baseResult.setData(newData);
            } else {
                // 单个对象处理
                baseResult.setData(processObjectFields(data));
            }
        } catch (Exception e) {
            log.error("解析异常:{}", JSONUtil.toJsonStr(baseResult));
            throw e;
        }
        log.info("切面结束");
        return result;
    }

    private Object processObjectFields(Object obj) throws IllegalAccessException, InvocationTargetException {
        Map<String, Object> addValue = new HashMap<>();
        Class<?> clazz = obj.getClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(UrlSign.class)) {
                UrlSign urlSign = field.getAnnotation(UrlSign.class);
                String fileNameProp = urlSign.fileNameProp();
                field.setAccessible(true);
                Object value = field.get(obj);
                if (value == null) {
                    addValue.put(field.getName() + EXT_URL_FIELD_NAME, null);
                } else if (value instanceof String) {

                    String url4GetUrl = null;
                    if (StrUtil.isNotBlank(fileNameProp)) {
                        Object fileName = ReflectUtil.getFieldValue(obj, fileNameProp);
                        String fileNameStr = fileName == null ? "" : String.valueOf(fileName);
                        url4GetUrl = HuaweiObsUtil.buildUrlCustomFileName4GetUrl((String) value, fileNameStr, expireSecond);
                    } else {
                        url4GetUrl = HuaweiObsUtil.buildUrl4GetUrl((String) value, expireSecond);
                    }
                    addValue.put(field.getName() + EXT_URL_FIELD_NAME, url4GetUrl);
                } else if (value instanceof Collection) {
                    Collection oldColl = (Collection) value;
                    if (!CollUtil.isEmpty(oldColl)) {
                        boolean isString = oldColl.stream().findFirst().get() instanceof String;
                        if (isString) {
                            List<String> fileNameColl = new ArrayList<>();
                            if (StrUtil.isNotBlank(fileNameProp)) {
                                Object fileName = ReflectUtil.getFieldValue(obj, fileNameProp);
                                if (fileName instanceof Collection) {
                                    fileNameColl.addAll((Collection<String>) fileName);
                                } else if (fileName instanceof String) {
                                    String fileNameStr = (String) fileName;
                                    for (int i = 0; i < oldColl.size(); i++) {
                                        fileNameColl.add(fileNameStr);
                                    }
                                } else {
                                    log.warn("无法处理的文件名类型" + fileName.getClass().getName());
                                    for (int i = 0; i < oldColl.size(); i++) {
                                        fileNameColl.add("");
                                    }
                                }
                            }
                            List<String> newUrlColl = new ArrayList<>();
                            int i = 0;
                            for (Object nestedItem : (Collection<?>) value) {
                                String sourceUrl = (String) nestedItem;
                                String sourceFileName = null;
                                if (CollUtil.isNotEmpty(fileNameColl)) {
                                    sourceFileName = fileNameColl.get(i);
                                }
                                if (StrUtil.isNotBlank(sourceFileName)) {
                                    newUrlColl.add(HuaweiObsUtil.buildUrlCustomFileName4GetUrl(sourceUrl, sourceFileName, expireSecond));
                                } else {
                                    newUrlColl.add(HuaweiObsUtil.buildUrl4GetUrl(sourceUrl, expireSecond));
                                }
                                i++;
                            }
                            addValue.put(field.getName() + EXT_URL_FIELD_NAME_LIST, newUrlColl);
                        } else {
                            List newColl = new ArrayList();
                            for (Object nestedItem : (Collection<?>) value) {
                                newColl.add(processObjectFields(nestedItem));
                            }
                            oldColl.clear();
                            oldColl.addAll(newColl);
                        }
                    }

                } else if (!isPrimitiveOrWrapper(value.getClass())) {
                    Object newValue = processObjectFields(value);
                    field.set(obj, newValue);
                }


            }
        }
        return PropertyAppendUtil.generate(obj, addValue);
    }

    /**
     * 判断是否为基本类型、包装类或标准库类,避免递归进入这些类型
     */
    private boolean isPrimitiveOrWrapper(Class<?> clazz) {
        return clazz.isPrimitive() || clazz.getName().startsWith("java.");
    }
}

使用示例:

import com.test.annotation.UrlSign;
import lombok.Getter;
import lombok.Setter;

import java.util.List;

@Getter
@Setter
public class TestVO {

    @UrlSign
    private String testFile1Url;

    @UrlSign(fileNameProp = "testFile2Name")
    private String testFile2Url;

    private String testFile2Name;

    @UrlSign
    private List<String> testFileList1;

    @UrlSign(fileNameProp = "testFileNameList2")
    private List<String> testFileList2;

    private List<String> testFileNameList2;

    @UrlSign
    private List<TestInnerVO> innerVOList;

    @UrlSign
    private TestInner2VO inner2VO;

    @Getter
    @Setter
    public static class TestInnerVO{

        @UrlSign(fileNameProp = "innerFile1UrlName")
        private String innerFile1Url;

        private String innerFile1UrlName;

        @UrlSign(fileNameProp = "innerFileNameList")
        private List<String> innerFileUrlList;

        private List<String> innerFileNameList;

    }

    @Getter
    @Setter
    public static class TestInner2VO{

        @UrlSign(fileNameProp = "innerFileName")
        private String innerFileUrl;

        private String innerFileName;

    }
}
import com.test.domain.BaseResult;
import com.test.annotation.UrlNeedSign;
import com.test.domain.vo.TestVO;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("/inner/test")
public class TestUrlController {

    @PostMapping("test")
    @UrlNeedSign
    public BaseResult<TestVO> test() {
        TestVO testVO=new TestVO();
        testVO.setTestFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testFile1");
        testVO.setTestFile2Url("https://test.obs.cn-south-1.myhuaweicloud.com/testFile2");
        testVO.setTestFile2Name("测试文件2.txt");
        testVO.setTestFileList1(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testFile3","https://test.obs.cn-south-1.myhuaweicloud.com/testFile4"));
        testVO.setTestFileList2(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testFile5","https://test.obs.cn-south-1.myhuaweicloud.com/testFile6"));
        testVO.setTestFileNameList2(Arrays.asList("测试文件5.txt","测试文件6.txt"));
        List<TestVO.TestInnerVO> innerVOList=new ArrayList<>();
        TestVO.TestInnerVO innerVO1=new TestVO.TestInnerVO();
        innerVO1.setInnerFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1");
        innerVO1.setInnerFile1UrlName("测试内部VO1文件1.txt");
        innerVO1.setInnerFileUrlList(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2","https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3"));
        innerVO1.setInnerFileNameList(Arrays.asList("测试内部VO1文件2.txt","测试内部VO1文件3.txt"));
        TestVO.TestInnerVO innerVO2=new TestVO.TestInnerVO();
        innerVO2.setInnerFile1Url("https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1");
        innerVO2.setInnerFile1UrlName("测试内部VO2文件1.txt");
        innerVO2.setInnerFileUrlList(Arrays.asList("https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2","https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3"));
        innerVO2.setInnerFileNameList(Arrays.asList("测试内部VO2文件2.txt","测试内部VO2文件3.txt"));
        innerVOList.add(innerVO1);
        innerVOList.add(innerVO2);
        testVO.setInnerVOList(innerVOList);
        TestVO.TestInner2VO inner2VO=new TestVO.TestInner2VO();
        inner2VO.setInnerFileUrl("https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1");
        inner2VO.setInnerFileName("测试内部单VO文件.txt");
        testVO.setInner2VO(inner2VO);
        return BaseResult.success(testVO);
    }
}

测试结果:

{
  "code": "success",
  "message": null,
  "data": {
    "testFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile1",
    "testFile2Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile2",
    "testFile2Name": "测试文件2.txt",
    "testFileList1": [
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile3",
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile4"
    ],
    "testFileList2": [
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile5",
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile6"
    ],
    "testFileNameList2": [
      "测试文件5.txt",
      "测试文件6.txt"
    ],
    "innerVOList": [
      {
        "innerFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1",
        "innerFile1UrlName": "测试内部VO1文件1.txt",
        "innerFileUrlList": [
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2",
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3"
        ],
        "innerFileNameList": [
          "测试内部VO1文件2.txt",
          "测试内部VO1文件3.txt"
        ],
        "innerFileUrlListSignUrlList": [
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File2?attname=测试内部VO1文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=KogXKMbij%2FF4MfV%2FOJdIe7Lq9a4%3D",
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File3?attname=测试内部VO1文件3.txt&AccessKeyId=testak&Expires=1725096532&Signature=XU1TbeJxpKFHP8x7w%2BlP14sm7lw%3D"
        ],
        "innerFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner1File1?attname=测试内部VO1文件1.txt&AccessKeyId=testak&Expires=1725096532&Signature=VdzxcyqI3fYc1o30eLeYrRIUdS8%3D"
      },
      {
        "innerFile1Url": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1",
        "innerFile1UrlName": "测试内部VO2文件1.txt",
        "innerFileUrlList": [
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2",
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3"
        ],
        "innerFileNameList": [
          "测试内部VO2文件2.txt",
          "测试内部VO2文件3.txt"
        ],
        "innerFileUrlListSignUrlList": [
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File2?attname=测试内部VO2文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=Tnr7LaigTrx%2FDMlzPOG%2BvFfcXsE%3D",
          "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File3?attname=测试内部VO2文件3.txt&AccessKeyId=testak&Expires=1725096532&Signature=uNK6V8V3ZKnvtHOXqZpWy%2FoCvxc%3D"
        ],
        "innerFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInner2File1?attname=测试内部VO2文件1.txt&AccessKeyId=testak&Expires=1725096532&Signature=PWxOUP1WuLYFaYZyx%2BVso1%2B%2BE%2Bg%3D"
      }
    ],
    "inner2VO": {
      "innerFileUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1",
      "innerFileName": "测试内部单VO文件.txt",
      "innerFileUrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testInnerVO2File1?attname=测试内部单VO文件.txt&AccessKeyId=testak&Expires=1725096532&Signature=X3uEPTw1LUj3VsrX9MJlUyOrPjo%3D"
    },
    "testFileList1SignUrlList": [
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile3?AccessKeyId=testak&Expires=1725096532&Signature=txZ80twXwEQJg9b29lpYliLulX8%3D",
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile4?AccessKeyId=testak&Expires=1725096532&Signature=mLFWq%2Fuelh3439fqi%2BqIFodB52U%3D"
    ],
    "testFile2UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile2?attname=测试文件2.txt&AccessKeyId=testak&Expires=1725096532&Signature=BDFn6ikqAQfAL8n4MHiRXQzosBk%3D",
    "testFileList2SignUrlList": [
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile5?attname=测试文件5.txt&AccessKeyId=testak&Expires=1725096532&Signature=1Zap0vq5xQXXK%2FfRjnMuf4d9a88%3D",
      "https://test.obs.cn-south-1.myhuaweicloud.com/testFile6?attname=测试文件6.txt&AccessKeyId=testak&Expires=1725096532&Signature=DjgupXge07HOvv56bXOKxf7T%2FNw%3D"
    ],
    "testFile1UrlSignUrl": "https://test.obs.cn-south-1.myhuaweicloud.com/testFile1?AccessKeyId=testak&Expires=1725096532&Signature=tE057WHVooZpz3K8AipDXrZmWvQ%3D"
  }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值