Java 反射操作之为文件加上绝对路径
需求
一段时间前,公司前端有个这种需求:“你返回的文件字段,要有两个,一个是给你存到数据库里面的,一个是给我获取文件地址的。”
什么意思呢?
比如说,一个用户的头像存入数据库里,字段名是img,相对地址是 “/upload/123.jpg”,但这个地址前端不能直接用,因为它没有详细地址,无法定位获取。
我需要返回一个如 “http://www.baidu.com/upload/123.jpg” 这种值给前端,他才能显示出这个用户的头像。但是这个值又不能放在 img 字段里,因为一改动,前端想要再保存到数据库就又麻烦了。那么我就需要一个新的字段来存储这个绝对地址。
一个最简单的办法是定义一个字段,set (【项目前缀】 + img)。但如果是很多的话,维护起来就麻烦了很多。这时迫切要一个所有的实体类通用的方法,来给前端返回这个字段。于是乎,一个映射路径的代码就被我弄出来了。
代码赏析
1. 允许加上地址的字段前缀
private static final String[] FILE_START = {
//普通图片
"/uploads/",
//视频
"/videos/",
"/video/",
//头像
"/avatar/",
//商品图片
"/goods/",
//背景图片
"/bg/",
//安装包
"/apk/",
};
2. 反射字段并加上路径的方法
/**
* 为特定字段加上全部路径
*
* @param url 路径
* @param o 任意对象
*/
public static void resetUrl(String url, Object o) {
if (o == null)
return;
Class<?> c = o.getClass();
List<Field> fields = Arrays.asList(c.getDeclaredFields());//反射字段
if (CollUtil.isEmpty(fields))
return;
fields.forEach(f -> {
String name = f.getName();//获取字段名字
//一般来说, 实体类会直接或间接继承Serializable,所以遇到这个不需要反射了,直接退出去执行下一轮
if (name.equals("serialVersionUID"))
return;
//要加上路径, 字段必须以 Temp 结尾并且必须是String类型的
if (! name.endsWith("Temp"))
return;
Type t = f.getGenericType();
if (!t.equals(String.class))
return;
try {
/**
* 取得这个字段去除后缀 Temp 后的字段
* 意思就是说, 现在我有一个 imgTemp 字段, 那么他就必须有一个 img 字段, 并且字段必须是第一步的任意前缀才能加上路径
*/
String relative = tempName.split("Temp")[0];
String getRelative = getMethods(relative);//获取 img 字段的get方法
Method method = c.getMethod(getRelative);//类似 getImg()
Object invoke = method.invoke(o);//调用 getImg()方法得到值
String value = invoke.toString();//得到字段的值
for (String s : FILE_START) {
if (value.startsWith(s)) {
f.setAccessible(true);//设置字段成可访问状态
String setTempName = setMethods(tempName);//获取temp字段的set方法
Method setMethod = c.getMethod(setTempName, String.class);//意思是: public String setImgTemp(String s){this.imgTemp = s;}
setMethod.invoke(o, url + value);//调用上一步的方法
break;
}
}
} catch (IllegalAccessException | InvocationTargetException ignored) {
log.error("invoke 异常, 字段: {}", tempName);
} catch (NoSuchMethodException ignored) {
log.error("getMethod 异常, 字段: {}", tempName);
} catch (NullPointerException ignored) {
log.warn("图像字段为空, 字段: {}", tempName);
} finally {
f.setAccessible(false);//最后需要将字段访问状态关闭, 防止被篡改
}
});
}
3. getMethods 和 setMethods
/**
* setMethod
*
* @param field
* @return
*/
private static String setMethods(String field) {
String s = field.substring(0, 1);
String s1 = field.replaceFirst(s, s.toUpperCase());
return "set" + s1;
}
/**
* getMethod
*
* @param field
* @return
*/
private static String getMethods(String field) {
String s = field.substring(0, 1);
String s1 = field.replaceFirst(s, s.toUpperCase());
return "get" + s1;
}
4. 我们可以使用这个方法来查看路径是否真的被加进去了
/**
* 查看字段
*
* @param o
*/
private static void seeFields(Object o) {
Class<?> c = o.getClass();
Field[] fields = c.getDeclaredFields();//获取所有字段
for (Field f : fields) {
f.setAccessible(true);//设置字段可访问
String fieldName = f.getName();//属性名
if (filedName.equals("serialVersionUID"))
continue;
String getFieldMethod = getMethods(fieldName);//getEntity
try {
Method method = c.getMethod(getFieldMethod);
String s = String.valueOf(method.invoke(o));
System.out.println(fieldName + ": " + s);
} catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
log.error(e.getMessage(), e);
}
}
}
测试
1. main() 方法模拟测试
public static void main(String[] args) {
Users users = new Users();
users.setId(1L);
users.setName("YCL");
users.setImg("/uploads/123.jpg");
seeFields(users);
System.out.println("===========");
//这个地址只是模拟地址,项目实际的地址是在yml文件里配置的
resetUrl("http://www.baidu.com", users);
seeFields(users);
}
输出:
id: 1
name: YCL
img: /uploads/123.jpg
imgTemp: null
===========
id: 1
name: YCL
img: /uploads/123.jpg
imgTemp: http://www.baidu.com/uploads/123.jpg
由上可以看出,在刚开始时 imgTemp 是没有值的,而调用了映射方法后值就出来了,前端就可以使用这个地址来显示用户头像,同时约定修改时传过来的是字段是 img,两不误,而且同是 String 类型的 name 却不受影响。
2. 实战
//我把这个方法定义成了服务层的全局方法,
//如此,所有继承这个服务层基类的服务层都可以使用这个方法
@Slf4j
@Component
public abstract class BaseApplication<M extends BaseMapper<T>, T>
extends ServiceImpl<M, T> {
//从配置文件中获取到项目文件存储路径
@Value("${store.domain:}")
public String storeUrl;
/**
* 重新设置字段文件路径
*
* @param o
*/
public void resetEntityUrl(Object o) {
UrlUtil.resetUrl(storeUrl, o);
}
}
现在我们访问一下项目中的购物车,购物车商品是有图片的,所以哪个图片都一样,关键是字段对上就没问题了。
优缺点总结
1. 优点
- 在项目中有多个地方都需要返回文件实际地址,我们可以不用一个一个地给他 set 进去,直接调用这个方法就搞定。
- 这个方法加深了我对反射的理解,我们可以利用这个反射简化出一堆无用的却有必要的开发。
2. 缺点
- 这个方法相对于直接 set 值消耗了很多性能,对于目前电商这种追求极致性能的项目或许很不友好,所以如果确定这个返回对象只有一个地方需要用到文件字段,就直接使用 set,没必要有这么大的开销。但如果是多个地方需要用到的话,它不失为一个好的选择。我们只要调用一下这个方法就省略了一堆set,至少看着也不会那么难受。
- 这个方法只有特定的String类型才能使用。但如用户的相册这种 List 字段存储的图片它就无能为力了。所以这个方法我一直都在改善中,期待有一天它可以兼容所有的类型,带着简化开发的目的,走进众多同行的视野。
后言
一直喜欢更底层的代码,希望一起携手共进,一起为自己的目标努力!!
由衷感谢您的观看,您的收获和支持就是我的最大动力!!再次感谢!!!