如何在数据库只保存oss上的文件名, 当查询数据时根据字段的文件名, 获取oss的公网访问地址,并对字段内容重写.
有这样一个需求, 图片上传到oss 上, 返回文件名和公网访问地址, 但是要求数据库中只存储文件名称.
有两个目的:
- 数据库只存储文件名称, 方便后期oss 上数据迁移到其他对象存储上, 迁移保证文件名不变, 后期就只需要更改获取公网地址的地方.
- 在查询数据时再获取文件的访问地址, 并设置链接的有效期, 可以提高文件的安全性, 也可以防止oss 流量被盗刷.
实现的思路有两种方式
-
都是基于java语言, 和springboot框架实现.
-
都需要使用自定义注解.
- 在返回时, 使用反射的方式, 递归的扫描返回对象的每一个字段属性上是否添加该注解, 符合条件了, 获取到链接替换掉原本的值.
- 同样是在返回时, 只是在返回体序列化上进行处理.在jack序列化每一个字段属性的时候,扫描是否存在该注解,后续操作与方法一相同.
-
利弊:
方法一, 使用到了反射, 去层层扫描对象中的所有基本类型属性, 并且需要记录当前字段所属的对象, 否则在重写的时候,就不知道该属性的对象了. 方法二使用了jack字段序列化时,进行修改, 本身就避免了我们自己写反射扫描的问题, 而且方法二的效率比方法一快.
下面介绍两种方法的实现方式.
方法一:
自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OssConverter {
}
需要定义一个 ResponseBodyAdvice的实现类
@Slf4j
@RestControllerAdvice(basePackages = "com.wdhcr") // 指定controller的路径, 可以范围大一点
public class ResponseBodyConfig implements ResponseBodyAdvice {
@Autowired
private OssComponent ossComponent;
@Autowired
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
long start = System.currentTimeMillis();
if (body instanceof R) {
R result = (R) body;
Object data = result.getData();
Map<Field,Object> fields = new HashMap<>();
// 使用反射工具类 获取到指定对象中所有的属性,包含嵌套对象的属性
ReflectUtil.getAllFields(data,fields);
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>();
for (Field field : fields.keySet()) {
// 遍历字段是否包含指定注解, OssConverter为自定义注解.
OssConverter annotation = field.getAnnotation(OssConverter.class);
if (annotation != null) {
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> setImageUrlWithOssUrl(field, fields),threadPoolTaskExecutor);
futures.add(future);
}
}
CompletableFuture[] completableFutures = new CompletableFuture[futures.size()];
try {
// 想法是太多的网络请求, 可能影响返回数据效率, 所以使用了多线程方式.
CompletableFuture.allOf(futures.toArray(completableFutures)).get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
long end = System.currentTimeMillis();
System.err.println("总计用时:" + ((end - start)));
return body;
}
private void setImageUrlWithOssUrl(Field field, Map<Field, Object> fields) {
Object value = null;
try {
field.setAccessible(true);
value = field.get(fields.get(field));
if (value != null) {
// 这个是oss 组件, 用来根据文件名获取访问地址的, 可见源码.
String url = ossComponent.getUrl(value.toString());
field.set(fields.get(field), url);
}
} catch (Exception e) {
log.error("更新返回体中oss地址的字段异常,值为:{}", value);
}
}
}
上述类就是方法一的核心思想, 代码标有备注信息. 完整的demo,可查看文章末尾.
测试类(com.wdhcr.osspolicy.bean.User)和测试方法(com.wdhcr.osspolicy.controller.GetOssUrlBodyDemoController)可查看demo.
测试接口: http://localhost:8080/user
方法二
自定义注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = UrlConverterJsonSerializer.class)
public @interface OssConverterUrl {
// 地址转换的类型, 默认为oss
UrlConverterType type() default UrlConverterType.OSS;
}
定义枚举类
@Getter
public enum UrlConverterType {
OSS(url -> {
// 枚举的参数是一个lambda表达式
// url是入参
OssComponent ossComponent = SpringUtils.getBean(OssComponent.class);
return ossComponent.getUrl(url);
});
// 使用了java8的新特性,函数式接口
private final Function<String, String> deserialize;
UrlConverterType(Function<String, String> deserialize) {
this.deserialize = deserialize;
}
}
核心类, 就是上述注解中引用的UrlConverterJsonSerializer.class
public class UrlConverterJsonSerializer extends JsonSerializer<String> implements ContextualSerializer {
private UrlConverterType urlConverterType;
@Override
public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
if (Objects.nonNull(urlConverterType)) {
// 核心代码就是 json生成器写string时, 将原始属性值传入当前属性上的注解中的枚举, 获取到函数式接口并执行. (.apply(s)方法就会调用到枚举参数的lambda表达式了)
jsonGenerator.writeString(urlConverterType.getDeserialize().apply(s));
}else {
jsonGenerator.writeString(s);
}
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
OssConverterUrl converterUrl = beanProperty.getAnnotation(OssConverterUrl.class);
if (Objects.nonNull(converterUrl) && Objects.equals(String.class, beanProperty.getType().getRawClass())) {
// 判断这个bean属性上OssConverterUrl自定义注解不为空, 并且该属性是string类型.
this.urlConverterType = converterUrl.type();
return this;
}
return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
}
}
上述类就是方法一的核心思想, 代码标有备注信息. 完整的demo,可查看文章末尾.
测试类(com.wdhcr.osspolicy.bean.UserJson)和测试方法(com.wdhcr.osspolicy.controller.GetOssUrlBodyDemoController)可查看demo.
测试接口: http://localhost:8080/getUserJson