文章作者:jqpeng
原文链接: RPC框架原理与实现

RPC,全称 Remote Procedure Call(远程过程调用),即调用远程计算机上的服务,就像调用本地服务一样。那么RPC的原理是什么呢?了解一个技术最好的思路就是寻找一个该类型麻雀虽小五脏俱全的开源项目,不负所期,找到一个轻量级分布式 RPC 框架,本文从这个项目入手来解读RPC的原理及其实现。

其实说到RPC,大家应该不会陌生才是,以往流行的Web Service就是一种RPC,一般来说RPC 可基于 HTTP 或 TCP 协议,因为Web Service 基于HTTP,所以具有良好的跨平台性,但由于HTTP是应用层协议,相比TCP性能有所损耗。

与本地调用不一样,远程调用需要通过网络层传输,因此涉及到的一个问题就是序列化,不同的序列化方式影响调用性能,流行的序列化包括Protobuf、Kryo、Hessian、Jackson、Thrift。

下面,让我们来一关如何从零开始实现分布式RPC框架。

RPC框架组件

建设一个框架,一个系统,首先要做的就是分析需要哪些组件,他们的关系是什么?

简单分析下,一个RPC框架需要包括:

  • APP :应用端,调用服务
  • Server 服务容器,对外提供服务
  • Service Registry 服务注册表

我们需要将服务部署在分布式环境下的不同节点上,通过服务注册的方式,让客户端来自动发现当前可用的服务,并调用这些服务。这需要一种服务注册表(Service Registry)的组件,让它来注册分布式环境下所有的服务地址(包括:主机名与端口号)。

RPC框架图
每台 Server 上可发布多个 Service,这些 Service 共用一个 host 与 port,在分布式环境下会提供 Server 共同对外提供 Service。此外,为防止 Service Registry 出现单点故障,因此需要将其搭建为集群环境。

RPC框架实现

定义服务

首先定义服务接口,接口可以单独放在一个jar包中

public interface HelloService {
String hello(String name);
String hello(Person person);
}

实现接口

然后,增加一种实现

@RpcService(HelloService.class)
public class HelloServiceImpl implements HelloService {

    @Override
    public String hello(String name) {
        return "Hello! " + name;
    }

    @Override
    public String hello(Person person) {
        return "Hello! " + person.getFirstName() + " " + person.getLastName();
    }
}

这里的RpcService注解,定义在服务接口的实现类上,可以让框架通过这个注解找到服务实现类。

更进一步,如果哪天服务版本升级了,但是历史服务还有人在使用,怎么办?解决方案就是服务需要分版本,按版本调用。

@RpcService(value = HelloService.class, version = "sample.hello2")
public class HelloServiceImpl2 implements HelloService {

    @Override
    public String hello(String name) {
        return "你好! " + name;
    }

    @Override
    public String hello(Person person) {
        return "你好! " + person.getFirstName() + " " + person.getLastName();
    }
}

再来看下 RPC 服务注解

/**
 * RPC 服务注解(标注在服务实现类上)
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Component
public @interface RpcService {

    /**
     * 服务接口类
     */
    Class<?> value();

    /**
     * 服务版本号
     */
    String version() default "";
}

服务端实现

Server端主要基于Netty(一个NIO框架)+Spring

回到开头讲的,RPC关键点之一就是传输序列化,简单来说就是客户端调用service时,需要构建一个请求,然后将这个请求序列化传输到服务端,服务端完成调用后,再将结果 序列化后返回,简单画一下:

RPC请求

定义Request

public class RpcRequest {

    private String requestId;
    private String interfaceName;
    private String serviceVersion;
    private String methodName;
    private Class<?>[] parameterTypes;
    private Object[] parameters;

}

定义RpcResponse

public class RpcResponse {

    private String requestId;
    private Exception exception;
    private Object result;

}

Encoder与Decoder

因为项目基于Netty,所以按Netty那一套搞就行,核心是SerializationUtil,这个根据需要可以采用不同的序列化框架,比如pb。

public class RpcEncoder extends MessageToByteEncoder {

    private Class<?> genericClass;

    public RpcEncoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }

    @Override
    public void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) throws Exception {
        if (genericClass.isInstance(in)) {
            byte[] data = SerializationUtil.serialize(in);
            out.writeInt(data.length);
            out.writeBytes(data);
        }
    }
}



public class RpcDecoder extends ByteToMessageDecoder {

    private Class<?> genericClass;

    public RpcDecoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }

    @Override
    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.readableBytes() < 4) {
            return;
        }
        in.markReaderIndex();
        int dataLength = in.readInt();
        if (in.readableBytes() < dataLength) {
            in.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        in.readBytes(data);
        out.add(SerializationUtil.deserialize(data, genericClass));
    }
}

扫描服务

服务端采用Spring,并且服务加了RpcService注解,所以服务器启动的时候扫描一下带RpcService的就行

下面的代码实现了将服务找出来,并放到handlerMap里,这样,调用服务的时候就可以根据服务名称从Map里找到服务对象,知道了服务对象和服务方法,就能直接调用了。

    private Map<String, Object> handlerMap = new HashMap<>();

    public void setApplicationContext(ApplicationContext ctx) throws BeansException {
        // 扫描带有 RpcService 注解的类并初始化 handlerMap 对象
        Map<String, Object> serviceBeanMap = ctx.getBeansWithAnnotation(RpcService.class);
        if (MapUtils.isNotEmpty(serviceBeanMap)) {
            for (Object serviceBean : serviceBeanMap.values()) {
                RpcService rpcService = serviceBean.getClass().getAnnotation(RpcService.class);
                String serviceName = rpcService.value().getName();
                String serviceVersion = rpcService.version();
                if (StringUtil.isNotEmpty(serviceVersion)) {
                    serviceName += "-" + serviceVersion;
                }
                handlerMap.put(serviceName, serviceBean);
            }
        }
    }

启动服务器

按照Netty服务器标准代码,启动服务,注意Encoder和Decoder

 @Override
    public void afterPropertiesSet() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            // 创建并初始化 Netty 服务端 Bootstrap 对象
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup);
            bootstrap.channel(NioServerSocketChannel.class);
            bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel channel) throws Exception {
                    ChannelPipeline pipeline = channel.pipeline();
                    pipeline.addLast(new RpcDecoder(RpcRequest.class)); // 解码 RPC 请求
                    pipeline.addLast(new RpcEncoder(RpcResponse.class)); // 编码 RPC 响应
                    pipeline.addLast(new RpcServerHandler(handlerMap)); // 处理 RPC 请求
                }
            });
            bootstrap.option(ChannelOption.SO_BACKLOG, 1024);
            bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
            // 获取 RPC 服务器的 IP 地址与端口号
            String[] addressArray = StringUtil.split(serviceAddress, ":");
            String ip = addressArray[0];
            int port = Integer.parseInt(addressArray[1]);
            // 启动 RPC 服务器
            ChannelFuture future = bootstrap.bind(ip, port).sync();
            // 注册 RPC 服务地址
            if (serviceRegistry != null) {
                for (String interfaceName : handlerMap.keySet()) {
                    serviceRegistry.register(interfaceName, serviceAddress);
                    LOGGER.debug("register service: {} => {}", interfaceName, serviceAddress);
                }
            }
            LOGGER.debug("server started on port {}", port);
            // 关闭 RPC 服务器
            future.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

处理请求

RpcServerHandler负责处理请求,熟悉Netty的应该知道,继承SimpleChannelInboundHandler,在channelRead0函数里处理即可,注意,因为pipline里前面已经解码为RpcRequest对象了,所以在这里可以直接使用。

    public class RpcServerHandler extends SimpleChannelInboundHandler<RpcRequest> {

    private static final Logger LOGGER = LoggerFactory.getLogger(RpcServerHandler.class);

    private final Map<String, Object> handlerMap;

    public RpcServerHandler(Map<String, Object> handlerMap) {
        this.handlerMap = handlerMap;
    }

    @Override
    public void channelRead0(final ChannelHandlerContext ctx, RpcRequest request) throws Exception {
        // 创建并初始化 RPC 响应对象
        RpcResponse response = new RpcResponse();
        response.setRequestId(request.getRequestId());
        try {
            Object result = handle(request);
            response.setResult(result);
        } catch (Exception e) {
            LOGGER.error("handle result failure", e);
            response.setException(e);
        }
        // 写入 RPC 响应对象并自动关闭连接
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}

框架没什么说的,核心是怎么handle,无非就是从Reques里获取到服务名称和版本号,然后从handlerMap里寻找服务对象,然后调用方法。

已知方法名和Class,可以通过反射进行调用,但是反射性能较低,可以使用cglib里的FastClass来执行invoke,详情参见说说 cglib 动态代理

    private Object handle(RpcRequest request) throws Exception {
        // 获取服务对象
        String serviceName = request.getInterfaceName();
        String serviceVersion = request.getServiceVersion();
        if (StringUtil.isNotEmpty(serviceVersion)) {
            serviceName += "-" + serviceVersion;
        }
        Object serviceBean = handlerMap.get(serviceName);
        if (serviceBean == null) {
            throw new RuntimeException(String.format("can not find service bean by key: %s", serviceName));
        }
        // 获取反射调用所需的参数
        Class<?> serviceClass = serviceBean.getClass();
        String methodName = request.getMethodName();
        Class<?>[] parameterTypes = request.getParameterTypes();
        Object[] parameters = request.getParameters();
        // 执行反射调用
//        Method method = serviceClass.getMethod(methodName, parameterTypes);
//        method.setAccessible(true);
//        return method.invoke(serviceBean, parameters);
        // 使用 CGLib 执行反射调用
        FastClass serviceFastClass = FastClass.create(serviceClass);
        FastMethod serviceFastMethod = serviceFastClass.getMethod(methodName, parameterTypes);
        return serviceFastMethod.invoke(serviceBean, parameters);
    }

服务发现与注册

在分布式系统里,服务的自动发现与注册是标配功能,一般来说都是使用集中配置中心,开源届有Zookeeper、etcd等实现。这里,使用zk作为配置中心。

服务发现与注册的核心是,服务启动时,将服务名称和服务地址写入到配置中心,客户端调用的时候,先从集中配置中心读取所要调用服务的服务器地址,如果有多个,随机挑选一个(当然随机的话会存在负载不均衡问题),连接服务器并调用。

个人认为较好的实现方式是,服务层面加一个HA层,客户端直接调用HA,HA负责负载Service。

回到代码解读,这里使用的zookeeper,我们来看怎么实现。

先是定义接口:

public interface ServiceRegistry {

    /**
     * 注册服务名称与服务地址
     *
     * @param serviceName    服务名称
     * @param serviceAddress 服务地址
     */
    void register(String serviceName, String serviceAddress);
}

public interface ServiceDiscovery {

    /**
     * 根据服务名称查找服务地址
     *
     * @param serviceName 服务名称
     * @return 服务地址
     */
    String discover(String serviceName);
}

再看谈实现,zk有两种类型的节点,永久节点和临时节点,这种特性非常适合做服务发现与注册。
试想:

  • 新启动一台Server,自动注册到ZK,写一个临时节点,客户端调用的时候就能读取到这个节点
  • 一台Server挂了,临时节点失效,客户端调用的时候就读取不到这个节点,自然不会调用
  • 当服务调用量太大,可以新启动服务,服务小的时候再停掉

不再赘述,看代码:

public class ZooKeeperServiceRegistry implements ServiceRegistry {

    private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceRegistry.class);

    private final ZkClient zkClient;

    public ZooKeeperServiceRegistry(String zkAddress) {
        // 创建 ZooKeeper 客户端
        zkClient = new ZkClient(zkAddress, Constant.ZK_SESSION_TIMEOUT, Constant.ZK_CONNECTION_TIMEOUT);
        LOGGER.debug("connect zookeeper");
    }

    @Override
    public void register(String serviceName, String serviceAddress) {
        // 创建 registry 节点(持久)
        String registryPath = Constant.ZK_REGISTRY_PATH;
        if (!zkClient.exists(registryPath)) {
            zkClient.createPersistent(registryPath);
            LOGGER.debug("create registry node: {}", registryPath);
        }
        // 创建 service 节点(持久)
        String servicePath = registryPath + "/" + serviceName;
        if (!zkClient.exists(servicePath)) {
            zkClient.createPersistent(servicePath);
            LOGGER.debug("create service node: {}", servicePath);
        }
        // 创建 address 节点(临时)
        String addressPath = servicePath + "/address-";
        String addressNode = zkClient.createEphemeralSequential(addressPath, serviceAddress);
        LOGGER.debug("create address node: {}", addressNode);
    }
}

原理就是创建了一个临时节点存储服务地址

再来看服务发现:

public class ZooKeeperServiceDiscovery implements ServiceDiscovery {

    private static final Logger LOGGER = LoggerFactory.getLogger(ZooKeeperServiceDiscovery.class);

    private String zkAddress;

    public ZooKeeperServiceDiscovery(String zkAddress) {
        this.zkAddress = zkAddress;
    }

    @Override
    public String discover(String name) {
        // 创建 ZooKeeper 客户端
        ZkClient zkClient = new ZkClient(zkAddress, Constant.ZK_SESSION_TIMEOUT, Constant.ZK_CONNECTION_TIMEOUT);
        LOGGER.debug("connect zookeeper");
        try {
            // 获取 service 节点
            String servicePath = Constant.ZK_REGISTRY_PATH + "/" + name;
            if (!zkClient.exists(servicePath)) {
                throw new RuntimeException(String.format("can not find any service node on path: %s", servicePath));
            }
            List<String> addressList = zkClient.getChildren(servicePath);
            if (CollectionUtil.isEmpty(addressList)) {
                throw new RuntimeException(String.format("can not find any address node on path: %s", servicePath));
            }
            // 获取 address 节点
            String address;
            int size = addressList.size();
            if (size == 1) {
                // 若只有一个地址,则获取该地址
                address = addressList.get(0);
                LOGGER.debug("get only address node: {}", address);
            } else {
                // 若存在多个地址,则随机获取一个地址
                address = addressList.get(ThreadLocalRandom.current().nextInt(size));
                LOGGER.debug("get random address node: {}", address);
            }
            // 获取 address 节点的值
            String addressPath = servicePath + "/" + address;
            return zkClient.readData(addressPath);
        } finally {
            zkClient.close();
        }
    }
}

客户端实现

服务代理

可以先查看(http://www.cnblogs.com/xiaoqi/p/java-proxy.html)了解java的动态代理。

使用 Java 提供的动态代理技术实现 RPC 代理(当然也可以使用 CGLib 来实现),具体代码如下:

public class RpcProxy {

    private static final Logger LOGGER = LoggerFactory.getLogger(RpcProxy.class);

    private String serviceAddress;

    private ServiceDiscovery serviceDiscovery;

    public RpcProxy(String serviceAddress) {
        this.serviceAddress = serviceAddress;
    }

    public RpcProxy(ServiceDiscovery serviceDiscovery) {
        this.serviceDiscovery = serviceDiscovery;
    }

    @SuppressWarnings("unchecked")
    public <T> T create(final Class<?> interfaceClass) {
        return create(interfaceClass, "");
    }

    @SuppressWarnings("unchecked")
    public <T> T create(final Class<?> interfaceClass, final String serviceVersion) {
        // 创建动态代理对象
        return (T) Proxy.newProxyInstance(
                interfaceClass.getClassLoader(),
                new Class<?>[]{interfaceClass},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 创建 RPC 请求对象并设置请求属性
                        RpcRequest request = new RpcRequest();
                        request.setRequestId(UUID.randomUUID().toString());
                        request.setInterfaceName(method.getDeclaringClass().getName());
                        request.setServiceVersion(serviceVersion);
                        request.setMethodName(method.getName());
                        request.setParameterTypes(method.getParameterTypes());
                        request.setParameters(args);
                        // 获取 RPC 服务地址
                        if (serviceDiscovery != null) {
                            String serviceName = interfaceClass.getName();
                            if (StringUtil.isNotEmpty(serviceVersion)) {
                                serviceName += "-" + serviceVersion;
                            }
                            serviceAddress = serviceDiscovery.discover(serviceName);
                            LOGGER.debug("discover service: {} => {}", serviceName, serviceAddress);
                        }
                        if (StringUtil.isEmpty(serviceAddress)) {
                            throw new RuntimeException("server address is empty");
                        }
                        // 从 RPC 服务地址中解析主机名与端口号
                        String[] array = StringUtil.split(serviceAddress, ":");
                        String host = array[0];
                        int port = Integer.parseInt(array[1]);
                        // 创建 RPC 客户端对象并发送 RPC 请求
                        RpcClient client = new RpcClient(host, port);
                        long time = System.currentTimeMillis();
                        RpcResponse response = client.send(request);
                        LOGGER.debug("time: {}ms", System.currentTimeMillis() - time);
                        if (response == null) {
                            throw new RuntimeException("response is null");
                        }
                        // 返回 RPC 响应结果
                        if (response.hasException()) {
                            throw response.getException();
                        } else {
                            return response.getResult();
                        }
                    }
                }
        );
    }
}

RPC客户端

使用RpcClient类实现 RPC 客户端,只需扩展 Netty 提供的SimpleChannelInboundHandler抽象类即可,代码如下:

public class RpcClient extends SimpleChannelInboundHandler<RpcResponse> {

    private static final Logger LOGGER = LoggerFactory.getLogger(RpcClient.class);

    private final String host;
    private final int port;

    private RpcResponse response;

    public RpcClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    @Override
    public void channelRead0(ChannelHandlerContext ctx, RpcResponse response) throws Exception {
        this.response = response;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        LOGGER.error("api caught exception", cause);
        ctx.close();
    }

    public RpcResponse send(RpcRequest request) throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            // 创建并初始化 Netty 客户端 Bootstrap 对象
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group);
            bootstrap.channel(NioSocketChannel.class);
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                public void initChannel(SocketChannel channel) throws Exception {
                    ChannelPipeline pipeline = channel.pipeline();
                    pipeline.addLast(new RpcEncoder(RpcRequest.class)); // 编码 RPC 请求
                    pipeline.addLast(new RpcDecoder(RpcResponse.class)); // 解码 RPC 响应
                    pipeline.addLast(RpcClient.this); // 处理 RPC 响应
                }
            });
            bootstrap.option(ChannelOption.TCP_NODELAY, true);
            // 连接 RPC 服务器
            ChannelFuture future = bootstrap.connect(host, port).sync();
            // 写入 RPC 请求数据并关闭连接
            Channel channel = future.channel();
            channel.writeAndFlush(request).sync();
            channel.closeFuture().sync();
            // 返回 RPC 响应对象
            return response;
        } finally {
            group.shutdownGracefully();
        }
    }
}

服务测试

public class HelloClient {

    public static void main(String[] args) throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext("spring.xml");
        RpcProxy rpcProxy = context.getBean(RpcProxy.class);

        HelloService helloService = rpcProxy.create(HelloService.class);
        String result = helloService.hello("World");
        System.out.println(result);

        HelloService helloService2 = rpcProxy.create(HelloService.class, "sample.hello2");
        String result2 = helloService2.hello("世界");
        System.out.println(result2);

        System.exit(0);
    }
}

输出结果

connect zookeeper
get only address node: address-0000000001
discover service: com.xxx.rpc.sample.api.HelloService => 127.0.0.1:8000
time: 569ms
Hello! World
connect zookeeper
get only address node: address-0000000001
discover service: com.xxx.rpc.sample.api.HelloService-sample.hello2 => 127.0.0.1:8000
time: 36ms
你好! 世界

作者:Jadepeng
出处:jqpeng的技术记事本–http://www.cnblogs.com/xiaoqi
您的支持是对博主最大的鼓励,感谢您的认真阅读。
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

文章作者:jqpeng
原文链接: 程序员主管之路(1)

作为码农,技术出生,随着能力的被认可,会逐步承担管理角色,但是在担任主管尝尝会遇到不少挑战,如何快速从一名个人贡献者转变为一名合格的团队领导,完成职责的华丽转身。

目录

  • 领导转型
    • 工作理念
    • 时间管理
    • 目标分解
    • 团队成果仪表盘
  • 成功领导三要素
    • 1、言行一致
    • 2、启导他人
    • 3、拥抱反馈
  • 规划团队行事历

领导转型

个人贡献者与领导者有什么不同呢?

工作理念

一人吃饱 VS 全家温暖
需要树立起对团队负责的思想,关注团队的目标、绩效,关心员工的诉求,为员工争取利益,承担对应的风险与责任,当然你的收获也更多。

时间管理

转岗团队领导后,你不仅需要承担原有的技术攻关工作,还需要承担团队的管理工作,有没有手忙脚乱,忙了一天也没有一个重点?试着列出自己每天的所有工作,看看时间都花在哪儿了?

列出目前工作中最重要的三项目标或者任务,这就是你目前的重中之重,你需要花费足够多的时间在重点工作上面,以确保目标达成。如果你每天的工作大部分都不在重点工作上面,那么你需要及时调整你的工作日程!

目标分解

有了重点工作计划,需要尝试把工作目标进行分解,根据工作内容,可以有不同的分解方法:

  • 线性分解法,例如

    • 1季度完成营业额XX万
    • 2季度完成营业额XX万
  • 里程碑

    • 04/30 提出新产品组合
    • 07/31 完成试销
  • 关键要素法

    • 04/30 完成XXX

团队成果仪表盘

有了目标,并进行了合适的分解,可以接着使用团队成果仪表盘来进行跟踪:

编号 达标率 目标 关键里程碑 灯号 进度/状况说明
1 50% 完成丁克玉组的解题demo开发 3月23日完成demo :—–: ————:

使用团队成果仪表盘,有什么好处呢?

  • 判断重点任务,调整工作优先级
  • 自我加压,追求卓越

成功领导三要素

好的团队领导需要与团队建立起信任,而建立信任有三个关键要素:

1、言行一致

言行一致的表现:

  • 表里如一
  • 所言即所行
  • 不会人前一套人后一套

为什么需要言行一致?

  • 能促进人与人之间的互信
  • 树立良好个人品牌

2、启导他人

启导他人就是培养团队成员,提升他们的技能,因此,需要了解团队每个人的知识能力、经验、人格特质,我们可以通过团队成员能力评估表来对成员进行评估

能力 员工1 员工2
专业能力 5 5
沟通协调 3 4
团队合作 2 3
决策能力 2 4
计划组织 4 2
质量导向 4 4
客户导向 4 4
持续学习 4 4

评估后选择员工的薄弱环节,有针对性的进行辅导。

3、拥抱反馈

为什么需要收集反馈?——了解员工心目中真实的自己,做更好的领导

如何拥抱反馈?

  • 表现谦虚的态度
  • 主动寻求他人反馈
  • 接纳改善反馈
  • 感谢反馈

规划团队行事历

何为行事历?就是按周、月把需要例行的事情,规划的里程碑等放在日历里,这样月度、年度重点工作一目了然,便于团队拥有共同的目标。

为什么需要建立行事历

  • 了解重要的例行事情,做好事先规划
  • 工作了然于胸,避免重要事情遗漏
  • 考核督促团队,达成目标

文章作者:jqpeng
原文链接: MarkDown 常用语法教程

MarkDown 语法说明

目录

  • MarkDown 语法说明
    • 标题
    • 列表
      • 无序列表
      • 有序列表
    • 引用
    • 图片与链接
    • 粗体与斜体
    • 表格
    • 插入代码
    • 分割线

标题

标题1
======

标题2
-----

## 大标题
### 小标题 
#### 小标题 

列表

无序列表

+ 列表文本前使用 [减号+空格]
* 列表文本前使用 [加号+空格]
- 列表文本前使用 [星号+空格]
  • 列表文本前使用 [减号+空格]
  • 列表文本前使用 [加号+空格]
  • 列表文本前使用 [星号+空格]

有序列表

1. 列表前使用 [数字+空格]
2. 我们会自动帮你添加数字
7. 不用担心数字不对,显示的时候我们会自动把这行的 7 纠正为 3
  1. 列表前使用 [数字+空格]
  2. 我们会自动帮你添加数字
  3. 不用担心数字不对,显示的时候我们会自动把这行的 7 纠正为 3

引用

> 引用文本前使用 [大于号+空格]
> 折行可以不加,新起一行都要加上哦

引用文本前使用 [大于号+空格]

图片与链接

插入链接与插入图片的语法很像,区别在一个 !号

图片为:![]()
链接为:[]()

插入链接:

Baidu

图片:
图片提示

粗体与斜体

**两个为粗体**

两个为粗体

*单个为斜体*

单个为斜体

表格

使用html

<table>
    <tr>
        <td>Foo</td>
    </tr>
</table>

markdown语法

name | age
---- | ---
LearnShare    | 12
Mike |  32
name age
LearnShare 12
Mike 32

对齐:

| Tables        |      Are      |  Cool |
|:--------------|:-------------:|------:|
| col 3 is      | right-aligned | $1600 |
| col 2 is      |   centered    |   $12 |
| zebra stripes |   are neat    |    $1 |
Tables Are Cool
col 3 is right-aligned $1600
col 2 is centered $12
zebra stripes are neat $1

插入代码

用两个 ``` 包裹一段代码,并指定一种语言

1
2
3
$(document).ready(function () {
alert('hello world');
});
$(document).ready(function () { alert('hello world'); });

支持常用的语言,下面试下go语言

package main

import (
    "fmt"
    "math"
    "runtime"
)

func sqrt(x float64) string {
    if x < 0 {
        return sqrt(-x) + "i"
    }
    return fmt.Sprint(math.Sqrt(x))
} 

分割线

分割线的语法只需要三个 * 号,

***

文章作者:jqpeng
原文链接: Solr vs. Elasticsearch谁是开源搜索引擎王者

当前是云计算和数据快速增长的时代,今天的应用程序正以PB级和ZB级的速度生产数据,但人们依然在不停的追求更高更快的性能需求。随着数据的堆积,如何快速有效的搜索这些数据,成为对后端服务的挑战。本文,我们将比较业界两个最流行的开源搜索引擎,Solr和ElasticSearch。两者都建立在Apache Lucene开源平台之上,它们的主要功能非常相似,但是在部署的易用性,可扩展性和其他功能方面也存在巨大差异。

关于Apache Solr

Apache Solr基于业界大名鼎鼎的java开源搜索引擎Lucene,Lucene更多的是一个软件包,还不能称之为搜索引擎,而solr则完成对lucene的封装,是一个真正意义上的搜索引擎框架。在过去的十年里,solr发展壮大,拥有广泛的用户群体。solr提供分布式索引、分片、副本集、负载均衡和自动故障转移和恢复功能。如果正确部署,良好管理,solr就能够成为一个高可靠、可扩展和高容错的搜索引擎。不少互联网巨头,如Netflix,eBay,Instagram和Amazon(CloudSearch)均使用Solr。

solr的主要特点:

  • 全文索引
  • 高亮
  • 分面搜索
  • 实时索引
  • 动态聚类
  • 数据库集成
  • NoSQL特性和丰富的文档处理(例如Word和PDF文件)

关于Elasticsearch

与solr一样,Elasticsearch构建在Apache Lucene库之上,同是开源搜索引擎。Elasticsearch在Solr推出几年后才面世的,通过REST和schema-free(不需要预先定义 Schema,solr是需要预先定义的)的JSON文档提供分布式、多租户全文搜索引擎。并且官方提供Java,Groovy,PHP,Ruby,Perl,Python,.NET和Javascript客户端。

分布式搜索引擎包含可以华为为分片(shard)的索引,每一个分片可以有多个副本(replicas)。每个Elasticsearch节点可以有一个或多个分片,其引擎既同时作为协调器(coordinator ),将操作转发给正确的分片。

Elasticsearch可扩展为准实时搜索引擎。其中一个关键特性是多租户功能,可根据不同的用途分索引,可以同时操作多个索引。

Elasticsearch主要特性:

  • 分布式搜索
  • 多租户
  • 查询统计分析
  • 分组和聚合

热度对比

在开始比较前,我们可以查看两者在google中的搜索热度,可以看出在2013年后,Elasticsearch与Solr相比具有很大的吸引力,但这并不意味着Apache Solr已经死了。虽然不少人不认可,但Solr仍然是最流行的搜索引擎之一,具有强大的开源社区支持。

安装与配置

相对来说,Elasticsearch更易于安装,与Solr相比非常轻量级。 Solr的分发软件包大小的当前版本(6.4.2)大约为150 MB,而Elasticsearch分发软件包大小的当前版本(5.2.2)仅为32.2MB。

但是,如果Elasticsearch管理不好,这种易于部署和使用可能会成为一个问题。基于JSON的配置很容易,但如果你想为文件中的每个配置指定注释,那么它不适合你。Solr也提供了Rest API,可以通过集合API创建自定义分片集合,记录聚类算法和执行自定义分片。

总的来说,如果你的应用程序使用JSON,那么Elasticsearch是一个更好的选择。否则,使用Solr,因为它的schema.xml和solrconfig.xml有很好的文档。

索引和搜索

数据源

Solr接受来自不同来源的数据,包括XML文件,逗号分隔符(CSV)文件和从数据库中的表提取的数据以及常见的文件格式(如Microsoft Word和PDF)。

Elasticsearch还支持其他来源的数据,例如ActiveMQ,AWS SQS,DynamoDB(Amazon NoSQL),FileSystem,Git,JDBC,JMS,Kafka,LDAP,MongoDB,neo4j,RabbitMQ,Redis,Solr和Twitter。还有各种插件可用。

搜索

Solr专注于文本搜索,而Elasticsearch则常用于查询、过滤和分组****分析统计,Elasticsearch背后的团队也努力让这些查询更为高效。因此当比较两者时,对那些不仅需要文本搜索,同时还需要复杂的时间序列搜索和聚合的应用程序而言,毫无疑问Elasticsearch是最佳选择。

索引

两者都支持使用停用词和同义词来匹配文档。

在Solr中,索引间进行join必须是单个分片和其他节点上的副本集进行关联来搜索文档间关系(例如SQL连接)。而Elasticsearch提供更高效的has_children和top_children查询来检索这样的相关文档。

可扩展性和分布式

搜索引擎需要处理数以百万级的文档,基于此搜索引擎应该是可复制的,模块化的和可扩展的,支持集群和分布式架构。

专为云而设计

Elasticsearch非常易于扩展,拥有足够多的需要大集群的使用案例。

Solr 基于Apache ZooKeeper也实现了类似ES的分布式部署模式。ZooKeeper是成熟和广泛使用的独立应用程序。

相对比,Elasticsearch有一个内置的类似ZooKeeper的名为Zen的组件,通过内部的协调机制来维护集群状态。

可以说Elasticsearch是转为云而设计,是分布式首选。

分片拆分和再平衡

shards是luence索引的分区单元,solr和elasticsearch均使用。你可以通过在集群中的不同计算机上运行shard来分发索引。随着SolrCloud的引入,Solr开始支持shard拆分,这允许您通过拆分现有shard来添加更多shard。相比之下,ElasticSearch仍然不支持这一点,事实上,实际上阻止了这种做法。ES通过向设置中添加更多计算机,可以使用自动碎片平衡功能。相比之下,Solr允许添加分片(使用隐式路由时)或分割(使用复合ID时),但不能删除分片。它允许您增加副本。在Elasticsearch中,默认情况下每个索引具有五个分片。它不允许您更改主分片的数量,但它允许您增加副本的数量。分片再平衡对于水平扩容非常有用。当添加新机器时,它将自动重新平衡不同机器中可用的分片。

社区

Solr有一个广泛的开源社区。任何人都可以贡献给Solr,新的Solr开发人员或代码提交者只能根据功能选择。 Elasticsearch在技术上是开源的,但不完全。所有贡献者都可以访问源代码,用户可以进行更改并提供。但最终的变化由Elastic(运行Elasticsearch和其他软件的公司)的员工确认和完成。因此,Elasticsearch更多地由单个公司驱动,而不是整个社区。

Solr贡献者和提交者跨越多个组织,而Elasticsearch提交者仅来自Elastic。还有人指出,Solr的强大社区有一个健康的项目管道和许多知名公司参与。这些成员还通过在整个开发和工程过程中做出贡献来投资该平台。

两者都有很好的用户群和丰富的开发人员社区,但ElasticSearch相较于Solr更新。 Solr已经存在了更长的时间,所以它的生态系统是发达的,拥有更大的用户群。

文档

Solr在这里得分很高。它是一个非常有据可查的产品,具有清晰的示例和API用例场景。 Elasticsearch的文档组织良好,但它缺乏好的示例和清晰的配置说明。

选Solr 还是 Elasticsearch?

通过上面的对比,很难确定谁是最终赢家。其实,无论选择Solr还是Elasticsearch,你首先需要了解您的用户场景和未来的需求。我们来总结一下:

请记住:

  • Elasticsearch由于其易用性而在较新的开发人员中更受欢迎
  • 但是如果你已经在使用solr了,请继续使用它,因为迁移到Elasticsearch并不会带来具体的优势
  • 如果您需要它来处理分析查询以及搜索文本,Elasticsearch是更好的选择,特别是收集日志,做分析处理(参考前面发的ELK 安装使用http://www.cnblogs.com/xiaoqi/p/elk-part1.html)

总之,两者都是功能丰富的搜索引擎,并且或多或少地给出相同的性能,只要它们被设计和实施得很好。

本文主要内容为翻译http://logz.io/solr-vs-elasticsearch/,感谢作者,感谢谷歌翻译!

文章作者:jqpeng
原文链接: ELK日志套件安装与使用

1、ELK介绍

ELK不是一款软件,而是elasticsearch+Logstash+kibana三款开源软件组合而成的日志收集处理套件,堪称神器。其中Logstash负责日志收集,elasticsearch负责日志的搜索、统计,而kibana则是ES的展示神器,前端炫丽,点几下鼠标简单配置,就可以完成搜索、聚合功能,生成华丽的报表。

目前我们的日志方案:

  • flume负责收集,服务写日志到文件,flume收集日志文件
  • flume汇总到数据通道kafka,供其他服务消费
  • 日志搜索:从kafka读取日志写入到solr cloud提供搜索
  • 日志统计:将kafka的日志写到hdfs,使用spark、hive来做统计
  • 日志展示:开发的java-web,读取数据库生成统计报表

当前日志方案问题分析:

  • 需要预先编程才能使用,开发工作量大
  • 不够灵活,新需求需要改代码
  • 离线统计,实时性不高
  • 未提供基于搜索结果的统计功能
  • 系统方案较为复杂,需要了解多项技术,学习维护成本高
  • ……
  • 新增需求都是泪,开发出来后变动很少

通过调研ELK,发现真是解救目前问题的一个神器,大部分问题都可以迎刃而解。

2、ELK安装

默认需要先安装jdk1.8,自行安装即可

2.1、安装ES

2.1.1 下载ES

下载地址:https://www.elastic.co/downloads/elasticsearch

最新版本是2.28发布的5.2.2版本

windows选择ZIP,linux选择tar,ubuntu选择DEB

测试服务器是ubuntu,直接下载deb包,然后安装即可

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-5.2.2.deb

sudo dpkg -i elasticsearch-5.2.2.deb

2.1.2 配置

如果使用deb包安装的,配置文件路径如下:

类型 描述 默认路经 Setting
home Elasticsearch home directory or $ES_HOME /usr/share/elasticsearch
bin Binary scripts including elasticsearch to start a node and elasticsearch-plugin to install plugins /usr/share/elasticsearch/bin
conf Configuration files including elasticsearch.yml /etc/elasticsearch path.conf
conf Environment variables including heap size, file descriptors. /etc/default/elasticsearch
data The location of the data files of each index / shard allocated on the node. Can hold multiple locations. /var/lib/elasticsearch path.data
logs Log files location. /var/log/elasticsearch path.logs
plugins Plugin files location. Each plugin will be contained in a subdirectory. /usr/share/elasticsearch/plugins
repo Shared file system repository locations. Can hold multiple locations. A file system repository can be placed in to any subdirectory of any directory specified here. Not configured path.repo
script Location of script files. /etc/elasticsearch/scripts path.scripts

修改/etc/elasticsearch/elasticsearch.yml即可

测试使用,主要设置网络:

network.host: 192.168.86.108

http.port: 9200

ES的集群配置比较方便,设置cluster.name就可以:

cluster.name: es-log

2.1.3 启动ES

由于我们使用的deb安装的,因此可以用服务方式启动:

service elasticsearch start

如果使用的压缩包,执行 bin/elasticsearch (or bin\elasticsearch.bat on Windows)即可

然后,打开 curl http://192.168.86.108:9200/ 测试(ip换成配置的ip)

2.2 安装 Logstash

现在收集处理框架非常多,像 facebook 出品的scribe ,apache 基金会的亲儿子flume,Logstash也是一个非常出名的日志框架,使用jruby开发,可以运行在jvm之上实现跨平台,具体的介绍可以到官网http://logstash.net查看。

Logstash安装比较简单,下载压缩包、解压、配置,启动即可。

2.2.1 下载安装

wget https://artifacts.elastic.co/downloads/logstash/logstash-5.2.2.tar.gz

tar zxvf logstash-5.2.2.tar.gz

ln -s logstash-5.2.2 logstash

2.2.2 配置

Logstash 和flume比较类似,有input和output的概念。不过logstash社区通常习惯用 shipper,broker 和 indexer 来描述数据流中不同进程各自的角色。

我们来看怎么配置logstash。

创建一个配置文件nginxlog2es.conf,读取nginx日志,输出到elasticsearch 。具体的配置格式参见官方文档。

input {
    file {
        path => "/var/log/nginx/access.log_json"
        codec => "json"
    }
}
filter {
    mutate {
        split => [ "upstreamtime", "," ]
    }
    mutate {
        convert => [ "upstreamtime", "float" ]
    }
}
output {
  stdout { codec => rubydebug }
  elasticsearch {
        hosts => ["192.168.86.108:9200"]
        index => "logstash-%{type}-%{+YYYY.MM.dd}"
        document_type => "%{type}"
        flush_size => 20000
        idle_flush_time => 10
        sniffing => true
        template_overwrite => true
    }
}

这里简单说明下,input是file类型,/var/log/nginx/access.log_json每行是一个json数据,codec => “json”指定按json解析。

output 配置了两个,stdout 是控制台输出,elasticsearch 则是输出到前面配置的es服务器,index 索引名称为logstash-nginx-日期,这样每天为一个索引。

Nginx 直接生成json日志可以通过指定logformat,拼接为json:

配置logformat:

log_format json '{"@timestamp":"$time_iso8601",'

'"host":"$server_addr",'

'"clientip":"$remote_addr",'

'"size":$body_bytes_sent,'

'"responsetime":$request_time,'

'"upstreamtime":"$upstream_response_time",'

'"upstreamhost":"$upstream_addr",'

'"http_host":"$host",'

'"url":"$uri",'

'"xff":"$http_x_forwarded_for",'

'"referer":"$http_referer",'

'"agent":"$http_user_agent",'

'"status":"$status"}';

然后使用:

access_log /var/log/nginx/access.log_json json;

这样nginx的日志就变为:

2.2.3 启动logstash

使用-f指定配置文件

bin/logstash -f nginxlog2es.conf

启动成功后就能看到收集到的日志:

2.3 kibana

2.3.1 安装

kibana安装也比较简单,下载,解压,配置即可

wget https://artifacts.elastic.co/downloads/kibana/kibana-5.2.2-linux-x86_64.tar.gz

解压后建立软连接

ln -s kibana-5.2.2-linux-x86_64 kibana

cd kibana

修改配置文件

vim config/kibana.yml

修改

server.host: “192.168.86.108”

elasticsearch.url: “http://192.168.86.108:9200

然后启动:

bin/kibana

2.3.2 配置报表

启动后打开http://192.168.86.108:5601,就能看到日志了,在输入框里还可以输入关键词进行搜索,并且自带流量统计。

下面来看如何生成报表,比如搞个响应时间折线图,进入visualize,选择折线图

分别配置X和y轴,Y为平均响应时间,X为时间,最后保存即可。

再来配置一个表格报表,通产各个url的访问量

Metrics 配置使用count作为聚合函数

buckets增加rows,字段选url

再增加一个状态码:

这样就完成了一个漂亮的表格

2.3.3 将报表添加到dashboard

在dashboard点add,将两个报表加入即可

3、总结

简单的介绍就到这里为止了,更多的功能后面继续挖掘。

总结下,借助ELK可以快速搭建日志收集、日志搜索和统计分析平台,可以不写一行代码完成漂亮的统计报表,简直是开发和运维的福音,是不是心动了?如果是,快行动起来吧!

文章作者:jqpeng
原文链接: Jupyter notebook安装与使用

  • Jupyter Notebook(此前被称为 IPython notebook)是一个交互式笔记本,支持运行 40 多种编程语言。

  • 安装

    • 安装python 3
    • pip安装
      pip3 install –upgrade pip
      pip3 install jupyter
  • 运行

  • 笔记操作

    • 打开已有笔记
      例如,打开网友分享的CS231课程笔记,https://github.com/zlotus/cs231n
      下载zip包,解压到用户目录,刷新dashboard就能看到了

      .ipynb就是保存的笔记,单击打开,enjoy it!
    • 运行实例管理

      在running里可以查看正在运行的实例,可以shutdown
    • 新建笔记
      点击new菜单里的Python 3就可以创建一个笔记了
    • 插入代码
    • 运行代码
    • 增加文本段
      点击新增(+),选择类型为markdown
    • 保存
      CTRL+S 或点击save按钮保存
    • 命名
      点击标题,会弹出rename框

文章作者:jqpeng
原文链接: java内嵌jetty服务器

有的时候需要将一个简单的功能封装为服务,相比python使用flask、web.py的简洁,使用java-web显得太重量级,幸好,我们可以直接在java项目中使用jetty来搭建简易服务

1、pom.xml加入jetty依赖

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.0.v20161208</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>9.4.0.v20161208</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-continuation</artifactId>
<version>9.4.0.v20161208</version>
</dependency>

<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-jsp</artifactId>
<version>9.1.4.v20140401</version>
</dependency>

2、增加Server

Serverserver=newServer(12580);

3、设置ServletContextHandler

ServletContextHandlercontext=newServletContextHandler(server,"/");
server.setHandler(context);

4、Context增加Servlet 
4.1 创建Servlet 继承HttpServlet,重载doGet,doPost即可

public class XXXHandler extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        JSONObject ret =  new JSONObject();
        try {
            String ttsTxt = req.getParameter("text");

            String outFile = System.nanoTime() + ".mp4";
            String url = xx.xxx(ttsTxt,...);
            ret.put("ret","0");
            ret.put("url",url);
        }catch (Exception ex){
            ret.put("ret","-1");
            ret.put("error",ex.getMessage());
        }
        if(req.getParameter("callback")!=null) {
            resp.getWriter().write(req.getParameter("callback")+"("+ret.toString()+")");
        }else {
            resp.getWriter().write(ret.toString());
        }
    }

}

4.2 将Servlet 加入Context

context.addServlet(xxxHandler.class,"/xxx");
context.addServlet(Image2VideoHandler.class,"/*");

5、启动server

server.start();
server.join();

6、在浏览器访问http://localhost:12580/XXX 即可

文章作者:jqpeng
原文链接: x 开头编码的数据解码成中文

Python 解码

在python里,直接decode(‘utf-8’)即可

>>> "\xE5\x85\x84\xE5\xBC\x9F\xE9\x9A\xBE\xE5\xBD\x93 \xE6\x9D\x9C\xE6\xAD\x8C".decode('utf-8')
u'\u5144\u5f1f\u96be\u5f53 \u675c\u6b4c'
>>> print "\xE5\x85\x84\xE5\xBC\x9F\xE9\x9A\xBE\xE5\xBD\x93 \xE6\x9D\x9C\xE6\xAD\x8C".decode('utf-8')
兄弟难当 杜歌
>>>

Java 解码

 public static void main(String[] args) throws UnsupportedEncodingException {
        String utf8String = "\\xE5\\x85\\x84\\xE5\\xBC\\x9F\\xE9\\x9A\\xBE\\xE5\\xBD\\x93 \\xE6\\x9D\\x9C\\xE6\\xAD\\x8C";
        System.out.println(decodeUTF8Str(utf8String));
    }

    public static String decodeUTF8Str(String xStr) throws UnsupportedEncodingException {
        return URLDecoder.decode(xStr.replaceAll("\\\\x", "%"), "utf-8");
    }

以上代码输出:

兄弟难当 杜歌

Process finished with exit code 0

深入理解:

要了解原理,推荐阅读http://www.ruanyifeng.com/2007/10/ascii\_unicode\_and\_utf-8.html

UTF-8是unicode编码的一种落地方案:

Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
——————–+———————————————
0000 0000-0000 007F | 0xxxxxxx
0000 0080-0000 07FF | 110xxxxx 10xxxxxx
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

\x对应的是UTF-8编码的数据,通过转化规则可以转换为Unicode编码,就能得到对应的汉字,转换规则很简单,先将\x去掉,转换为数字,然后进行对应的位移操作即可,需要注意的是先要判断utf-8的位数.

可以通过下面的scala代码深入了解:

 val pattern = """(\d+\.\d+\.\d+\.\d+) \- (\S+) (\S+) \[([^\]]+)\] \"(\w+) (\S+) \S+\" (\S+) (\S+) \"([^\"]+)\" \"([^\"]+)\" \"([^\"]+)\" \"([^\"]+)""".r
  val decodeDataPattern = """(\\x([0-9A-Z]){2})+""".r
  def decodeUtf8(utf8Str:String):String={
    var data =   decodeDataPattern.replaceAllIn(utf8Str, m=>{
        var item = decodeXdata(m.toString())
        item
     }) 
     return data
   }
     
   def decodeXdata(utf8Str:String):String={
     var arr = utf8Str.split("\\\\x")
     var result = new StringBuilder()
     var isMatchEnd = true
     var matchIndex = 0
     var currentWordLength = 0
     var current = 0
     var e0=0xe0;
     
     for(item <-arr){
        var str = item.trim
        if(str.length()>0){
           var currentCode =  Integer.parseInt(str, 16);
           if(isMatchEnd){
             isMatchEnd = false
             var and = currentCode & e0;
             if(and == 0xe0){
                matchIndex = 1;
                currentWordLength = 3;
                current =  (currentCode & 0x1f) <<12  // 3位编码的
             }else if(and==96){
                matchIndex = 1;
                currentWordLength = 2;
                current =  (currentCode & 0x1f) <<6 // 2位编码的
             }else{
               current = currentCode  // 1位编码的
             }
          }else{
            matchIndex = matchIndex+1;
            if(matchIndex == 2)
            {
              current+=(currentCode & 0x3f) <<6
            }else{
               current+=(currentCode & 0x3f) 
            }
          }
           if(matchIndex==currentWordLength){
               var hex = Integer.toHexString(current)
               hex = if(hex.length()<4) "\\u00"+hex else "\\u"+hex  //补0
               result.append(new String(StringEscapeUtils.unescapeJava(hex).getBytes,"utf-8")) 
               current = 0
               matchIndex=0
               isMatchEnd = true
           }
        }
     }
     
     return result.toString()
   }

Javascript解码:

Javascript \x 反斜杠x 16进制 编解码