記一次工作報表項目中開發的動態生成實體工具類,全程無硬編碼

說明:最近項目中開發報表項目需要把一個產品不同日期的數據展示爲一行數據的不同屬性,也就是要行轉列。由於日期值是個要在前端點擊查詢按鈕後才確定的值,因此當時想想到了後臺的實體類使用動態生成,然後再結合反射的知識完成需求。這對業務不是很複雜的項目也是一種不錯的選擇,當時也做出了嘗試,對一些簡單的實體類屬性填充也是可行的。但是由於我們的報表項目設計特別的多的聚合和合並等邏輯,發現使用動態類還是太不方便了,最後只得給實體類增加一個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的。

參考鏈接: java動態創建class-兩種方式(寫入文件和非文件)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章