说明:最近项目中开发报表项目需要把一个产品不同日期的数据展示为一行数据的不同属性,也就是要行转列。由于日期值是个要在前端点击查询按钮后才确定的值,因此当时想想到了后台的实体类使用动态生成,然后再结合反射的知识完成需求。这对业务不是很复杂的项目也是一种不错的选择,当时也做出了尝试,对一些简单的实体类属性填充也是可行的。但是由于我们的报表项目设计特别的多的聚合和合并等逻辑,发现使用动态类还是太不方便了,最后只得给实体类增加一个Map类型的字段以存储不通日期的值。但是本人还是想把开发动态生成实体类的笔记记录到博客上来,以供自己和需要的同行在做此类相关需求时得到有效参考。下面呈上代码:
1. 与数据库相关的dto类ProductStaticsDto
package com.hsf.cloudweb.model.dto;
import java.io.Serializable;
public class ProductStaticsDto implements Serializable {
private String prdCode;
private String prdName;
private String reportDate; //报告期,日期字符串MMMMdd
private float value; //日期对应的值
public ProductStaticsDto(String prdCode, String prdName, String reportDate, float value) {
this.prdCode = prdCode;
this.prdName = prdName;
this.reportDate = reportDate;
this.value = value;
}
public String getPrdCode() {
return prdCode;
}
public void setPrdCode(String prdCode) {
this.prdCode = prdCode;
}
public String getPrdName() {
return prdName;
}
public void setPrdName(String prdName) {
this.prdName = prdName;
}
public String getReportDate() {
return reportDate;
}
public void setReportDate(String reportDate) {
this.reportDate = reportDate;
}
public float getValue() {
return value;
}
public void setValue(float value) {
this.value = value;
}
}
2. 以写入文件的方式创建动态实体类的工具类DynamicGenClassUtil
package com.hsf.cloudweb.utils;
import org.springframework.util.StringUtils;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class DynamicGenClassUtil {
/**java动态创建class的两种方式之一:写入文件
* className.java文件在packageName包下,生成的class文件在对应项目根目录下的target/class目录下
* 重点:1-编译class; 2-加载class文件
* @param subDir 当前项目根目录下子项目目录,针对聚合项目下的子项目目录
* @param importClasses 需要导入的带包名的类集合
* @param fieldNameTypeMap //属性名-类型Map
* @param packageName //包名
* @param className //类名
* @param methodBody //非set,get方法体字符串
* @throws IOException,ClassNotFoundException,InstantiationException,IllegalAccessException
* @return Object instance
*/
public static Object genDynamicClassInstance(String subDir,List<String> importClasses,Map<String,String> fieldNameTypeMap, String packageName, String className,String methodBody) throws IOException,ClassNotFoundException,InstantiationException,IllegalAccessException {
checkArgs(packageName,className);
String workDir = System.getProperty("user.dir")+"\\"+subDir;
String dirPath = workDir+"\\src\\main\\java\\";
System.out.println("dirPath:"+dirPath);
//"."前必须加转义字符,否则无法准换成目录
String packagePath = packageName.replaceAll("\\.","\\\\");
String filename =dirPath+packagePath+"\\"+className+".java";
System.out.println("filename:"+filename);
File file = new File(filename);
if(!file.exists()){ //如果当前目录不存在,则创建目录
// file.mkdir(); 由于目录已存在,无需再创建目录
file.createNewFile();
}
FileWriter fileWriter = new FileWriter(file);
String classString = genClassString(importClasses,fieldNameTypeMap,packageName,className,methodBody);
fileWriter.write(classString);
fileWriter.flush();
fileWriter.close();
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
StandardJavaFileManager manager = compiler.getStandardFileManager(null,null,null);
Iterable<?extends JavaFileObject> javaFileObjects = manager.getJavaFileObjects(file);
String dest = workDir+"\\target\\classes"; //注意目录分隔符之间必须使用\转义字符
//options就是指定编译输入目录,与我们命令行写javac -d C://是一样的
List<String> options = new ArrayList<>();
options.add("-d");
options.add(dest);
JavaCompiler.CompilationTask task = compiler.getTask(null,manager,null,options,null,javaFileObjects);
task.call();
manager.close();
URL url = new URL("file:/"+dest);
URL[] urls = new URL[]{url};
//加载class时要告诉你的classloader去哪个位置加载class文件
ClassLoader classLoader = new URLClassLoader(urls);
Object instance = classLoader.loadClass(packageName+"."+className).newInstance();
return instance;
}
public static String genClassString(List<String> importClasses,Map<String,String> fieldNameTypeMap,String packageName, String className, String methodBody){
StringBuilder builder = new StringBuilder("package\t"+packageName+";\n"); //每一行代码后面注意分号和换行符的使用
builder.append("import java.io.Serializable;\n");
if(importClasses.size()>0){
for(String importClass: importClasses){
builder.append("import ").append(importClass).append(";\n");
}
}
//在多线程中为了线程安全需要将StringBuilder换为StringBuffer
builder.append("public class "+className+"\timplements Serializable\t{\n");
for(Map.Entry<String,String> entry: fieldNameTypeMap.entrySet()){
builder.append("\n\t private "+entry.getValue()+"\t"+entry.getKey()+";\n");
}
for(Map.Entry<String,String> entry: fieldNameTypeMap.entrySet()){
String methodNameSuffix = upperCaseFirstChar(entry.getKey());
builder.append("\n\t public void set"+methodNameSuffix+'('+entry.getValue()+' '+entry.getKey()+"){\n");
builder.append("\n\t this."+entry.getKey()+" = "+entry.getKey()+";\n\t }\n");
builder.append("\n\t public "+entry.getValue()+ " get"+methodNameSuffix+"(){\n");
builder.append("\n\t return this."+entry.getKey()+";\n\t }\n");
}
if(!StringUtils.isEmpty(methodBody)){
builder.append(methodBody); //方法体字符窜必须带完整的{},如果需要加入多个方法体,则可将methodBody的参数类型换为List<String>
}
builder.append("\n}");
return builder.toString();
}
public static String upperCaseFirstChar(String fieldName){
if(fieldName==null || fieldName.length()==0){
return null;
}
char[] chs = fieldName.toCharArray();
if(chs[0]>='a' && chs[0]<='z'){
chs[0] = (char) (chs[0]-32);
}
return new String(chs);
}
public static void checkArgs(String packageName,String className){
if(StringUtils.isEmpty(packageName) || StringUtils.isEmpty(className)){
throw new IllegalArgumentException("parameter packageName or className cannot be empty!");
}
}
/**通过反射填充属性值
*
* @param fieldMap 属性名-值 键值对
* @param instance 反射生成的类实例
*/
public static void fillFields(Map<String,Object> fieldMap,Object instance)throws NoSuchFieldException,NoSuchMethodException,IllegalAccessException, InvocationTargetException {
Method method;
Field field;
Class clazz = instance.getClass();
Class fieldClass;
for(Map.Entry<String,Object> entry: fieldMap.entrySet()){
field = clazz.getDeclaredField(entry.getKey());
fieldClass = field.getType();
method = clazz.getDeclaredMethod("set"+upperCaseFirstChar(entry.getKey()),fieldClass);
method.invoke(instance,entry.getValue());
}
}
}
3. 测试类
package com.hsf.cloudweb;
import com.hsf.cloudweb.model.dto.ProductStaticsDto;
import com.hsf.cloudweb.utils.DynamicGenClassUtil;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DynamicClassTest {
public static void main(String[] args) {
Map<String,String> fieldTypeMap = new HashMap<>();
fieldTypeMap.put("prdName","String");
fieldTypeMap.put("prdCode","String");
fieldTypeMap.put("staticsData","Map<String,Float>");
String subDir = "cloud-web";
List<String> importClasses = new ArrayList<>();
importClasses.add("java.util.List");
importClasses.add("java.util.Map");
importClasses.add("java.util.HashMap");
importClasses.add("com.hsf.cloudweb.model.dto.ProductStaticsDto");
String packageName = "com.hsf.cloudweb.model.vo";
String className = "ProductStaticsVo";
StringBuffer methodBodyBuff = new StringBuffer("\t public "+className+" listToVo(List<ProductStaticsDto> dtoList){\n");
methodBodyBuff.append("\t "+className+" instance = new "+className+"();\n");
methodBodyBuff.append("\t instance.setPrdCode(dtoList.get(0).getPrdCode());\n");
methodBodyBuff.append("\t instance.setPrdName(dtoList.get(0).getPrdName());\n");
methodBodyBuff.append("\t Map<String,Float> staticsMap = new HashMap<>();\n");
methodBodyBuff.append("\t for(ProductStaticsDto dto: dtoList){\n");
methodBodyBuff.append("\t staticsMap.put(dto.getReportDate(),new Float(dto.getValue()));\n");
methodBodyBuff.append("\t }\n");
methodBodyBuff.append("\t instance.setStaticsData(staticsMap);\n");
methodBodyBuff.append("\t return instance;\n");
methodBodyBuff.append("\t }\n");
String methodBody = methodBodyBuff.toString();
try {
Object instance = DynamicGenClassUtil.genDynamicClassInstance(subDir,importClasses,fieldTypeMap,packageName,className,methodBody);
List<ProductStaticsDto> dtoList = new ArrayList<>();
//这里为了方便demo测试,没有通过dao类从数据库取数据了,直接造了点数据
ProductStaticsDto dto1 = new ProductStaticsDto("huaWeiP30","华为P30","201901",30000.3f);
ProductStaticsDto dto2 = new ProductStaticsDto("huaWeiP30","华为P30","201902",40800.8f);
ProductStaticsDto dto3 = new ProductStaticsDto("huaWeiP30","华为P30","201903",52600.5f);
ProductStaticsDto dto4 = new ProductStaticsDto("huaWeiP30","华为P30","201904",58600.2f);
ProductStaticsDto dto5 = new ProductStaticsDto("huaWeiP30","华为P30","201905",64600.6f);
ProductStaticsDto dto6 = new ProductStaticsDto("huaWeiP30","华为P30","201906",74600.5f);
dtoList.add(dto1);
dtoList.add(dto2);
dtoList.add(dto3);
dtoList.add(dto4);
dtoList.add(dto5);
dtoList.add(dto6);
Class clazz = instance.getClass();
Method method = clazz.getDeclaredMethod("listToVo",List.class);
Object obj = method.invoke(instance,dtoList);
System.out.println(obj);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在 测试类的 System.out.println(obj) 一行代码打个断点,然后Debug模式运行,鼠标悬停进入obj,我们看到多个dto列表转换成了一个Vo实例中的数据,如下图所示:
小结:动态生成实体类的方法其实很多,还有一种在内存中生成动态类,以及利用jdk或者cglib动态代理类也可实现,其本质都是在程勋运行期间将动态生成的类文件编译成字节码文件,然后利用类加载器加载字节码文件生成具体类并实例化;对于动态类属性的获取和方法的执行可以借助反射相关的Class, Method和Field三个类中的API完成。关于另外几种方式生成动态类本人有时间也会写几个具体的Demo的。