Java has a relatively complete ecosystem in microservices. Common ones in China are:

  • Spring Ecology based on Consul.nest-cloudIs to build microservices based on Consul.
  • alibaba.comdubbowithsofa.

Basically the domestic Java microservices are using the above ecology. Nodejs, however, does not have a well-developed system for interworking with Java microservices, except for nest-Cloud microservices. Today, I’m going to show you how to build Java microservices using Dubbo on NodeJS.

Our main character is Dubo.ts

If it wasn’t written by TS, you’d be embarrassed to mention it.

TCP communications

All microservices are basically based on TCP long connection. The main problems we need to solve are as follows:

  • Packet sticking and unpacking during TCP data sending and receiving
  • TCP connection heartbeat detection and reconnection retry mechanism
  • Serialization and serialization algorithms for data transmission
  • Use the registry’s subscription mechanism

TCP transmission has very reliable security, unlike UDP transmission will not lose packets, so the communication between micro services is basically completed by TCP, once connected, the communication speed is very fast.

Service registration and discovery

In general, in Dubbo, we use Zookeeper for service registration and discovery, but there are also ecology implementations that use Redis. But today we’ll talk about microservices in the context of ZK.

ZK is essentially a KEY/VALUE store in the cloud with a subscription notification function. Node-zookeeper-client is recommended. The library is stable and has many downloads per week.

Creating a connection to the registry using dubo.ts is very simple:

import { Registry, RegistryInitOptions } from 'dubbo.ts';
const registry = new Registry({
  host: '127.0.0.1:2181' / / the zk address
} as RegistryInitOptions);
await registry.connect(); / / the connection
registry.close(); / / disconnect
Copy the code

To provide services

Throughout the NPM, no library of service providers is found. In general, nodeJS programs using Dubbo, which only call Java side services, cannot provide the registration and invocation of NodeJs-based microservices. That is, with Dubo.ts we can register our microservices in a registry, just like Java, for NodeJS or Java to call.

In Dubo.ts, we can play like this:

  1. Create a service provider object
import { 
  Provider, 
  ProviderInitOptions, 
  ProviderServiceChunkInitOptions 
} from 'dubbo.ts';

const provider = new Provider({
  application: 'test',
  dubbo_version: '2.0.2',
  port: 8080, pid: process.pid, registry: registry, heartbeat? :60000,}as ProviderInitOptions);
Copy the code
  1. Add a microservice interface definition for it
class CUATOM_SERVICE {
  xxx() {}
  ddd() {}
}
provider.addService(CUATOM_SERVICE, {
  interface: 'xxx',
  version: 'x.x.x',
  group; 'xxxx',
  methods: ['xxx', 'ddd'],
  timeout: 3000
} as ProviderServiceChunkInitOptions);
// ..,.
Copy the code
  1. Start the service to automatically register with the center or uninstall the service
await provider.listen();
await provider.close();
Copy the code

If you have zK’s monitoring platform, you can see that microservices have been registered on the platform.

consumers

Consumers are used to connect to microservices and get the final data through methods and metrics. It automatically discovers and connects to the service through ZK, and automatically unconnects when the service is unregistered. The following rules apply when requesting a service:

  • If the service method times out, it will be retried automatically.
  • If there are multiple microservice providers during retry, different calls to the same microservice interface will be selected during retry.
  • If there is only one microservice provider when retrying, retry the interface N times.
  • If the retry does not have a provider, it is reportedno prividersError.

Create a consumer object:

import { Consumer } from 'dubbo.ts';
const consumer = new Consumer({
  application: 'dist',
  dubbo_version: '2.0.2',
  pid: process.pid,
  registry: registry,
});
await consumer.listen();
await consumer.close();
Copy the code

Connect to microservices for data

const invoker = await consumer.get('com.mifa.stib.service.ProviderService');
const java = require('js-to-java');
type resultData = {
  name: string,
  age: number,}const result = await invoker.invoke<resultData>('testRpc', [
  java.combine('com.mifa.stib.common.RpcData', {
    "name":"gxh"."age":"18",})])Copy the code

With a very simple call, we can get microservice data. All the customers on NPM have similar designs, and some have fuse breakers. The architecture does not handle this, and users can do it themselves as required.

The framework constitutes the AOP pattern

Using TS to create Java-like annotations is very convenient. It uses the parameters of provider. addService. You can design by yourself. Here is an example of our final use (you can also refer to @nelts/ Dubbo’s annotated design) :

import { provide, inject } from 'injection';
import { rpc } from '@nelts/dubbo';
import { RPC_INPUT_SCHEMA, MIN_PROGRAM_TYPE, error } from '@node/com.stib.utils'; // Package on private source, ignore function when reference
import WX from './wx';
import * as ioredis from 'ioredis';

@provide('User')
@rpc.interface('com.mifa.stib.service.User')
@rpc.version('1.0.0')
export default class UserService {
  @inject('wx')
  private wx: WX;

  @inject('redis')
  private redis: ioredis.Redis;

  @rpc.method
  @rpc.middleware(OutputConsole)
  login(req: RPC_INPUT_SCHEMA) {
    switch (req.headers.platform) {
      case MIN_PROGRAM_TYPE.WX:
        if (req.data.code) return this.wx.codeSession(req.data.code);
        return this.wx.jsLogin(req.data, req.headers.appName);
      case MIN_PROGRAM_TYPE.WX_SDK: return this.wx.sdkLogin(req.data.code, req.headers.appName);
      default: throw error('Unsupported login types'); }}@rpc.method
  async status(req: RPC_INPUT_SCHEMA) {
    if(! req.headers.userToken)throw error('401 Not logined'.401);
    const value = await this.redis.get(req.headers.userToken);
    if(! value)throw error('401 Not logined'.401);
    const user = await this.redis.hgetall(value);
    if(! value)throw error('401 Not logined'.401);
    user.sex = Number(user.sex);
    user.id = undefined;
    user.create_time = undefined;
    user.modify_time = undefined;
    user.unionid = undefined;
    returnuser; }}async function OutputConsole(ctx, next) {
  console.log('in middleware');
  await next()
}
Copy the code

I recommend using the Injection module of Midway. Js to design the IOC model between services, which is more conducive to development and maintenance.

Swagger

Generally speaking, in Java, Swagger is built on microservices, and Spring’s entire swagger is pure HTTP. I referred to Dubbo’s Swagger mode, and found that the single-service single-swagger mode was not conducive to developers’ consulting, so we agreed on a distributed swagger mode. Duboo.ts is built in.

Microservice Swagger method, using zooKeeper self-management scheme. After microservices are started, interface and Method information is collected and reported to the customized ZooKeeper node to complete data reporting. The front-end service can read this node information to obtain specific interfaces and methods.

Report format:

/swagger/{subject}/{interface}/exports/{base64 data}
Copy the code

The url parameter:

  • subjectTotal project name node name
  • interfaceThe interface name
  • base64 dataIt is an array (eventually base64) that records the methods and arguments under the interface, as shown in the parameter format below.

Base64 data parameter Description

typeBase64DataType = { description? :string.// Description of the interface
  group: string.// Group name If there is no group, use the string '-'
  version: string.If there is no version, use the string '0.0.0'
  methods: [
    {
      name: string./ / the method namesummary? :string.// Method description, abstract
      input: ArrayThe < {$class: string, $schema: JSONSCHEMA; } >./ / into the refs
      output: JSONSCHEMA / / the refs
    },
    // ...]}Copy the code

EncodeURIComponent is performed after data base64, and the ZooKeeper node is inserted.

In the Provider application, we can use this to publish to ZooKeeper:

import { SwaggerProvider, Provider } from 'dubbo.ts';
const swagger = new SwaggerProvider('subject name', provider as Provider);
await swagger.publish(); / / release
await swagger.unPublish(); / / unloading
Copy the code

Data obtained after calling distributed Swgger with SwaggerConsumer.

import { SwaggerConsumer, Registry } from 'dubbo.ts';
const swgger = new SwaggerConsumer('subject name', registry as Registry);
const resultTree = await swgger.get();
Copy the code

Let’s look at an example based on @nelts/ Dubbo. On a specific microservice service, we can write this

import { provide, inject } from 'injection';
import { rpc } from '@nelts/dubbo';
import { RPC_INPUT_SCHEMA, MIN_PROGRAM_TYPE, error, RpcRequestParameter, RpcResponseParameter } from '@node/com.stib.utils';
import WX from './wx';
import * as ioredis from 'ioredis';
import Relations from './relations';
import { tableName as WxTableName } from '.. /tables/stib.user.wx';

@provide('User')
@rpc.interface('com.mifa.stib.service.UserService')
@rpc.version('1.0.0')
@rpc.description('User Center Service Interface')
export default class UserService {
  @inject('wx')
  private wx: WX;

  @inject('redis')
  private redis: ioredis.Redis;

  @inject('relation')
  private rel: Relations;

  @rpc.method
  @rpc.summay('Unified User Login')
  @rpc.parameters(RpcRequestParameter({
    type: 'object',
    properties: {
      code: {
        type: 'string'}}}))@rpc.response(RpcResponseParameter({ type: 'string' }))
  login(req: RPC_INPUT_SCHEMA) {
    switch (req.headers.platform) {
      case MIN_PROGRAM_TYPE.WX:
        if (req.data.code) return this.wx.codeSession(req.data.code);
        return this.wx.jsLogin(req.data, req.headers.appName);
      case MIN_PROGRAM_TYPE.WX_SDK: return this.wx.sdkLogin(req.data.code, req.headers.appName);
      default: throw error('Unsupported login types'); }}@rpc.method
  @rpc.parameters(RpcRequestParameter())
  @rpc.summay('Get current user state')
  async status(req: RPC_INPUT_SCHEMA) {
    if(! req.headers.userToken)throw error('401 Not logined'.401);
    const rid = await this.redis.get(req.headers.userToken);
    if(! rid)throw error('401 Not logined'.401);
    const user = await this.getUserDetailInfoByRelationId(Number(rid)).catch(e= > Promise.reject(error('401 Not logined'.401)));
    user.sex = Number(user.sex);
    Reflect.deleteProperty(user, 'id');
    Reflect.deleteProperty(user, 'create_time');
    Reflect.deleteProperty(user, 'modify_time');
    Reflect.deleteProperty(user, 'unionid');
    return user;
  }

  @rpc.method
  @rpc.summay('Get a user's details')
  @rpc.parameters(RpcRequestParameter({
    type: 'object',
    properties: {
      rid: {
        type: 'integer'}}}))async getUserDetailInfo(req: RPC_INPUT_SCHEMA) {
    return await this.getUserDetailInfoByRelationId(req.data.rid as number);
  }

  async getUserDetailInfoByRelationId(sid: number) {
    const relations: {
      f: string,
      p: string,
      s: string,} =await this.rel.get(sid);
    switch (relations.f) {
      case WxTableName: return await this.wx.getUserinfo(relations.f, Number(relations.s)); }}}Copy the code

The advantage of this Swagger pattern, called distributed Swagger, is that if you use the same ZK registry, you can aggregate Swagger regardless of which server the service is deployed on.

The last

Both Java calling NodeJS microservices and NodeJS calling Java microservices are very convenient. The purpose of this article is to explain how to build a set of dubbo based microservices on NodeJS. This system has been used in our company’s internal new projects and is very stable. If you like, please refer to the documentation of Dubo.ts for more information about API usage. Hopefully dubo.ts will help you in a real-world business scenario. Thank you!