java实现控制台打印表格

2022-07-28,,,

背景

判断给定数据库是否可以连接,习惯做法是安装一个客户端,输入连接信息后连接测试。但是客户现场通常只提供一个linux系统,没有相关客户端。因此,需要一个能在linux上运行的数据库连接测试工具。我的实现思路:使用jdbc连接目标服务器,并执行一条给定的sql语句,能够连接成功则在控制台输出执行结果,连接失败则打印异常信息。
在实现该功能的过程中,对我来说最麻烦的是要在控制台输出表格,难点在于控制表格列宽相等(涉及中英文长度不一致)以及表格内容要居中对齐

效果

最终实现效果如下:

核心代码分享

分享此内容的目的有二:

  1. 为要实现同样功能的童鞋提供参考
  2. 请大佬们从实现思路或者具体方法上指点一下是否有更佳实现方式

主要写了一个PrintTable类:

定义了一个Table内部类,实现以下方法:

  1. buildTable(List<List< String>> content): 传入二维list,构建表格
  2. getLimitTable(Table table, int maxWidth, int maxCount):限制宽度,最大条数后的表格
  3. getMaxWidthLenList(Table table):得到表格的每列最大宽度,用于实现列宽相等
  4. getFormatTable(Table table, String symbol):根据指定分隔符得到最终格式化后的表格
  5. printTable(Table table):打印表格

Table定义:

 @Data
    private class Table {
        /* 表格内容(含表头)*/
        private List<List<String>> content = new ArrayList<>();
        
        /* 表格列总字符长度:便于打印行分割符号*/
        private Integer totalColSize;
        
        /* 每列最大宽度*/
        private List<Integer> maxWidthList;
        
		/**
         *  如果计算过则直接从成员属性获取,否则根据maxWidthList求和并保存到成员属性中方便下次获取
         */
        Integer getTotalColSize() {
            if (totalColSize == null && maxWidthList != null && maxWidthList.size() != 0) {
                this.totalColSize = maxWidthList.stream().reduce(Integer::sum).get();
            }
            return totalColSize;
        }

        //private限制只能通过外部类构造
        private Table(List<List<String>> content) {
            this.content = content;
        }

        //获取表格行数
        int getRowCount() {
            return content.size();
        }

        //获取表格列数,0行代表表头,默认认为content中至少含有表头
        int getColCount() {
            return content.get(0).size();
        }

        /* 转置二维数组*/
        private String[][] transpose() {
            int rowCount = getRowCount();
            int colCount = getColCount();
            String[][] result = new String[colCount][rowCount];
            for (int i = 0; i < rowCount; i++) {
                for (int j = 0; j < colCount; j++) {
                    result[j][i] = content.get(i).get(j);
                }
            }
            return result;
        }
    }

buildTable(List<List< String>> content):

public Table buildTable(List<List<String>> content) {
        return new Table(content);
    }

getLimitTable(Table table, int maxWidth, int maxCount):

	/**
     * @param table 原始表格
     * @param maxWidth 最大列宽:sql查询结果某列内容可能过大,不想完全显示,因此限制最大列宽
     * @param maxCount 最大条数:sql查询结果可能有非常多,通常不必完全显示,因此限制最大条数
     * @return 获取经过条件过滤的表格
     */
    private Table getLimitTable(Table table, int maxWidth, int maxCount) {
        List<List<String>> limitContent = table.getContent().stream()
                .limit(maxCount)
                .map(row -> row.stream()
                    //cell超过列宽,则截取到最大宽度
                    .map(cell -> cell != null && cell.length() > maxWidth ? cell.substring(0, maxWidth) : cell)
                    .collect(Collectors.toList())
                ).collect(Collectors.toList());
        return buildTable(limitContent);
    }

getMaxWidthLenList(Table table):

    /**
     * 计算table每行的最大宽度
     * 要使列宽相等,就需要将每个单元格宽度设置为该列最大宽度,二计算每行最大宽度相对容易些
     * 故将content转置后得到的每行最大宽度即为所求
     * 需要考虑单双字节的情况,比如有数组arr:{"aabb","sql表格","编程学习"},
     * 按照String.length计算,arr[1]最长,但是实际上arr[2]看起来才是最宽的
     * 因此计算宽度时,将双字节字符看做2个单位长度,即:每出现一个双字节字符,长度+1
     */
    private List<Integer> getMaxWidthLenList(Table table) {
        //得到转置数组每个元素的长度,一个中文算两个长度
        return Arrays.stream(table.transpose())
                .map(rows -> Arrays.stream(rows)
                        .mapToInt(s -> {
                            //sql查询结果如果为null,则认为长度为4
                            if (s == null) {
                                return 4;
                            } else {
                                //加上双字节字符出现的次数,最短为null,四个字符
                                return s.length() + StringUtils.getZHCharCount(s);
                            }
                        }).max().orElse(0)
                ).collect(Collectors.toList());
    }

getFormatTable(Table table, String symbol): 核心方法,输出格式化后的表格数据

    /**
     * 格式化表格
     *
     * @param table  待格式化的table
     * @param symbol 定义每列间隔符号
     * @return
     */
    private Table getFormatTable(Table table, String symbol) {
        //获取原表每列最大宽度
        List<Integer> originMaxWidthList = table.getMaxWidthList();
        //除了间隔符号外,固定在每个单元格前后加两个空格
        int symbolLen = symbol.length() + 2;
        //遍历原table,将每个单元格填充到该列最大长度
        List<List<String>> formatList = table.getContent().stream().map(
                row -> {
                    //用于流在遍历每行的过程中,获取列序号
                    AtomicInteger atomicInteger = new AtomicInteger(0);
                    return row.stream().map(cell -> {
                        //当前遍历的列序号
                        int j = atomicInteger.getAndIncrement();
                        //原表该列的最大宽度+间隔符号宽度-双字节出现的次数
                        int cellSize = originMaxWidthList.get(j) + symbolLen - StringUtils.getZHCharCount(cell);
                        //如果是首行,还需要再前面加一个分割符号|,故长度加1
                        cellSize = j == 0 ? cellSize + 1 : cellSize;
                        //返回原始字符串按照指定symbol填充到指定长度cellSize,并居中对齐的字符
                        return StringUtils.getPadString(cell, cellSize, symbol, new int[]{j});
                    }).collect(Collectors.toList());
                }
        ).collect(Collectors.toList());
        //存储格式化后的表格数据
        Table formatTable = buildTable(formatList);
        //设置格式化表格的总宽度:原始宽度+自定义分割符号的总宽度(列数*符号宽度)+首列前面的符号宽度
        int totalColSize = table.getTotalColSize() + table.getColCount() * symbolLen + 1;
        formatTable.setTotalColSize(totalColSize);
        return formatTable;
    }

printTable(Table table):打印表格

    public void printTable(Table table) {
        //从配置文件读取最大列宽、最大数据量
        int maxWidth = Integer.parseInt(ConfigParseUtil.get("maxWidth"));
        int maxCount = Integer.valueOf(ConfigParseUtil.get("maxCount")) + 1;
        //按照最大列宽、最大数据量过滤后的表格
        Table limitTable = getLimitTable(table, maxWidth, maxCount);
        //设置表格的最每列最大宽度originMaxWidthList
        List<Integer> originMaxWidthList = getMaxWidthLenList(limitTable);
        limitTable.setMaxWidthList(originMaxWidthList);

        //得到格式化后的表格数据
        Table formatTable = getFormatTable(limitTable, "|");
        Integer totalColSize = formatTable.getTotalColSize();
        //打印首行分割符号
        System.out.println(StringUtils.getRepeatChar("-", totalColSize));
        formatTable.getContent()
                .forEach(row -> {
                    row.forEach(System.out::print);
                    //每列完成后需要换行
                    System.out.println();
                    //打印每行分割符号
                    System.out.println(StringUtils.getRepeatChar("-", totalColSize));
                });
    }

用到的其他工具类

StringUtils:

package com.sw.utils;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.IntStream;

public class StringUtils {

    /**
     * 判断字符串是否为空
     * @param str
     * @return
     */
    public static boolean isEmpty(String str) {
        return str == null || "".equals(str);
    }

    /**
     * 将content按照正则匹配,返回可以匹配的字符串列表
     * @param reg
     * @param content
     * @return
     */
    public static List<String> extractMessage(String reg, String content) {
        Pattern compile = Pattern.compile(reg, Pattern.CASE_INSENSITIVE);
        Matcher matcher = compile.matcher(content);
        List<String> list = new ArrayList<>();
        while (matcher.find()) {
            list.add(matcher.group());
        }
        return list;
    }


    /**
     *将str重复count次,返回结果
     * @param str
     * @param count
     * @return
     */
    public static String getRepeatChar(String str, int count) {
        StringBuilder res = new StringBuilder();
        IntStream.range(0, count).forEach(i -> res.append(str));
        return res.toString();
    }

    /**
     * 将字符串填充到指定长度并居中对齐
     * @param str
     * @param len
     * @return
     */
    public static String getPadString(String str, Integer len) {
        StringBuilder res = new StringBuilder();
        str = str.trim();
        if (str.length() < len) {
            int diff = len - str.length();
            int fixLen = diff / 2;
            String fix = getRepeatChar(" ", fixLen);
            res.append(fix).append(str).append(fix);
            if (res.length() > len) {
                return res.substring(0, len);
            }else{
                res.append(getRepeatChar(" ", len - res.length()));
                return res.toString();
            }
        }
        return str.substring(0,len);
    }

    /**
     * 此方法主要为表格的单元格数据按照指定长度填充并居中对齐并带上分割符号
     * @param str 原始字符串
     * @param len 输出字符串的总长度
     * @param symbol 分割符号
     * @param index 传入的cell在list的索引,如果为第一个则需要在前面增加分割符号
     * @return
     */
    public static String getPadString(String str, Integer len, String symbol, int[] index) {
        String origin = str + "  ";
        if (index.length != 0 && index[0] == 0) {
            String tmp = getPadString(origin, len - 2);
            return symbol + tmp + symbol;
        } else {

            String tmp = getPadString(origin, len - 1);
            return tmp + symbol;
        }
    }

    /**
     * 得到一个字符串中单字节出现的次数
     * @param cell
     * @return
     */
    public static Integer getENCharCount(String cell) {
        if (cell == null) {
            return 0;
        }
        String reg = "[^\\x00-\\xff]";
        return cell.replaceAll(reg, "").length();
    }

    /**
     * 得到一个字符串中双字节出现的次数
     * @param cell
     * @return
     */
    public static Integer getZHCharCount(String cell) {
        if (cell == null) {
            return 0;
        }
        return cell.length() - getENCharCount(cell);
    }
}

调用方法:
将sql得到的rsultSet封装成二维list,再调用PrintTable即可完成控制台打印

备注

初学java,如有问题,还望不吝赐教:
完整工具github地址:https://github.com/Yanqin25/jdbcConnectTool

本文地址:https://blog.csdn.net/qq_31076523/article/details/109642722

《java实现控制台打印表格.doc》

下载本文的Word格式文档,以方便收藏与打印。