Java第三方库经验汇总

2021-12-01 fishedee 后端

0 概述

Java第三方库经验汇总,Java库的总体质量还是相当不错的。

1 jsoup

代码在这里

官网在这里

jsoup是一个以jQuery的语法,对于html文档获取对应节点的工具,是后端爬虫的基础套路了,这个没啥好说的。它的优点在于,成熟,bug少,对jQuery语法支持比较完备。这里实名举报js的cheerio库,同样的查询语法,在cherrio是跑不起来的,在jsoup轻松查找出来,js的库的总体质量真的比较随意。

package spring_test;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.TextNode;
import org.jsoup.select.Elements;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static org.junit.jupiter.api.Assertions.*;

public class JsoupTest {
    @Test
    public void testBasic(){
        String xml = "<!doc html>" +
                "<html>" +
                "<head></head>" +
                "<body>" +
                "<h1>你好</h1>" +
                "</body>" +
                "</html>";
        Document document = Jsoup.parse(xml);
        String target = document.select("h1").text();
        assertEquals(target,"你好");
    }

    @Test
    public void testText(){
        String xml = "<!doc html>" +
                "<html>" +
                "<head></head>" +
                "<body>" +
                "<h1>用户:<span>mk</span>你好</h1>" +
                "</body>" +
                "</html>";
        Document document = Jsoup.parse(xml);
        //text方法会取自身,和子级所有的textNode
        String target = document.select("h1").text();
        assertEquals(target,"用户:mk你好");

        //textNodes方法只会取自身的textNode
        List<TextNode> textNodes = document.select("h1").textNodes();
        List<String> texts = textNodes.stream().map(single->{
            return single.getWholeText();
        }).collect(Collectors.toList());

        assertIterableEquals(texts, Arrays.asList("用户:","你好"));
    }

    @Test
    public void testSelect(){
        String xml = "<!doc html>" +
                "<html>" +
                "<head></head>" +
                "<body>" +
                "<h1>" +
                "<div class=\"u\">bb</div>"+
                "</h1>" +
                "<h1>" +
                "<div class=\"u\">cc</div>"+
                "</h1>" +
                "</body>" +
                "</html>";
        Document document = Jsoup.parse(xml);

        //select可以匹配多个
        Elements elements = document.select("h1 .u");
        List<String> nameList = elements.stream().map(single->{
            return single.text();
        }).collect(Collectors.toList());
        assertIterableEquals(nameList,Arrays.asList("bb","cc"));

        //匹配不到的时候,也是返回有对象的Elements,只是size为0而已,不会返回null
        Elements elements2 = document.select("h1 .uk");
        List<String> nameList2 = elements2.stream().map(single->{
            return single.text();
        }).collect(Collectors.toList());
        assertIterableEquals(nameList2,Arrays.asList());
    }

    @Test
    public void testSelectEq(){
        String xml = "<!doc html>" +
                "<html>" +
                "<head></head>" +
                "<body>" +
                "<h1>" +
                "<div class=\"u\">b1</div>"+
                "<div class=\"u\">b2</div>"+
                "<div class=\"u\">b3</div>"+
                "</h1>" +
                "<h1>" +
                "<div class=\"u\">c1</div>"+
                "<div class=\"u\">c2</div>"+
                "</h1>" +
                "<h1>" +
                "<div class=\"u\">d1</div>"+
                "<div>"+
                "<div class=\"u\">d2</div>"+
                "</div>"+
                "</h1>" +
                "</body>" +
                "</html>";
        Document document = Jsoup.parse(xml);

        //eq代表.u同级里面的第几个,注意下标从0开始
        Elements elements = document.select("h1 .u:eq(1)");
        List<String> nameList = elements.stream().map(single->{
            return single.text();
        }).collect(Collectors.toList());
        assertIterableEquals(nameList,Arrays.asList("b2","c2"));

        //匹配不到的时候,也会只返回1个
        Elements elements2 = document.select("h1 .u:eq(2)");
        List<String> nameList2 = elements2.stream().map(single->{
            return single.text();
        }).collect(Collectors.toList());
        assertIterableEquals(nameList2,Arrays.asList("b3"));
    }

    @Test
    public void testSelectInner(){
        String xml = "<!doc html>" +
                "<html>" +
                "<head></head>" +
                "<body>" +
                "<h1>" +
                "<div class=\"u\">b1</div>"+
                "<div class=\"u\">b2</div>"+
                "<div class=\"u\">b3</div>"+
                "</h1>" +
                "<h1>" +
                "<div class=\"u\">c1</div>"+
                "<div class=\"u\">c2</div>"+
                "</h1>" +
                "<h1>" +
                "<div class=\"u\">d1</div>"+
                "<div>"+
                "<div class=\"u\">d2</div>"+
                "<div class=\"g\">d3</div>"+
                "</div>"+
                "</h1>" +
                "</body>" +
                "</html>";
        Document document = Jsoup.parse(xml);

        //嵌套查询,可以拿到顶数据以后,嵌套往下查询
        Elements elements = document.select("h1:eq(2)");

        //这里匹配了2个Element,转换为text的时候,中间直接加空格
        String text1 = elements.select(".u").text();
        assertEquals(text1,"d1 d2");

        //匹配了1个Element
        String text2 = elements.select(".g").text();
        assertEquals(text2,"d3");
    }
}

这代码真的没啥好说的

  • 输入String或者InputStream,用Jsoup.parse生成Document对象。
  • 使用Document对象,传入jQuery查询语法到select方法中,得到Elements对象。Elements对象可以用进一步使用select方法,得到子级的Elements对象。
  • 对于每个Elements,可以用text(),textNode()的方法获取值

基本上就是和jQuery一样的语法

注意,爬虫拉到的数据都是不可靠的,最好用ValidatorUtil来校验一下比较稳妥

2 poi

代码在这里

官网在这里

poi是Java里面的读写MS Documents的工具,主要是针对xls,xlsx的工具。另外,国内有一家商业做excel读写,和展示编辑的工具,看这里,唯一的缺点是要花钱。

poi的接口本身就写得够简单的,而且Bug相对较少,不建议使用EasyPoi等的类似工具,无法获得第一手的poi更新,而且生态与成熟度都不高。

2.1 依赖

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
    <version>5.2.3</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.3</version>
</dependency>

ooxml是对excel 2007+以上的处理,可以单独引入。poi是excel 2003的处理。

加入以上的maven依赖即可

A part name shall not have a forward slash as the last character

在使用poi 5.1.0的版本的时候会有以上的报错,改为最新版本即可。

2.2 生成与导出excel

package spring_test;

import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

@Component
public class ExportExcelService {

    public void injectContent(Sheet sheet){
        //创建行头
        Row row = sheet.createRow(0);
        Cell cell = row.createCell(0);
        cell.setCellValue("名称");
        cell = row.createCell(1);
        cell.setCellValue("年龄");

        //创建数据行1
        row = sheet.createRow(1);
        cell = row.createCell(0);
        cell.setCellValue("fish");
        cell = row.createCell(1);
        //通过写入的数据类型,来确定填充什么类型
        cell.setCellValue(12);

        //创建数据行2
        row = sheet.createRow(2);
        cell = row.createCell(0);
        cell.setCellValue("cat");
        cell = row.createCell(1);
        cell.setCellValue(89);
    }

    public Workbook createWorkbook2003(){
        Workbook w = new HSSFWorkbook();
        Sheet sheet = w.createSheet("表格1");
        this.injectContent(sheet);
        return w;
    }

    public Workbook createWorkbook2007(){
        Workbook w = new XSSFWorkbook();
        Sheet sheet = w.createSheet("表格1");
        this.injectContent(sheet);
        return w;
    }

    public Workbook createWorkbook2007_large(){
        Workbook w = new SXSSFWorkbook();
        Sheet sheet = w.createSheet("表格1");
        this.injectContent(sheet);
        return w;
    }
}

生成Excel的步骤:

  • 通过XSSFWorkbook来创建Workbook,HSSFWorkbook是2003版本,XSSFWorkbook是2007版本,SXSSFWorkbook是2007的优化版本,以流方式写入文档,减少内存使用,但是对随机访问的能力要差一点。具体性能比较看这里,原理看这里
  • 通过Workboot的createSheet生成表格
  • 通过Sheet的createRow与createCell创建单元格

总体来说,真的挺简单的。注意,单元格的数据类型,就是由传入setCellValue的数据类型来确定的,这点要小心。

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.h2.util.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;

/**
 * Created by fish on 2021/4/25.
 */
@RestController
@RequestMapping("/hello")
@Slf4j
public class MyController {

    @GetMapping("/go")
    public String go(){
        return "Hello World";
    }

    @Autowired
    private ExportExcelService excelService;

    @GetMapping("get1")
    public void get1(){

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = requestAttributes.getResponse();
        HttpServletRequest request = requestAttributes.getRequest();

        String filename = "地址列表.xls";
        OutputStream out = null;
        Workbook workbook = null;
        try {
            // 下面几行是为了解决文件名乱码的问题
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename,"UTF-8"));
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expires", 0);
            out = response.getOutputStream();

            workbook = excelService.createWorkbook2003();
            workbook.write(out);
        }catch(IOException e){
            throw new RuntimeException(e);
        }finally {
            try{
                if( workbook != null ){
                    workbook.close();
                }
                if( out != null ){
                        out.close();
                }
            }catch(IOException e ){
                e.printStackTrace();
            }
        }
    }
}

生成workbook以后,直接写入到HttpServletResponse就可以了。注意要设置HttpServletResponse的header信息。另外,这里用到了URLEncoder,将返回数据使用URLEncode来编码先,以符合HTTP标准,也避免乱码。

2.3 导入与分析excel

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.h2.util.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLEncoder;

/**
 * Created by fish on 2021/4/25.
 */
@RestController
@RequestMapping("/hello")
@Slf4j
public class MyController {

    @Autowired
    private ImportExcelSevice importExcelSevice;

    //使用curl提交,curl http://localhost:8585/hello/post -X POST -F "data=@./excel.xlsx"
    @PostMapping("post1")
    public Object post1(@RequestParam(name="data") MultipartFile file,
                        @RequestParam(name="type") String type){
        log.info("type {}",type);
        InputStream is = null;
        try{
            is = file.getInputStream();
            return importExcelSevice.read(is);
        }catch(IOException e ){
            throw new RuntimeException(e);
        }finally {
            if( is != null ){
                try{
                    is.close();
                }catch(IOException e){
                    e.printStackTrace();
                }
            }
        }
    }
}

在SpringBoot中使用@RequestParam注解,和MultipartFile参数来接收文件参数。

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Component
@Slf4j
public class ImportExcelSevice {
    private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public Optional<List<List<Object>>> read(InputStream is){
        Workbook workbook;
        try{
            workbook = new XSSFWorkbook(is);
        }catch(IOException e){
            return Optional.empty();
        }
        Sheet sheet = workbook.getSheetAt(0);
        int firstRow = sheet.getFirstRowNum();
        int lastRow = sheet.getLastRowNum();
        List<List<Object>> result = new ArrayList<>();
        //这里需要有等于号
        for( int i = firstRow ;i <= lastRow;i++){
            Row row =  sheet.getRow(i);
            if( row == null ){
                continue;
            }
            int firstColumn = row.getFirstCellNum();
            int lastColumn = row.getLastCellNum();
            List<Object> rowData = new ArrayList<>();
            //这里不能有等于号
            for( int j = firstColumn ; j < lastColumn;j++ ){
                Cell cell = row.getCell(j);
                if( cell == null ){
                    rowData.add("");
                    continue;
                }
                if( cell.getCellType() == CellType.STRING){
                    rowData.add(cell.getStringCellValue());
                }else if(cell.getCellType() == CellType.NUMERIC){
                    if (DateUtil.isCellDateFormatted(cell)) {
                        rowData.add(simpleDateFormat.format(cell.getDateCellValue()));
                    }else{
                        rowData.add(cell.getNumericCellValue());
                    }
                }
            }
            result.add(rowData);
        }
        return Optional.of(result);
    }
}

最后,直接使用XSSFWorkbook来反序列化一个Workbook对象。然后慢慢地使用getRow与getCell来获取数据就可以了,接口也比较简单。注意Date的读取方式。

要注意的是,row与cell是可能为null的,需要判断一下

2.4 样式和基础工具

package spring_test;

import lombok.Data;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Function;

public class ExcelUtil {

    public enum TableColumnType{
        STRING,
        INTEGER,
        BIG_DECIMAL,
        BYTE,
    }

    public static class TableColumn{
        private String title;

        private Function getter;

        private int width;

        public TableColumn(String title,Function getter,int width){
            this.title = title;
            this.getter = getter;
            this.width = width;
        }
    }

    public static class Table<T>{
        private List<TableColumn> data = new ArrayList<>();

        public Table(){

        }

        public <R> void addColumn(String title,Function<T,R> getter,int width){
            this.data.add(new TableColumn(title,getter,width));
        }

        public  int getColumnSize(){
            return data.size();
        }
    }

    private static SimpleDateFormat ft = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static <T> void createTable(Sheet sheet,int beginRow,Table<T> table,List<T> data){
        Row row = sheet.createRow(beginRow);
        for( int i = 0 ;i != table.data.size();i++){
            TableColumn column = table.data.get(i);
            Cell cell = row.createCell(i);
            cell.setCellValue(column.title);
            //设置列宽度,建议设置为256的倍数,单位为字符宽度
            sheet.setColumnWidth(i,column.width*256);
        }

        for( int i = 0 ;i != data.size() ;i ++){
            T rowData = data.get(i);
            beginRow++;
            row = sheet.createRow(beginRow);
            for( int j = 0 ; j != table.data.size() ;j ++){
                TableColumn column = table.data.get(j);
                Cell cell = row.createCell(j);
                Object cellValue = column.getter.apply(rowData);
                if( cellValue instanceof Long){
                    cell.setCellValue((Long)cellValue);
                }else if( cellValue instanceof Integer){
                    cell.setCellValue((Integer)cellValue);
                }else if( cellValue instanceof Byte){
                    cell.setCellValue((Byte)cellValue);
                }else if( cellValue instanceof Date){
                    cell.setCellValue(ft.format((Date)cellValue));
                }else if( cellValue instanceof String){
                    cell.setCellValue((String)cellValue);
                }else if(cellValue instanceof BigDecimal){
                    cell.setCellValue(((BigDecimal) cellValue)..doubleValue());
                }else{
                    throw new RuntimeException("unknown cellValue type "+cellValue.getClass().getName());
                }
            }
        }
    }

    public static void exportToWriter(Workbook workbook,String filename){

        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletResponse response = requestAttributes.getResponse();
        HttpServletRequest request = requestAttributes.getRequest();

        OutputStream out = null;
        try {
            // 下面几行是为了解决文件名乱码的问题
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename,"UTF-8"));
            response.setContentType("application/vnd.ms-excel;charset=UTF-8");
            response.setHeader("Pragma", "no-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expires", 0);
            out = response.getOutputStream();

            workbook.write(out);
        }catch(IOException e){
            throw new RuntimeException(e);
        }finally {
            try{
                if( workbook != null ){
                    workbook.close();
                }
                if( out != null ){
                    out.close();
                }
            }catch(IOException e ){
                e.printStackTrace();
            }
        }
    }
}

大部分情况下,我们后端都是将List<T>转换为excel,以及将excel导出到HttpResponse里面,ExecelUtil做了以上工作,这点就不多说了。注意,设置列宽的代码。

package spring_test;

import lombok.AllArgsConstructor;
import lombok.Data;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.*;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.List;

@Component
public class ExportExcelService2 {

    @Data
    @AllArgsConstructor
    public static class Person {
        private Long id;

        private String name;
    }

    private List<Person> getData() {
        ArrayList<Person> result = new ArrayList<>();
        for (int i = 0; i != 100; i++) {
            result.add(new Person(Long.valueOf(i + 10001), "fish"));
        }
        return result;
    }

    public XSSFCellStyle getTitleStyle(XSSFWorkbook wb){
        XSSFFont font = wb.createFont();
        font.setFontHeight(20);
        font.setFontName("微软雅黑");
        //font.setColor(Font.COLOR_RED);
        font.setColor(new XSSFColor(java.awt.Color.decode("#F0C0E0"),new DefaultIndexedColorMap()));

        XSSFCellStyle style = wb.createCellStyle();
        //边框
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);

        //字体
        style.setFont(font);

        //自动换行
        style.setWrapText(false);

        //水平对齐
        style.setAlignment(HorizontalAlignment.CENTER);

        //垂直对齐
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }

    public void export(){
        List<Person> person = this.getData();

        XSSFWorkbook wb = new XSSFWorkbook();
        Sheet sheet1 = wb.createSheet("Sheet1");

        //创建标题
        Row row = sheet1.createRow(0);
        Cell cell = row.createCell(0);
        cell.setCellValue("我是标题");

        //合并单元格
        //参数分别为firstRow,lastRow,firstCol和lastCol
        sheet1.addMergedRegion(new CellRangeAddress(0,0,0,1));

        //设置标题样式
        XSSFCellStyle style = this.getTitleStyle(wb);
        cell.setCellStyle(style);

        //行高
        //建议设置为20的倍数,单位为磅
        row.setHeight((short)(60*20));

        //创建表格
        ExcelUtil.Table<Person> table = new ExcelUtil.Table<>();
        table.addColumn("ID",Person::getId,20);
        table.addColumn("名称",Person::getName,40);
        ExcelUtil.createTable(sheet1,1,table,person);

        //导出
        ExcelUtil.exportToWriter(wb,"测试.xlsx");
    }
}

在这个例子中,要点如下:

  • 展示了如何使用ExportUtil将List数组转换为excel的行与列
  • 展示了如何导出Excel到当前线程的HttpResponse里面
  • 设置行高,row.setHeight
  • 设置合并单元格,sheet.addMergedRegion
  • 设置样式,setCellStyle,包括字体,换行,边框和对齐。

3 FreeMarker

FreeMarker是一个很成熟的Java模板工具了,现在前后端分离的开发环境下,已经很少用模板生成html了,更多是用模板来生成代码。

参考资料:

代码在这里

3.1 依赖

<dependency>
    <groupId>org.freemarker</groupId>
    <artifactId>freemarker</artifactId>
    <version>2.3.31</version>
</dependency>

这个也是比较简单的

3.2 加载Configuration

package spring_test;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.io.StringWriter;

@Component
public class FreeMarkerService {
    private Configuration freeMarkConfiguration;

    @PostConstruct
    public void init(){
        try{
            freeMarkConfiguration = new Configuration(Configuration.VERSION_2_3_20);
            //当前工作目录下的tpl文件夹
            //freeMarkConfiguration.setDirectoryForTemplateLoading(new File("tpl"));
            //使用类的资源方式加载
            freeMarkConfiguration.setClassForTemplateLoading(this.getClass(),"/tpl");
            freeMarkConfiguration.setDefaultEncoding("UTF-8");
            freeMarkConfiguration.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        }catch(Exception e){
            throw new RuntimeException(e);
        }
    }

    public String execute(String templateName,Object data){
        try{
            StringWriter stream = new StringWriter();
            Template tpl = freeMarkConfiguration.getTemplate(templateName);
            tpl.process(data,stream);
            return stream.toString();
        }catch(IOException e){
            throw new RuntimeException(e);
        }catch(TemplateException e){
            throw new RuntimeException(e);
        }
    }
}

我们一般都是直接按目录来加载模板的,注意模板文件的后缀名都必须是ftl。另外,Configuration类应该设计为单例,因为他自身包含了Cache的能力,重新创建Configuration的成本很高。最后,注意一下,加载本地文件夹,和加载类路径文件夹的不同写法。

3.3 模板语法

3.3.1 控制流

<html>
<head>
    <title>Welcome!</title>
</head>
<body>
<!--if与else,注意用等号,而不是equals-->
<#if sex == 'mm'>
    Welcome Lady ${user}
<#else>
    Welcome Man ${user}
</#if>
<!--for,一个独特的as语法,并且支持sep指令来指定非末尾行的数,index从0开始-->
<p>共有${cars?size}个车子</p>
<ul>
<#list cars as car>
    <li>${car?index} name为${car.name}<#sep>,<#sep></li>
</#list>
</ul>

<!--for的另外一种写法,当cars为空的时候,连ul标签都不输出。我们还能额外用else标签来输出,index从1开始-->
<#list cars >
    <ul>
    <#items as car>
        <li>${car?counter} name2为${car.name}<#sep>,</#sep></li>
    </#items>
    </ul>
    <#else>
        <p>Nothing</p>
</#list>

<!--for,访问哈希表-->
<#assign keys = brands?keys>
<p>共有${keys?size}个种类的袋子</p>
<ul>
    <#list keys as key>
        <li>${key} => ${brands[key]}</li>
    </#list>
</ul>

<#--注释标签,不会输出到前端-->
<#include "/footer.ftl">
<body>
</html>

这些控制流语法都是比较简单的,注意点都在注释上了,没啥好说的。注意访问Map的方法

package spring_test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.*;

/**
 * Created by fish on 2021/4/25.
 */
@RestController
@RequestMapping("/hello")
@Slf4j
public class MyController {

    @Autowired
    private FreeMarkerService freeMarkerService;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Product{
        private String url;

        private String name;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Car{
        private String name;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class MainData{
        private String user;

        private String sex;

        private List<Car> cars = new ArrayList<>();

        private BigDecimal assets;

        private Long assets2;

        private Product latestProduct;

        private Map<String,String> brands = new HashMap<>();
    }

    @GetMapping("/mainTpl")
    public String mainTpl(){
        Product product = new Product();
        product.setUrl("https://www.baidu.com");
        product.setName("百度");
        MainData data = new MainData();
        data.setUser("fish");
        data.setLatestProduct(product);
        return freeMarkerService.execute("main.ftl",data);
    }

    @GetMapping("/conditionTpl")
    public String conditionTpl(){
        Car car = new Car();
        car.setName("Toyota");
        Car car2 = new Car();
        car2.setName("Mercedes");
        MainData data = new MainData();
        data.setUser("fish");
        data.setSex("Man");
        data.setCars(Arrays.asList(car,car2));
        return freeMarkerService.execute("condition.ftl",data);
    }

    @GetMapping("/conditionTpl2")
    public String conditionTpl2(){
        MainData data = new MainData();
        data.setUser("Cat");
        data.setSex("mm");
        data.brands.put("LV","10个");
        data.brands.put("Coach","20个");
        return freeMarkerService.execute("condition.ftl",data);
    }
}

我们能够使用Map,或者Java Bean来传送数据,这个都是基本艺能,没啥好说的。

conditionTpl的结果

conditionTpl2的结果

3.3.2 外部导入

就是3.3.1节的#include指令

3.3.3 插值

<html>
<head>
    <title>Welcome!</title>
</head>
<body>
<#--默认是没有转义的-->
<p>${user}</p>
<#--使用指定转义-->
<p>${user?html}</p>
<#--放入escape块的变量,指定默认使用x?html的语法来转义-->
<#escape x as x?html>
    <p>${user}</p>
    <#--我们用noescape来指定部分变量不转义-->
    <p><#noescape>${user}</#noescape></p>
</#escape>

<#--默认数字输出有分割符-->
<p>${assets}</p>
<p>${assets2}</p>


<#--默认数字输出有分割符,需要用?c来避免这个问题-->
<p>${assets?c}</p>
<p>${assets2?c}</p>

<#--字符串组合-->
<p>${"hello ${assets}"}</p>
<p>${"hello ${user[0..1]}"}</p>
</body>
</html>

插值语法就比较多坑了,首先要注意数字输出默认就有分隔符,字符串输出默认没有escape,这些都需要根据环境来做好准备

package spring_test;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by fish on 2021/4/25.
 */
@RestController
@RequestMapping("/hello")
@Slf4j
public class MyController {

    @Autowired
    private FreeMarkerService freeMarkerService;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Product{
        private String url;

        private String name;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Car{
        private String name;
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class MainData{
        private String user;

        private String sex;

        private List<Car> cars = new ArrayList<>();

        private BigDecimal assets;

        private Long assets2;

        private Product latestProduct;
    }

    @GetMapping("/valueTpl")
    public String valueTpl(){
        MainData data = new MainData();
        data.setAssets(new BigDecimal("123456789.123"));
        data.setAssets2(1234567890L);
        data.setUser("<div style=\"color:red\">你好</div>");
        return freeMarkerService.execute("value.ftl",data);
    }
}

导入数据

valueTpl的结果

4 Reflections

官网在这里,Reflections是一个比较简单的,扫描包的工具,输入一个包名,将它的所有类都抽取出来

4.1 依赖

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

加入以上依赖即可

4.2 扫描

package spring_test;

import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Component
public class MyReflections {
    @Value("${reflections.packageName}")
    private String scanPackageName;

    @PostConstruct
    public void init(){
        if(Strings.isBlank(scanPackageName)){
            throw new RuntimeException("reflections.packageName为空");
        }
        log.info("scanPackageName {}",scanPackageName);
        Reflections reflections = new Reflections(this.scanPackageName);

        //获取所有枚举体
        Set<Class<? extends Enum>> allEnums = reflections.getSubTypesOf(Enum.class);
        log.info("allEnums {}",allEnums.stream().map(single->single.getName()).collect(Collectors.toList()));

        //获取所有注解的类
        Set<Class<?>> allAnnotations = reflections.getTypesAnnotatedWith(RestController.class);
        log.info("allControllers {}",allAnnotations.stream().map(single->single.getName()).collect(Collectors.toList()));
    }
}

扫描包的代码,比较简单,没啥好说

/**
 * Created by fish on 2021/3/15.
 */
server.port=8585
logging.level.root = INFO
spring.datasource.url  = jdbc:h2:mem:testdb
reflections.packageName = spring_test

我们可以在properties中加入指定的扫描位置

相关文章