SpringMVC项目同时启动Netty服务,并解决Netty服务中Dao层依赖注入为null问题

前言:
项目为SpringMVC架构,在此基础上新增加了Netty服务用来接收客户端发来的消息,在Netty的handler业务处理类内需要将消息存入数据库.
后续补充: 限制了客户端接入的数量不超过两个

流程:
先看一下包的结构图
按照此包结构图奉上精心修改过的伪代码,基本上可以拿走就直接使用的
在这里插入图片描述

图片蓝色背景的都为netty(ats)相关服务新增加的文件

1.netty服务端,实现了Runnable接口,为的是可以在重写的run()方法内启动服务端(没整合之前是在下边写了一个main函数启动的,当然也没有实现Runnable接口);因为在初始化时候使用了new关键字创建出来的ChannelInitializer《SocketChannel》对象以及包含的对象,这些对象是在netty创建的,并没有将此对象交由spring容器管理(SpringMVC启动时,只有被扫描到的bean才会被初始化,只有被初始化的bean才会加入spring容器,只有加入spring容器的bean才可以用@Autowired或@Resource来获得),所以这是在handler业务处类或是数据存库类里注入的dao层依赖为一直为null的原因

/**
 * netty服务端
 */
public class ServerP implements Runnable{

    public void bind(int port) throws InterruptedException {
        //创建用于接受客户端的线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        //创建用于处理网络操作的线程组
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            //创建服务端启动助手用于配置参数
            ServerBootstrap b = new ServerBootstrap();
            //设置2个线程组
            b.group(bossGroup,workerGroup)
                    //设置通道的底层实现
                    .channel(NioServerSocketChannel.class)
                    //option是NioServerSocketChannel提供的用来接收进来的连接
                    .option(ChannelOption.SO_BACKLOG,2048)
                    //childOption是NioServerSocketChannel接收到的连接
                    .childOption(ChannelOption.SO_KEEPALIVE,true)
                    //childHandler为建立连接之后的执行器
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel sc) throws Exception {
                        	//0.ByteOrder.LITTLE_ENDIAN设置接收的字节序为小端传输(ByteOrder.BIG_ENDIAN默认为大端传输)
                            //1.单个包最大长度
                            //2.长度字段开始位置偏移量
                            //3.长度字段所占字节数
                            //4.长度字段默认表示后续报文的长度(公式:数据包总长度-长度字段偏移量-长度字段字节数-长度字段表示的长度值)
                            //5.解码过程中没有丢弃任何数据
                            //6.默认为true,表示读取到长度域的值超过Integer.MAX_VALUE时抛异常,false表示真正读取完才抛异常,建议不要修改,否则可能会造成内存溢出
                            sc.pipeline().addLast("frameDecoder",new LengthFieldBasedFrameDecoder(ByteOrder.LITTLE_ENDIAN,Integer.MAX_VALUE,2,2,3,0,true));
                            //对接收到的客户端消息进行解码
                            sc.pipeline().addLast("decoder",new MsgDeCode());
                            //sc.pipeline().addLast("encode",new MsgEnCode());
                            sc.pipeline().addLast("handler",new SHandler());
                        }
                    });
            //绑定端口,同步等待
            ChannelFuture cf = b.bind(port).sync();
            //监听服务器端口关闭
            cf.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            //优雅推出,释放线程池资源
            System.out.println("服务器关闭 >>>");
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
    /**
     * run方法开启netty
     */
    @Override
    public void run() {
        int port = 9527;
        System.out.println("netty服务端已经启动 >>>");
        try {
            new ServerP().bind(port);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.消息解码类,将接收到的消息解析为java对象;继承MessageToMessageDecoder《ByteBuf》,重写decode方法;对于处理ByteBuf缓冲区数据,个人目前使用的有两种方法,第一可以直接读取ByteBuf缓冲区内的数据进行操作,第二也可以将接收到的数据存放arr数组内,后续对arr数组内的数据进行操作;两种方法可以根据实际情况使用

/**
 * Netty解码类
 * 将消息解析并存放到java对象
 */
public class MsgDeCode extends MessageToMessageDecoder<ByteBuf> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> list) throws Exception {
        //可读字节数
        int i = msg.readableBytes();
        System.out.println("可读字节 = " + i);
        //先判断已知字节数
        if(i < 28){
            System.out.println("单个包长度小于28字节,退出!");
            return;
        }
        //将读到的数据放入arr数组,后续也可以直接操作数组内存放的数据
        byte[] arr = new byte[i];
        //从指定的绝对索引开始,将缓冲区的数据传输到指定的目的地
        msg.getBytes(msg.readerIndex(),arr,0,i);

        //准备存储对象:
        // 1 接收的数据
        MsgInfo msgInfo = new MsgInfo();
        // 2 存库数据
        ArrayList<DataInfo> listDataInfo = new ArrayList<>();
        //标记ByteBuf缓存内指针的起始位
        //msg.markReaderIndex();
        //获取帧头
        byte head1 = msg.readByte();
        byte head2 = msg.readByte();
        if((head1 & 0xff) != 0xb0 || (head2 & 0xff) != 0xb0){
        	//指针复位
            //msg.resetReaderIndex();
            System.out.println("帧头不正确,退出!");
            return;
        }
        int head = (head1 & 0xff * 256) + (head2 & 0xff);
        msgInfo.setHead(head);
        System.out.println("head = " + head);
        //获取len
        byte len1 = msg.readByte();
        byte len2 = msg.readByte();
        int len = (len1 & 0xff * 256) + (len2 & 0xff);
        msgInfo.setDataLen(len);
        System.out.println("len = " + len);
        //获取帧类型
        byte type = msg.readByte();
        if(type != 0x51){
        	//指针复位
            //msg.resetReaderIndex();
            System.out.println("帧类型不正确,退出!");
            return;
        }
        msgInfo.setHeadType(type);
        System.out.println("type = " + type);
        //存入时间
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String inTime = simpleDateFormat.format(new Date());
        System.out.println("inTime = " + inTime);
        //获取carNum 
        byte num = msg.readByte();
        int carNum = num & 0xff;
        String carNums = String.valueOf(carNum);
        System.out.println("carNum = " + carNum);
        //准备后续需要校验的数据
        byte[] crc = new byte[(carNum * 20) + 1];
        System.arraycopy(arr,5,crc,0,crc.length);
        //定义一个变量用来控制do-while循环
        int count = 0;
        do {
            DataInfo dataInfo = new DataInfo();
            //存入接收时间
            dataInfo.setInTime(inTime);
            //存入carNum
            dataInfo.setCarNum(carNums);
            //获取id
            byte id1 = msg.readByte();
            byte id2 = msg.readByte();
            String s1 = String.valueOf(id1 & 0xff);
            String s2 = String.valueOf(id2 & 0xff);
            StringBuilder sb = new StringBuilder();
            StringBuilder append = sb.append(s1).append(s2);
            dataInfo.setCarId(append.toString());
            System.out.println("id = " + append.toString());
            //获取经度
            long jd1 = msg.readLong();
            String jds = String.valueOf(jd1);
            String jd = sTos(3, 7, jds);
            dataInfo.setLongitude(jd);
            System.out.println("经度 = " + jd);
            //获取纬度
            long wd1 = msg.readLong();
            String wds = String.valueOf(wd1);
            String wd = sTos(2, 7, wds);
            dataInfo.setLatitude(wd);
            System.out.println("纬度 = " + wd);
            //获取速度
            byte spe1 = msg.readByte();
            byte spe2 = msg.readByte();
            int speed = (spe1 & 0xff * 256) + (spe2 & 0xff);
            String speeds = String.valueOf(speed);
            dataInfo.setCarSpeed(speeds);
            System.out.println("speed = " + speed);
            listDataInfo.add(dataInfo);
            count++;
        }while (count < carNum);
        //获取校验码
        byte check1 = msg.readByte();
        byte check2 = msg.readByte();
        int checkCode = (check1 & 0xff * 256) + (check2 & 0xff);
        msgInfo.setCheckCode(checkCode);
        System.out.println("checkCode = " + checkCode);
        msgInfo.setDataInfo(listDataInfo);
        int crc16yh = CRC16.getCRC16yh(crc);
        if(checkCode == crc16yh){
            list.add(msgInfo);
        }else {
            System.out.println("CRC校验码异常!");
            return;
        }
    }
}

3.服务端处理器类,继承的SimpleChannelInboundHandler《MsgInfo》,泛型为接收完整消息的实体类,个人感觉还是很方便后续操作的

/**
 * Netty服务端处理器类
 */
public class SHandler extends SimpleChannelInboundHandler<MsgInfo> {

	//用于高并发计数的类,保证在自增或者自减的情况下的线程安全
    //private AtomicInteger atomicInteger;
    
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("调用 channelActive方法 >>>");
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println("来自客户端:" + ctx.channel().remoteAddress() + " >>> 时间:" + time);
    }
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, MsgInfo msg) throws Exception {
        System.out.println("进入业务处理 >>>");
        //每有一个连接就自增1
        //atomicInteger.incrementAndGet();
        //判断是否大于2个连接
        if(atomicInteger.get() > 2){
            ctx.channel().close();
        }
        //将msg消息json格式化
        String s = JSON.toJSONString(msg);
        System.out.println("s = " + s);
        //声明生产者发送数据给kafka的类
        //ProducerGotoKafka producer = new ProducerGotoKafka();
        //输入kafka的topic,传入json
        //将数据发送至kafka
        //producer.send("",s);
        //将数据存入数据库
        MsgInfo msgInfo= JSON.parseObject(s, MsgInfo.class);
        System.out.println("msgInfo= " + msgInfo.toString());
        //创建存库类
        AddInfo addInfo = new AddInfo(msgInfo);
        addInfo.start();
        
        String resp = "OK";
        ByteBuf buf = Unpooled.copiedBuffer(resp.getBytes());
        ctx.write(buf);
    }

	//@Override
    //public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //连接结束自减1
        //atomicInteger.decrementAndGet();
    //}

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

4.数据存库类,该类继承了Thread重写了run()方法,在这个类里边对需要的数据存入数据库,这里用到了dao层的依赖注入,首先在本类加上@Component注解(将这个对象交由spring容器管理),然后声明静态变量,依赖正常注入,声明传入构造方法的参数,声明构造方法,然后@PostConstruct这个是重点

/**
 * 开启新线程执行数据存库
 */
//此注解等同于在xml文件内注册
//xml文件写法
//<bean id="AddInfo" class="com.xx.xx.AddInfo"></bean>
@Component
public class AddInfo extends Thread{
   private static AddInfo addInfo;
    //注入依赖
    @Resource
    private DaoSupport dao;
    
    //声明传入构造方法的参数
    private MsgInfo msgInfo;

    public AddInfo(MsgInfo msgInfo) {
        this.msgInfo = msgInfo;
    }

    public AddInfo() {
    }
    //容器初始化的时候执行这里,这里是重点
    //因为没有交给spring容器管理,所以一定要先初始化,否则依赖注入失效
    @PostConstruct
    public void init(){
        addInfo = this;
        addInfo.dao = this.dao;
    }
    @Override
    public void run() {
        List<DataInfo> dataInfo = msgInfo.getDataInfo();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        for (DataInfo info : dataInfo) {
            try {
                int row1 = (int) addInfo.dao.save("AtsMapper.add1", info);
                int row2 = (int) addInfo.dao.save("AtsMapper.add2", info);
                if(row1 > 0){
                    System.out.println(">>> 信息存库成功 >>>" + time);
                }else {
                    System.out.println("<<< 信息存库失败 <<<" + time);
                }
                if(row2 > 0){
                    System.out.println(">>> 信息存库成功 >>>" + time);
                }else {
                    System.out.println("<<< 信息存库失败 <<<" + time);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

5.监听项目启动同时启动netty服务,实现的是ServletContextListener接口:对应监控application内置对象的创建和销毁,注解相比修改配置文件内容来说既方便又快捷,不用再去修改别人之前写好的配置文件内容,这样也更有利于维护代码

//此注解方法等同于在web.xml文件内注册
//web.xml写法
//<listener>
//<listener-class>com.xx.xx.StartNettyContextListener</listener-class>
//</listener>
@WebListener
public class StartNettyContextListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServerP serverP = new ServerP();
        System.out.println("项目启动netty启动 >>>");
        //需要在新建的线程内启动netty,如果在这里直接调用serverP.run()方法则会造成项目主线程的阻塞
        Thread t = new Thread(serverP);
        t.start();
    }
    
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {

    }
}

最后奉上Netty API Reference (5.0.0.Alpha2),希望大家在使用过程中更顺手
https://netty.io/5.0/api/?spm=a2c4e.10696291.0.0.7fde19a4ZR9zc3

总结:参考了网上很多的方法,里边还有很多可以优化的地方,写这样的业务经验也不足,如有更好的解决办法,还请各位不吝赐教,共同研讨,共同进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值