java实际项目反射、自定义注解的运用实现itext生成PDF的详细应用教程

时间:2024-03-02 20:52:10

开篇引语

小伙伴在学习java是否有这样的困混不知道反射是干嘛的不知道注解有什么用。导致很多人看 java基础的时候迷迷糊糊,那是你还没有在实际项目中遇到,不知道该如何使用它们。接下来我会为你们详细讲解实际项目中是如何运用反射和自定义注解的,不管你现在对反射和注解了解多少,这篇博客站在实际应用角度从业务介绍到代码实现每一步都给出了详细贴图和代码竭力为大家讲解,无论你是否感兴趣你都应该先点赞收藏起来方便以后用到了过来查看。

一、业务场景描述

将14天的气象预报数据以PDF的形式展示,现在已有14天预报的数据,如下。

 要将上面的14条数据放进下面的这个模板中,最终以PDF的形式生成。效果图如下

 二、使用工具准备好模板,最后保存生成PDF

这里需要使用软件工具Adobe Acrobat DC,需要工具的小伙伴可以评论区留言发送:资料。

具体如何使用工具和生成PDF可以看:java使用itex生成PDF-CSDN博客

  

三、代码实现

 实体类展示,这里建议可以大概看一下,先不用看为什么这样设计实体类,以及自定义注解的定义,后面代码具体地方引用了下面的实体类再来看就会清晰很多。

package com.hw.ioz.web.dto;

import com.zye.ioz.common.annotations.ReportFiled;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * @author:Ttc
 * @date 2024/1/22  14:36
 * @description: 导出大屏预报数据DTO
 */
@Data
public class ForecastDataDTO {
    /**
     * 日期
     */
    @ApiModelProperty(value = "日期", notes = "")
    @ReportFiled(value = "date")
    private String formattedDate;
    /**
     * 天气
     */
    @ApiModelProperty(value = "天气", notes = "")
    @ReportFiled(value = "weather")
    private String weaDay;
    /**
     * 大屏显示风力等级
     */
    @ApiModelProperty(value = "大屏显示风力等级", notes = "")
    @ReportFiled(value = "wind")
    private String largeScreenWind;
    /**
     * 最高温度
     */
    @ApiModelProperty(value = "最高温度", notes = "")
    @ReportFiled(value = "tem_max")
    private String tem1;

    /**
     * 最低温度
     */
    @ApiModelProperty(value = "最低温度", notes = "")
    @ReportFiled(value = "tem_min")
    private String tem2;
}
package com.hw.ioz.web.dto;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @author:Ttc
 * @date 2024/1/22  15:40
 */
@Data
@AllArgsConstructor
public class PdfFiledDto {
    /**
     * 文本域内容
     */
    private String val;

    /**
     * 字体大小
     */
    private float fontsize;

    /**
     * 自定义字体
     */
    private String fontPath;
}
package com.hw.ioz.web.dto;

import com.zye.ioz.common.annotations.ReportFiled;
import com.zye.ioz.common.annotations.ReportList;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

import static com.zye.ioz.common.constants.PdfFontConstants.FONT_MS_YH;

/**
 * @author:Ttc
 * @date 2024/1/18  17:51
 * @description: 大屏天气14天预报导出pdf日期显示dto
 */

@Data
public class ReportDateDTO {

    /**
     * 年
     */
    @ApiModelProperty(value = "年", notes = "")
    @ReportFiled(value = "year", description = "date", font = FONT_MS_YH, fontsize = 13.5f)
    private String year;

    /**
     * 月
     */
    @ApiModelProperty(value = "月", notes = "")
    @ReportFiled(value = "month", description = "date", font = FONT_MS_YH, fontsize = 13.5f)
    private String month;

    /**
     * 日
     */
    @ApiModelProperty(value = "日", notes = "")
    @ReportFiled(value = "day", description = "date", font = FONT_MS_YH, fontsize = 13.5f)
    private String day;

    @ReportList("report_")
    private List<ForecastDataDTO> forecastDataDTOList;
}
package com.zye.ioz.common.annotations;

import java.lang.annotation.*;

/**
 * @author:Ttc
 * @date 2024/1/23  14:48
 * @description: pdf
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ReportList {
    /**
     * 报表集合名称
     * 作为其中元素的前缀
     * @return
     */
    String value() default "";
}
package com.zye.ioz.common.annotations;

import java.lang.annotation.*;

/**
 * @author:Ttc
 * @date 2024/1/18  17:54
 * @description: 报告字段
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ReportFiled {
    String value() default "";

    float fontsize() default 12.0f;

    /**
     * 字体路径
     * 如果为空,使用默认字体
     * @return
     */
    String font() default "";
    String prefix() default "";
    String description() default "";
}

下面controller层代码调用的方法中传递的参数templatePath为pdf模板的存放路径地址
我是从yml文件中读取的,部分yml展示:

template:
  weather: D:\lightning2022\template
  font: D:\lightning2022\font\
@Api(tags = "PDF下载")
@RestController
@RequestMapping("/api/wea")
@Slf4j
public class BnWeaInterfaceHistoryController {

@Value("${template.weather}")
private String templatePath;

@GetMapping("/pdfExport")
    @ApiOperation(value = "大屏14天预报数据PDF下载")
    public void parkingPdfExport(HttpServletResponse response) {
        bnWeaInterfaceHistoryService.exportReport(response,templatePath);
    }
}

下面这段代码是将处理完成后的地址返回,拿到地址以后,使用字符输入流读取刚刚返回的对应PDF地址的文件,然后再用字节输出流/二进制输出流返回给前端进行下载处理。到此下载功能完成!后面是核心方法实现的详细介绍。

@Service
@Slf4j
public class BnWeaInterfaceHistoryServiceImpl extends ServiceImpl<BnWeaInterfaceHistoryMapper, BnWeaInterfaceHistory> implements BnWeaInterfaceHistoryService {
    @Value("${template.font}")
    String fontPath;
    @Override
    public void exportReport(HttpServletResponse response, String templatePath) {
        List<ForecastDataDTO> list = bnWeaInterfaceHistoryMapper.exportLargeScreenForecastData("舟山");
        LocalDateTime now = LocalDateTime.now();
        Integer year = now.getYear();
        Integer month = now.getMonth().getValue();
        Integer day = now.getDayOfMonth();
        ReportDateDTO reportDateDTO = new ReportDateDTO();
        reportDateDTO.setDay(day.toString());
        reportDateDTO.setMonth(month.toString());
        reportDateDTO.setYear(year.toString());
        reportDateDTO.setForecastDataDTOList(list);
        String tmpPath = generateByTemplate(templatePath, reportDateDTO);
        if (org.springframework.util.StringUtils.isEmpty(tmpPath)) {
            log.error("找不到PDF路径!");
            return;
        }
        String path = tmpPath;
        File file = new File(path);
        log.info("PDF文件:{}", path);
        response.reset();
        response.setContentType("application/octet-stream");
        response.setCharacterEncoding("utf-8");
        response.setContentLength((int) file.length());
        response.setHeader("Content-Disposition", "attachment;filename=" + "weather"); // 导出名称

        try {
            BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
            byte[] buff = new byte[1024];
            OutputStream os = response.getOutputStream(); // 输出流
            int i = 0;
            while ((i = bis.read(buff)) != -1) { // 读取文件
                os.write(buff, 0, i);
                os.flush();
            }
            log.info("发送完成");
        } catch (IOException e) {
            log.error("导出失败", e);
        }
    }
}

这里主要完成制作好的PDF模板中文本域所需要的值,这里的值还要经过一系列处理收集到map集合中。而这段主要是在收集完map集合的数据以后,再循环取出map集合中的值,将value中存放的PdfFiledDto对象取出来,把该对象中存放的文本域的值,大小,字体设置到PDF模板中。所有数据设置完成以后方法返回生成的PDF文件的保存地址,供下载使用。

/**
     * 由模板生成PDF,返回模板文件路径
     *
     * @return
     */
    private String generateByTemplate(String templatePath, ReportDateDTO reportDateDTO) {

        String tmpName = "ZSWeather14Days.pdf";
        String tmpPath = templatePath + "/" + tmpName;
        String newTmpFileName = PdfUtils.getNewFileName(tmpName);
        SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd");
        String todayStr = df.format(new Date());
        // 生成的新文件路径(方法返回的模板路径)
        String pdfPath = templatePath + "/" + todayStr + "/" + newTmpFileName;
        String filePath = templatePath + "/" + todayStr;
        File path = new File(filePath);
        if (!path.exists()) {
            path.mkdirs();
        }
        log.info("pdf文件路径:{}", filePath);
        try {
            //设置中文字体
            BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
            PdfReader reader = new PdfReader(tmpPath);// 读取pdf模板
            ByteArrayOutputStream bos = new ByteArrayOutputStream();     //获取字节数组输出流
            PdfStamper stamper = new PdfStamper(reader, bos);  //操作pdf文件
            AcroFields form = stamper.getAcroFields(); //获取pdf模板中的属性
            form.addSubstitutionFont(bf);      //为属性设置字体
            Map<String, PdfFiledDto> data = getParamMap(reportDateDTO, null, 0);
            for (Map.Entry<String, PdfFiledDto> entry : data.entrySet()) {
                if (entry.getValue().getFontsize() > 0) {
                    form.setFieldProperty(entry.getKey(), "textsize", entry.getValue().getFontsize(), null);
                }
                BaseFont baseFont;
                if (!org.springframework.util.StringUtils.isEmpty(entry.getValue().getFontPath())) {
                    baseFont = BaseFont.createFont(fontPath + entry.getValue().getFontPath(), BaseFont.IDENTITY_H, BaseFont.NOT_EMBEDDED);
                } else {
                    baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
                }
                form.setFieldProperty(entry.getKey(), "textfont", baseFont, null);
//                    form.setFieldProperty(entry.getKey(), "textfont", BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED), null);
                form.setField(entry.getKey(), entry.getValue().getVal());

            }
            stamper.setFormFlattening(true);// 如果为false,生成的PDF文件可以编辑,如果为true,生成的PDF文件不可以编辑
            stamper.close();
            reader.close();
            //生成pdf路径
            FileOutputStream fos = new FileOutputStream(pdfPath);
            fos.write(bos.toByteArray());
            fos.flush();
            if (fos != null) {
                fos.close();
            }
            if (bos != null) {
                bos.close();
            }
            log.info("pdf模板生成了");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return pdfPath;
    }

 这里将查询的数据进行反射处理,经过反射得到数据对象的字段后,再获取每个字段上的自定义注解。

public Map<String, PdfFiledDto> getParamMap(Object param, String prefix, int index) {
        Map<String, PdfFiledDto> map = new HashMap<>(16);
        if (param == null) {
            return map;
        }
        Field[] fields = param.getClass().getDeclaredFields();
        for (Field field : fields) {
            Object formVal = ReflectUtil.getFieldValue(param, field.getName());
            ReportFiled reportFiled = field.getAnnotation(ReportFiled.class);
            if (reportFiled != null) {
                fillParamMapByReportFiled(map, formVal, reportFiled, prefix, index);
            }
            ReportList reportList = field.getAnnotation(ReportList.class);
            if (reportList != null) {
                String formKeyPrefix = reportList.value();
                if (formVal instanceof List) {
                    List<Object> formObjList = (List<Object>) formVal;
                    for (int i = 0; i < formObjList.size(); i++) {
                        Object subObj = formObjList.get(i);
                        Map<String, PdfFiledDto> subMap = getParamMap(subObj, formKeyPrefix, i + 1);
                        map.putAll(subMap);
                    }
                }
            }
        }
        return map;
    }

private static void fillParamMapByReportFiled(Map<String, PdfFiledDto> map,
                                                  Object formVal, ReportFiled reportFiled, String prefix, int index) {
        String formKey = reportFiled.value();
        if (StrUtil.isNotBlank(prefix)) {
            // 获取实际key
            formKey = handleFormKey(prefix, index, formKey);
        }
        if (formVal == null) {
            map.put(formKey, getPdfFiledDto("", -1.0f));
            return;
        }
        fillParamMapByObject(map, reportFiled, formKey, formVal, index);
    }

两个自定义注解:

package com.zye.ioz.common.annotations;

import java.lang.annotation.*;

/**
 * @author:Ttc
 * @date 2024/1/18  17:54
 * @description: 报告字段
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ReportFiled {
    String value() default "";

    float fontsize() default 12.0f;

    /**
     * 字体路径
     * 如果为空,使用默认字体
     * @return
     */
    String font() default "";
    String prefix() default "";
    String description() default "";
}

package com.zye.ioz.common.annotations;

import java.lang.annotation.*;

/**
 * @author:Ttc
 * @date 2024/1/23  14:48
 * @description: pdf
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface ReportList {
    /**
     * 报表集合名称
     * 作为其中元素的前缀
     * @return
     */
    String value() default "";
}

 具体的实现方法代码如下,这里的代码完成了PDF中需要数据的查询,以及数据设置到对应的map集合中使得,map集合数据的key对应PDF模板设置的文本域的名称,value则是我们提前查好的数据的值。 

/**
     * 解析注解中的内容,构造 PdfFiledDto
     * 并添加进map
     *
     * @param map
     * @param reportFiled
     * @param formKey
     * @param item
     */
    private static void fillParamMapByObject(Map<String, PdfFiledDto> map, ReportFiled reportFiled,
                                             String formKey, Object item, int index) {
//        formKey = formKey + StrPool.UNDERLINE + index;
        if (item == null || StrUtil.isEmpty(item.toString())) {
            // 没有数据时,显示 "/"
            map.put(formKey, getPdfFiledDto("/", reportFiled.fontsize()));
        } else if ("date".equals(reportFiled.description())) {
            map.put(formKey, getPdfFiledDto(reportFiled.prefix() + item, reportFiled.fontsize(), reportFiled.font()));
        } else {
            map.put(formKey, getPdfFiledDto(reportFiled.prefix() + item, reportFiled.fontsize(), reportFiled.font()));
        }
    }

private static String handleFormKey(String prefix, int index, String formKey) {
        prefix = prefix.endsWith(StrPool.UNDERLINE) ? prefix : prefix + StrPool.UNDERLINE;
        return StrUtil.isBlank(formKey) ?
                prefix + index
                : prefix + formKey + StrPool.UNDERLINE + index;
    }

private static PdfFiledDto getPdfFiledDto(String val, float fontsize) {
        return new PdfFiledDto(val, fontsize, null);
    }

    private static PdfFiledDto getPdfFiledDto(String val, float fontsize, String fontPath) {
        return new PdfFiledDto(val, fontsize, fontPath);
    }

 来看看运行后的下载结果吧