你的浏览器不支持canvas

做你害怕做的事情,然后你会发现,不过如此。

项目多租户区分OSS时,出参和入参url处理

时间: 作者: 黄运鑫

本文章属原创文章,未经作者许可,禁止转载,复制,下载,以及用作商业用途。原作者保留所有解释权。


概况

  • 项目使用第三方BladeX-Boot单体版本框架,Spring BootMybatisMyBatis-Plus
  • 项目数据库表使用租户字段tenant_id隔离租户数据
  • 使用阿里云OSS存储文件,并且每个租户使用独立的OSS空间和域名,方便租户使用自己的OSS和域名
  • 前端上传到OSS的文件名必须为nocheck_前缀开头,OSS设置的生命周期规则会删除一天前上传的nocheck_前缀文件,避免未保存到数据库的文件占用OSS空间(比如用户上传文件后,没有提交表单)

需求

  • 1.由于每个租户OSS域名不同,需要接口出参时,根据本条数据的租户来拼接OSS域名
  • 2.前端上传到OSS的文件名称都是nocheck_前缀开头,接口保存数据时需要去掉nocheck_前缀
  • 3.数据库保存或修改数据时,需要去掉OSS域名,只保存OSS文件路径

实现

  • 创建注解,在实体类使用注解标识OSS字段,方便后续处理,注解如下:
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 AliOssCheck {
    /**
     * 是否 List<String>,false代表String,true代表List<String>
     */
    boolean isJsonList() default false;
}
  • 在实体类的OSS字段添加注解,比如用户头像:
@Data
@TableName("gecco_user")
@EqualsAndHashCode(callSuper = true)
public class User extends TenantEntity {
    private static final long serialVersionUID = 1L;

    /**
     * 头像
     */
    @AliOssCheck
    private String avatar;
}

接口出参拼接OSS域名

  • 添加域名的方法,通过判断传入对象是否有OSS注解进行拼接,方法如下:
/**
 * 添加域名
 */
public void addOssDomain(Collection<Object> list) {
    for (Object obj : list) {
        if (obj == null) {
            continue;
        }
        //处理有AliOssCheck的字段
        Field[] fields = ReflectUtil.getFields(obj.getClass());
        for (Field field : fields) {
            AliOssCheck annotation = field.getAnnotation(AliOssCheck.class);
            if (annotation == null) {
                continue;
            }
            Object value = ReflectUtil.getFieldValue(obj, field);
            if (value == null) {
                continue;
            }
            //获取租户oss
            Object tenantId = ReflectUtil.getFieldValue(obj, GeccoConstant.DB_TENANT_KEY);
            Oss oss = SpringUtil.getBean(IOssService.class).selectEnableOssCache(tenantId != null ? tenantId.toString() : TokenUtil.DEFAULT_TENANT_ID);
            if (oss == null || StrUtil.isBlank(oss.getDomain())) {
                continue;
            }
            if (annotation.isJsonList()) {
                List<Object> valueList = (List) value;
                ReflectUtil.setFieldValue(obj, field, valueList.stream().map(item -> {
                    if (item instanceof String) {
                        //富文本不处理
                        return item.toString().contains("<") ? item : oss.getDomain() + "/" + item;
                    } else {
                        return item;
                    }
                }).collect(Collectors.toList()));
            } else {
                ReflectUtil.setFieldValue(obj, field, oss.getDomain() + "/" + value);
            }
        }
    }
}
  • UserWrapper中调用拼接域名的方法,代码如下:
/**
 * 包装类,返回视图层所需的字段
 */
public class UserWrapper extends BaseEntityWrapper<User, UserVO> {
    public static UserWrapper build() {
        return new UserWrapper();
    }

    @Override
    public UserVO entityVO(User user) {
        UserVO userVO = Objects.requireNonNull(BeanUtil.copy(user, UserVO.class));
        //添加oss域名
        OssWrapper.build().addOssDomain(Collections.singletonList(userVO));
        return userVO;
    }
}

保存时去掉域名和前缀

  • 比如https://www.xinpapa.com/aaa/bbb/nocheck_file.jpg保存为aaa/bbb/file.jpg
  • 使用切面实现,切入点为框架的保存和更新方法,代码如下:
  • :调用save、update等方法时,不能使用this调用,否则切面不生效;比如this.save(user)改为SpringUtil.getBean(IUserService.class).save(user)
/**
 * 阿里文件切面
 */
@Aspect
@Component
@Slf4j
public class OssCheckAspect {
    static final String NOCHECK = "nocheck_";

    /**
     * Pointcut 切入点
     */
    @Pointcut("execution(public * com.transnal.core.mp.base.BaseServiceImpl.save*(..)) || " +
        "execution(public * com.transnal.core.mp.base.BaseServiceImpl.update*(..))")
    public void ossCheck() {
    }


    /**
     * 方法执行前
     */
    @Before(value = "ossCheck()")
    public void before(JoinPoint joinPoint) {
        if (log.isDebugEnabled()) {
            log.debug("AOP 开始替换上传{}文件名", NOCHECK);
        }
        Object[] args = joinPoint.getArgs();
        Object firstParam = args[0];
        if (firstParam instanceof Collection) {
            Collection list = (Collection) firstParam;
            for (Object o : list) {
                checkFile(o);
            }
        } else {
            checkFile(firstParam);
        }
    }

    private Object checkFile(Object m) {
        try {
            Field[] fields = ReflectUtil.getFields(m.getClass());
            for (Field field : fields) {
                boolean fieldExist = field.isAnnotationPresent(AliOssCheck.class);
                if (fieldExist) {
                    Object oldValue = ReflectUtil.getFieldValue(m, field);
                    if (oldValue == null) {
                        continue;
                    }
                    AliOssCheck annotation = field.getAnnotation(AliOssCheck.class);
                    Object newValue = oldValue;
                    if (annotation.isJsonList()) {
                        newValue = checkFileList((List) oldValue);
                    } else {
                        newValue = checkFile((String) oldValue);
                    }
                    ReflectUtil.setFieldValue(m, field.getName(), newValue);
                }
            }
        } catch (SecurityException e) {
            log.error("SecurityException", e);
        } catch (UtilException e) {
            log.error("UtilException", e);
        }
        return m;
    }


    private String checkFile(String fileName) {
        if (StrUtil.isBlank(fileName)) {
            return null;
        }

        //如果不是富文本但是有域名,则去掉域名
        if (!fileName.contains("<") && fileName.contains("http")) {
            fileName = CommonUtil.getOssPath(fileName);
        }

        if (!fileName.contains(NOCHECK)) {
            return fileName;
        }
        IOssService iOssService = SpringUtil.getBean(IOssService.class);
        Oss oss = iOssService.selectEnableOssCache(SecureUtil.getTenantId());
        OSS aliOssClient = new OSSClientBuilder().build(oss.getEndpoint(), oss.getAccessKey(), oss.getSecretKey());
        Pattern pattern = Pattern.compile("([\\w|-]+/)*" + NOCHECK + "\\S{1,60}\\.\\w{1,5}");
        Matcher matcher = pattern.matcher(fileName);
        StringBuffer stringBuffer = new StringBuffer();
        while (matcher.find()) {
            String nocheckName = matcher.group();
            String newName = nocheckName.replace(NOCHECK, "");
            matcher.appendReplacement(stringBuffer, newName);
            try {
                aliOssClient.copyObject(oss.getBucketName(), nocheckName, oss.getBucketName(), newName);
            } catch (Exception e) {
                log.error("ossCheck异常", e);
            }
        }
        return stringBuffer.toString();
    }

    private List<String> checkFileList(List<String> fileNames) {
        if (fileNames == null) {
            return null;
        }
        return fileNames.stream().map(this::checkFile).collect(Collectors.toList());
    }
}

对于本文内容有问题或建议的小伙伴,欢迎在文章底部留言交流讨论。