springboot+netty+tcp 实现智能模块的控制

44 阅读6分钟

项目背景

近期公司数字工厂项目因生产需要,需要引入一个智能模块控制器,来实现产品在产线上的智能流转,以减少人工干预、降低人为的识别错误率。因此通过在市场上的调研、采购,最终选中了一款产品来实现此需求功能。

智能模块

选品的模块是一个智能模块,模块支持串口通信、RS485/网口通信、Modbus RTU/TCP互转模式。从此可以看出,该模块支持tcp协议通信。根据我们的业务场景,将此模块作为tcp client模块,自己搭建tcp service端,来实现功能控制实现。

交互流程图

image.png

  1. A服务作为上游系统,通过调用B服务的http接口将触发指令通知到B服务
  2. B服务作为中间节点承上启下,就是我们需要开发出来的应用程序
  3. iot模块就是上文提到的智能模块硬件,能够接收参数信息。当然该模块本身内嵌了处理程序,以及暴露了接口供我们调用
  4. B服务有两个端口:1)http端口:8080 供与A服务做http业务交互 2)tcp端口:502 供与IoT模块做tcp交互

上代码

pom:

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- netty -->
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.63.Final</version>
    </dependency>

    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>

    <!-- hutool -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.15</version>
    </dependency>

</dependencies>

<!-- maven package -->
<build>shang
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <excludes>
                    <exclude>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                    </exclude>
                </excludes>
            </configuration>
        </plugin>
    </plugins>
</build>

TcpServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;


@Slf4j
@Component
public class TcpServer {

    public static final Charset UTF_8 = StandardCharsets.UTF_8;

    @Value("${server.address}")
    private String SERVER_IP;
    @Value("${netty.port}")
    private Integer SERVER_PORT;


    @SneakyThrows
    @Async
    public void run() {
        final EventLoopGroup bossGroup = new NioEventLoopGroup();
        final EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 主从多线程模型
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
                    // 启用TCP保活检测,多久检测一次还是取决于操作系统本身
//                    .option(ChannelOption.TCP_NODELAY, true)
                    // 关闭Nagle算法,有数据发送时就马上发送
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    .option(ChannelOption.SO_REUSEADDR, true)
                    .localAddress(SERVER_IP, SERVER_PORT)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel channel) {
                            ChannelPipeline pipeline = channel.pipeline();
                            pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
                            pipeline.addLast(new StringDecoder(UTF_8));
                            pipeline.addLast(new JsonDecoder());
                            pipeline.addLast(new StringEncoder(UTF_8));
                            pipeline.addLast(new JsonEncoder());

                            pipeline.addLast(new TcpServerHandler());
                        }
                    });
            // 指定连接队列大小,可以暂存1024个连接
            bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
            // 启用Nagle算法,将小的数据包组装为更大的帧然后进行发送
            // bootstrap.option(ChannelOption.TCP_NODELAY, true);
            // 使用内存池
            bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);


            // 异步地绑定服务器,调用sync()方法阻塞,等待直到绑定完成
            ChannelFuture bindFuture = bootstrap.bind().sync();
            bindFuture.addListener((ChannelFutureListener) channelFuture -> {
                String flag = bindFuture.isSuccess() ? " ok" : " failure";
                log.info("[TcpServer] start on {}:{} {}", SERVER_IP, SERVER_PORT, flag);

            });

            // 获取Channel的CloseFuture,并且阻塞当前线程直到它完成
            ChannelFuture closeFuture = bindFuture.channel().closeFuture().sync();
            closeFuture.addListener((ChannelFutureListener) channelFuture -> {
                String flag = bindFuture.isSuccess() ? " ok" : " failure";
                log.info("[TcpServer] stop on {}:{} {}", SERVER_IP, SERVER_PORT, flag);
            });

        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            // 关闭EventLoopGroup释放所有的资源
            workerGroup.shutdownGracefully().sync();
            bossGroup.shutdownGracefully().sync();
        }
    }

}

TcpServerHandler

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import lombok.extern.slf4j.Slf4j;

import java.net.InetSocketAddress;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;


@Slf4j
public class TcpServerHandler extends SimpleChannelInboundHandler<String> {
    
    
    //定义一个channle 组,管理所有的channel
    //GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
    public static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    
    /**
     * key: 设备编号 value: 设备编号绑定的模块channel
     */
    public static Map<String, Channel> CHANNEL_MAP = new ConcurrentHashMap<>();
    
    
    /**
     * 有客户端与服务器发生连接时执行此方法 1.打印提示信息 2.将客户端保存到 channelGroup 中
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            CHANNEL_MAP.put(model.getEquipmentNumber(), channel);
            log.info("[TcpServerHandler]有新的客户端与服务器发生连接 客户端地址:{}, 设备编号:{}", channel.remoteAddress(), model.getEquipmentNumber());
        }
    }
    
    /**
     * 获取到channel内的ip地址,找到与之绑定的设备配置信息
     *
     * @param ctx
     * @return
     */
    private EquipmentBindingModuleModel findEquipmentModel(ChannelHandlerContext ctx) {
        Channel channel = ctx.channel();
        InetSocketAddress inetSocketAddress = (InetSocketAddress) channel.remoteAddress();
        // 获取到channel的ip
        String address = inetSocketAddress.getAddress()
                                          .getHostAddress();
        if (InitConfigurationFileServer.EQUIPMENT_BINDING_MODULE_MODEL_MAP.containsKey(address)) {
            return InitConfigurationFileServer.EQUIPMENT_BINDING_MODULE_MODEL_MAP.get(address);
        }
        return null;
    }
    
    /**
     * 当有客户端与服务器断开连接时执行此方法,此时会自动将此客户端从 channelGroup 中移除 1.打印提示信息
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            CHANNEL_MAP.remove(model.getEquipmentNumber());
            log.info("[TcpServerHandler]有客户端与服务器断开连接 客户端地址:{}, 设备编号:{}", channel.remoteAddress(), model.getEquipmentNumber());
        }
    }
    
    /**
     * 表示channel 处于活动状态
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            log.info(ctx.channel()
                        .remoteAddress() + " [TcpServerHandler].处于活动状态, 设备编号:{} ", model.getEquipmentNumber());
        }
    }
    
    /**
     * 表示channel 处于不活动状态
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            log.info(ctx.channel()
                        .remoteAddress() + " [TcpServerHandler].处于不活动状态, , 设备编号:{} ", model.getEquipmentNumber());
        }
    }
    
    /**
     * 读取到客户端发来的数据数据
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) {
        Channel channel = ctx.channel();
        log.info("[TcpServerHandler] 有客户端发来的数据 地址{}, 内容{}", channel.remoteAddress(), msg);
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            log.info("[TcpServerHandler] 有客户端发来的数据 地址: {}, 设备编号: {}, 内容: {}", ctx.channel()
                                                                                                  .remoteAddress(), model.getEquipmentNumber(), msg);
        }
        
    }
    
    @Override
    public boolean acceptInboundMessage(Object msg) throws Exception {
        log.info("[TcpServerHandler] 有客户端发来的数据 msg={}", msg.toString());
        return super.acceptInboundMessage(msg);
    }
    
    /**
     * 处理异常
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        EquipmentBindingModuleModel model = this.findEquipmentModel(ctx);
        if (Objects.nonNull(model)) {
            log.error("[TcpServerHandler].发生异常。设备编号:{} , 异常信息:{}", model.getEquipmentNumber(), cause.getMessage());
        } else {
            log.error("[TcpServerHandler].发生异常。异常信息:{}", cause.getMessage());
        }
        //关闭通道
        ctx.close();
    }
    
    
}

JsonEncoder

import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import io.netty.util.ReferenceCountUtil;

import java.util.List;
import java.util.Map;

/**
 * @createTime 2023-08-08 20:11
 * @Description TODO
 */

public class JsonEncoder extends MessageToMessageEncoder<Map<String, Object>> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Map<String, Object> msg, List<Object> out) throws Exception {
        ReferenceCountUtil.release(msg);
    }

}

JsonDecoder不写了,extends MessageToMessageDecoder即可。

NoticeIotProcessor

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.util.HashedWheelTimer;
import io.netty.util.Timer;
import io.netty.util.TimerTask;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StopWatch;
import org.springframework.util.StringUtils;

import java.net.InetSocketAddress;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;


@Slf4j
@Component
public class NoticeIotProcessor implements BiConsumer<Integer, String> {

    final Timer timer = new HashedWheelTimer(Executors.defaultThreadFactory(), 100, TimeUnit.MILLISECONDS, 1024);

    @Override
    public void accept(Integer craftId, String equipmentNumber) {
        log.info("[NoticeIotProcessor] craftId:{},equipmentNumber:{}", craftId, equipmentNumber);

        CompletableFuture.runAsync(() -> {
            StopWatch stopWatch = new StopWatch("equipmentNumber");
            stopWatch.start(equipmentNumber);

            if (StringUtils.isEmpty(craftId) || StringUtils.isEmpty(equipmentNumber)) {
                log.info("[NoticeIotProcessor] 请求参数为空.return");
                return;
            }

            CraftEnum currentCraftEnum = CraftEnum.getCraftEnumByCode(craftId);
            if (Objects.isNull(currentCraftEnum)) {
                log.info("[NoticeIotProcessor] 根据工艺id:{}未找到对应面板工艺信息.return", craftId);
                return;
            }

            LineWayEnum currentLineWayEnum = LineWayEnum.getLineWayEnumByCode(currentCraftEnum.getLineId());
            if (Objects.isNull(currentLineWayEnum)) {
                log.info("[NoticeIotProcessor] 根据模块线路:{}为未找到对应modbus控制线路.return", currentCraftEnum.getLineId());
                return;
            }

            if (!TcpServerHandler.CHANNEL_MAP.containsKey(equipmentNumber)) {
                log.error("[NoticeIotProcessor] 根据设备编号:{}未找到对应channel.return", equipmentNumber);
                return;
            }

            Channel channel = TcpServerHandler.CHANNEL_MAP.get(equipmentNumber);
            InetSocketAddress inetSocketAddress = (InetSocketAddress) channel.remoteAddress();
            // 获取到channel的ip
            String address = inetSocketAddress.getAddress().getHostAddress();
            Integer delayTime = InitConfigurationFileServer.EQUIPMENT_BINDING_MODULE_MODEL_MAP.get(address).getDelayTime();

            TimerTask task = timeout -> {
                // 先断开
                channel.writeAndFlush(this.getByteBuf(currentLineWayEnum.getDisconnectCommand())).sync();
                log.info("[NoticeIotProcessor] 1发送[断开]指令完成,equipmentNumber={}", equipmentNumber);
            };
            timer.newTimeout(task, delayTime, TimeUnit.MILLISECONDS);

            task = timeout -> {
                // 再闭合
                channel.writeAndFlush(this.getByteBuf(currentLineWayEnum.getCloseCommand())).sync();
                log.info("[NoticeIotProcessor] 2发送[闭合]指令完成,equipmentNumber={}", equipmentNumber);
            };
            timer.newTimeout(task, 2L * delayTime, TimeUnit.MILLISECONDS);

            task = timeout -> {
                // 后断开
                channel.writeAndFlush(this.getByteBuf(currentLineWayEnum.getDisconnectCommand())).sync();
                log.info("[NoticeIotProcessor] 3发送[断开]指令完成,equipmentNumber={}", equipmentNumber);
            };
            timer.newTimeout(task, 3L * delayTime, TimeUnit.MILLISECONDS);

            stopWatch.stop();
            log.info("[PcbCraftProcessor] stopWatch:{}", StopWatchUtil.prettyPrintBySecond(stopWatch));
        });

    }

    private ByteBuf getByteBuf(String command) {
        ByteBuf buff = Unpooled.buffer();
        buff.writeBytes(ConvertCode.hexString2Bytes(command));
        return buff;
    }

}

可以看到NoticeIotProcessor类中,使用到了Netty的时间轮。主要是为了解决连续给设备发多条指令的问题。当时在调试阶段时发现如果不对每次发送的指令做控制,设备似乎会响应不过来即没有回应。

当时第一个尝试方案就是用来Thread.sleep()来控制的,可以达到效果。 但是后期将服务部署起来,设备也接入工厂后,调试时发现设备的运作根本不在预期的效果控制里,不是乱控制就是没响应,脑袋嗡嗡嗡~

另外,这个类中用到的InitConfigurationFileServer是通过预加载机制把相关配置读进来使用而已,不用即可忽略。

随后通过思考便干掉了sleep,引入了Netty的Timer来做时间间隔的控制,单位控制在毫秒,可以达到效果。 再次也将该问题抛出来,即:要连续给设备连续发多个指令时行业内的解决方案是什么,欢迎有大佬指导,感谢~

LineWayEnum

import java.util.Objects;

/**
 * @createTime 2023-08-09 16:53
 * @Description modbus线路指令
 */
public enum LineWayEnum {

    ONE(1, "01050000FF008C3A", "010500000000CDCA"),

    TWO(2, "01050001FF00DDFA", "0105000100009C0A"),

    THREE(3, "01050002FF002DFA", "0105000200006C0A"),

    FOUR(4, "01050003FF007C3A", "0105000300003DCA");

    /**
     * 线路/第几路
     */

    private final Integer lineId;

    /**
     * 闭合指令
     */
    private final String closeCommand;

    /**
     * 断开指令
     */
    private final String disconnectCommand;


    public Integer getLineId() {
        return lineId;
    }

    public String getCloseCommand() {
        return closeCommand;
    }

    public String getDisconnectCommand() {
        return disconnectCommand;
    }

    LineWayEnum(Integer lineId, String closeCommand, String disconnectCommand) {
        this.lineId = lineId;
        this.closeCommand = closeCommand;
        this.disconnectCommand = disconnectCommand;
    }

    public static LineWayEnum getLineWayEnumByCode(Integer lineId) {
        if (Objects.isNull(lineId)) {
            return null;
        }

        LineWayEnum[] values = LineWayEnum.values();
        for (LineWayEnum value : values) {
            if (value.getLineId().equals(lineId)) {
                return value;
            }
        }
        return null;
    }

}

这里是将设备调试后,便把ModBus的指令写死在枚举中,通过字符串的方式获取,发送时再转成16进制字节码的形式与硬件通信。

这里再引入一个点,即ModBus的指令可能随着设备参数的修改而变化,安装规则是需要对末尾2位做检查码(CRC),这里有篇文章有介绍:blog.csdn.net/qq_35358125…

ConvertCode

public class ConvertCode {
    /**
     * @Title:bytes2HexString
     * @Description:字节数组转16进制字符串
     * @param b
     *            字节数组
     * @return 16进制字符串
     * @throws
     */
    public static String bytes2HexString(byte[] b) {
        StringBuffer result = new StringBuffer();
        String hex;
        for (int i = 0; i < b.length; i++) {
            hex = Integer.toHexString(b[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            result.append(hex.toUpperCase());
        }
        return result.toString();
    }
    /**
     * @Title:hexString2Bytes
     * @Description:16进制字符串转字节数组
     * @param src  16进制字符串
     * @return 字节数组
     */
    public static byte[] hexString2Bytes(String src) {
        int l = src.length() / 2;
        byte[] ret = new byte[l];
        for (int i = 0; i < l; i++) {
            ret[i] = (byte) Integer.valueOf(src.substring(i * 2, i * 2 + 2), 16).byteValue();
        }
        return ret;
    }
    /**
     * @Title:string2HexString
     * @Description:字符串转16进制字符串
     * @param strPart  字符串
     * @return 16进制字符串
     */
    public static String string2HexString(String strPart) {
        StringBuffer hexString = new StringBuffer();
        for (int i = 0; i < strPart.length(); i++) {
            int ch = (int) strPart.charAt(i);
            String strHex = Integer.toHexString(ch);
            hexString.append(strHex);
        }
        return hexString.toString();
    }
    /**
     * @Title:hexString2String
     * @Description:16进制字符串转字符串
     * @param src
     *            16进制字符串
     * @return 字节数组
     * @throws
     */
    public static String hexString2String(String src) {
        String temp = "";
        for (int i = 0; i < src.length() / 2; i++) {
            //System.out.println(Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue());
            temp = temp+ (char)Integer.valueOf(src.substring(i * 2, i * 2 + 2),16).byteValue();
        }
        return temp;
    }

    /**
     * @Title:char2Byte
     * @Description:字符转成字节数据char-->integer-->byte
     * @param src
     * @return
     * @throws
     */
    public static Byte char2Byte(Character src) {
        return Integer.valueOf((int)src).byteValue();
    }

    /**
     * @Title:intToHexString
     * @Description:10进制数字转成16进制
     * @param a 转化数据
     * @param len 占用字节数
     * @return
     * @throws
     */
    public static String intToHexString(int a,int len){
        len<<=1;
        String hexString = Integer.toHexString(a);
        int b = len -hexString.length();
        if(b>0){
            for(int i=0;i<b;i++)  {
                hexString = "0" + hexString;
            }
        }
        return hexString;
    }


    /**
     * 将16进制的2个字符串进行异或运算
     * http://blog.csdn.net/acrambler/article/details/45743157
     * @param strHex_X
     * @param strHex_Y
     * 注意:此方法是针对一个十六进制字符串一字节之间的异或运算,如对十五字节的十六进制字符串异或运算:1312f70f900168d900007df57b4884
    先进行拆分:13 12 f7 0f 90 01 68 d9 00 00 7d f5 7b 48 84
    13 xor 12-->1
    1 xor f7-->f6
    f6 xor 0f-->f9
    ....
    62 xor 84-->e6
    即,得到的一字节校验码为:e6
     * @return
     */
    public static String xor(String strHex_X,String strHex_Y){
        //将x、y转成二进制形式
        String anotherBinary=Integer.toBinaryString(Integer.valueOf(strHex_X,16));
        String thisBinary=Integer.toBinaryString(Integer.valueOf(strHex_Y,16));
        String result = "";
        //判断是否为8位二进制,否则左补零
        if(anotherBinary.length() != 8){
            for (int i = anotherBinary.length(); i <8; i++) {
                anotherBinary = "0"+anotherBinary;
            }
        }
        if(thisBinary.length() != 8){
            for (int i = thisBinary.length(); i <8; i++) {
                thisBinary = "0"+thisBinary;
            }
        }
        //异或运算
        for(int i=0;i<anotherBinary.length();i++){
            //如果相同位置数相同,则补0,否则补1
            if(thisBinary.charAt(i)==anotherBinary.charAt(i))
                result+="0";
            else{
                result+="1";
            }
        }
        return Integer.toHexString(Integer.parseInt(result, 2));
    }


    /**
     *  Convert byte[] to hex string.这里我们可以将byte转换成int
     * @param src byte[] data
     * @return hex string
     */
    public static String bytes2Str(byte[] src){
        StringBuilder stringBuilder = new StringBuilder("");
        if (src == null || src.length <= 0) {
            return null;
        }
        for (int i = 0; i < src.length; i++) {
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if (hv.length() < 2) {
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }
    /**
     * @return 接收字节数据并转为16进制字符串
     */
    public static String receiveHexToString(byte[] by) {
        try {
            /*io.netty.buffer.WrappedByteBuf buf = (WrappedByteBuf)msg;
            ByteBufInputStream is = new ByteBufInputStream(buf);
            byte[] by = input2byte(is);*/
            String str = bytes2Str(by);
            str = str.toLowerCase();
            return str;
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("接收字节数据并转为16进制字符串异常");
        }
        return null;
    }

    /**
     * "7dd",4,'0'==>"07dd"
     * @param input 需要补位的字符串
     * @param size 补位后的最终长度
     * @param symbol 按symol补充 如'0'
     * @return
     * N_TimeCheck中用到了
     */
    public static String fill(String input, int size, char symbol) {
        while (input.length() < size) {
            input = symbol + input;
        }
        return input;
    }

}

ConvertCode工具类从网上抄来的,记录一下,感谢大佬。