阅读这篇文章之前,建议先阅读和这篇文章关联的内容。

[1]详细剖析分布式微服务架构下网络通信的底层实现原理(图解)

[2][年薪60W的技巧]工作了5年,你真的理解Netty以及为什么要用吗?(深度干货)

[3]深度解析Netty中的核心组件(图解+实例)

[4]BAT面试必问细节:关于Netty中的ByteBuf详解

[5]通过大量实战案例分解Netty中是如何解决拆包黏包问题的?

[6]基于Netty实现自定义消息通信协议(协议设计及解析应用实战)

[7]全网最详细最齐全的序列化技术及深度解析与应用实战

在前面的内容中,我们已经由浅入深的理解了Netty的基础知识和实现原理,相信大家已经对Netty有了一个较为全面的理解。那么接下来,我们通过一个手写RPC通信的实战案例来带大家了解Netty的实际应用。

为什么要选择RPC来作为实战呢?因为Netty本身就是解决通信问题,而在实际应用中,RPC协议框架是我们接触得最多的一种,所以这个实战能让大家了解到Netty实际应用之外,还能理解RPC的底层原理。

什么是RPC

RPC全称为(Remote Procedure Call),是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,简单理解就是让开发者能够像调用本地服务一样调用远程服务。

既然是协议,那么它必然有协议的规范,如图6-1所示。

为了达到“让开发者能够像调用本地服务那样调用远程服务”的目的,RPC协议需像图6-1那样实现远程交互。

  • 客户端调用远程服务时,必须要通过本地动态代理模块来屏蔽网络通信的细节,所以动态代理模块需要负责将请求参数、方法等数据组装成数据包发送到目标服务器
  • 这个数据包在发送时,还需要遵循约定的消息协议以及序列化协议,最终转化为二进制数据流传输
  • 服务端收到数据包后,先按照约定的消息协议解码,得到请求信息。
  • 服务端再根据请求信息路由调用到目标服务,获得结果并返回给客户端。

图6-1

业内主流的RPC框架

凡是满足RPC协议的框架,我们成为RPC框架,在实际开发中,我们可以使用开源且相对成熟的RPC框架解决微服务架构下的远程通信问题,常见的rpc框架:

  1. Thrift:thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml 这些编程语言间无缝结合的、高效的服务。
  2. Dubbo:Dubbo是一个分布式服务框架,以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。 Dubbo是阿里巴巴内部的SOA服务化治理方案的核心框架,Dubbo自2011年开源后,已被许多非阿里系公司使用。

手写RPC注意要点

基于上文中对于RPC协议的理解,如果我们自己去实现,需要考虑哪些技术呢? 其实基于图6-1的整个流程应该有一个大概的理解。

  • 通信协议,RPC框架对性能的要求非常高,所以通信协议应该是越简单越好,这样可以减少编解码带来的性能损耗,大部分主流的RPC框架会直接选择TCP、HTTP协议。
  • 序列化和反序列化,数据要进行网络传输,需要对数据进行序列化和反序列化,前面我们说过,所谓的序列化和反序列化是不把对象转化成二进制流以及将二进制流转化成对象的过程。在序列化框架选择上,我们一般会选择高效且通用的算法,比如FastJson、Protobuf、Hessian等。这些序列化技术都要比原生的序列化操作更加高效,压缩比也较高。
  • 动态代理, 客户端调用远程服务时,需要通过动态代理来屏蔽网络通信细节。而动态代理又是在运行过程中生成的,所以动态代理类的生成速度、字节码大小都会影响到RPC整体框架的性能和资源消耗。常见的动态代理技术: Javassist、Cglib、JDK的动态代理等。

基于Netty手写实现RPC

理解了RPC协议后,我们基于Netty来实现一个RPC通信框架。

代码详见附件 netty-rpc-example

图6-2 项目模块组成

需要引入的jar包:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>

模块依赖关系:

  • provider依赖 netty-rpc-protocol和netty-rpc-api

  • cosumer依赖 netty-rpc-protocol和netty-rpc-api

netty-rpc-api模块

图6-3 netty-rpc-api模块组成

IUserService

public interface IUserService {

    String saveUser(String name);
}

netty-rpc-provider模块

图6-4 netty-rpc-provider模块组成

UserServiceImpl

@Service
@Slf4j
public class UserServiceImpl implements IUserService {
@Override
public String saveUser(String name) {
log.info("begin saveUser:"+name);
return "Save User Success!";
}
}

NettyRpcProviderMain

注意,在当前步骤中,描述了case的部分,暂时先不用加,后续再加上

@ComponentScan(basePackages = {"com.example.spring","com.example.service"})  //case1(后续再加上)
@SpringBootApplication
public class NettyRpcProviderMain { public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcProviderMain.class, args);
new NettyServer("127.0.0.1",8080).startNettyServer(); //case2(后续再加上)
}
}

netty-rpc-protocol

开始写通信协议模块,这个模块主要做几个事情

  • 定义消息协议
  • 定义序列化反序列化方法
  • 建立netty通信

图6-5

定义消息协议

之前我们讲过自定义消息协议,我们在这里可以按照下面这个协议格式来定义好。

    /*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/

Header

@AllArgsConstructor
@Data
public class Header implements Serializable {
/*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
private short magic; //魔数-用来验证报文的身份(2个字节)
private byte serialType; //序列化类型(1个字节)
private byte reqType; //操作类型(1个字节)
private long requestId; //请求id(8个字节)
private int length; //数据长度(4个字节) }

RpcRequest

@Data
public class RpcRequest implements Serializable {
private String className;
private String methodName;
private Object[] params;
private Class<?>[] parameterTypes;
}

RpcResponse

@Data
public class RpcResponse implements Serializable { private Object data;
private String msg;
}

RpcProtocol

@Data
public class RpcProtocol<T> implements Serializable {
private Header header;
private T content;
}

定义相关常量

上述消息协议定义中,涉及到几个枚举相关的类,定义如下

ReqType

消息类型

public enum ReqType {

    REQUEST((byte)1),
RESPONSE((byte)2),
HEARTBEAT((byte)3); private byte code; private ReqType(byte code) {
this.code=code;
} public byte code(){
return this.code;
}
public static ReqType findByCode(int code) {
for (ReqType msgType : ReqType.values()) {
if (msgType.code() == code) {
return msgType;
}
}
return null;
}
}

SerialType

序列化类型

public enum SerialType {

    JSON_SERIAL((byte)0),
JAVA_SERIAL((byte)1); private byte code; SerialType(byte code) {
this.code=code;
} public byte code(){
return this.code;
}
}

RpcConstant

public class RpcConstant {
//header部分的总字节数
public final static int HEAD_TOTAL_LEN=16;
//魔数
public final static short MAGIC=0xca;
}

定义序列化相关实现

这里演示两种,一种是JSON方式,另一种是Java原生的方式

ISerializer

public interface ISerializer {

    <T> byte[] serialize(T obj);

    <T> T deserialize(byte[] data,Class<T> clazz);

    byte getType();
}

JavaSerializer

public class JavaSerializer implements ISerializer{

    @Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArrayOutputStream=
new ByteArrayOutputStream();
try {
ObjectOutputStream outputStream=
new ObjectOutputStream(byteArrayOutputStream); outputStream.writeObject(obj); return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return new byte[0];
} @Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
ByteArrayInputStream byteArrayInputStream=new ByteArrayInputStream(data);
try {
ObjectInputStream objectInputStream=
new ObjectInputStream(byteArrayInputStream); return (T) objectInputStream.readObject(); } catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
} @Override
public byte getType() {
return SerialType.JAVA_SERIAL.code();
}
}

JsonSerializer

public class JsonSerializer implements ISerializer{
@Override
public <T> byte[] serialize(T obj) {
return JSON.toJSONString(obj).getBytes();
} @Override
public <T> T deserialize(byte[] data, Class<T> clazz) {
return JSON.parseObject(new String(data),clazz);
} @Override
public byte getType() {
return SerialType.JSON_SERIAL.code();
}
}

SerializerManager

实现对序列化机制的管理

public class SerializerManager {

    private final static ConcurrentHashMap<Byte, ISerializer> serializers=new ConcurrentHashMap<Byte, ISerializer>();

    static {
ISerializer jsonSerializer=new JsonSerializer();
ISerializer javaSerializer=new JavaSerializer();
serializers.put(jsonSerializer.getType(),jsonSerializer);
serializers.put(javaSerializer.getType(),javaSerializer);
} public static ISerializer getSerializer(byte key){
ISerializer serializer=serializers.get(key);
if(serializer==null){
return new JavaSerializer();
}
return serializer;
}
}

定义编码和解码实现

由于自定义了消息协议,所以 需要自己实现编码和解码,代码如下

RpcDecoder

@Slf4j
public class RpcDecoder extends ByteToMessageDecoder { /*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
log.info("==========begin RpcDecoder ==============");
if(in.readableBytes()< RpcConstant.HEAD_TOTAL_LEN){
//消息长度不够,不需要解析
return;
}
in.markReaderIndex();//标记一个读取数据的索引,后续用来重置。
short magic=in.readShort(); //读取magic
if(magic!=RpcConstant.MAGIC){
throw new IllegalArgumentException("Illegal request parameter 'magic',"+magic);
}
byte serialType=in.readByte(); //读取序列化算法类型
byte reqType=in.readByte(); //请求类型
long requestId=in.readLong(); //请求消息id
int dataLength=in.readInt(); //请求数据长度
//可读区域的字节数小于实际数据长度
if(in.readableBytes()<dataLength){
in.resetReaderIndex();
return;
}
//读取消息内容
byte[] content=new byte[dataLength];
in.readBytes(content); //构建header头信息
Header header=new Header(magic,serialType,reqType,requestId,dataLength);
ISerializer serializer=SerializerManager.getSerializer(serialType);
ReqType rt=ReqType.findByCode(reqType);
switch(rt){
case REQUEST:
RpcRequest request=serializer.deserialize(content, RpcRequest.class);
RpcProtocol<RpcRequest> reqProtocol=new RpcProtocol<>();
reqProtocol.setHeader(header);
reqProtocol.setContent(request);
out.add(reqProtocol);
break;
case RESPONSE:
RpcResponse response=serializer.deserialize(content,RpcResponse.class);
RpcProtocol<RpcResponse> resProtocol=new RpcProtocol<>();
resProtocol.setHeader(header);
resProtocol.setContent(response);
out.add(resProtocol);
break;
case HEARTBEAT:
break;
default:
break;
} }
}

RpcEncoder

@Slf4j
public class RpcEncoder extends MessageToByteEncoder<RpcProtocol<Object>> { /*
+----------------------------------------------+
| 魔数 2byte | 序列化算法 1byte | 请求类型 1byte |
+----------------------------------------------+
| 消息 ID 8byte | 数据长度 4byte |
+----------------------------------------------+
*/
@Override
protected void encode(ChannelHandlerContext ctx, RpcProtocol<Object> msg, ByteBuf out) throws Exception {
log.info("=============begin RpcEncoder============");
Header header=msg.getHeader();
out.writeShort(header.getMagic()); //写入魔数
out.writeByte(header.getSerialType()); //写入序列化类型
out.writeByte(header.getReqType());//写入请求类型
out.writeLong(header.getRequestId()); //写入请求id
ISerializer serializer= SerializerManager.getSerializer(header.getSerialType());
byte[] data=serializer.serialize(msg.getContent()); //序列化
header.setLength(data.length);
out.writeInt(data.length); //写入消息长度
out.writeBytes(data);
}
}

NettyServer

实现NettyServer构建。

@Slf4j
public class NettyServer{
private String serverAddress; //地址
private int serverPort; //端口 public NettyServer(String serverAddress, int serverPort) {
this.serverAddress = serverAddress;
this.serverPort = serverPort;
} public void startNettyServer() throws Exception {
log.info("begin start Netty Server");
EventLoopGroup bossGroup=new NioEventLoopGroup();
EventLoopGroup workGroup=new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new RpcServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(this.serverAddress, this.serverPort).sync();
log.info("Server started Success on Port:{}", this.serverPort);
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
log.error("Rpc Server Exception",e);
}finally {
workGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}

RpcServerInitializer

public class RpcServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,12,4,0,0))
.addLast(new RpcDecoder())
.addLast(new RpcEncoder())
.addLast(new RpcServerHandler());
}
}

RpcServerHandler

public class RpcServerHandler extends SimpleChannelInboundHandler<RpcProtocol<RpcRequest>> {

    @Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol<RpcRequest> msg) throws Exception {
RpcProtocol resProtocol=new RpcProtocol<>();
Header header=msg.getHeader();
header.setReqType(ReqType.RESPONSE.code());
Object result=invoke(msg.getContent());
resProtocol.setHeader(header);
RpcResponse response=new RpcResponse();
response.setData(result);
response.setMsg("success");
resProtocol.setContent(response); ctx.writeAndFlush(resProtocol);
} private Object invoke(RpcRequest request){
try {
Class<?> clazz=Class.forName(request.getClassName());
Object bean= SpringBeansManager.getBean(clazz); //获取实例对象(CASE)
Method declaredMethod=clazz.getDeclaredMethod(request.getMethodName(),request.getParameterTypes());
return declaredMethod.invoke(bean,request.getParams());
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
} @Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
}
}

SpringBeansManager

@Component
public class SpringBeansManager implements ApplicationContextAware {
private static ApplicationContext applicationContext; @Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringBeansManager.applicationContext=applicationContext;
} public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}

需要注意,这个类的构建好之后,需要在netty-rpc-provider模块的main方法中增加compone-scan进行扫描

@ComponentScan(basePackages = {"com.example.spring","com.example.service"})  //修改这里
@SpringBootApplication
public class NettyRpcProviderMain { public static void main(String[] args) throws Exception {
SpringApplication.run(NettyRpcProviderMain.class, args);
new NettyServer("127.0.0.1",8080).startNettyServer(); // 修改这里
}
}

netty-rpc-consumer

接下来开始实现消费端

RpcClientProxy

public class RpcClientProxy {

    public <T> T clientProxy(final Class<T> interfaceCls,final String host,final int port){
return (T) Proxy.newProxyInstance
(interfaceCls.getClassLoader(),
new Class<?>[]{interfaceCls},
new RpcInvokerProxy(host,port));
}
}

RpcInvokerProxy

@Slf4j
public class RpcInvokerProxy implements InvocationHandler { private String serviceAddress;
private int servicePort; public RpcInvokerProxy(String serviceAddress, int servicePort) {
this.serviceAddress = serviceAddress;
this.servicePort = servicePort;
} @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("begin invoke target server");
//组装参数
RpcProtocol<RpcRequest> protocol=new RpcProtocol<>();
long requestId= RequestHolder.REQUEST_ID.incrementAndGet();
Header header=new Header(RpcConstant.MAGIC, SerialType.JSON_SERIAL.code(), ReqType.REQUEST.code(),requestId,0);
protocol.setHeader(header);
RpcRequest request=new RpcRequest();
request.setClassName(method.getDeclaringClass().getName());
request.setMethodName(method.getName());
request.setParameterTypes(method.getParameterTypes());
request.setParams(args);
protocol.setContent(request);
//发送请求
NettyClient nettyClient=new NettyClient(serviceAddress,servicePort);
//构建异步数据处理
RpcFuture<RpcResponse> future=new RpcFuture<>(new DefaultPromise<>(new DefaultEventLoop()));
RequestHolder.REQUEST_MAP.put(requestId,future);
nettyClient.sendRequest(protocol);
return future.getPromise().get().getData();
}
}

定义客户端连接

在netty-rpc-protocol这个模块的protocol包路径下,创建NettyClient

@Slf4j
public class NettyClient {
private final Bootstrap bootstrap;
private final EventLoopGroup eventLoopGroup=new NioEventLoopGroup();
private String serviceAddress;
private int servicePort;
public NettyClient(String serviceAddress,int servicePort){
log.info("begin init NettyClient");
bootstrap=new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new RpcClientInitializer());
this.serviceAddress=serviceAddress;
this.servicePort=servicePort;
} public void sendRequest(RpcProtocol<RpcRequest> protocol) throws InterruptedException {
ChannelFuture future=bootstrap.connect(this.serviceAddress,this.servicePort).sync();
future.addListener(listener->{
if(future.isSuccess()){
log.info("connect rpc server {} success.",this.serviceAddress);
}else{
log.error("connect rpc server {} failed .",this.serviceAddress);
future.cause().printStackTrace();
eventLoopGroup.shutdownGracefully();
}
});
log.info("begin transfer data");
future.channel().writeAndFlush(protocol);
}
}

RpcClientInitializer

@Slf4j
public class RpcClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
log.info("begin initChannel");
ch.pipeline()
.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,12,4,0,0))
.addLast(new LoggingHandler())
.addLast(new RpcEncoder())
.addLast(new RpcDecoder())
.addLast(new RpcClientHandler());
}
}

RpcClientHandler

需要注意,Netty的通信过程是基于入站出站分离的,所以在获取结果时,我们需要借助一个Future对象来完成。

@Slf4j
public class RpcClientHandler extends SimpleChannelInboundHandler<RpcProtocol<RpcResponse>> { @Override
protected void channelRead0(ChannelHandlerContext ctx, RpcProtocol<RpcResponse> msg) throws Exception {
log.info("receive rpc server result");
long requestId=msg.getHeader().getRequestId();
RpcFuture<RpcResponse> future=RequestHolder.REQUEST_MAP.remove(requestId);
future.getPromise().setSuccess(msg.getContent()); //返回结果
}
}

Future的实现

在netty-rpc-protocol模块中添加rpcFuture实现

RpcFuture

@Data
public class RpcFuture<T> {
//Promise是可写的 Future, Future自身并没有写操作相关的接口,
// Netty通过 Promise对 Future进行扩展,用于设置IO操作的结果
private Promise<T> promise; public RpcFuture(Promise<T> promise) {
this.promise = promise;
}
}

RequestHolder

保存requestid和future的对应结果

public class RequestHolder {

    public static final AtomicLong REQUEST_ID=new AtomicLong();

    public static final Map<Long,RpcFuture> REQUEST_MAP=new ConcurrentHashMap<>();
}

需要源码的同学,请关注公众号[跟着Mic学架构],回复关键字[rpc],即可获得

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Mic带你学架构

如果本篇文章对您有帮助,还请帮忙点个关注和赞,您的坚持是我不断创作的动力。欢迎关注「跟着Mic学架构」公众号公众号获取更多技术干货!

手把手教你基于Netty实现一个基础的RPC框架(通俗易懂)的更多相关文章

  1. 教你用 Netty 实现一个简单的 RPC!

    众所周知,dubbo 底层使用了 Netty 作为网络通讯框架,而 Netty 的高性能我们之前也分析过源码,对他也算还是比较了解了. 今天我们就自己用 Netty 实现一个简单的 RPC 框架. 1 ...

  2. 手把手教你用netty撸一个ZkClient

    原文地址: https://juejin.im/post/5dd296c0e51d4508182449a6 前言 有这个想法的缘由是前一阵子突发奇想, 想尝试能不能直接利用js连接到zookeeper ...

  3. netty系列之:来,手把手教你使用netty搭建一个DNS tcp服务器

    目录 简介 搭建netty服务器 DNS服务器的消息处理 DNS客户端消息请求 总结 简介 在前面的文章中,我们提到了使用netty构建tcp和udp的客户端向已经公布的DNS服务器进行域名请求服务. ...

  4. 手把手教你基于SqlSugar4编写一个可视化代码生成器(生成实体,以SqlServer为例,文末附源码)

    在开发过程中免不了创建实体类,字段少的表可以手动编写,但是字段多还用手动创建的话不免有些浪费时间,假如一张表有100多个字段,手写有些不现实. 这时我们会借助一些工具,如:动软代码生成器.各种ORM框 ...

  5. 手写一个类SpringBoot的HTTP框架:几十行代码基于Netty搭建一个 HTTP Server

    本文已经收录进 : https://github.com/Snailclimb/netty-practical-tutorial (Netty 从入门到实战:手写 HTTP Server+RPC 框架 ...

  6. 自己用 Netty 实现一个简单的 RPC

    目录: 需求 设计 实现 创建 maven 项目,导入 Netty 4.1.16. 项目目录结构 设计接口 提供者相关实现 消费者相关实现 测试结果 总结 源码地址:github 地址 前言 众所周知 ...

  7. Delphi - 手把手教你基于D7+Access常用管理系统架构的设计与实现 (更新中)

    前言 从事软件开发工作好多年了,学的越深入越觉得自己无知,所以还是要对知识保持敬畏之心,活到老,学到老! 健身和代码一样都不能少,身体是革命的本钱,特别是我们这种高危工种,所以小伙伴们运动起来!有没有 ...

  8. 基于Netty的一个WeoSocket通信服务器与客户端代码(非JS代码)

    基于Netty的一个WeoSocket通信服务器与客户端代码(非JS代码) 咳咳,在这里呢,小轩就不多说什么是WebSocket的,还有呢,小轩为什么不给出JS-Client代码?网上太多代码可以用了 ...

  9. 手把手教你从零写一个简单的 VUE--模板篇

    教程目录1.手把手教你从零写一个简单的 VUE2.手把手教你从零写一个简单的 VUE--模板篇 Hello,我又回来了,上一次的文章教会了大家如何书写一个简单 VUE,里面实现了VUE 的数据驱动视图 ...

随机推荐

  1. 四种引用类型在Springboot中的使用

    今天 4ye 来和小伙伴们聊聊这个 强引用,软引用,弱引用,幻象引用(虚引用)啦 嘿嘿,主要是最近读源码的时候经常看到,然后又想到自己第一次知道这个神奇的东西是在 2020-8-21 为啥记得这么清楚 ...

  2. Unity——观察者模式

    观察者模式 一.Demo展示 二.设计思路 我们假设一种情况,在app中修改了头像,在所有显示头像的UI中都需要更改相应的图片,一个个去获取然后调用刷新会非常麻烦: 因此我们需要一个自动响应机制--观 ...

  3. docker - compose 部署 Nginx

    主要介绍 docker 中 Nginx 的部署及项目目录挂载券的方法.docker 中部署一个服务,有三种方法,分别是 docker run.Dockerfile.docker-compose . 下 ...

  4. ansible远程运维操作

    1.command 用于查看文件内容,查看磁盘,内存,启动命令等纯命令信息 ansible portal -m command -a "cat /test1/test"2.ping ...

  5. gRPC,爆赞

    原文链接: gRPC,爆赞 gRPC 这项技术真是太棒了,接口约束严格,性能还高,在 k8s 和很多微服务框架中都有应用. 作为一名程序员,学就对了. 之前用 Python 写过一些 gRPC 服务, ...

  6. SpringBoot之日志注解和缓存优化

    SpringBoot之日志注解和缓存优化 日志注解: 关于SpringBoot中的日志处理,在之前的文章中页写过: 点击进入 这次通过注解+Aop的方式来实现日志的输出: 首先需要定义一个注解类: @ ...

  7. Bert文本分类实践(三):处理样本不均衡和提升模型鲁棒性trick

    目录 写在前面 缓解样本不均衡 模型层面解决样本不均衡 Focal Loss pytorch代码实现 数据层面解决样本不均衡 提升模型鲁棒性 对抗训练 对抗训练pytorch代码实现 知识蒸馏 防止模 ...

  8. spring boot log4j2 最佳实践

    为什么选择 log4j2 Log4j2 使用了 LMAX Disruptor 库.在多线程场景中,异步 Logger 的吞吐量比 Log4j 1.x 和 Logback 高 18 倍,延迟低几个数量级 ...

  9. MyBatis 中实现SQL语句中in的操作 (11)

    MyBatis 中实现SQL语句中in的操作 概括:应用myBatis实现SQL查询中IN的操作 1.数据库结构及其数据 2.mapper.xml文件 <?xml version="1 ...

  10. 【二食堂】Beta - Scrum Meeting 10

    Scrum Meeting 10 例会时间:5.25 18:30~18:50 进度情况 组员 当前进度 今日任务 李健 1. 继续文本导入.保存部分的工作issue 2. 完成了技术博客 1. 继续文 ...