背景: 最近接了个需求, 要求生成日报, 大概如下图所示:
其中'*'表示变量, 看到要动态生成doc给我难受坏了,为什么会有这种需求?
然后看到里面还要动态生成饼图, oh, no.........没有办法, 硬着头皮上吧.
于是就搜了下java生成docx的方式, 看到的, 比较靠谱的一种通过freemaker生成, 替换其中的动态数据即可.
这种的好处就是提供一个模板, 替换完里面的数据之后格式不会乱, 于是愉快的决定就用这个了, 过程如下
1.让产品给一个最终的日报文档, 然后另存为xml文件, 没错, 就是xml文件, 放着备用
2.项目中加入freemaker依赖及相关配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
<version>2.5.0</version>
</dependency>
spring:
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于0会有缓存不方便进行模板测试
suffix: .ftl #指定Freemarker模板文件的后缀名
enabled: true
expose-spring-macro-helpers: true
3.开始搞利用模板生成docx的代码
3.1 在resources文件夹下新建 templates 文件夹, 将第1步docx转存的xml文件复制到该目录下,并更改文件后缀为 .ftl (文件后缀和配置里保持一致即可)
之后对这个模板文件的代码进行格式化一下(方便找需要替换的地方), 找到需要替换的地方之后,使用${变量}进行替换就行了, 这里都是freemaker的基础操作, 就是docx的结构有点麻烦, 不过xml文件仔细看看很容易就发现规律了. 大部分都是这种, 样式后面跟着文字, 如果遇到表格什么的可能稍微不一样.
3.2 写一下服务类
public interface TemplateService {
/**
* 创建模板文件
* @param outputPath 输出路径
* @param templateFile 模板文件名
* @param param 参数
* @return 是否创建成功
*/
boolean createTemplateFile(String outputPath,String fileName, String templateFile, Map<String, Object> param);
}
@Slf4j
@Service
public class TemplateServiceImpl implements TemplateService {
@Autowired
private FreeMarkerConfigurer freeMarkerConfigurer;
@Override
public boolean createTemplateFile(String outputPath, String fileName, String templateFile, Map<String, Object> param) {
Configuration cfg = freeMarkerConfigurer.getConfiguration();
Writer out = null;
File file = new File(outputPath);
if (!file.exists()) {
file.mkdirs();
}
file = new File(outputPath + fileName);
try {
cfg.setDefaultEncoding(StandardCharsets.UTF_8.name());
Template template = cfg.getTemplate(templateFile);
out = new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8);
//将数据放在map里,然后就会自动渲染模版
template.process(param, out);
return true;
} catch (Exception e) {
log.error("TemplateServiceImpl#createTemplateFile生成文件失败............." + e);
} finally {
if (out != null) {
try {
out.flush();
out.close();
} catch (IOException e) {
log.error("TemplateServiceImpl#createTemplateFile流关闭失败..........." + e);
}
}
}
return false;
}
}
3.3 写个测试的controller调用试一试
@GetMapping("/createdoc")
public String createdoc() throws Exception {
//1.文档里要动态替换的数据
Map<String, Object> param = new HashMap<>();
String company = "测试工作室";
param.put("company", company);//公司
param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//日报时间
param.put("order", 4500);//订单数
param.put("amount", 304614.23);//金额
//2.根据模板生成docx文档
//用来输出生成的文档目录
String outputPath = "D:/data/hotspot/docx/";
String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
String fileName = company + "电商日报【" + dayStr + "】.docx";
templateService.createTemplateFile(outputPath, fileName, "模板.ftl", param);
//3.到这里,文档就已经生成了, 去目录下看生成的文档是否符合要求就行了
//我这里后面又把文件上传到oss上面去了, 然后将数据更新到数据库了
String fileAbsolutePath = outputPath + fileName;
//3.1 todo 上传到oss, 并将url更新到数据库
//3.2 删除本地机器的文件
File file = new File(fileAbsolutePath);
boolean delete = file.delete();
return "success";
}
4.文字是没问题了, 那么图片怎么解决呢? 还要动态生成, 还要写到docx里面去.
于是开始发功, 百度大法....... 找到了jfreechart这个框架, 话不多说, 开始干活.
4.1 依赖
<!--用于jfreechart生成图片 -->
<dependency>
<groupId>org.jfree</groupId>
<artifactId>jfreechart</artifactId>
<version>1.5.0</version>
</dependency>
<!--非必要 -这个里面少一些内置包 版本比较高,需要单独引入-->
<dependency>
<groupId>com.guicedee.services</groupId>
<artifactId>jfreechart</artifactId>
<version>1.1.1.5-jre15</version>
</dependency>
4.2 jfree工具类
import org.apache.commons.lang3.StringUtils;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartUtils;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.block.BlockBorder;
import org.jfree.chart.labels.StandardCategoryItemLabelGenerator;
import org.jfree.chart.labels.StandardPieSectionLabelGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PiePlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.BarRenderer;
import org.jfree.chart.renderer.category.LineAndShapeRenderer;
import org.jfree.chart.ui.HorizontalAlignment;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.category.DefaultCategoryDataset;
import org.jfree.data.general.DefaultPieDataset;
import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.math.RoundingMode;
import java.rmi.server.ExportException;
import java.text.NumberFormat;
import java.util.Iterator;
import java.util.Map;
/**
* 创建图集图表
*/
public class JfreeUtil {
private static final Font FONT = new Font("宋体", Font.PLAIN, 12);
private static StandardChartTheme defaultTheme(){
//创建主题样式
StandardChartTheme theme=new StandardChartTheme("CN");
//设置标题字体
theme.setExtraLargeFont(new Font("隶书",Font.BOLD,20));
//设置图例的字体
theme.setRegularFont(new Font("宋书",Font.PLAIN,15));
//设置轴向的字体
theme.setLargeFont(new Font("宋书",Font.PLAIN,15));
return theme;
}
public static String createPieChart(String title, Map<String, Double> datas, int width, int height) throws IOException {
//根据jfree生成一个本地饼状图
DefaultPieDataset pds = new DefaultPieDataset();
datas.forEach(pds::setValue);
//应用主题样式
ChartFactory.setChartTheme(defaultTheme());
//图标标题、数据集合、是否显示图例标识、是否显示tooltips、是否支持超链接
JFreeChart chart = ChartFactory.createPieChart(title, pds, true, false, false);
chart.getTitle().setFont(FONT);
chart.getLegend().setItemFont(FONT);
//设置抗锯齿
chart.setTextAntiAlias(false);
PiePlot plot = (PiePlot) chart.getPlot();
plot.setStartAngle(90);
plot.setNoDataMessage("暂无数据");
plot.setNoDataMessagePaint(Color.blue); // 设置无数据时的信息显示颜色
//忽略无值的分类
plot.setIgnoreNullValues(true);
plot.setIgnoreZeroValues(true);
plot.setBackgroundAlpha(0f);
//设置标签阴影颜色
plot.setShadowPaint(new Color(255, 255, 255));
//设置标签是否显示在饼块内部,默认时在外部
// plot.setSimpleLabels(true);
chart.getLegend().setHorizontalAlignment(HorizontalAlignment.CENTER);//设置水平对齐 左对齐;
chart.getLegend().setMargin(0, 0, 0, 0);//参数是:上,左,下,右. 设置饼图的位置
chart.getLegend().setPadding(0, 0, 20, 0);// 设置饼图下文字的位置
chart.getLegend().setFrame(new BlockBorder(0, 0, 0, 0));// 设置饼图下文字边框的位置
// 图片中显示百分比:自定义方式,{0} 表示选项, {1} 表示数值, {2} 表示所占比例,小数点后两位
NumberFormat percentInstance = NumberFormat.getPercentInstance();
percentInstance.setRoundingMode(RoundingMode.HALF_UP);
percentInstance.setMaximumFractionDigits(2);
plot.setLabelGenerator(new StandardPieSectionLabelGenerator("{0},{2}", NumberFormat.getNumberInstance(), percentInstance));
return createFile(chart, width, height);
}
public static String createBarChart(String title, Map<String, Object> datas, String type, String units, PlotOrientation orientation, int width, int height) throws IOException {
//数据集
DefaultCategoryDataset ds = new DefaultCategoryDataset();
Iterator<Map.Entry<String, Object>> iterator = datas.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
ds.setValue(Double.valueOf(String.valueOf(entry.getValue())), "工单数量", StringUtils.defaultString(entry.getKey(), ""));
}
//创建柱状图,柱状图分水平显示和垂直显示两种
JFreeChart chart = ChartFactory.createBarChart(title, type, units, ds, orientation, false, false, false);
//设置文本抗锯齿,防止乱码
chart.setTextAntiAlias(false);
//得到绘图区
CategoryPlot plot = (CategoryPlot) chart.getPlot();
plot.setNoDataMessage("no data");
//设置柱的透明度
plot.setForegroundAlpha(1.0f);
plot.setOutlineVisible(false);
//获取X轴的对象
CategoryAxis categoryAxis = plot.getDomainAxis();
//坐标轴标尺值是否显示
categoryAxis.setTickLabelsVisible(true);
//坐标轴标尺是否显示
categoryAxis.setTickMarksVisible(false);
categoryAxis.setTickLabelFont(FONT);
categoryAxis.setTickLabelPaint(Color.BLACK);
categoryAxis.setLabelFont(FONT);// X轴标题
//categoryAxis.setCategoryLabelPositionOffset(2);//图表横轴与标签的距离(10像素)
//获取Y轴对象
ValueAxis valueAxis = plot.getRangeAxis();
valueAxis.setTickLabelsVisible(true);
valueAxis.setTickMarksVisible(false);
valueAxis.setUpperMargin(0.15);//设置最高的一个柱与图片顶端的距离(最高柱的20%)
valueAxis.setLowerMargin(0d);
valueAxis.setTickLabelFont(FONT);//Y轴数值
valueAxis.setLabelPaint(Color.BLACK);//字体颜色
valueAxis.setLabelFont(FONT);//Y轴标题
NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
//设置Y轴刻度为整数
numberAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
//设置网格横线颜色
plot.setRangeGridlinePaint(Color.gray);
plot.setRangeGridlinesVisible(true);
//图片背景色
plot.setBackgroundPaint(Color.white);
plot.setOutlineVisible(false);
// 设置原点xy轴相交,柱子从横轴开始,否则会有间隙
plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
//设置网格横线大小
plot.setDomainGridlineStroke(new BasicStroke(0.5F));
plot.setRangeGridlineStroke(new BasicStroke(0.5F));
//设置柱状图柱子相关
CategoryPlot categoryPlot = chart.getCategoryPlot();
BarRenderer rendererBar = (BarRenderer) categoryPlot.getRenderer();
//组内柱子间隔为组宽的10%,调整柱子宽度
rendererBar.setItemMargin(0.6);
rendererBar.setMaximumBarWidth(0.07);
rendererBar.setDrawBarOutline(true);
rendererBar.setSeriesOutlinePaint(0, Color.decode("#4F97D5"));
//设置柱的颜色#5B9BE6
rendererBar.setSeriesPaint(0, Color.decode("#4F97D5"));
//设置柱子上显示值
rendererBar.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
rendererBar.setDefaultItemLabelFont(FONT);
rendererBar.setDefaultItemLabelsVisible(true);
rendererBar.setDefaultItemLabelPaint(Color.BLACK);
return createFile(chart, width, height);
}
public static String createLineChart(String title, Map<String, Object> datas, String type, String unit, PlotOrientation orientation, int width, int hight) throws IOException {
DefaultCategoryDataset ds = new DefaultCategoryDataset();
Iterator iterator = datas.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry entry = (Map.Entry) iterator.next();
ds.setValue(Double.valueOf(String.valueOf(entry.getValue())), "工单数量", entry.getKey().toString());
}
//创建折线图,折线图分水平显示和垂直显示两种
JFreeChart chart = ChartFactory.createLineChart(title, type, unit, ds, orientation, false, true, true);
//设置文本抗锯齿,防止乱码
chart.setTextAntiAlias(false);
//chart.setBorderVisible(true);
//得到绘图区
CategoryPlot plot = (CategoryPlot) chart.getPlot();
//设置横轴标签项字体
CategoryAxis categoryAxis = plot.getDomainAxis();
categoryAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_45);
categoryAxis.setTickMarksVisible(false);
categoryAxis.setTickLabelsVisible(true);
categoryAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
categoryAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));
ValueAxis valueAxis = plot.getRangeAxis();
valueAxis.setTickMarksVisible(false);
valueAxis.setTickLabelsVisible(true);
valueAxis.setTickLabelFont(new Font("宋体", Font.PLAIN, 12));
valueAxis.setLabelFont(new Font("宋体", Font.PLAIN, 12));
NumberAxis numberAxis = (NumberAxis) plot.getRangeAxis();
//设置Y轴刻度跨度
numberAxis.setUpperMargin(0.15);
numberAxis.setLowerMargin(0);
numberAxis.setAutoRangeMinimumSize(5);
// 设置背景透明度
plot.setBackgroundAlpha(0.1f);
plot.setForegroundAlpha(1.0f);
// 设置网格横线颜色
plot.setRangeGridlinePaint(Color.gray);
// 设置网格横线大小
plot.setDomainGridlineStroke(new BasicStroke(0.5F));
plot.setRangeGridlineStroke(new BasicStroke(0.5F));
plot.setBackgroundPaint(Color.white);
plot.setOutlineVisible(false);
// 设置原点xy轴相交,y轴为0时,点在横坐标上,否则不在横坐标上
plot.setAxisOffset(new RectangleInsets(0d, 0d, 0d, 0d));
// 生成折线图上的数字
//绘图区域(红色矩形框的部分)
LineAndShapeRenderer renderer = (LineAndShapeRenderer) plot.getRenderer();
renderer.setDefaultItemLabelGenerator(new StandardCategoryItemLabelGenerator());
//设置图表上的数字可见
renderer.setDefaultItemLabelsVisible(true);
//设置图表上的数字字体
renderer.setDefaultItemLabelFont(new Font("宋体", Font.PLAIN, 12));
// 设置线条是否被显示填充颜色
renderer.setUseFillPaint(true);
renderer.setSeriesStroke(0, new BasicStroke(4.0f));
renderer.setSeriesPaint(0, Color.decode("#4472C4"));
return createFile(chart, width, hight);
}
public static String createFile(JFreeChart chart, int width, int hight) throws IOException {
File templateFile = File.createTempFile("jfreetemp", ".png");
String filePath = templateFile.getParent() + File.separator + templateFile.getName();
try {
if (templateFile.exists()) {
templateFile.delete();
}
ChartUtils.saveChartAsPNG(templateFile, chart, width, hight);
} catch (IOException e) {
throw new ExportException("创建图表文件失败!");
}
return filePath;
}
}
4.2 对图片进行base64编码的工具类, 因为我们的模板是由docx另存为xml文件, 然后又改的.ftl后缀,
但其本质仍然是xml文件, 那么我们就相当于要往xml文件中写图片,就需要对图片进行编码了.
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.util.Base64;
public class EncodeUtil {
public static String readImage(String str_FileName) {
BufferedInputStream bis = null;
byte[] bytes = null;
try {
try {
bis = new BufferedInputStream(new FileInputStream(str_FileName));
bytes = new byte[bis.available()];
bis.read(bytes);
} finally {
bis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return encryptBASE64(bytes);
}
public static String readImage(FileInputStream in) {
BufferedInputStream bis = null;
byte[] bytes = null;
try {
try {
bis = new BufferedInputStream(in);
bytes = new byte[bis.available()];
bis.read(bytes);
} finally {
bis.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return encryptBASE64(bytes);
}
public static String encryptBASE64(byte[] key) {
Base64.Encoder encoder= Base64.getMimeEncoder();
return encoder.encodeToString(key);
}
4.3 重新往测试的controller写下代码
@GetMapping("/createdoc")
public String createdoc() throws Exception {
//1.文档里要动态替换的数据
Map<String, Object> param = new HashMap<>();
String company = "测试工作室";
param.put("company", company);//公司
param.put("time", "2023-12-10 11:00 - 2023-12-11 11:00");//日报时间
param.put("order", 4500);//订单数
param.put("amount", 304614.23);//金额
//1.1 图片数据
Map<String, Double> map = new LinkedHashMap<>();
map.put("美食",53.00);
map.put("箱包",23.00);
map.put("运动",13.11);
map.put("衣服",73.25);
map.put("其他",40.36);
map.put("宠物",3.21);
//使用Jfreechart创建饼图
String pictureUrl = JfreeUtil.createPieChart("", doubleMap, 600, 500);
//对图片进行编码,转换为string类型
FileInputStream inputStream = new FileInputStream(pictureUrl);
String image = EncodeUtil.readImage(inputStream);
param.put("image", image);//图片.....将模板里的图片(很长的一串)替换为 ${image}即可
//2.根据模板生成docx文档
//用来输出生成的文档目录
String outputPath = "D:/data/hotspot/docx/";
String dayStr = DateTimeUtil.getDayStr(convertToDateTime(new Date()));
String fileName = company + "电商日报【" + dayStr + "】.docx";
templateService.createTemplateFile(outputPath, fileName, "模板.ftl", param);
//3.到这里,文档就已经生成了, 去目录下看生成的文档是否符合要求就行了
//我这里后面又把文件上传到oss上面去了, 然后将数据更新到数据库了
String fileAbsolutePath = outputPath + fileName;
//3.1 todo 上传到oss, 并将url更新到数据库
//3.2 删除本地机器的文件
File file = new File(fileAbsolutePath);
boolean delete = file.delete();
return "success";
}
一般图片是放在如下标签里的.
到此结束, 生成的文档和需求里的差不多, 就是这个饼图有点丑.