1. 程式人生 > >java使用freemarker完成試卷的Word匯出

java使用freemarker完成試卷的Word匯出

第一次近距離接觸freemarker,先讓我們談一談這個技術:
1、什麼是freemarker?
freemarker是一款模板搜尋引擎,簡單講就是用於生成靜態化頁面的工具;
2、有什麼作用?
可以將資料與模板進行結合,統一一次性批量生成靜態化頁面,也就是html頁面,放到硬碟上,訪問的時候,直接訪問生成好的靜態頁面,這樣可以不用訪問資料庫,給資料庫降低併發訪問壓力;也不用訪問應用伺服器,給應用伺服器降低併發壓力;客戶因為直接訪問的是靜態頁面,所以不需要Tomcat解析,瀏覽器可以直接訪問,速度快,客戶體驗好;
應用場景:
對於一些不經常變化的頁面,資料也不經常變化,可以通過freemarker,統一生成靜態化頁面;
當然還有今天的主題,可以生成Word文件;
3、怎麼用?
首先要匯入freemarker的jar包:這裡寫圖片描述


還要準備好模板(這裡要注意模板的格式是ftl或html)
準備好資料
然後使用freemarker中的Configuration類進行頁面的生成;
還有就是freemarker可以與spring進行整合:

<!-- 靜態化類例項化 -->
        <bean id="StaticPageServiceImpl" class="cn.itcast.core.service.StaticPageServiceImpl">
            <property name="freeMarkerConfigurer">
                <bean
class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/ftl/"/> <property name="defaultEncoding" value="utf-8"/> </bean> </property> </bean
>
</beans>
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.Map;
import javax.servlet.ServletContext;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import freemarker.template.Configuration;
import freemarker.template.Template;

/**
 * 配置式靜態化開發
 * @author zj
 *
 */
public class StaticPageServiceImpl implements StaticPageService, ServletContextAware{
    //注入
    private Configuration conf;
    public void setFreeMarkerConfigurer(FreeMarkerConfigurer freeMarkerConfigurer) {
        this.conf = freeMarkerConfigurer.getConfiguration();
    }

    //靜態化程式
    @Override
    public void index(Map<String, Object> map, String id) throws Exception{
        String path = "/html/product/" + id + ".html";
        String url = getAllPath(path);

        //判斷沒有父級資料夾
        File f = new File(url);
        File parentFile = f.getParentFile();
        if(!parentFile.exists()){
            parentFile.mkdirs();
        }

        //讀
        Template template = conf.getTemplate("product.html");
        //輸出到指定流檔案
        Writer out = new OutputStreamWriter(new FileOutputStream(f) , "utf-8");
        //處理
        template.process(map, out);
        out.close();
    }

    //宣告一個上下文
    private ServletContext servletContext;

    //獲取全路徑
    public String getAllPath(String path){
        return servletContext.getRealPath(path);
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }
}

下來就進入正題,完成Word的匯出:
一、模板準備工作:
首先要知道如何製作模板:我是需要匯出試卷,所以暫時沒有涉及到表格的問題(可能是需求不太一樣,本人只是做了簡單的試卷匯出);所以我在準備的時候只是準備了一個簡單的模板:
先用Word文件寫好,然後將其儲存為xml的形式進行模板的編輯:這裡寫圖片描述看到上邊的黑字就是咱們Word文件中的佔位字,也是下邊需要替換的字,說到替換就必須要了解其特有的模板語言:(本人只是瞭解了一些最基本的)這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
這裡寫圖片描述
最後是一個map的遍歷方式
這裡寫圖片描述

<w:body>
<w:p w:rsidR="00A42CA5" w:rsidRDefault="00A42CA5" w:rsidP="003206CE">
<w:pPr>
<w:spacing w:line="360" w:lineRule="auto"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:bookmarkStart w:id="0" w:name="_GoBack"/>
<w:bookmarkEnd w:id="0"/>
</w:p>
<w:p w:rsidR="006B1E33" w:rsidRDefault="003064E2">
<w:pPr>
<w:jc w:val="center"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:b/>
<w:bCs/>
<w:sz w:val="28"/>
<w:szCs w:val="28"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${paperName}</w:t>
</w:r>
</w:p>
<#list titles as title>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00E84B1E">
<w:pPr>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:b/>
<w:bCs/>
<w:sz w:val="24"/>
<w:szCs w:val="24"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${title.title}</w:t>
</w:r>
</w:p>
<#list (title.problemList) as subtitle>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00E84B1E">
<w:pPr>
<w:spacing w:after="0"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${subtitle.problemContent}</w:t>
</w:r>
</w:p>
<#if subtitle.option??>
<#list subtitle.option?keys as key>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00E84B1E">
<w:pPr>
<w:spacing w:after="0"/>
<w:ind w:left="150"/>
</w:pPr>
<w:r w:rsidRPr="00E84B1E">
<w:t>${key}:${subtitle.option["${key}"]}</w:t>
</w:r>
</w:p>
</#list>
</#if>
</#list>
</#list>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00BA3C07">
<w:pPr>
<w:spacing w:after="0"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00BA3C07">
<w:pPr>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:br w:type="page"/>
</w:r>
</w:p>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00BA3C07">
<w:pPr>
<w:jc w:val="center"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:b/>
<w:bCs/>
<w:sz w:val="28"/>
<w:szCs w:val="28"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>答案解析部分</w:t>
</w:r>
</w:p>
<#list answerTitles as answerTitle>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00A15A44">
<w:pPr>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answerTitle.title}</w:t>
</w:r>
</w:p>
<#list (answerTitle.answerList) as answers>
<w:p w:rsidR="006B1E33" w:rsidRPr="00A15A44" w:rsidRDefault="00A15A44">
<w:pPr>
<w:spacing w:after="0"/>
<w:rPr>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answers.answerSort}</w:t>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:color w:val="0000FF"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>【答案】</w:t>
</w:r>
<w:r>
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answers.answer}</w:t>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:br/>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:color w:val="0000FF"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>【考點】</w:t>
</w:r>
<w:r w:rsidR="009D5F5A">
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answers.answerTestCenter}</w:t>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:br/>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:color w:val="0000FF"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>【解析】</w:t>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>【分析】</w:t>
</w:r>
<w:r w:rsidR="000568F2">
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answers.answerAnalysis}</w:t>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:br/>
</w:r>
<w:r w:rsidR="00BA3C07">
<w:rPr>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>【點評】</w:t>
</w:r>
<w:r w:rsidR="000568F2">
<w:rPr>
<w:rFonts w:hint="eastAsia"/>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t>${answers.answerComment}</w:t>
</w:r>
</w:p>
</#list>
</#list>
<w:p w:rsidR="006B1E33" w:rsidRDefault="00BA3C07">
<w:pPr>
<w:spacing w:after="0"/>
<w:rPr>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
</w:pPr>
<w:r>
<w:rPr>
<w:color w:val="000000"/>
<w:lang w:eastAsia="zh-CN"/>
</w:rPr>
<w:t xml:space="preserve"> </w:t>
</w:r>
</w:p>

這裡是我替換的程式碼,裡邊使用了多個巢狀迴圈,暫時看不明白也不要緊,這要根據資料一起看;模板編輯好以後,將模板改為ftl格式放到專案中備用;
二、資料的準備工作,封裝資料(其實都是從資料庫中查詢的,在這也將程式碼拿出來)
程式碼菜鳥,程式碼質量或邏輯上欠考慮的也請看到的大神指出:

public void createDoc(Map<String,Object> dataMap,String fileName) throws BusinessException {
        //建立Configuration物件
        Configuration configuration = new Configuration();
        configuration.setDefaultEncoding("utf-8");
        //dataMap 要填入模本的資料檔案
        //設定模本裝置方法和路徑,
        Template t=null;
        try {
            configuration.setDirectoryForTemplateLoading(new File(exportPath));
            //test.ftl為要裝載的模板
            t = configuration.getTemplate("paperModel.ftl");
            //輸出文件路徑及名稱
            File outFile = new File(localPath.concat("/").concat(fileName).concat(".doc"));
            Writer out = null;
            FileOutputStream fos=null;
            fos = new FileOutputStream(outFile);
            OutputStreamWriter oWriter = new OutputStreamWriter(fos,"UTF-8");
            //這個地方對流的編碼不可或缺,使用main()單獨呼叫時,應該可以,但是如果是web請求匯出時匯出後word文件就會打不開,並且包XML檔案錯誤。主要是編碼格式不正確,無法解析。
            //out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outFile)));
            out = new BufferedWriter(oWriter); 
            t.process(dataMap, out);
            out.close();
            fos.close();
        } catch (Exception e) {
            logger.error("匯出出錯", e);
            e.printStackTrace();
            throw new BusinessException(CommonResultEnum.COMMON_ERROR_637);
        } 
    }
    //下邊是整個資料的封裝過程
public Map<String, Object> getPaperByPaperId(Integer paperId) throws BusinessException{
        //校驗試卷id
        if(paperId == null || paperId <= 0){
            throw new BusinessException(CommonResultEnum.COMMON_ERROR_603, "試卷id");
        }
        //先通過paperId查詢出試卷的名字
        Paper paper = paperMapper.selectByPrimaryKey(paperId);
        //校驗paper物件
        if(paper == null){
            throw new BusinessException(CommonResultEnum.COMMON_ERROR_637);
        }
        //建立封裝資料的dataMap
        Map<String, Object> dataMap = new HashMap<String, Object>();
        //試卷標題
        dataMap.put("paperName", paper.getPaperName());
        //獲取試卷的資訊:試卷的題型+題號+問題的主體
        List<Map<String,Object>> problemTypeList = problemMapper.selectProblemNameByPaperId(paperId);
        //校驗查詢出的資訊是否為null
        if(problemTypeList.isEmpty() || null == problemTypeList){
            throw new BusinessException(CommonResultEnum.COMMON_ERROR_637);
        }
        //建立一個list集合來封裝problemMap資料
        List<Object> titles = new ArrayList<Object>();
        //建立一個list集合來封裝
        List<Object> answerTitles = new ArrayList<Object>();
        int index = 1;
        for (Map<String, Object> map : problemTypeList) {
            //獲取問題的id和試卷的id查詢出本試卷中本類問題資訊
            Integer typeId = StringUtils.objToInt(map.get("id"));
            if(typeId == 0){
                throw new BusinessException(CommonResultEnum.COMMON_ERROR_637);
            }
            List<Map<String,Object>> problemList = problemMapper.selectProblemByTypeIdAndPaperId(paperId,typeId);
            //校驗problemList
            if(problemList.isEmpty() || null == problemList){
                throw new BusinessException(CommonResultEnum.COMMON_ERROR_637);
            }
            //獲取試卷型別,並封裝資料
            Map<String, Object> problemMap = new HashMap<String, Object>();
            problemMap.put("title",StringUtils.toChinese(index+"").concat("、").concat(StringUtils.objToStr(map.get("typeName"))));
            //獲取試卷答案
            Map<String, Object> answerMap = new HashMap<>();
            answerMap.put("title",StringUtils.toChinese(index+"").concat("、").concat(StringUtils.objToStr(map.get("typeName"))));
            index++;
            //建立一個List<Map<String,Object>> 來封裝問題資料
            List<Map<String,Object>> problemList2 = new ArrayList<>();
            //建立一個List<Map<String,Object>> 來封裝問題資料
            List<Map<String,Object>> answerList =  new ArrayList<>();
            for (Map<String, Object> map2 : problemList) {
                //獲取map2中的部分資料將其封裝
                Map<String,Object> problemMap2 = new HashMap<>();
                //問題主體
                problemMap2.put("problemContent",StringUtils.toReplace(StringEscapeUtils.unescapeHtml4(StringUtils.objToStr(map2.get("problemContent")))));
                //問題選項
                String options = StringUtils.toReplace(StringEscapeUtils.unescapeHtml4(StringUtils.objToStr(map2.get("problemAnswer"))));
                TreeMap<String, Object> itemMap = null;
                if(options != null && !"\"\"".equals(options) && !"".equals(options)){
                    itemMap = JSON.parseObject(options, TreeMap.class);
                }
                //問題的選項,將問題的選項轉為map
                problemMap2.put("option",itemMap);
                problemList2.add(problemMap2);
                //封裝答案的資料
                Map<String,Object> answerMap2 = new HashMap<>();
                //題號
                answerMap2.put("answerSort",StringUtils.objToStr(map2.get("sort")));
                //答案
                answerMap2.put("answer",StringUtils.objToStr(map2.get("answer")));
                //考點
                answerMap2.put("answerTestCenter","");
                //解析
                answerMap2.put("answerAnalysis",StringUtils.objToStr(map2.get("analyses")));
                answerMap2.put("answerComment","");
                //將答案資料封裝到list集合中
                answerList.add(answerMap2);
            }
            problemMap.put("problemList",problemList2);
            titles.add(problemMap);
            //建立封裝答案的map
            answerMap.put("answerList",answerList);
            //建立一個list封裝answerMap
            answerTitles.add(answerMap);
        }
        //將問題的資訊封裝到大的資料Map中集合中
        dataMap.put("titles", titles);
        dataMap.put("answerTitles", answerTitles);
        return dataMap;
    }

上邊是整個資料的封裝過程以及使用freemarker中的Configuration類進行封裝:當然裡邊使用的一些方法(將阿拉伯數字轉為大寫,將JSon資料轉為map等),看思路就好;最後完成的結果:
這裡寫圖片描述
本功能的完成比較耗時間的點:
①模板的搭建,需要了解模板語言,並與資料進行邏輯結合;
②資料的封裝,其實思路清晰這都不是事情
③一定要細心,尤其在準備模板的時候,一定要細心;