3. Rpc优化:Netty通信

基础逻辑

因为socket通信是阻塞通信,会导致线程资源浪费,因此采用同步NIO框架netty来代替原本的BIOsocket通信。二者之间的原理对比可以参考

其实区别就在于多个客户端同时向服务端发出请求时,服务端是开启多线程处理(宕机风险BIO)还是只开启一个线程处理(BIO)。

因此本次demo的改动就是用Netty通信代替socket通信,同时创建自己的序列化协议以及创建一个通用的序列化接口(可以后续采用多种序列化方式)来代替之前的Serializable接口。

参考了博客以及博客

知识点

  1. Netty

参考博客:博客以及异步原理和部件

API

ChannelHandlerContext:参考博客

.sync(); 返回一个ChannelFuture对象,参考博客

  1. 拆包粘包(了解概念)
  2. 缓存
  3. 写入和刷新方法
  4. 心跳机制:在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。
  5. json类, jackson序列化

实现步骤

首先再Maven工程下的pom.xml中添加依赖

    <dependency>
      <groupId>io.netty</groupId>
      <artifactId>netty-all</artifactId>
      <version>4.1.48.Final</version>
      <scope>compile</scope>
    </dependency>
  1. 因为换成了netty通信,所以要创建抽象的客户端接口RpcClient和服务端接口RpcServer,使得采用不同网络通信方式就是对这两个接口的不同实现类,例如NettyRpcServer和SocketRpcServer。
public interface RpcServer {
    public void connect(int port);
}
public interface RpcClient {
    public Object sendRequest(RpcClientRequest rpcClientRequest);
}
  1. 接下来是对服务端进行接口实现,但是因为对netty’服务端配置时,要对序列化后的二进制流进行传输和处理,所以要先定义序列化协议、序列化统用接口、以及反序列化后的信息处理。

    自定义协议:定义序列化协议就像是can通讯协议一样,参考博客来定义,首先是协议的id,然后是传输的信息的类型,然后是序列化方式的类型编码、然后是传输信息的长度(防止粘包,上述四个都采用四个字节,也就是可以用writeInt方法直接服务,最后是传输的信息

    因为后续会采用jackson和kyro两种序列化方式,所以需要抽象出一个统用的序列化接口CommonSerializer,采用自定义编码的方式来识别序列化方式,接口的方法分别是序列化、反序列化、获取当前序列化方式的编码、根据编码选择序列化方式。

    public interface CommonSerializer {
       // 序列化类型的编码单独列出,获取编码
        int getCode();
        // 序列化方法
        byte[] serialize(Object obj);
        //反序列化 按照传输包的类型进行读取传输字节数组信息
        Object deserialize(byte[] bytes,Class<?> clazz);
        // 通过编码获取序列化类 静态方法可以不实例化对象,直接引用,但是需要定义
        static CommonSerializer getByCode(int code){
            switch (code){
                case 1:
                    return new JsonSerializer();
                default:
                    return null;
            }
        };
    }
    

    其中编码是协议中包含的信息,自定义一个枚举数组SerializableCode来决定。

    @AllArgsConstructor
    // 创建一个序列化类型的枚举,后续会加上kyro序列化
    public enum SerializableCode {
        JSON_Serializable(1);
        @Getter
        private final int code;
    }
    

    接下来是创建一个jackson序列化类JsonSerializer,对上述接口进行实现,需要注意的是jacson方式会导致传递参数的类型出错(因为jackson是转换成json字符串而不是字节数组),因此需要再RequestHandle方法中重新判断矫正(按照理想类型写、读)。但是首先要在pom.xml中导入依赖

        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.11.0</version>
        </dependency>
    
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-core</artifactId>
          <version>2.11.0</version>
        </dependency>
        <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-annotations</artifactId>
          <version>2.11.0</version>
        </dependency>
    
    public class JsonSerializer implements CommonSerializer{
        private static final Logger logger = LoggerFactory.getLogger(JsonSerializer.class);
        // 使用jackson序列器,其中ObjectMapper类是java对象和json结构之间转换的json工具类
        private ObjectMapper objectMapper = new ObjectMapper();
        @Override
        public int getCode() {
            return SerializableCode.valueOf("JSON_Serializable").getCode();
        }
    
        // 序列化
        @Override
        public byte[] serialize(Object obj) {
            try {
                return objectMapper.writeValueAsBytes(obj);
            } catch (JsonProcessingException e) {
                logger.error("序列化过程出错:{}",e.getMessage());
                e.printStackTrace();
                return null;
            }
        }
    
        // 反序列化
        @Override
        public Object deserialize(byte[] bytes, Class<?> clazz) {
            try {
                Object read = objectMapper.readValue(bytes,clazz);
                if (read instanceof RpcClientRequest){
                    logger.info("服务端可以反序列化得到一个request对象");
                    Object obj = RequestHandle(read);
                    // 这里不要忘了return!!!!
                    return obj;
                }else if(read instanceof Response){
                    logger.info("客户端可以反序列化得到一个Response对象");
                    return (Response)read;
                }
    
            } catch (IOException e) {
                logger.error("反序列化中出错:{}",e.getMessage());
                e.printStackTrace();
            }
            return null;
        }
    
        /* 处理反序列化得到的信息,因为Json反序列化过程中,不会保障原先传递的参数还是之前的类型,因此需要验证,如果
        不是就重新把参数信息转换成流,按照理想的参数类型重新写入。(参数类型是Class对象在反序列化中不会被修改)*/
        public Object RequestHandle(Object read){
            RpcClientRequest request = (RpcClientRequest)read;
            for (int i = 0; i < request.getParamTypes().length; i++ ){
                // 理想参数类型
                Class<?> paramType = request.getParamTypes()[i];
                // 反序列化后的参数类型和理想的参数类型不匹配
                if (!paramType.isAssignableFrom(request.getParameters()[i].getClass())){
                    try {
                        // 重写、读参数
                        byte[] paramByte = objectMapper.writeValueAsBytes(request.getParameters()[i]);
                        request.getParameters()[i] = objectMapper.readValue(paramByte,paramType);
                    } catch ( IOException e) {
                        logger.error("反序列化中参数重写出错:{}",e.getMessage());
                        e.printStackTrace();
                    }
                }
            }
            return request;
        }
    }
    

    这里注意几个点:ObjectMapper类是java对象和json结构之间转换的json工具类,进行读取和写入;其次是反序列化再客户端和服务端都需要用到,因此需要判断反序列化得到信息的类型是请求还是响应;最后的注意点是及时return信息,尤其是trycatch代码块中,要不然会报空指针异常(别问我是怎么知道的)。。

  2. 定义好序列化方式,就进行编码器和解码器类的定义,通过ByteBuf类来写入读取。

    编码器:定义一个通用编码器类CommonEncoder,继承netty自带的MessageToByteEncoder类。其实就是根据定义的协议进行数据写入,注意writeInt一次写入四个字节。实例化需要传输具体的序列化方法。

    public class CommonEncoder extends MessageToByteEncoder {
        /* 协议包括编码协议id、传递数据包类型、序列化类型、信息长度以及传输信息
         */
        private static final Logger logger = LoggerFactory.getLogger(CommonEncoder.class);
        private static final int id = 0x00;
        private final CommonSerializer serializer;
        public CommonEncoder(CommonSerializer serializer) {
            this.serializer = serializer;
        }
    
        @Override
        protected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) throws Exception {
            // id
            byteBuf.writeInt(id);
            // 传输包类型
            if(o instanceof RpcClientRequest){
                byteBuf.writeInt(PackageType.RpcClientRequest.getCode());
            }else if(o instanceof Response){
                byteBuf.writeInt(PackageType.RpcServerResponse.getCode());
            }else {
                byteBuf.writeInt(0);
                logger.error("传递的信息类型不在范围内");
            }
            // 序列化方式类型
            byteBuf.writeInt(serializer.getCode());
            // 信息长度
            byte[] msgBytes = serializer.serialize(o);
            byteBuf.writeInt(msgBytes.length);
            // 信息
            byteBuf.writeBytes(msgBytes);
        }
    }
    

    注意这里传输包的类型也是自定义一个枚举,包含客户端请求以及服务端的响应信息。

    @AllArgsConstructor
    public enum PackageType {
        RpcClientRequest(1),
        RpcServerResponse(2);
        @Getter
        private final int code;
    }
    

    解码器:继承ByteToMessageDecoder类,根据定义协议进行依次读取,需要注意的是注意当前读取次数和当前索引,确定当前读取数据,然后进行解码,解码之后的数据对象需要添加到 list 类参数中。

    public class CommonDecoder extends ByteToMessageDecoder {
        private static final Logger logger = LoggerFactory.getLogger(CommonDecoder.class);
        // 这里的id要和编码器的id相同,协议才能对的上
        private static final int id = 0x00;
        @Override
        protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
            // id 解码
            if (in.readInt() != id){
                logger.error("协议的id没对上,未识别的协议包");
                throw new RpcException(RpcError.Unknown_id);
            };
            // 传输包类型解码,这里注意用in.readInt() == 判断两次相当于读取两次,应该预定义变量
            int packageType = in.readInt();
            Class<?> packageTypeClass;
            if (packageType == PackageType.RpcClientRequest.getCode() ){
                packageTypeClass = RpcClientRequest.class;
            }else if(packageType == PackageType.RpcServerResponse.getCode()){
                packageTypeClass = Response.class;
            }else{
                logger.error("未识别的传输包类型");
                throw new RpcException(RpcError.Unknown_packageType);
            }
            // 序列化解码
            int serializableCode = in.readInt();
            CommonSerializer serializer = CommonSerializer.getByCode(serializableCode);
            if (serializer == null){
                logger.error("未识别的序列化方法");
                throw new RpcException(RpcError.Unknown_serializable);
            }
            // 信息长度解码
            int length = in.readInt();
            // 信息解码
            byte[] msg = new byte[length];
            in.readBytes(msg); //读取到msg中
            Object obj =  serializer.deserialize(msg,packageTypeClass);
            if (obj == null){
                logger.error("反序列化出错成为null");
            }
            //  !!!解码之后的数据对象需要添加到 list 对象里面
            list.add(obj);
        }
    }
    

    需要对demo2中的RpcError枚举进行扩充。

  3. 定义一个服务端的处理信息类NettyRpcServerHandler,注意父类是ChannelInboundHandlerAdapter。

和demo2中的处理逻辑类似,都是从注册表中取出服务,具体的反射调用流程再ThreadHandle中进行。因此需要工作现场和注册服务表两个属性,static代码块进行初始化,重写父类中的channelRead方法来处理channel中解码后传递的信息,exceptionCaught是异常处理。

public class NettyRpcServerHandler extends ChannelInboundHandlerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(NettyRpcServerHandler.class);
    private static ThreadHandle  threadHandle;
    private static ServiceRegistry serviceRegistry;
    // 初始化工作线程和注册服务表
    static{
        threadHandle = new ThreadHandle();
        serviceRegistry = new DefaultServiceRegistry();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        try{
            RpcClientRequest request = (RpcClientRequest)msg;
            logger.info("服务器收到请求信息");
            String interfaceName = request.getInterfacename();
            Object service =  serviceRegistry.getService(interfaceName);
            Object result = threadHandle.handle(service,request);
            // 写出数据
            ChannelFuture future = ctx.writeAndFlush(result);
            // 异步机制,添加监听确认消息发送完成后再关闭连接
            future.addListener(ChannelFutureListener.CLOSE);
        }finally {
            // 自动释放msg对象,防止对象池溢出,必须执行,所以放在finally中。
            ReferenceCountUtil.release(msg);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error("服务端处理请求出错",cause);
        cause.getMessage();
        // 关闭通道
        ctx.close();
    }
}

注意其中netty中的操作是异步的,ChannelFuture future=ch.writeAndFlush(message)也是异步的,一个ChannelFuture对象代表尚未发生的IO操作,该方法只是把发送消息加入了任务队列,这时直接关闭连接会导致问题。所以我们需要在消息发送完毕后在去关闭连接。 future.addListener(ChannelFutureListener.CLOSE);就是添加监听,当future确实完成之后关闭后发出回调信息。

除此之外无论try成功与否都要及时释放对象,防止对象池内存溢出。所以在finally中加ReferenceCountUtil.release(msg);可以参考博客

ThreadHandle类和demo2中相同,逻辑都是接收请求,反射调用,赋值给response类对象。

public class ThreadHandle {
    public Object handle(Object service, RpcClientRequest request) throws InvocationTargetException, IllegalAccessException {
        private static final Logger logger = LoggerFactory.getLogger(ThreadHandle.class);
        Method method;
        Object result;
        try{
            method = service.getClass().getMethod(request.getMethodname(),request.getParamTypes());
        }catch (Exception e){
            Response response = new Response().fail();
            return new RpcException(RpcError.Method_not_found);
        }
        // 参数是方法作用对象(服务端的实现类服务),以及参数
        logger.info("服务器找到方法:"+method.getName());
        try{
            result = method.invoke(service,request.getParameters());
        }catch (Exception e){
            Response response = new Response().fail();
            return new RpcException(RpcError.Method_not_used);
        }
        logger.info("服务器成功调用方法:"+method.getName());
        //设置响应信息
        Response response = new Response().success(result);
        return response;
    }
}
  1. 定义好上述服务端中需要的解码、编码、信息处理等类,就可以配置netty服务器,定义一个实现接口RpcServer的NettyRpcServer类。
public class NettyRpcServer implements RpcServer {
    private static final Logger logger = LoggerFactory.getLogger(NettyRpcServer.class);
    @Override
    public void connect(int port) {
        // 只有一个EventLoop,负责连接,如果连接成功,把channel转给workerGroup,EventLoopGroup相当于线程池
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        // 多个EventLoop,选择处理多个channel。
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        // 创建一个netty服务器
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup,workerGroup) //两个线程池作为参数
                .channel(NioServerSocketChannel.class)   //通道类型 定为非阻塞NIO
                .handler(new LoggingHandler(LogLevel.INFO)) 
                //表示请求线程全满时,系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数
                .option(ChannelOption.SO_BACKLOG,1024)
                // 是否开启 TCP 底层心跳机制 
                .childOption(ChannelOption.SO_KEEPALIVE,true)
                // TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。
                .childOption(ChannelOption.TCP_NODELAY,true)
                // 增加对输入流的处理逻辑 !!
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline channelPipeline = socketChannel.pipeline();
                        channelPipeline.addLast(new CommonEncoder(new JsonSerializer()));
                        channelPipeline.addLast(new CommonDecoder());

                        channelPipeline.addLast(new NettyRpcServerHandler());
                    }
                });

        try {
            // 监听客户端请求信息
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }
}

netty服务端类是ServerBootstrap,创建对象后对其配置,传入参数分别是bossGroup以及workerGroup,对应连接以及处理信息线程。

通道为阻塞NIO通道,打印日志级别为info,option中的配置可以参考博客。Nagle算法是为了尽可能发送大的数据块,TCP_NODELAY就是用于启用或关于Nagle算法。如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false。

最重要的就是配置处理逻辑,对当前通道的channelpipeline配置2、3中定义的编码器、解码器、请求处理类。使用bind方法监听指定端口的请求,channelFuture.channel().closeFuture().sync();的作用:个人理解是现有两个线程,一个是服务器监听线程,一个是信息处理的main线程,异步同时进行,但是信息处理后关闭服务器需要确定通道断开,所以该语句就是暂时令main线程处于wait状态,监听线程监听到通道关闭后,main线程继续在进入finally语句块关闭两个EventLoopGroup,可以参考博客

最后关闭两个EventLoopGroup。

  1. 客户端类似,首先定义一个处理服务端发回信息的NettyRpcClientHandler类,继承ChannelInboundHandlerAdapter。含有信息处理和异常处理两个方法。

    public class NettyRpcClientHandler extends ChannelInboundHandlerAdapter {
        private static final Logger logger = LoggerFactory.getLogger(NettyRpcClientHandler.class);
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            try{
                logger.info(String.format("客户端收到服务端返回的响应信息:%s",msg.toString()) );
                AttributeKey<Response> key = AttributeKey.valueOf("rpcResponse");
                ctx.channel().attr(key).set((Response) msg);
                // 关闭通道
                ctx.channel().close();
            }finally {
                // 释放对象
                ReferenceCountUtil.release(msg);
            }
    
        }
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            logger.error("客户端处理响应信息时发生错误:",cause);
            ctx.close();
        }
    }
    

    关键语句ctx.channel().attr(key).set((Response) msg);就是在通道中定义attr数组,使用返回的响应进行赋值。注意需要关闭通道以及释放对象,防止内存溢出。

  2. 创建Netty客户端NettyRpcClient类,需要配置通道,和服务端差不多,但是需要发送请求的sendRequest方法(逻辑和demo2一样)。

@AllArgsConstructor
public class NettyRpcClient implements RpcClient {
    private static final Logger logger = LoggerFactory.getLogger(NettyRpcClient.class);
    private String host;
    private int port;
    private static final Bootstrap bootstrap;
    // 初始化过程,定义客户端的连接
    static{
        //只有一个非阻塞处理线程
        EventLoopGroup eventGroup = new NioEventLoopGroup();
        // 客户端定义及配置
        bootstrap = new Bootstrap();
        bootstrap.group(eventGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE,true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    // 对SocketChannel类型通道进行 逻辑处理初始化
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new CommonDecoder())
                                .addLast(new CommonEncoder(new JsonSerializer()))
                                .addLast(new NettyRpcClientHandler());
                    }
                });
        }
    @Override
    public Object sendRequest(RpcClientRequest rpcClientRequest) {
        try {
            // 客户端连接
            ChannelFuture future = bootstrap.connect(host,port).sync();
            logger.info("客户端连接到服务器{}:{}",host,port);
            // 看未来客户端的通道情况
            Channel channel = future.channel();
            if (channel != null){
                channel.writeAndFlush(rpcClientRequest)
                        // 监听发送过程
                        .addListener(future1 ->{
                            if(future1.isSuccess()){
                                logger.info(String.format("客户端发送请求:%s",rpcClientRequest.toString()));
                            }else{
                                logger.error("客户端发送信息失败",future1.cause());
                            }
                        } );
                channel.closeFuture().sync();
                AttributeKey<Response> key = AttributeKey.valueOf("rpcResponse");
                Response response = channel.attr(key).get();
                return response;
            }
        } catch (InterruptedException e) {
            logger.error("客户端发送请求过程出错",e);
        }
        return null;
    }
}

注意通道是NioSocketChannel.class,sendRequest方法包括发送请求以及返回响应(从attr中取出),和服务端一样需要增加一个监听器满足异步回调机制,channel.closeFuture().sync();满足发送完成接收关闭事件之前不中断连接。

  1. 测试

    首先是服务端,和demo2逻辑一样(因为都是扩展的同一个接口),都是先创建服务对象,注册服务,再创建netty服务器对象,调用connect方法

    public class TestServer {
        public static void main(String[] args) {
            Remote_game_interface Tx_net = new Player_info_find(); // 接口实现类,也就是服务
            ServiceRegistry serviceRegistry = new DefaultServiceRegistry();
            serviceRegistry.register(Tx_net);
            NettyRpcServer rpcServer = new NettyRpcServer(); //服务器
            rpcServer.connect(9000);
        }
    }
    

    客户端首先需要修正一下动态代理类的代码,测试代码中需要创建一个新的netty客户端,再创建一个动态代理类,传入动态代理的接口Class对象,最后调用服务端处的方法。

    public class TestClient {
        public static void main(String[] args) {
            NettyRpcClient client = new NettyRpcClient("Localhost",9000);
            RpcClientProxy rpcClientProxy = new RpcClientProxy(client);
            Remote_game_interface client_proxy = rpcClientProxy.getProxy(Remote_game_interface.class);
            // 创建要查询的玩家 就是客户端向服务器传输的信息
            Player player = new Player(2508,"倪粑粑");
            // 使用客户端的代理调用服务器端的方法,信息由player提供
            String result = client_proxy.playerinfo(player);
            // 输出官网回复结果
            System.out.println("官网回复为:"+result);
        }
    }
    

    结果太长就贴一下服务端和客户端的最后的info

    查一查id为2508名字为倪粑粑的玩家,查封
     INFO [nioEventLoopGroup-3-1] - 服务器成功调用方法:playerinfo
    
     INFO [main] - 响应成功,服务器发回响应数据。。。
    官网回复为:告诉你好消息:官网查封该外挂玩家
    

bug以及解决

  1. slf4j日志输出的配置问题

    使用slf4j来代替System.out.println();打印,首先是再pom.xml中增加依赖

    <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-log4j12</artifactId>
          <version>1.7.28</version>
        </dependency>
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>1.7.28</version>
        </dependency>
    

    然后再使用过程中发现报错:WARN No appenders could be found for logger。参考博客 ,新建resource目录(Maven结构),新建log4j.properties,代码为

    log4j.rootLogger=INFO, stdout
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    

    注意这里log4j.rootLogger=INFO, stdout,如果是WARN等级大于INFO,所以控制台显示不了info信息。

    控制台如果没有显示info信息,在resource目录下新建logback.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    
    <configuration scan="true" scanPeriod="3 seconds">
        <!--设置日志输出为控制台-->
        <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%X{userId}] [%X{requestId}] %logger - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!--设置日志输出为文件-->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <File>logFile.log</File>
            <rollingPolicy  class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <FileNamePattern>logFile.%d{yyyy-MM-dd_HH-mm}.log.zip</FileNamePattern>
            </rollingPolicy>
    
            <layout class="ch.qos.logback.classic.PatternLayout">
                <Pattern>%d{HH:mm:ss,SSS} [%thread] %-5level %logger{32} - %msg%n</Pattern>
            </layout>
        </appender>
    
        <root>
            <level value="DEBUG"/>
            <appender-ref ref="STDOUT"/>
            <appender-ref ref="FILE"/>
        </root>
    </configuration>
    
  2. lombok注解的使用,不用再程序中自己写构造器和get方法,可以参考博客,本例中主要使用的就是无参构造@NoArgsConstructor、有参构造@AllArgsConstructor(类前)、参数get方法@Getter(成员变量前)

    需要在idea中file setting plugin 中下载lombok插件,安装完成后再pom.xml导入依赖

    <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
          <version>1.18.10</version>
          <scope>provided</scope>
        </dependency>
    
  3. 报错: jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of

    jackson反序列化必须要存在一个无参构造函数,因此在本例中的Player类,也就是传入服务的参数类,需要加上一个无参构造@NoArgsConstructor。(之前为了传递信息只有一个全参构造@AllArgsConstructor)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值