From e280fdbb483a53edeed25302ab8adbad569e71d9 Mon Sep 17 00:00:00 2001 From: gxcsoccer Date: Thu, 23 Aug 2018 23:33:26 +0800 Subject: [PATCH] feat: integrated with sofa-rpc-node --- .travis.yml | 4 +- README.md | 131 +- appveyor.yml | 6 +- example/client.js | 33 + example/server.js | 36 + index.js | 23 - lib/decoder.js | 68 +- lib/encoder.js | 187 +++ lib/index.js | 18 + lib/protocol/dubbo/index.js | 41 - lib/protocol/dubbo/result.js | 10 - lib/protocol/index.js | 73 ++ lib/protocol/{dubbo => }/invocation.js | 0 lib/protocol/{dubbo => }/request.js | 72 +- lib/protocol/{dubbo => }/response.js | 92 +- lib/serialize/hessian/compile.js | 192 +++ .../{hessian.js => hessian/index.js} | 9 +- .../hessian/primitive_type/boolean.js | 6 + .../hessian/primitive_type/custom_map.js | 135 ++ .../hessian/primitive_type/double.js | 6 + lib/serialize/hessian/primitive_type/int.js | 6 + .../primitive_type/java.lang.boolean.js | 6 + .../hessian/primitive_type/java.lang.class.js | 29 + .../primitive_type/java.lang.double.js | 6 + .../primitive_type/java.lang.exception.js | 73 ++ .../primitive_type/java.lang.integer.js | 6 + .../hessian/primitive_type/java.lang.long.js | 6 + .../primitive_type/java.lang.object.js | 11 + .../java.lang.stacktraceelement.js | 50 + .../primitive_type/java.lang.string.js | 7 + .../primitive_type/java.math.bigdecimal.js | 25 + .../primitive_type/java.util.arraylist.js | 28 + .../primitive_type/java.util.currency.js | 25 + .../hessian/primitive_type/java.util.date.js | 7 + .../hessian/primitive_type/java.util.list.js | 43 + .../primitive_type/java.util.locale.js | 25 + .../hessian/primitive_type/java.util.map.js | 72 ++ lib/serialize/hessian/primitive_type/long.js | 6 + lib/serialize/hessian/utils.js | 50 + lib/utils.js | 14 +- package.json | 40 +- test/fixtures/class_map.js | 263 ++++ test/hessian.test.js | 1139 +++++++++++++++++ test/index.test.js | 457 ++++--- test/protocol.test.js | 166 +++ test/utils.test.js | 2 +- 46 files changed, 3221 insertions(+), 483 deletions(-) create mode 100644 example/client.js create mode 100644 example/server.js delete mode 100644 index.js create mode 100644 lib/encoder.js create mode 100644 lib/index.js delete mode 100644 lib/protocol/dubbo/index.js delete mode 100644 lib/protocol/dubbo/result.js create mode 100644 lib/protocol/index.js rename lib/protocol/{dubbo => }/invocation.js (100%) rename lib/protocol/{dubbo => }/request.js (62%) rename lib/protocol/{dubbo => }/response.js (53%) create mode 100644 lib/serialize/hessian/compile.js rename lib/serialize/{hessian.js => hessian/index.js} (89%) create mode 100644 lib/serialize/hessian/primitive_type/boolean.js create mode 100644 lib/serialize/hessian/primitive_type/custom_map.js create mode 100644 lib/serialize/hessian/primitive_type/double.js create mode 100644 lib/serialize/hessian/primitive_type/int.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.boolean.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.class.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.double.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.exception.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.integer.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.long.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.object.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.stacktraceelement.js create mode 100644 lib/serialize/hessian/primitive_type/java.lang.string.js create mode 100644 lib/serialize/hessian/primitive_type/java.math.bigdecimal.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.arraylist.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.currency.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.date.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.list.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.locale.js create mode 100644 lib/serialize/hessian/primitive_type/java.util.map.js create mode 100644 lib/serialize/hessian/primitive_type/long.js create mode 100644 lib/serialize/hessian/utils.js create mode 100644 test/fixtures/class_map.js create mode 100644 test/hessian.test.js create mode 100644 test/protocol.test.js diff --git a/.travis.yml b/.travis.yml index 5e7a967..ce21122 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ sudo: false language: node_js node_js: - - '6' - - '7' + - '8' + - '10' install: - npm i npminstall && npminstall script: diff --git a/README.md b/README.md index 1037ee8..a4c30a1 100644 --- a/README.md +++ b/README.md @@ -85,49 +85,96 @@ packet status not ok $ npm install dubbo-remoting --save ``` -## API - -- `decoder(url)` get decoder of the connection with certain url - - @param {String} connection url - - @return {DubboDecoder} - - ```js - const net = require('net'); - const protocol = require('dubbo-remoting'); - const url = 'dubbo://127.0.0.0:12200/com.xxx.DemoService?_TIMEOUT=2000&_p=4&application=xx&default.service.filter=dragoon&dubbo=2.6.1&interface=com.xxx.DemoService&methods=sayHello&pid=25381&revision=2.6.1&side=provider&threads=300&timeout=2000×tamp=1487081081346&v=2.0&version=1.0.0'; - const decoder = protocol.decoder(url) - - const socket = net.connect(12200, '127.0.0.1'); - socket.pipe(decoder); - - decoder.on('packet', p => { - console.log('packet', p); +## Usage + +You can use this dubbo protocol implementation with the [sofa-rpc-node](https://github.com/alipay/sofa-rpc-node) + +### 1. Install & Launch zk + +```bash +$ brew install zookeeper + +$ zkServer start +ZooKeeper JMX enabled by default +Using config: /usr/local/etc/zookeeper/zoo.cfg +Starting zookeeper ... STARTED +``` + +### 2. Expose a dubbo service + +```js +'use strict'; + +const { RpcServer } = require('sofa-rpc-node').server; +const { ZookeeperRegistry } = require('sofa-rpc-node').registry; +const protocol = require('dubbo-remoting'); + +const logger = console; + +// 1. create zk registry client +const registry = new ZookeeperRegistry({ + logger, + address: '127.0.0.1:2181', +}); + +// 2. create rpc server +const server = new RpcServer({ + logger, + registry, + port: 12200, + protocol, +}); + +// 3. add service +server.addService({ + interfaceName: 'com.nodejs.test.TestService', +}, { + async plus(a, b) { + return a + b; + }, +}); + +// 4. launch the server +server.start() + .then(() => { + server.publish(); }); - socket.on('connect', () => { - console.log('connected'); +``` + +### 3. Call the dubbo service + +```js +'use strict'; + +const { RpcClient } = require('sofa-rpc-node').client; +const { ZookeeperRegistry } = require('sofa-rpc-node').registry; +const protocol = require('dubbo-remoting'); +const logger = console; + +// 1. create zk registry client +const registry = new ZookeeperRegistry({ + logger, + address: '127.0.0.1:2181', +}); + +async function invoke() { + // 2. create rpc client with dubbo protocol + const client = new RpcClient({ + logger, + registry, + protocol, }); - socket.on('error', err => { - console.error('err', err); + // 3. create rpc service consumer + const consumer = client.createConsumer({ + interfaceName: 'com.nodejs.test.TestService', }); + // 4. wait consumer ready + await consumer.ready(); - const Request = protocol.Request; - const Invocation = protocol.Invocation; - const req = new Request(); - req.data = new Invocation({ - methodName: 'sayHello', - args: ['zongyu'], - attachments: { - path: 'com.xxx.DemoService', - interface: 'com.xxx.DemoService', - version: '1.0.0', - timeout: 2000, - }, - }); - socket.write(req.encode()); - ``` - -- `DubboDecoder` an writable stream, your can pipe socket to it -- `Request` the Dubbo request -- `Invocation` the abstraction of the Dubbo service invocation -- `Response` the Dubbo response -- `Result` the abstraction of the Dubbo service result + // 5. call the service + const result = await consumer.invoke('plus', [1, 2], { responseTimeout: 3000 }); + console.log('1 + 2 = ' + result); +} + +invoke().catch(console.error); +``` diff --git a/appveyor.yml b/appveyor.yml index ec7400e..981e82b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ environment: matrix: - - nodejs_version: '6' - - nodejs_version: '7' + - nodejs_version: '8' + - nodejs_version: '10' install: - ps: Install-Product node $env:nodejs_version @@ -10,6 +10,6 @@ install: test_script: - node --version - npm --version - - npm run ci + - npm run test build: off diff --git a/example/client.js b/example/client.js new file mode 100644 index 0000000..4fa6f59 --- /dev/null +++ b/example/client.js @@ -0,0 +1,33 @@ +'use strict'; + +const { RpcClient } = require('sofa-rpc-node').client; +const { ZookeeperRegistry } = require('sofa-rpc-node').registry; +const protocol = require('..'); +const logger = console; + +// 1. 创建 zk 注册中心客户端 +const registry = new ZookeeperRegistry({ + logger, + address: '127.0.0.1:2181', +}); + +async function invoke() { + // 2. 创建 RPC Client 实例 + const client = new RpcClient({ + logger, + registry, + protocol, + }); + // 3. 创建服务的 consumer + const consumer = client.createConsumer({ + interfaceName: 'com.nodejs.test.TestService', + }); + // 4. 等待 consumer ready(从注册中心订阅服务列表...) + await consumer.ready(); + + // 5. 执行泛化调用 + const result = await consumer.invoke('plus', [1, 2], { responseTimeout: 3000 }); + console.log('1 + 2 = ' + result); +} + +invoke().catch(console.error); diff --git a/example/server.js b/example/server.js new file mode 100644 index 0000000..7f645e6 --- /dev/null +++ b/example/server.js @@ -0,0 +1,36 @@ +'use strict'; + +const { RpcServer } = require('sofa-rpc-node').server; +const { ZookeeperRegistry } = require('sofa-rpc-node').registry; +const protocol = require('..'); + +const logger = console; + +// 1. 创建 zk 注册中心客户端 +const registry = new ZookeeperRegistry({ + logger, + address: '127.0.0.1:2181', // 需要本地启动一个 zkServer +}); + +// 2. 创建 RPC Server 实例 +const server = new RpcServer({ + logger, + registry, // 传入注册中心客户端 + port: 12200, + protocol, +}); + +// 3. 添加服务 +server.addService({ + interfaceName: 'com.nodejs.test.TestService', +}, { + async plus(a, b) { + return a + b; + }, +}); + +// 4. 启动 Server 并发布服务 +server.start() + .then(() => { + server.publish(); + }); diff --git a/index.js b/index.js deleted file mode 100644 index 636eec9..0000000 --- a/index.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -const DubboDecoder = require('./lib/decoder'); -const Result = require('./lib/protocol/dubbo/result'); -const Request = require('./lib/protocol/dubbo/request'); -const Response = require('./lib/protocol/dubbo/response'); -const Invocation = require('./lib/protocol/dubbo/invocation'); - -/** - * get dubbo decoder - * - * @param {String} url - the url - * @return {DubboDecoder} decoder - */ -exports.decoder = url => { - return new DubboDecoder({ url }); -}; - -exports.DubboDecoder = DubboDecoder; -exports.Result = Result; -exports.Request = Request; -exports.Response = Response; -exports.Invocation = Invocation; diff --git a/lib/decoder.js b/lib/decoder.js index ce3ad61..c1a2fa7 100644 --- a/lib/decoder.js +++ b/lib/decoder.js @@ -1,40 +1,20 @@ 'use strict'; -const is = require('is-type-of'); -const assert = require('assert'); -const utils = require('./utils'); -const urlparse = require('url').parse; +const protocol = require('./protocol'); const Writable = require('stream').Writable; -// 协议实现 -const protocolMap = { - dubbo: require('./protocol/dubbo'), - exchange: require('./protocol/dubbo'), -}; +const HEADER_LENGTH = 16; class DubboDecoder extends Writable { - constructor(options) { - assert(options && is.string(options.url), '[dubbo-remoting] options.url is required'); + constructor(options = {}) { super(options); this._buf = null; - this._url = urlparse(options.url, true); - const proto = this._url.protocol.replace(/:?$/, ''); // trim tail ":" - this._protocol = protocolMap[proto]; - assert(this._protocol, `[dubbo-remoting] unsupport protocol => ${proto}`); - } - - /** - * 根据 url 返回匹配的协议 - * - * @property {Object} DubboDecoder#protocol - */ - get protocol() { - return this._protocol; + this.options = options; } _write(chunk, encoding, callback) { - // merge old & new bytes - this._buf = this._buf ? utils.concatBuffer(this._buf, chunk) : chunk; + // 合并 buf 中的数据 + this._buf = this._buf ? Buffer.concat([ this._buf, chunk ]) : chunk; try { let unfinish = false; do { @@ -42,26 +22,40 @@ class DubboDecoder extends Writable { } while (unfinish); callback(); } catch (err) { + err.name = 'DubboDecodeError'; + err.data = this._buf ? this._buf.toString('base64') : ''; callback(err); } } _decode() { - const ret = this.protocol.decode(this._buf); - if (ret) { - // 这里异步化是为了避免 listeners 业务报错影响到 decoder - process.nextTick(() => { this.emit('packet', ret.packet); }); + const bufLength = this._buf.length; - const bufSize = this._buf.length; - const rest = bufSize - ret.total; - if (rest > 0) { - this._buf = this._buf.slice(ret.total); - return true; - } - this._buf = null; + if (bufLength < HEADER_LENGTH) { + return false; + } + const bodyLen = this._buf.readInt32BE(12); + const packetLength = bodyLen + HEADER_LENGTH; + if (packetLength === 0 || bufLength < packetLength) { + return false; + } + const packet = this._buf.slice(0, packetLength); + // 调用反序列化方法获取对象 + const obj = protocol.decode(packet, this.options); + this.emit(obj.packetType, obj); + const restLen = bufLength - packetLength; + if (restLen) { + this._buf = this._buf.slice(packetLength); + return true; } + this._buf = null; return false; } + + _destroy() { + this._buf = null; + this.emit('close'); + } } module.exports = DubboDecoder; diff --git a/lib/encoder.js b/lib/encoder.js new file mode 100644 index 0000000..bf25806 --- /dev/null +++ b/lib/encoder.js @@ -0,0 +1,187 @@ +'use strict'; + +const noop = require('utility').noop; +const protocol = require('./protocol'); +const Transform = require('stream').Transform; + +class DubboEncoder extends Transform { + /** + * 协议编码器 + * + * @param {Object} options + * - {Map} sentReqs - 请求集合 + * - {Map} classCache - 类定义缓存 + * - {Object} classMap - 针对 hessian 序列化的 schema + * - {URL} [address] - TCP socket 地址 + * @constructor + */ + constructor(options = {}) { + super(options); + this.options = options; + + let codecType = 'hessian2'; + if (options.codecType) { + codecType = options.codecType; + } else if (options.address && options.address.query && options.address.query.serialization) { + codecType = options.address.query.serialization; + } + this.encodeOptions = { + protocolType: 'dubbo', + codecType, + classMap: options.classMap, + classCache: options.classCache, + }; + this.sentReqs = options.sentReqs; + + this._limited = false; + this._queue = []; + this.once('close', () => { this._queue = []; }); + this.on('drain', () => { + this._limited = false; + do { + const item = this._queue.shift(); + if (!item) break; + + const packet = item[0]; + const callback = item[1]; + this._writePacket(packet, callback); + } while (!this._limited); + }); + } + + get protocol() { + return protocol; + } + + get protocolType() { + return this.encodeOptions.protocolType; + } + + get codecType() { + return this.encodeOptions.codecType; + } + + set codecType(val) { + this.encodeOptions.codecType = val; + } + + writeRequest(id, req, callback) { + this._writePacket({ + packetId: id, + packetType: 'request', + req, + meta: this._createMeta(this.encodeOptions), + }, callback); + } + + writeResponse(req, res, callback) { + this._writePacket({ + packetId: req.packetId, + packetType: 'response', + req, + res, + meta: this._createMeta(req.options), + }, callback); + } + + writeHeartbeat(id, hb, callback) { + this._writePacket({ + packetId: id, + packetType: 'heartbeat', + hb, + meta: this._createMeta(this.encodeOptions), + }, callback); + } + + writeHeartbeatAck(hb, callback) { + this._writePacket({ + packetId: hb.packetId, + packetType: 'heartbeatAck', + hb, + meta: this._createMeta(hb.options), + }, callback); + } + + _createMeta(proto) { + return Object.create(proto, { + start: { + writable: true, + configurable: true, + value: Date.now(), + }, + data: { + writable: true, + configurable: true, + value: null, + }, + size: { + writable: true, + configurable: true, + value: 0, + }, + encodeRT: { + writable: true, + configurable: true, + value: 0, + }, + }); + } + + _writePacket(packet, callback = noop) { + if (this._limited) { + this._queue.push([ packet, callback ]); + } else { + const start = Date.now(); + let buf; + try { + buf = this['_' + packet.packetType + 'Encode'](packet); + } catch (err) { + return callback(err, packet); + } + packet.meta.data = buf; + packet.meta.size = buf.length; + packet.meta.encodeRT = Date.now() - start; + // @refer: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback + // The return value is true if the internal buffer is less than the highWaterMark configured + // when the stream was created after admitting chunk. If false is returned, further attempts to + // write data to the stream should stop until the 'drain' event is emitted. + this._limited = !this.write(buf, err => { + callback(err, packet); + }); + } + } + + _requestEncode(packet) { + const id = packet.packetId; + const req = packet.req; + if (req.codecType) { + packet.meta.codecType = req.codecType; + } + return this.protocol.requestEncode(id, req, packet.meta); + } + + _responseEncode(packet) { + const id = packet.packetId; + const req = packet.req; + const res = packet.res; + res.appRequest = req.data; + return this.protocol.responseEncode(id, res, packet.meta); + } + + _heartbeatEncode(packet) { + const id = packet.packetId; + const hb = packet.hb; + return this.protocol.heartbeatEncode(id, hb, this.encodeOptions); + } + + _heartbeatAckEncode(packet) { + const id = packet.packetId; + return this.protocol.heartbeatAckEncode(id, packet.meta); + } + + _transform(buf, encoding, callback) { + callback(null, buf); + } +} + +module.exports = DubboEncoder; diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..61e4207 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,18 @@ +'use strict'; + +const ProtocolEncoder = require('./encoder'); +const ProtocolDecoder = require('./decoder'); + +const globalOptions = {}; + +exports.setOptions = options => { + Object.assign(globalOptions, options); +}; + +exports.decoder = options => { + return new ProtocolDecoder(Object.assign({}, globalOptions, options)); +}; + +exports.encoder = options => { + return new ProtocolEncoder(Object.assign({}, globalOptions, options)); +}; diff --git a/lib/protocol/dubbo/index.js b/lib/protocol/dubbo/index.js deleted file mode 100644 index 223461d..0000000 --- a/lib/protocol/dubbo/index.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -const Long = require('long'); -const utils = require('../../utils'); -const Request = require('./request'); -const Response = require('./response'); - -const MAGIC_HIGH = 0xda; -const HEADER_LENGTH = 16; -const FLAG_REQUEST = 0x80; - -exports.decode = buf => { - if (buf[0] !== MAGIC_HIGH) { - throw new Error(`[dubbo-remoting] invalid packet with magic => ${buf[0]}`); - } - - const bufSize = buf.length; - if (bufSize < HEADER_LENGTH) { - return null; - } - const bodyLen = buf.readInt32BE(12); - const total = bodyLen + HEADER_LENGTH; - if (bufSize < total) { - return null; - } - const flag = buf[2]; - const id = utils.handleLong(new Long( - buf.readInt32BE(8), // low, high - buf.readInt32BE(4) - )); - const packet = (flag & FLAG_REQUEST) === 0 ? new Response(id) : new Request(id); - packet.decode(buf); - return { - packet, - total, - }; -}; - -exports.encode = (msg, sType) => { - return msg.encode(sType); -}; diff --git a/lib/protocol/dubbo/result.js b/lib/protocol/dubbo/result.js deleted file mode 100644 index 60636a2..0000000 --- a/lib/protocol/dubbo/result.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -class Result { - constructor(options) { - this.value = options && options.value; - this.error = options && options.error; - } -} - -module.exports = Result; diff --git a/lib/protocol/index.js b/lib/protocol/index.js new file mode 100644 index 0000000..be4d33e --- /dev/null +++ b/lib/protocol/index.js @@ -0,0 +1,73 @@ +'use strict'; + +const Request = require('./request'); +const Response = require('./response'); +const Invocation = require('./invocation'); + +exports.requestEncode = (id, data, options) => { + const req = new Request(id); + const arr = data.serverSignature.split(':'); + let serviceName = data.serverSignature; + let version = '1.0.0'; + if (arr.length === 2) { + serviceName = arr[0]; + version = arr[1]; + } + req.data = new Invocation({ + methodName: data.methodName, + args: data.args, + attachments: Object.assign({ + dubbo: '5.3.0', + path: serviceName, + version, + }, data.requestProps), + }); + return req.encode(options); +}; + +exports.responseEncode = (id, data, options) => { + const res = new Response(id); + res.status = data.isError ? Response.SERVER_ERROR : Response.OK; + res.errorMsg = data.errorMsg; + res.data = data.appResponse; + return res.encode(options); +}; + +exports.heartbeatEncode = (id, hb, options) => { + const req = new Request(id); + req.event = null; + return req.encode(options); +}; + +exports.heartbeatAckEncode = (id, options) => { + const res = new Response(id); + res.event = null; + return res.encode(options); +}; + +const FLAG_REQUEST = 0x80; + +/** + * 反序列化 + * @param {ByteBuffer} buf - 二进制 + * @param {Object} options + * - {Map} reqs - 请求集合 + * - {Object} [classCache] - 类定义缓存 + * @return {Object} 反序列化后的对象 + */ +exports.decode = (buf, options) => { + const start = Date.now(); + + const bufSize = buf.length; + const flag = buf[2]; + const ret = (flag & FLAG_REQUEST) === 0 ? + Response.decode(buf, options) : + Request.decode(buf, options); + + ret.meta = { + size: bufSize, + start, + rt: Date.now() - start, + }; + return ret; +}; diff --git a/lib/protocol/dubbo/invocation.js b/lib/protocol/invocation.js similarity index 100% rename from lib/protocol/dubbo/invocation.js rename to lib/protocol/invocation.js diff --git a/lib/protocol/dubbo/request.js b/lib/protocol/request.js similarity index 62% rename from lib/protocol/dubbo/request.js rename to lib/protocol/request.js index c66c359..b3dbccb 100644 --- a/lib/protocol/dubbo/request.js +++ b/lib/protocol/request.js @@ -1,11 +1,12 @@ 'use strict'; +const Long = require('long'); const is = require('is-type-of'); -const utils = require('../../utils'); -const Constants = require('../../const'); +const utils = require('../utils'); +const Constants = require('../const'); const Invocation = require('./invocation'); -const getSerializationById = require('../../serialize').getSerializationById; -const getSerializationByName = require('../../serialize').getSerializationByName; +const getSerializationById = require('../serialize').getSerializationById; +const getSerializationByName = require('../serialize').getSerializationByName; const DUBBO_VERSION = '2.5.3'; const HEADER_LENGTH = 16; @@ -44,9 +45,9 @@ class Request { return this.isEvent && this.event === HEARTBEAT_EVENT; } - encode(sType) { - sType = sType || Constants.DEFAULT_REMOTING_SERIALIZATION; - const output = getSerializationByName(sType).serialize(); + encode(options = {}) { + const { codecType, classMap } = options; + const output = getSerializationByName(codecType || Constants.DEFAULT_REMOTING_SERIALIZATION).serialize(); const bytes = output.bytes; bytes.put(MAGIC_HIGH); @@ -74,60 +75,77 @@ class Request { output.writeUTF(utils.getJavaArgsDesc(inv.args)); for (const arg of inv.args) { - output.writeObject(arg); + output.writeObject(arg, classMap); } - output.writeObject(inv.attachments); + output.writeObject(inv.attachments, classMap); } else { - output.writeObject(this.data); + output.writeObject(this.data, classMap); } const bodyLen = bytes.position() - HEADER_LENGTH; bytes._bytes.writeInt32BE(bodyLen, 12); return output.get(); } - decode(buf) { + static decode(buf, options = {}) { + const packetId = utils.handleLong(new Long( + buf.readInt32BE(8), // low, high + buf.readInt32BE(4) + )); + const flag = buf[2]; const sType = flag & SERIALIZATION_MASK; const input = getSerializationById(sType).deserialize(buf); // skip header input.skip(16); - this.isTwoWay = (flag & FLAG_TWOWAY) !== 0; + let packetType = 'request'; + let data; if ((flag & FLAG_EVENT) !== 0) { - this.event = HEARTBEAT_EVENT; - } - if (this.isEvent) { - this.data = input.readObject(); + packetType = 'heartbeat'; + data = input.readObject(); } else { - const data = input.readObject(); - if (is.string(data)) { + const field = input.readObject(); + if (is.string(field)) { const attachments = { - [Constants.DUBBO_VERSION_KEY]: data, + [Constants.DUBBO_VERSION_KEY]: field, [Constants.PATH_KEY]: input.readUTF(), [Constants.VERSION_KEY]: input.readUTF(), }; const methodName = input.readUTF(); const desc = input.readUTF(); - const parameterTypes = utils.desc2classArray(desc); - const argLen = parameterTypes.length; + const methodArgSigs = utils.desc2classArray(desc); + const argLen = methodArgSigs.length; const args = []; for (let i = 0; i < argLen; ++i) { args.push(input.readObject()); } - Object.assign(attachments, input.readObject()); - this.data = new Invocation({ + const serverSignature = attachments.version ? `${attachments.path}:${attachments.version}` : attachments.path; + data = { methodName, - // parameterTypes, + serverSignature, args, - attachments, - }); + methodArgSigs, + requestProps: attachments, + }; } else { - this.data = data; + data = field; } } + + return { + packetId, + packetType, + data, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: options.classMap, + }, + meta: null, + }; } } diff --git a/lib/protocol/dubbo/response.js b/lib/protocol/response.js similarity index 53% rename from lib/protocol/dubbo/response.js rename to lib/protocol/response.js index caa1991..056bb3d 100644 --- a/lib/protocol/dubbo/response.js +++ b/lib/protocol/response.js @@ -1,10 +1,10 @@ 'use strict'; -const java = require('js-to-java'); -const Result = require('./result'); -const Constants = require('../../const'); -const getSerializationById = require('../../serialize').getSerializationById; -const getSerializationByName = require('../../serialize').getSerializationByName; +const Long = require('long'); +const utils = require('../utils'); +const Constants = require('../const'); +const getSerializationById = require('../serialize').getSerializationById; +const getSerializationByName = require('../serialize').getSerializationByName; const HEADER_LENGTH = 16; const MAGIC_HIGH = 0xda; @@ -22,7 +22,7 @@ class Response { this.version = null; this.status = Response.OK; this.errorMsg = null; - this.data = {}; + this.data = null; this.isEvent = false; } @@ -47,9 +47,9 @@ class Response { return this.isEvent && this.event === HEARTBEAT_EVENT; } - encode(sType) { - sType = sType || Constants.DEFAULT_REMOTING_SERIALIZATION; - const output = getSerializationByName(sType).serialize(); + encode(options = {}) { + const { codecType, classMap } = options; + const output = getSerializationByName(codecType || Constants.DEFAULT_REMOTING_SERIALIZATION).serialize(); const bytes = output.bytes; bytes.put(MAGIC_HIGH); @@ -64,68 +64,84 @@ class Response { bytes.skip(4); if (this.status === Response.OK) { - if (this.data instanceof Result) { - if (this.data.error) { + if (this.data) { + if (this.data instanceof Error || (this.data.$class && this.data.$class.includes('Exception'))) { output.writeByte(RESPONSE_WITH_EXCEPTION); - output.writeObject(java.exception(this.data.error)); + output.writeObject(this.data.$class ? this.data : { + $class: 'java.lang.Exception', + $: this.data, + }, classMap); } else { - const ret = this.data.value; - if (ret) { + if (this.data) { output.writeByte(RESPONSE_VALUE); - output.writeObject(ret); + output.writeObject(this.data, classMap); } else { output.writeByte(RESPONSE_NULL_VALUE); } } } else { - output.writeObject(this.data); + output.writeObject(this.data, classMap); } } else { - output.writeUTF(this.errorMsg); + output.writeUTF(this.errorMsg || 'Exception caught in invocation'); } const bodyLen = bytes.position() - HEADER_LENGTH; bytes._bytes.writeInt32BE(bodyLen, 12); return output.get(); } - decode(buf) { + static decode(buf, options = {}) { + const packetId = utils.handleLong(new Long( + buf.readInt32BE(8), // low, high + buf.readInt32BE(4) + )); const flag = buf[2]; const sType = flag & SERIALIZATION_MASK; const input = getSerializationById(sType).deserialize(buf); // skip header input.skip(16); + let packetType = 'response'; if ((flag & FLAG_EVENT) !== 0) { - this.event = HEARTBEAT_EVENT; + packetType = 'heartbeat_ack'; } - this.status = buf[3]; - if (this.status === Response.OK) { - if (this.isHeartbeat) { - this.data = input.readObject(); + const status = buf[3]; + let data = null; + if (status === Response.OK) { + if (packetType === 'heartbeat_ack') { + data = input.readObject(); } else { + data = { + appResponse: null, + error: null, + }; const rFlag = input.readObject(); if (rFlag === RESPONSE_VALUE) { - this.data = new Result({ - value: input.readObject(), - error: null, - }); + data.appResponse = input.readObject(); } else if (rFlag === RESPONSE_WITH_EXCEPTION) { - this.data = new Result({ - value: null, - error: input.readObject(), - }); - } else if (rFlag === RESPONSE_NULL_VALUE) { - this.data = new Result({ - value: null, - error: null, - }); + data.error = input.readObject(); } else { - this.data = rFlag; + data.appResponse = rFlag; } } } else { - this.errorMsg = input.readUTF(); + const errorMsg = input.readUTF(); + data = { + appResponse: null, + error: new Error(errorMsg), + }; } + return { + packetId, + packetType, + data, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: options.classMap, + }, + meta: null, + }; } } diff --git a/lib/serialize/hessian/compile.js b/lib/serialize/hessian/compile.js new file mode 100644 index 0000000..19bc4b1 --- /dev/null +++ b/lib/serialize/hessian/compile.js @@ -0,0 +1,192 @@ +'use strict'; + +const debug = require('debug')('hessian#compile'); +const utils = require('./utils'); +const has = require('utility').has; +const codegen = require('@protobufjs/codegen'); + +const cache = new Map(); +const typeMap = { + bool: require('./primitive_type/boolean'), + boolean: require('./primitive_type/boolean'), + 'java.lang.Boolean': require('./primitive_type/java.lang.boolean'), + int: require('./primitive_type/int'), + 'java.lang.Integer': require('./primitive_type/java.lang.integer'), + short: require('./primitive_type/int'), + 'java.lang.Short': require('./primitive_type/java.lang.integer'), + long: require('./primitive_type/long'), + 'java.lang.Long': require('./primitive_type/java.lang.long'), + double: require('./primitive_type/double'), + 'java.lang.Double': require('./primitive_type/java.lang.double'), + float: require('./primitive_type/double'), + 'java.lang.Float': require('./primitive_type/java.lang.double'), + byte: require('./primitive_type/int'), + 'java.lang.Byte': require('./primitive_type/java.lang.integer'), + char: require('./primitive_type/java.lang.string'), + 'java.lang.Character': require('./primitive_type/java.lang.string'), + 'java.lang.String': require('./primitive_type/java.lang.string'), + 'java.util.Map': require('./primitive_type/java.util.map'), + 'java.util.HashMap': require('./primitive_type/java.util.map'), + 'java.util.List': require('./primitive_type/java.util.list'), + 'java.util.Set': require('./primitive_type/java.util.list'), + 'java.util.Collection': require('./primitive_type/java.util.list'), + 'java.util.ArrayList': require('./primitive_type/java.util.arraylist'), + 'java.util.Date': require('./primitive_type/java.util.date'), + 'java.lang.Class': require('./primitive_type/java.lang.class'), + 'java.util.Currency': require('./primitive_type/java.util.currency'), + 'java.math.BigDecimal': require('./primitive_type/java.math.bigdecimal'), + 'java.util.Locale': require('./primitive_type/java.util.locale'), + 'java.lang.Exception': require('./primitive_type/java.lang.exception'), + 'java.lang.StackTraceElement': require('./primitive_type/java.lang.stacktraceelement'), + 'java.lang.Object': require('./primitive_type/java.lang.object'), + customMap: require('./primitive_type/custom_map'), +}; +const arrayTypeMap = { + 'java.util.Locale': 'com.caucho.hessian.io.LocaleHandle', +}; +const bufferType = { + byte: true, + 'java.lang.Byte': true, +}; + +/** + * 预编译 + * + * @param {Object} info + * - {String} $class - 类名 + * @param {String} version - hessian 版本:1.0, 2.0 + * @param {Object} classMap - 类型映射 + * @return {Function} serializeFn + */ +module.exports = (info, version, classMap) => { + info.type = info.type || info.$class; + const uniqueId = utils.normalizeUniqId(info, version); + return compile(uniqueId, info, classMap, version); +}; + +function compile(uniqueId, info, classMap, version) { + let encodeFn = cache.get(uniqueId); + if (encodeFn) return encodeFn; + + const type = info.type || info.$class; + // 先获取 classInfo,因为 type 后面会变 + const classInfo = classMap && classMap[type]; + + const gen = codegen([ 'obj', 'encoder', 'appClassMap' ], 'encode'); + // 默认值 + if (info.defaultValue) { + gen('if (obj == null) { obj = %j; }', info.defaultValue); + } + if (info.isArray) { + gen('if (obj == null) { return encoder.writeNull(); }'); + const arrayDepth = info.arrayDepth || 1; + if (bufferType[type] && arrayDepth === 1) { + gen('encoder.writeBytes(obj);'); + } else { + let arrayType = arrayTypeMap[type] || type; + for (let i = 0; i < arrayDepth; i++) arrayType = '[' + arrayType; + + gen('if (encoder._checkRef(obj)) { return; }'); + gen('const hasEnd = encoder._writeListBegin(obj.length, \'%s\');', arrayType); + + const item = arrayDepth > 1 ? { + type, + arrayDepth: arrayDepth - 1, + isMap: info.isMap, + isEnum: info.isEnum, + isArray: info.isArray, + generic: info.generic, + abstractClass: info.abstractClass, + } : { + type, + isMap: info.isMap, + isEnum: info.isEnum, + generic: info.generic, + abstractClass: info.abstractClass, + }; + const uniqueId = utils.normalizeUniqId(item, version); + gen('for (const item of obj) {'); + gen(' compile(\'%s\', %j, classMap, version)(item, encoder, appClassMap);', uniqueId, item); + gen('}'); + gen('if (hasEnd) { encoder.byteBuffer.putChar(\'z\'); }'); + } + } else if (typeMap[type]) { + typeMap[type](gen, info, version); + } else if (info.isMap) { + typeMap.customMap(gen, info, version); + } else if (classInfo && !info.abstractClass && !info.$abstractClass) { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (obj && obj.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(obj, version);'); + gen(' compile(fnKey, obj, classMap, version)(obj.$, encoder, appClassMap);'); + gen(' return;'); + gen('}'); + gen('if (encoder._checkRef(obj)) { return; }'); + + const keys = classInfo ? Object.keys(classInfo).filter(key => { + const attr = classInfo[key]; + return !attr.isStatic && !attr.isTransient; + }) : []; + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', type); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + const attr = Object.assign({}, classInfo[key]); + if (has(attr, 'typeAliasIndex') && Array.isArray(info.generic)) { + attr.type = info.generic[attr.typeAliasIndex].type; + } + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder, appClassMap);', uniqueId, attr, key); + } + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'%s\');', type); + gen('if (ref === -1) {'); + gen('encoder.writeInt(%d);', keys.length); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + } + gen('encoder._writeObjectBegin(\'%s\'); }', type); + + for (const key of keys) { + const attr = Object.assign({}, classInfo[key]); + if (has(attr, 'typeAliasIndex') && Array.isArray(info.generic)) { + attr.type = info.generic[attr.typeAliasIndex].type; + } + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder, appClassMap);', uniqueId, attr, key); + } + } + } else if (info.isEnum) { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', type); + gen('encoder.writeString(\'name\');'); + gen('encoder.writeString(typeof obj.name === \'string\' ? obj.name : obj);'); + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'%s\');', type); + gen('if (ref === -1) {'); + gen('encoder.writeInt(1);'); + gen('encoder.writeString(\'name\');'); + gen('encoder._writeObjectBegin(\'%s\'); }', type); + gen('encoder.writeString(typeof obj.name === \'string\' ? obj.name : obj);'); + } + } else { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (obj && obj.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(obj, version);'); + gen(' compile(fnKey, obj, classMap, version)(obj.$, encoder, appClassMap);'); + gen('}'); + gen('else { encoder.write({ $class: \'%s\', $: obj }); }', type); + } + encodeFn = gen({ compile, classMap, version, utils }); + debug('gen encodeFn for [%s] => %s\n', uniqueId, '\n' + encodeFn.toString()); + cache.set(uniqueId, encodeFn); + return encodeFn; +} diff --git a/lib/serialize/hessian.js b/lib/serialize/hessian/index.js similarity index 89% rename from lib/serialize/hessian.js rename to lib/serialize/hessian/index.js index e18d377..63373f9 100644 --- a/lib/serialize/hessian.js +++ b/lib/serialize/hessian/index.js @@ -1,6 +1,7 @@ 'use strict'; const hessian = require('hessian.js'); +const compile = require('./compile'); const encoder = hessian.encoderV2; const decoder = new hessian.DecoderV2(); @@ -76,8 +77,12 @@ const output = { writeUTF(v) { return encoder.writeString(v); }, - writeObject(v) { - return encoder.write(v); + writeObject(v, classMap) { + if (v && v.$class) { + compile(v, '2.0', classMap)(v.$, encoder); + } else { + encoder.write(v); + } }, }; diff --git a/lib/serialize/hessian/primitive_type/boolean.js b/lib/serialize/hessian/primitive_type/boolean.js new file mode 100644 index 0000000..2c09479 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/boolean.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { obj = false; }'); + gen('encoder.writeBool(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/custom_map.js b/lib/serialize/hessian/primitive_type/custom_map.js new file mode 100644 index 0000000..fc5d5ff --- /dev/null +++ b/lib/serialize/hessian/primitive_type/custom_map.js @@ -0,0 +1,135 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + const generic = classInfo.generic; + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', classInfo.type); + if (generic && generic.length === 2) { + gen('if (obj instanceof Map) {'); + gen(' for (const entry of obj.entries()) {'); + gen(' const key = entry[0];'); + gen(' const value = entry[1];'); + const genericKeyDefine = utils.normalizeType(generic[0]); + const genericValueDefine = utils.normalizeType(generic[1]); + const keyId = utils.normalizeUniqId(genericKeyDefine, version); + const valueId = utils.normalizeUniqId(genericValueDefine, version); + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }\n } else {'); + gen(' for (const key in obj) {'); + gen(' const value = obj[key];'); + const convertor = utils.converts[genericKeyDefine.type]; + if (convertor) { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(%s(key), encoder, appClassMap);', keyId, genericKeyDefine, convertor); + } else { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + } + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }\n }'); + } else { + gen('if (obj instanceof Map) {'); + gen(' for (const entry of obj.entries()) {'); + gen(' const key = entry[0];'); + gen(' const value = entry[1];'); + gen(' encoder.writeString(key);'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }\n } else {'); + gen(' for (const key in obj) {'); + gen(' const value = obj[key];'); + gen(' encoder.writeString(key);'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }\n }'); + } + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const keys = obj instanceof Map ? Array.from(obj.keys()) : Object.keys(obj)'); + gen('const ref = encoder._writeObjectBegin(\'%s\', keys);', classInfo.type); + if (generic && generic.length === 2) { + const genericKeyDefine = utils.normalizeType(generic[0]); + const genericValueDefine = utils.normalizeType(generic[1]); + const keyId = utils.normalizeUniqId(genericKeyDefine, version); + const valueId = utils.normalizeUniqId(genericValueDefine, version); + gen('if (obj instanceof Map) {'); + gen(' if (ref === -1) {'); + gen(' encoder.writeInt(keys.length);'); + gen(' for (const key of keys) {'); + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + gen(' }'); + gen(' encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen(' }'); + gen(' for (const value of obj.values()) {'); + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }'); + gen('} else {'); + gen(' if (ref === -1) {'); + gen(' encoder.writeInt(keys.length);'); + gen(' for (const key of keys) {'); + const convertor = utils.converts[genericKeyDefine.type]; + if (convertor) { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(%s(key), encoder, appClassMap);', keyId, genericKeyDefine, convertor); + } else { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + } + gen(' encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen(' }'); + gen(' }'); + gen(' for (const key of keys) {'); + gen(' const value = obj[key];'); + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }'); + gen('}'); + } else { + gen('if (obj instanceof Map) {'); + gen(' if (ref === -1) {'); + gen(' encoder.writeInt(keys.length);'); + gen(' for (const key of keys) {'); + gen(' encoder.writeString(key);'); + gen(' }'); + gen(' encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen(' }'); + gen(' for (const value of obj.values()) {'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }'); + gen('} else {'); + gen(' if (ref === -1) {'); + gen(' encoder.writeInt(keys.length);'); + gen(' for (const key of keys) {'); + gen(' encoder.writeString(key);'); + gen(' }'); + gen(' encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen(' }'); + gen(' for (const key of keys) {'); + gen(' const value = obj[key];'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }'); + gen('}'); + } + } +}; diff --git a/lib/serialize/hessian/primitive_type/double.js b/lib/serialize/hessian/primitive_type/double.js new file mode 100644 index 0000000..a4270bd --- /dev/null +++ b/lib/serialize/hessian/primitive_type/double.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { obj = 0; }'); + gen('encoder.writeDouble(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/int.js b/lib/serialize/hessian/primitive_type/int.js new file mode 100644 index 0000000..0ae7820 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/int.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { obj = 0; }'); + gen('encoder.writeInt(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.boolean.js b/lib/serialize/hessian/primitive_type/java.lang.boolean.js new file mode 100644 index 0000000..c30fb66 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.boolean.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('encoder.writeBool(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.class.js b/lib/serialize/hessian/primitive_type/java.lang.class.js new file mode 100644 index 0000000..2cf2512 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.class.js @@ -0,0 +1,29 @@ +'use strict'; + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('if (typeof obj === \'string\') {'); + gen(' obj = obj.indexOf(\'[\') !== -1 ? (\'[L\' + obj.replace(/(\\[L)|(\\[)|;/g, \'\') + \';\') : obj'); + gen('}'); + + gen('if (typeof obj === \'object\') {'); + gen(' obj = obj.name'); + gen('}'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', classInfo.type); + gen('encoder.writeString(\'name\');'); + gen('encoder.writeString(obj);'); + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen('if (ref === -1) {'); + gen('encoder.writeInt(1);'); + gen('encoder.writeString(\'name\');'); + gen('encoder._writeObjectBegin(\'%s\'); }', classInfo.type); + gen('encoder.writeString(obj);'); + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.double.js b/lib/serialize/hessian/primitive_type/java.lang.double.js new file mode 100644 index 0000000..e594929 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.double.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('encoder.writeDouble(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.exception.js b/lib/serialize/hessian/primitive_type/java.lang.exception.js new file mode 100644 index 0000000..5cb8a36 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.exception.js @@ -0,0 +1,73 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + + gen('if (encoder._checkRef(obj)) { return; }'); + + classInfo = { + detailMessage: { + type: 'java.lang.String', + }, + stackTrace: { + type: 'java.lang.StackTraceElement', + isArray: true, + }, + cause: { + type: 'java.lang.Throwable', + }, + }; + const keys = [ 'detailMessage', 'stackTrace', 'cause' ]; + gen('if (obj instanceof Error) {'); + gen(' const stackTraceElements = [];'); + gen(' const lines = obj.stack.match(/ at .+$/gm);'); + gen(' if (lines) {'); + gen(' for (var line of lines) {'); + gen(' const splits = line.replace(\' at \', \'\').split(\'(\');'); + gen(' if (splits.length < 2) {'); + gen(' splits.push(splits[0]);'); + gen(' splits[0] = \'.\';'); + gen(' }'); + gen(' const declaring = splits[0];'); + gen(' const lastIndexDot = declaring.lastIndexOf(\'.\');'); + gen(' const declaringClass = declaring.substring(0, lastIndexDot) || \'\';'); + gen(' const methodName = declaring.substring(lastIndexDot + 1).trim();'); + gen(' const fileSplits = splits[1].split(\':\');'); + gen(' const fileName = fileSplits[0].replace(\')\', \'\');'); + gen(' const lineNumber = parseInt(fileSplits[1]) || 0;'); + gen(' stackTraceElements.push({ declaringClass, methodName, fileName, lineNumber });'); + gen(' }'); + gen(' }'); + gen(' obj = { detailMessage: obj.name + \': \' + obj.message, stackTrace: stackTraceElements }'); + gen('} else {'); + gen(' return encoder.write({ $class: \'java.lang.Exception\', $: obj });'); + gen('}'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'java.lang.Exception\');'); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + const attr = classInfo[key]; + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder);', uniqueId, attr, key); + } + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'java.lang.Exception\');'); + gen('if (ref === -1) {'); + gen('encoder.writeInt(%d);', keys.length); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + } + gen('encoder._writeObjectBegin(\'java.lang.Exception\'); }'); + + for (const key of keys) { + const attr = classInfo[key]; + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder);', uniqueId, attr, key); + } + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.integer.js b/lib/serialize/hessian/primitive_type/java.lang.integer.js new file mode 100644 index 0000000..1c78d7b --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.integer.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('encoder.writeInt(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.long.js b/lib/serialize/hessian/primitive_type/java.lang.long.js new file mode 100644 index 0000000..53826f4 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.long.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('encoder.writeLong(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.object.js b/lib/serialize/hessian/primitive_type/java.lang.object.js new file mode 100644 index 0000000..1c24017 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.object.js @@ -0,0 +1,11 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (obj && obj.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(obj, version);'); + gen(' compile(fnKey, obj, appClassMap, version)(obj.$, encoder);'); + gen('} else {'); + gen(' encoder.write(obj);'); + gen('}'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.stacktraceelement.js b/lib/serialize/hessian/primitive_type/java.lang.stacktraceelement.js new file mode 100644 index 0000000..e949768 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.stacktraceelement.js @@ -0,0 +1,50 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + classInfo = { + declaringClass: { + type: 'java.lang.String', + }, + methodName: { + type: 'java.lang.String', + }, + fileName: { + type: 'java.lang.String', + }, + lineNumber: { + type: 'int', + }, + }; + const keys = [ 'declaringClass', 'methodName', 'fileName', 'lineNumber' ]; + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'java.lang.StackTraceElement\');'); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + const attr = classInfo[key]; + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder);', uniqueId, attr, key); + } + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'java.lang.StackTraceElement\');'); + gen('if (ref === -1) {'); + gen('encoder.writeInt(%d);', keys.length); + for (const key of keys) { + gen('encoder.writeString(\'%s\');', key); + } + gen('encoder._writeObjectBegin(\'java.lang.StackTraceElement\'); }'); + + for (const key of keys) { + const attr = classInfo[key]; + const uniqueId = utils.normalizeUniqId(attr, version); + gen('compile(\'%s\', %j, classMap, version)(obj[\'%s\'], encoder);', uniqueId, attr, key); + } + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.lang.string.js b/lib/serialize/hessian/primitive_type/java.lang.string.js new file mode 100644 index 0000000..e459544 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.lang.string.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (typeof obj === \'number\') { obj = obj.toString(); }'); + gen('encoder.writeString(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.math.bigdecimal.js b/lib/serialize/hessian/primitive_type/java.math.bigdecimal.js new file mode 100644 index 0000000..dcb5df9 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.math.bigdecimal.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('if (typeof obj === \'object\') {'); + gen(' obj = obj.value || 0'); + gen('}'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', classInfo.type); + gen('encoder.writeString(\'value\');'); + gen('encoder.writeString(String(obj));'); + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen('if (ref === -1) {'); + gen('encoder.writeInt(1);'); + gen('encoder.writeString(\'value\');'); + gen('encoder._writeObjectBegin(\'%s\'); }', classInfo.type); + gen('encoder.writeString(String(obj));'); + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.arraylist.js b/lib/serialize/hessian/primitive_type/java.util.arraylist.js new file mode 100644 index 0000000..af481f3 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.arraylist.js @@ -0,0 +1,28 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('const hasEnd = encoder._writeListBegin(obj.length, \'\');'); + + const generic = classInfo.generic; + if (generic && generic.length === 1) { + gen('for (const item of obj) {'); + const genericDefine = utils.normalizeType(generic[0]); + const uniqueId = utils.normalizeUniqId(genericDefine, version); + gen(' let desc;'); + gen(' if (item && item.$class) {'); + gen(' desc = item;'); + gen(' } else {'); + gen(' desc = %j;', genericDefine); + gen(' }'); + gen(' compile(\'%s\', desc, classMap, version)(item, encoder, appClassMap);', uniqueId); + gen('}'); + } else { + gen('for (const item of obj) { encoder.write(item); }'); + } + gen('if (hasEnd) { encoder.byteBuffer.putChar(\'z\'); }'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.currency.js b/lib/serialize/hessian/primitive_type/java.util.currency.js new file mode 100644 index 0000000..dabb0a2 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.currency.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('if (typeof obj === \'object\' && obj.currencyCode) {'); + gen(' obj = obj.currencyCode'); + gen('}'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'%s\');', classInfo.type); + gen('encoder.writeString(\'currencyCode\');'); + gen('encoder.writeString(obj);'); + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'%s\');', classInfo.type); + gen('if (ref === -1) {'); + gen(' encoder.writeInt(1);'); + gen(' encoder.writeString(\'currencyCode\');'); + gen(' encoder._writeObjectBegin(\'%s\');\n}', classInfo.type); + gen('encoder.writeString(obj);'); + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.date.js b/lib/serialize/hessian/primitive_type/java.util.date.js new file mode 100644 index 0000000..6b24377 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.date.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (typeof obj === \'string\') { obj = new Date(obj); }'); + gen('encoder.writeDate(obj);'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.list.js b/lib/serialize/hessian/primitive_type/java.util.list.js new file mode 100644 index 0000000..bd6c72e --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.list.js @@ -0,0 +1,43 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('const hasEnd = encoder._writeListBegin(obj.length, \'%s\');', classInfo.type); + + const generic = classInfo.generic; + if (generic && generic.length === 1) { + gen('for (const item of obj) {'); + const genericDefine = utils.normalizeType(generic[0]); + const uniqueId = utils.normalizeUniqId(genericDefine, version); + gen(' let desc;'); + gen(' let val = item;'); + gen(' let uniqueId = \'%s\'', uniqueId); + gen(' if (item && item.$class) {'); + gen(' desc = item;'); + gen(' const type = item.$class;'); + gen(' if (item.isArray) {'); + gen(' let arrayDepth = item.arrayDepth || 1;'); + gen(' while (arrayDepth--) type = \'[\' + type;'); + gen(' }'); + gen(' uniqueId = type;'); + gen(' if (item.generic) {'); + gen(' for (const it of item.generic) {'); + gen(' uniqueId += (\'#\' + it.type);'); + gen(' }'); + gen(' }'); + gen(' uniqueId += \'#\' + version;'); + gen(' val = item.$;'); + gen(' } else {'); + gen(' desc = %j;', genericDefine); + gen(' }'); + gen(' compile(uniqueId, desc, classMap, version)(val, encoder, appClassMap);'); + gen('}'); + } else { + gen('for (const item of obj) { encoder.write(item); }'); + } + gen('if (hasEnd) { encoder.byteBuffer.putChar(\'z\'); }'); +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.locale.js b/lib/serialize/hessian/primitive_type/java.util.locale.js new file mode 100644 index 0000000..5410039 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.locale.js @@ -0,0 +1,25 @@ +'use strict'; + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + gen('if (typeof obj === \'object\') {'); + gen(' obj = obj.value'); + gen('}'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + gen('encoder.writeType(\'com.caucho.hessian.io.LocaleHandle\');'); + gen('encoder.writeString(\'value\');'); + gen('encoder.writeString(obj);'); + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('const ref = encoder._writeObjectBegin(\'com.caucho.hessian.io.LocaleHandle\');'); + gen('if (ref === -1) {'); + gen('encoder.writeInt(1);'); + gen('encoder.writeString(\'value\');'); + gen('encoder._writeObjectBegin(\'com.caucho.hessian.io.LocaleHandle\'); }'); + gen('encoder.writeString(obj);'); + } +}; diff --git a/lib/serialize/hessian/primitive_type/java.util.map.js b/lib/serialize/hessian/primitive_type/java.util.map.js new file mode 100644 index 0000000..b0cc7e8 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/java.util.map.js @@ -0,0 +1,72 @@ +'use strict'; + +const utils = require('../utils'); + +module.exports = (gen, classInfo, version) => { + gen('if (obj == null) { return encoder.writeNull(); }'); + gen('if (encoder._checkRef(obj)) { return; }'); + + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x4d);'); + } else { + gen('encoder.byteBuffer.put(0x48);'); + } + if (classInfo.type === 'java.util.HashMap' || classInfo.type === 'java.util.Map') { + gen('encoder.writeType(\'\');'); + } else { + gen('encoder.writeType(\'%s\');', classInfo.type); + } + + const generic = classInfo.generic; + if (generic && generic.length === 2) { + gen('if (obj instanceof Map) {'); + gen(' for (const entry of obj.entries()) {'); + gen(' const key = entry[0];'); + gen(' const value = entry[1];'); + const genericKeyDefine = utils.normalizeType(generic[0]); + const genericValueDefine = utils.normalizeType(generic[1]); + const keyId = utils.normalizeUniqId(genericKeyDefine, version); + const valueId = utils.normalizeUniqId(genericValueDefine, version); + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }\n } else {'); + gen(' for (const key in obj) {'); + gen(' const value = obj[key];'); + const convertor = utils.converts[genericKeyDefine.type]; + if (convertor) { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(%s(key), encoder, appClassMap);', keyId, genericKeyDefine, convertor); + } else { + gen(' const encodeKey = compile(\'%s\', %j, classMap, version); encodeKey(key, encoder, appClassMap);', keyId, genericKeyDefine); + } + gen(' const encodeValue = compile(\'%s\', %j, classMap, version); encodeValue(value, encoder, appClassMap);', valueId, genericValueDefine); + gen(' }\n }'); + } else { + gen('if (obj instanceof Map) {'); + gen(' for (const entry of obj.entries()) {'); + gen(' const key = entry[0];'); + gen(' const value = entry[1];'); + gen(' encoder.writeString(key);'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }\n } else {'); + gen(' for (const key in obj) {'); + gen(' const value = obj[key];'); + gen(' encoder.writeString(key);'); + gen(' if (value && value.$class) {'); + gen(' const fnKey = utils.normalizeUniqId(value, version);'); + gen(' compile(fnKey, value, appClassMap, version)(value.$, encoder);'); + gen(' } else {'); + gen(' encoder.write(value);'); + gen(' }'); + gen(' }\n }'); + } + if (version === '1.0') { + gen('encoder.byteBuffer.put(0x7a);'); + } else { + gen('encoder.byteBuffer.put(0x5a);'); + } +}; diff --git a/lib/serialize/hessian/primitive_type/long.js b/lib/serialize/hessian/primitive_type/long.js new file mode 100644 index 0000000..e986bd8 --- /dev/null +++ b/lib/serialize/hessian/primitive_type/long.js @@ -0,0 +1,6 @@ +'use strict'; + +module.exports = gen => { + gen('if (obj == null) { obj = 0; }'); + gen('encoder.writeLong(obj);'); +}; diff --git a/lib/serialize/hessian/utils.js b/lib/serialize/hessian/utils.js new file mode 100644 index 0000000..f99dc48 --- /dev/null +++ b/lib/serialize/hessian/utils.js @@ -0,0 +1,50 @@ +'use strict'; + +const defaultValueMap = new Map(); +let defaultValueId = 0; + +function normalizeUniqId(info, version) { + let type = info.type || info.$class || info.$abstractClass; + if (info.isArray) { + let arrayDepth = info.arrayDepth || 1; + while (arrayDepth--) type = '[' + type; + } + let fnKey = type; + if (info.generic) { + for (const item of info.generic) { + fnKey += ('#' + item.type); + } + } + if (info.defaultValue) { + if (!defaultValueMap.has(info.defaultValue)) { + defaultValueMap.set(info.defaultValue, defaultValueId++); + } + fnKey += '#' + defaultValueId; + } + fnKey += '#' + version; + return fnKey; +} + +exports.normalizeUniqId = normalizeUniqId; + +const converts = { + 'java.lang.Boolean': 'Boolean', + boolean: 'Boolean', + 'java.lang.Integer': 'Number', + int: 'Number', + 'java.lang.Short': 'Number', + short: 'Number', + 'java.lang.Double': 'Number', + double: 'Number', + 'java.lang.Float': 'Number', + float: 'Number', +}; + +exports.converts = converts; + +exports.normalizeType = type => { + if (typeof type === 'string') { + return { type }; + } + return type; +}; diff --git a/lib/utils.js b/lib/utils.js index 77b0e48..c0e1cf8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -135,14 +135,14 @@ exports.desc2classArray = desc => { javaType = 'short'; break; case 'L': - { - let clazz = ''; - while (i < len && desc[++i] !== ';') { - clazz += desc[i]; - } - javaType = clazz.replace(/\//g, '.'); - break; + { + let clazz = ''; + while (i < len && desc[++i] !== ';') { + clazz += desc[i]; } + javaType = clazz.replace(/\//g, '.'); + break; + } default: throw new Error(`[double-remoting] unknown class type => ${type}`); } diff --git a/package.json b/package.json index 0e072bd..278015a 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,19 @@ "name": "dubbo-remoting", "version": "1.0.0", "description": "dubbo remoting nodejs implement", - "main": "index.js", + "main": "lib/index.js", + "files": [ + "lib", + "index.js" + ], "scripts": { "autod": "autod", + "pkgfiles": "egg-bin pkgfiles --check", "lint": "eslint --ext .js lib test", "test": "npm run lint && npm run test-local", "test-local": "egg-bin test", "cov": "egg-bin cov", - "ci": "npm run lint && npm run cov" + "ci": "npm run autod -- --check && npm run pkgfiles && npm run lint && npm run cov" }, "repository": { "type": "git", @@ -26,26 +31,29 @@ }, "homepage": "https://github.com/dubbo-js/dubbo-remoting#readme", "dependencies": { - "hessian.js": "^2.2.1", - "is-type-of": "^1.0.0", - "js-to-java": "^2.4.0", - "long": "^3.2.0", - "utility": "^1.11.0" + "@protobufjs/codegen": "^2.0.4", + "debug": "^3.1.0", + "hessian.js": "^2.8.1", + "is-type-of": "^1.2.0", + "long": "^4.0.0", + "utility": "^1.14.0" }, "devDependencies": { - "autod": "^2.7.1", + "autod": "^3.0.1", + "await-event": "^2.1.0", "beautify-benchmark": "^0.2.4", - "benchmark": "^2.1.3", - "egg-bin": "^2.2.3", - "egg-ci": "^1.2.0", - "eslint": "^3.16.1", - "eslint-config-egg": "^3.2.0", - "pedding": "^1.1.0" + "benchmark": "^2.1.4", + "egg-bin": "^4.8.2", + "egg-ci": "^1.8.0", + "eslint": "^5.4.0", + "eslint-config-egg": "^7.0.0", + "js-to-java": "^2.6.0", + "sofa-rpc-node": "^1.1.0" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 8.0.0" }, "ci": { - "version": "6, 7" + "version": "8, 10" } } diff --git a/test/fixtures/class_map.js b/test/fixtures/class_map.js new file mode 100644 index 0000000..2fcb70f --- /dev/null +++ b/test/fixtures/class_map.js @@ -0,0 +1,263 @@ +'use strict'; + +module.exports = { + 'com.alipay.test.Father': { + foo: { + type: 'java.lang.String', + }, + }, + 'com.alipay.test.Child': { + foo: { + type: 'java.lang.String', + }, + bar: { + type: 'java.lang.String', + }, + }, + 'com.test.service.ctx.UniformContextHeaders': { + invokeId: { + type: 'java.lang.String', + }, + serviceUniqueName: { + type: 'java.lang.String', + }, + read: { + type: 'boolean', + defaultValue: false, + }, + idempotent: { + type: 'boolean', + defaultValue: false, + }, + batch: { + type: 'boolean', + defaultValue: false, + }, + version: { + type: 'java.lang.String', + }, + counter: { + type: 'java.util.concurrent.atomic.AtomicLong', + }, + ipGroup: { + type: 'java.lang.String', + }, + caller: { + type: 'com.test.service.ctx.Caller', + }, + callee: { + type: 'com.test.service.ctx.Callee', + }, + webInfo: { + type: 'com.test.service.ctx.WebUniformContextInfo', + }, + serviceProperies: { + type: 'java.util.Properties', + isMap: true, + generic: [ + { type: 'java.lang.Object' }, + { type: 'java.lang.Object' }, + ], + }, + protocol: { + type: 'java.lang.String', + }, + invokeType: { + type: 'java.lang.String', + }, + from_msg: { + type: 'boolean', + defaultValue: false, + }, + }, + 'java.util.concurrent.atomic.AtomicLong': { + value: { + type: 'long', + defaultValue: '0', + }, + }, + 'com.test.service.ctx.Caller': { + ip: { + type: 'java.lang.String', + }, + hostName: { + type: 'java.lang.String', + }, + appName: { + type: 'java.lang.String', + }, + requestTime: { + type: 'java.lang.String', + }, + }, + 'com.test.service.ctx.Callee': { + sid: { + type: 'java.lang.String', + }, + timeout: { + type: 'java.lang.String', + }, + version: { + type: 'java.lang.String', + }, + }, + 'com.test.service.ctx.WebUniformContextInfo': { + pageUrl: { + type: 'java.lang.String', + }, + uid: { + type: 'java.lang.String', + }, + jsessionId: { + type: 'java.lang.String', + }, + pageParams: { + type: 'java.lang.String', + }, + ipGroup: { + type: 'java.lang.String', + }, + from_msg: { + type: 'boolean', + defaultValue: false, + }, + business_type_id: { + type: 'java.lang.String', + }, + extend_props: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.String' }, + { type: 'java.lang.Object' }, + ], + }, + }, + + 'com.alipay.test.TestObj': { + staticField: { + type: 'java.lang.String', + isStatic: true, + }, + transientField: { + type: 'java.lang.String', + isTransient: true, + }, + b: { + type: 'boolean', + defaultValue: false, + }, + testObj2: { + type: 'com.alipay.test.sub.TestObj2', + }, + name: { + type: 'java.lang.String', + }, + field: { + type: 'java.lang.String', + }, + testEnum: { + type: 'com.alipay.test.TestEnum', + isEnum: true, + }, + testEnum2: { + type: 'com.alipay.test.TestEnum', + isArray: true, + arrayDepth: 1, + isEnum: true, + }, + bs: { + type: 'byte', + isArray: true, + arrayDepth: 1, + }, + list1: { + type: 'java.util.List', + generic: [ + { isEnum: true, type: 'com.alipay.test.TestEnum' }, + ], + }, + list2: { + type: 'java.util.List', + generic: [ + { type: 'java.lang.Integer' }, + ], + }, + list3: { + type: 'java.util.List', + generic: [ + { type: 'com.alipay.test.sub.TestObj2' }, + ], + }, + list4: { + type: 'java.util.List', + generic: [ + { type: 'java.lang.String' }, + ], + }, + list5: { + type: 'java.util.List', + generic: [ + { isArray: true, type: 'byte' }, + ], + }, + map1: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.Long' }, + { isEnum: true, type: 'com.alipay.test.TestEnum' }, + ], + }, + map2: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.Integer' }, + { type: 'java.lang.Integer' }, + ], + }, + map3: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.Boolean' }, + { type: 'com.alipay.test.sub.TestObj2' }, + ], + }, + map4: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.String' }, + { type: 'java.lang.String' }, + ], + }, + map5: { + type: 'java.util.Map', + generic: [ + { type: 'java.lang.String' }, + { isArray: true, type: 'byte' }, + ], + }, + }, + 'com.alipay.test.sub.TestObj2': { + name: { + type: 'java.lang.String', + }, + transientField: { + type: 'java.lang.String', + isTransient: true, + }, + finalField: { + type: 'java.lang.String', + defaultValue: 'xxx', + }, + staticField: { + type: 'java.lang.String', + isStatic: true, + }, + }, + + 'com.alipay.test.Request': { + 'data': { + 'type': 'T', + 'typeAliasIndex': 0, + }, + }, +}; diff --git a/test/hessian.test.js b/test/hessian.test.js new file mode 100644 index 0000000..faeaafd --- /dev/null +++ b/test/hessian.test.js @@ -0,0 +1,1139 @@ +'use strict'; + +const Long = require('long'); +const assert = require('assert'); +const java = require('js-to-java'); +const hessian = require('hessian.js'); +const classMap = require('./fixtures/class_map'); +const { encoderV1, encoderV2 } = require('hessian.js'); +const compile = require('../lib/serialize/hessian/compile'); + +const versions = [ '1.0', '2.0' ]; + +function encode(obj, version, classMap, appClassMap) { + const encoder = version === '2.0' ? encoderV2 : encoderV1; + encoder.reset(); + if (classMap) { + compile(obj, version, classMap)(obj.$, encoder, appClassMap); + } else { + encoder.write(obj); + } + return encoder.get(); +} + +describe('test/hessian.test.js', () => { + versions.forEach(version => { + describe(version, () => { + it('should encode java.util.Map without generic', () => { + const obj = { + $class: 'java.util.Map', + $: { foo: 'bar' }, + isMap: true, + }; + const buf1 = hessian.encode({ $class: 'java.util.Map', $: { foo: 'bar' } }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + + const map = new Map(); + map.set('foo', 'bar'); + obj.$ = map; + const buf4 = hessian.encode({ + $class: 'java.util.Map', + $: { foo: { $class: 'java.lang.String', $: 'bar' } }, + }, version); + const buf5 = encode(obj, version, {}); + assert.deepEqual(buf4, buf5); + + const buf6 = encode(obj, version, {}); + assert.deepEqual(buf4, buf6); + }); + + it('should encode java.util.Map with generic', () => { + const map = new Map(); + map.set(1, 'xxx'); + const obj = { + $class: 'java.util.Map', + $: map, + generic: [{ + type: 'java.lang.Integer', + }, { + type: 'java.lang.String', + }], + }; + + const converted = { + $class: 'java.util.Map', + $: new Map(), + }; + converted.$.set({ $class: 'java.lang.Integer', $: 1 }, { $class: 'java.lang.String', $: 'xxx' }); + + const buf1 = hessian.encode(converted, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + + obj.$ = { 1: 'xxx' }; + const buf4 = hessian.encode(converted, version); + const buf5 = encode(obj, version, {}); + assert.deepEqual(buf4, buf5); + + const buf6 = encode(obj, version, {}); + assert.deepEqual(buf4, buf6); + }); + + it('should encode java.util.List with generic', () => { + const obj = { + $class: 'java.util.List', + $: [ 'foo', 'bar' ], + generic: [ + 'java.lang.String', + ], + }; + const buf1 = hessian.encode({ + $class: 'java.util.List', + $: [{ $class: 'java.lang.String', $: 'foo' }, + { $class: 'java.lang.String', $: 'bar' }, + ], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.List without generic', () => { + const obj = { + $class: 'java.util.List', + $: [ 'foo', 'bar' ], + }; + const buf1 = hessian.encode({ $class: 'java.util.List', $: [ 'foo', 'bar' ] }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.ArrayList with generic', () => { + const obj = { + $class: 'java.util.ArrayList', + $: [ 'foo', 'bar' ], + generic: [ + 'java.lang.String', + ], + }; + const buf1 = hessian.encode({ + $class: 'java.util.ArrayList', + $: [ 'foo', 'bar' ], + generic: [ 'java.lang.String' ], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.ArrayList without generic', () => { + const obj = { + $class: 'java.util.ArrayList', + $: [ 'foo', 'bar' ], + }; + const buf1 = hessian.encode({ $class: 'java.util.ArrayList', $: [ 'foo', 'bar' ] }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Set with generic', () => { + const obj = { + $class: 'java.util.Set', + $: [ 'foo', 'bar' ], + generic: [ + 'java.lang.String', + ], + }; + const buf1 = hessian.encode({ + $class: 'java.util.Set', + $: [{ $class: 'java.lang.String', $: 'foo' }, + { $class: 'java.lang.String', $: 'bar' }, + ], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Set without generic', () => { + const obj = { + $class: 'java.util.Set', + $: [ 'foo', 'bar' ], + }; + const buf1 = hessian.encode({ $class: 'java.util.Set', $: [ 'foo', 'bar' ] }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode enum', () => { + const obj = { + $class: 'com.test.model.datum.DatumStaus', + $: { + code: 'PRERELEASING', + name: 'PRERELEASING', + message: '预发中', + ordinal: 1, + eql() { + // + }, + }, + isEnum: true, + }; + const buf1 = hessian.encode({ + $class: 'com.test.model.datum.DatumStaus', + $: { name: 'PRERELEASING' }, + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode class', () => { + const obj = { + $class: 'com.test.service.ctx.WebUniformContextInfo', + $: { + extend_props: { + foo: 'bar', + }, + pageUrl: 'pageUrl', + uid: 'uid', + jsessionId: 'jsessionId', + pageParams: 'pageParams', + ipGroup: 'ipGroup', + business_type_id: 'business_type_id', + }, + }; + const buf1 = hessian.encode({ + $class: 'com.test.service.ctx.WebUniformContextInfo', + $: { + pageUrl: { $class: 'java.lang.String', $: 'pageUrl' }, + uid: { $class: 'java.lang.String', $: 'uid' }, + jsessionId: { $class: 'java.lang.String', $: 'jsessionId' }, + pageParams: { $class: 'java.lang.String', $: 'pageParams' }, + ipGroup: { $class: 'java.lang.String', $: 'ipGroup' }, + from_msg: { $class: 'boolean', $: false }, + business_type_id: { $class: 'java.lang.String', $: 'business_type_id' }, + extend_props: { $class: 'java.util.Map', $: { foo: 'bar' } }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode class that isMap = true', () => { + const obj = { + $class: 'com.test.service.ctx.UniformContextHeaders', + $: { + serviceProperies: { foo: 'bar' }, + }, + }; + const buf1 = hessian.encode({ + $class: 'com.test.service.ctx.UniformContextHeaders', + $: { + invokeId: null, + serviceUniqueName: null, + read: { $class: 'boolean', $: false }, + idempotent: { $class: 'boolean', $: false }, + batch: { $class: 'boolean', $: false }, + version: null, + counter: null, + ipGroup: null, + caller: null, + callee: null, + webInfo: null, + serviceProperies: { $class: 'java.util.Properties', $: { foo: 'bar' } }, + protocol: null, + invokeType: null, + from_msg: { $class: 'boolean', $: false }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode class that isMap = true 2', () => { + const obj = { + $class: 'com.test.service.ctx.UniformContextHeaders', + $: { + serviceProperies: new Map([ + [ 'foo', 'bar' ], + ]), + }, + }; + const buf1 = hessian.encode({ + $class: 'com.test.service.ctx.UniformContextHeaders', + $: { + invokeId: null, + serviceUniqueName: null, + read: { $class: 'boolean', $: false }, + idempotent: { $class: 'boolean', $: false }, + batch: { $class: 'boolean', $: false }, + version: null, + counter: null, + ipGroup: null, + caller: null, + callee: null, + webInfo: null, + serviceProperies: { $class: 'java.util.Properties', $: { foo: 'bar' } }, + protocol: null, + invokeType: null, + from_msg: { $class: 'boolean', $: false }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode class that isMap = true 2', () => { + const obj = { + $class: 'com.test.TestClass', + $: { + serviceProperies: { foo: 'bar' }, + }, + }; + const buf1 = hessian.encode({ + $class: 'com.test.TestClass', + $: { + serviceProperies: { + $class: 'com.test.Map', + $: { foo: 'bar' }, + }, + }, + }, version); + const buf2 = encode(obj, version, { + 'com.test.TestClass': { + serviceProperies: { + type: 'com.test.Map', + isMap: true, + }, + }, + }); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, { + 'com.test.TestClass': { + serviceProperies: { + type: 'com.test.Map', + isMap: true, + }, + }, + }); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Date', () => { + const obj = { + $class: 'java.util.Date', + $: new Date(), + }; + const buf1 = hessian.encode(obj, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.lang.Class', () => { + const obj = { + $class: 'java.lang.Class', + $: { + name: '[java.lang.String', + }, + }; + const buf1 = hessian.encode(obj, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Currency', () => { + const obj = { + $class: 'java.util.Currency', + $: 'CNY', + }; + const buf1 = hessian.encode({ $class: 'java.util.Currency', $: { currencyCode: 'CNY' } }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.math.BigDecimal', () => { + const obj = { + $class: 'java.math.BigDecimal', + $: { value: '100.06' }, + }; + const buf1 = hessian.encode({ $class: 'java.math.BigDecimal', $: { value: '100.06' } }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode abstractClass', () => { + const user = { + $class: 'com.test.service.UserPrincipal', + $: { + name: 'name', + }, + }; + const obj = { + $class: 'java.security.Principal', + $abstractClass: 'java.security.Principal', + $: user, + }; + const buf1 = hessian.encode({ + $abstractClass: 'java.security.Principal', + $class: 'com.test.service.UserPrincipal', + $: { name: 'name' }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Collection', () => { + const classMap = { + 'com.test.service.PermissionQuery': { + vAccountId: { + type: 'java.lang.String', + isFinal: true, + }, + personId: { + type: 'java.lang.String', + isFinal: true, + }, + packageNames: { + type: 'java.util.Collection', + isFinal: true, + abstractClass: 'java.util.Collection', + generic: [ + { type: 'java.lang.String' }, + ], + }, + roleNames: { + type: 'java.util.Collection', + isFinal: true, + abstractClass: 'java.util.Collection', + generic: [ + { type: 'java.lang.String' }, + ], + }, + featureNames: { + type: 'java.util.Collection', + isFinal: true, + abstractClass: 'java.util.Collection', + generic: [ + { type: 'java.lang.String' }, + ], + }, + featureParams: { + type: 'java.util.Map', + isFinal: true, + generic: [ + { type: 'java.lang.String' }, + { generic: [{ type: 'java.lang.String' }], type: 'java.util.Collection' }, + ], + }, + filters: { + type: 'java.util.Collection', + isFinal: true, + abstractClass: 'java.util.Collection', + generic: [ + { type: 'java.lang.String' }, + ], + }, + }, + }; + const queries = [{ + vAccountId: '200012245', + personId: null, + packageNames: null, + roleNames: null, + featureNames: [ 'China_GS', 'China_Free' ], + featureParams: null, + filters: null, + }]; + const obj = { + $class: 'java.util.Collection', + $: queries, + $abstractClass: 'java.util.Collection', + generic: [{ type: 'com.test.service.PermissionQuery' }], + }; + + const buf1 = hessian.encode({ + $class: 'java.util.Collection', + $: [{ + $class: 'com.test.service.PermissionQuery', + $: { + vAccountId: { + $class: 'java.lang.String', + $: '200012245', + }, + personId: null, + packageNames: null, + roleNames: null, + featureNames: { + $class: 'java.util.Collection', + $: [ + { $class: 'java.lang.String', $: 'China_GS' }, + { $class: 'java.lang.String', $: 'China_Free' }, + ], + }, + featureParams: null, + filters: null, + }, + }], + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('classMap 中含有子类申明时, 子类类型不能丢掉', () => { + const classMap = { + 'com.test.service.facade.model.triggerrequests.Request': {}, + 'com.test.service.facade.model.triggerrequests.UserTriggerRequest': { + specifiedPrize: { + type: 'com.alipay.promocore.common.service.facade.model.camp.trigger.SpecifiedPrize', + }, + prizeId: { + type: 'java.lang.String', + }, + type: { + type: 'com.test.service.facade.events.TriggerType', + defaultValue: 'User', + isEnum: true, + }, + campaignId: { + type: 'java.lang.String', + }, + userId: { + type: 'java.lang.String', + }, + seedId: { + type: 'java.lang.String', + }, + requestContext: { + type: 'java.util.Map', + defaultValue: {}, + generic: [ + { type: 'java.lang.String' }, + { type: 'java.lang.Object' }, + ], + }, + transferProps: { + type: 'java.util.Map', + defaultValue: {}, + generic: [ + { type: 'java.lang.String' }, + { type: 'java.lang.String' }, + ], + }, + gmtDt: { + type: 'java.util.Date', + defaultValue: 1471096717898, + }, + idempotentNo: { + type: 'java.lang.String', + }, + idempotent: { + type: 'boolean', + defaultValue: false, + }, + }, + }; + const userId = 'x'; + const campaignId = 'a'; + const request = { + $class: 'com.test.service.facade.model.triggerrequests.UserTriggerRequest', + $: { + userId, + campaignId, + requestContext: { termId: 'd' }, + transferProps: { fundOrderId: 'o' }, + }, + }; + const obj = { + $class: 'com.test.service.facade.model.triggerrequests.Request', + $abstractClass: 'com.test.service.facade.model.triggerrequests.Request', + $: request, + }; + + const buf1 = hessian.encode({ + $class: 'com.test.service.facade.model.triggerrequests.UserTriggerRequest', + $: { + specifiedPrize: null, + prizeId: null, + type: { $class: 'com.test.service.facade.events.TriggerType', $: { name: 'User' } }, + campaignId: { $class: 'java.lang.String', $: 'a' }, + userId: { $class: 'java.lang.String', $: 'x' }, + seedId: null, + requestContext: { $class: 'java.util.Map', $: { termId: 'd' } }, + transferProps: { + $class: 'java.util.Map', + $: { + fundOrderId: { $class: 'java.lang.String', $: 'o' }, + }, + }, + gmtDt: { $class: 'java.util.Date', $: 1471096717898 }, + idempotentNo: null, + idempotent: { $class: 'boolean', $: false }, + }, + $abstractClass: 'com.test.service.facade.model.triggerrequests.Request', + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('#convert() abstractClass manual appoint 2.', () => { + const user = { + $class: 'com.test.service.UserPrincipal', + $: { + name: 'name', + test: null, + }, + }; + const obj = { + $class: 'java.util.Map', + $: { + value: { + $class: 'java.security.Principal', + $abstractClass: 'java.security.Principal', + $: user, + }, + }, + }; + const buf1 = hessian.encode({ + $class: 'java.util.Map', + $: { + value: { + $abstractClass: 'java.security.Principal', + $class: 'com.test.service.UserPrincipal', + $: { name: 'name', test: null }, + }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('#convert() abstractClass manual appoint 21.', () => { + const classMap = { + 'com.test.service.MethodParam': { + value: { + type: 'java.security.Principal', + $abstractClass: 'java.security.Principal', + }, + }, + }; + const user = { + $class: 'com.test.service.UserPrincipal', + $: { + name: 'name', + test: null, + }, + }; + const obj = { + $class: 'com.test.service.MethodParam', + $: [ + { value: user }, + ], + isArray: true, + }; + const buf1 = hessian.encode({ + $class: '[com.test.service.MethodParam', + $: [{ + $class: 'com.test.service.MethodParam', + $: { + value: { + $abstractClass: 'java.security.Principal', + $class: 'com.test.service.UserPrincipal', + $: { name: 'name', test: null }, + }, + }, + }], + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Locale', () => { + const obj = { + $class: 'java.util.Locale', + $: 'zh_CN', + }; + const buf1 = hessian.encode({ $class: 'com.caucho.hessian.io.LocaleHandle', $: { value: 'zh_CN' } }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.lang.Exception', () => { + const err = new Error('mock'); + const buf1 = hessian.encode(java.exception(err), version); + const buf2 = encode({ + $class: 'java.lang.Exception', + $: err, + }, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode({ + $class: 'java.lang.Exception', + $: err, + }, version, {}); + assert.deepEqual(buf1, buf3); + + const buf4 = encode(java.exception(err), version, {}); + assert.deepEqual(buf1, buf4); + }); + + describe('primitive types', () => { + [ + [ 'boolean', true ], + [ 'java.lang.Boolean', false ], + [ 'int', 10 ], + [ 'java.lang.Integer', 1000 ], + [ 'short', 7 ], + [ 'java.lang.Short', 9 ], + [ 'long', Date.now() ], + [ 'java.lang.Long', Date.now() ], + [ 'double', 9.99 ], + [ 'java.lang.Double', 100.1 ], + [ 'float', 20.2 ], + [ 'java.lang.Float', 7.77 ], + [ 'byte', 1 ], + [ 'java.lang.Byte', 0 ], + [ 'char', 's' ], + [ 'java.lang.Character', 'S' ], + [ 'java.lang.String', 'hello world' ], + ].forEach(item => { + it('should encode ' + item[0], () => { + const obj = { + $class: item[0], + $: item[1], + }; + const buf1 = hessian.encode(obj, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + }); + }); + + it('should auto convert number to string for java.lang.String', () => { + const obj = { + $class: 'java.lang.String', + $: 100, + }; + const buf1 = hessian.encode({ + $class: 'java.lang.String', + $: '100', + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + describe('array', () => { + it('should encode array', () => { + const obj = { + $class: 'int', + $: [ 1, 2, 3 ], + isArray: true, + }; + const buf1 = hessian.encode({ $class: '[int', $: [{ $class: 'int', $: 1 }, { $class: 'int', $: 2 }, { $class: 'int', $: 3 }] }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode multi-dimentional array', () => { + const obj = { + $class: 'java.lang.String', + $: [ + [ null, 'a', '1' ], + ], + isArray: true, + arrayDepth: 2, + }; + const buf1 = hessian.encode({ + $class: '[[java.lang.String', + $: [{ + $class: '[java.lang.String', + $: [ + { $class: 'java.lang.String', $: null }, + { $class: 'java.lang.String', $: 'a' }, + { $class: 'java.lang.String', $: '1' }, + ], + }], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.lang.Class array', () => { + const obj = { + $class: 'java.lang.Class', + $: [{ + name: '[java.lang.String', + }, '[Ljava.lang.String;', '[Ljava.lang.String' ], + isArray: true, + }; + const buf1 = hessian.encode({ + $class: '[java.lang.Class', + $: [{ + $class: 'java.lang.Class', + $: { name: '[java.lang.String' }, + }, { + $class: 'java.lang.Class', + $: { name: '[Ljava.lang.String;' }, + }, { + $class: 'java.lang.Class', + $: { name: '[Ljava.lang.String;' }, + }], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Locale array', () => { + const obj = { + $class: 'java.util.Locale', + $: [ + 'zh_CN', + 'en_US', + ], + isArray: true, + }; + const buf1 = hessian.encode({ + $class: '[com.caucho.hessian.io.LocaleHandle', + $: [ + { $class: 'com.caucho.hessian.io.LocaleHandle', $: { value: 'zh_CN' } }, + { $class: 'com.caucho.hessian.io.LocaleHandle', $: { value: 'en_US' } }, + ], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.math.BigDecimal array', () => { + const obj = { + $class: 'java.math.BigDecimal', + $: [ + '100.06', + '200.07', + ], + isArray: true, + }; + const buf1 = hessian.encode(java.array.BigDecimal(obj.$), version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.util.Currency array', () => { + const obj = { + $class: 'java.util.Currency', + $: [ + 'CNY', + 'USD', + ], + isArray: true, + }; + const buf1 = hessian.encode({ + $class: '[java.util.Currency', + $: [ + { $class: 'java.util.Currency', $: { currencyCode: 'CNY' } }, + { $class: 'java.util.Currency', $: { currencyCode: 'USD' } }, + ], + }, version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode byte array', () => { + const obj = { + $class: 'byte', + $: Buffer.from('hello world'), + isArray: true, + }; + const buf1 = hessian.encode(Buffer.from('hello world'), version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + + it('should encode java.lang.Byte array', () => { + const obj = { + $class: 'java.lang.Byte', + $: Buffer.from('hello world'), + isArray: true, + }; + const buf1 = hessian.encode(Buffer.from('hello world'), version); + const buf2 = encode(obj, version, {}); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, {}); + assert.deepEqual(buf1, buf3); + }); + }); + + describe('java.lang.Object', () => { + [ + [ 'boolean', true ], + [ 'java.lang.Boolean', false ], + [ 'int', 10 ], + [ 'java.lang.Integer', 1000 ], + [ 'short', 7 ], + [ 'java.lang.Short', 9 ], + [ 'long', Date.now() ], + [ 'java.lang.Long', Date.now() ], + [ 'double', 9.99 ], + [ 'java.lang.Double', 100.1 ], + [ 'float', 20.2 ], + [ 'java.lang.Float', 7.77 ], + [ 'byte', 1 ], + [ 'java.lang.Byte', 0 ], + [ 'char', 's' ], + [ 'java.lang.Character', 'S' ], + [ 'java.lang.String', 'hello world' ], + [ 'java.util.Map', { foo: 'bar' }], + [ 'java.util.HashMap', { foo: 'bar' }], + [ 'java.util.Date', new Date() ], + [ 'array', [ 1, 2 ]], + [ 'buffer', Buffer.from('hello world') ], + [ 'long', Long.ZERO ], + [ 'null', null ], + ].forEach(item => { + it('should encode ' + item[0], () => { + const obj = { + $class: 'java.lang.Object', + $: item[1], + }; + const buf1 = hessian.encode(item[1], version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + }); + }); + + it('should encode complex object', () => { + const obj = { + $class: 'com.alipay.test.TestObj', + $: { + b: true, + name: 'testname', + field: 'xxxxx', + testObj2: { name: 'xxx', finalField: 'xxx' }, + testEnum: { name: 'B' }, + testEnum2: [{ name: 'B' }, { name: 'C' }], + bs: new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), + list1: [{ name: 'A' }, { name: 'B' }], + list2: [ 2017, 2016 ], + list3: [{ name: 'aaa', finalField: 'xxx' }, + { name: 'bbb', finalField: 'xxx' }, + ], + list4: [ 'xxx', 'yyy' ], + list5: [ new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), new Buffer([ 0x02, 0x00, 0x01, 0x06 ]) ], + map1: { 2017: { name: 'B' } }, + map2: new Map([ + [ 2107, 2016 ], + ]), + map3: {}, + map4: { xxx: 'yyy' }, + map5: { 2017: new Buffer([ 0x02, 0x00, 0x01, 0x06 ]) }, + }, + }; + + const converted = { + $class: 'com.alipay.test.TestObj', + $: { + b: { $class: 'boolean', $: true }, + testObj2: { $class: 'com.alipay.test.sub.TestObj2', $: { name: { $class: 'java.lang.String', $: 'xxx' }, finalField: { $class: 'java.lang.String', $: 'xxx' } } }, + name: { $class: 'java.lang.String', $: 'testname' }, + field: { $class: 'java.lang.String', $: 'xxxxx' }, + testEnum: { $class: 'com.alipay.test.TestEnum', $: { name: 'B' } }, + testEnum2: { + $class: '[com.alipay.test.TestEnum', + $: [{ $class: 'com.alipay.test.TestEnum', $: { name: 'B' } }, + { $class: 'com.alipay.test.TestEnum', $: { name: 'C' } }, + ], + }, + bs: new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), + list1: { $class: 'java.util.List', $: [{ $class: 'com.alipay.test.TestEnum', $: { name: 'A' } }, { $class: 'com.alipay.test.TestEnum', $: { name: 'B' } }] }, + list2: { $class: 'java.util.List', $: [{ $class: 'java.lang.Integer', $: 2017 }, { $class: 'java.lang.Integer', $: 2016 }] }, + list3: { + $class: 'java.util.List', + $: [ + { $class: 'com.alipay.test.sub.TestObj2', $: { name: { $class: 'java.lang.String', $: 'aaa' }, finalField: { $class: 'java.lang.String', $: 'xxx' } } }, + { $class: 'com.alipay.test.sub.TestObj2', $: { name: { $class: 'java.lang.String', $: 'bbb' }, finalField: { $class: 'java.lang.String', $: 'xxx' } } }, + ], + }, + list4: { $class: 'java.util.List', $: [{ $class: 'java.lang.String', $: 'xxx' }, { $class: 'java.lang.String', $: 'yyy' }] }, + list5: { + $class: 'java.util.List', + $: [ + new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), new Buffer([ 0x02, 0x00, 0x01, 0x06 ]), + ], + }, + map1: { + $class: 'java.util.Map', + $: new Map([ + [{ $class: 'java.lang.Long', $: 2017 }, { $class: 'com.alipay.test.TestEnum', $: { name: 'B' } }], + ]), + }, + map2: { + $class: 'java.util.Map', + $: new Map([ + [{ $class: 'java.lang.Integer', $: 2107 }, { $class: 'java.lang.Integer', $: 2016 }], + ]), + }, + map3: { $class: 'java.util.Map', $: {} }, + map4: { $class: 'java.util.Map', $: { xxx: { $class: 'java.lang.String', $: 'yyy' } } }, + map5: { $class: 'java.util.Map', $: { 2017: new Buffer([ 0x02, 0x00, 0x01, 0x06 ]) } }, + }, + }; + const buf1 = hessian.encode(converted, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + + const buf4 = encode(converted, version); + assert.deepEqual(buf1, buf4); + }); + + it('should support generic with typeAliasIndex', () => { + const obj = { + $class: 'com.alipay.test.Request', + $: { + data: '123', + }, + generic: [{ type: 'java.lang.String' }], + }; + const buf1 = hessian.encode({ + $class: 'com.alipay.test.Request', + $: { + data: { $class: 'java.lang.String', $: '123' }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + + it('should class inheritance', () => { + const obj = { + $class: 'com.alipay.test.Father', + $: { + $class: 'com.alipay.test.Child', + $: { + foo: 'bar', + bar: 'foo', + }, + }, + }; + const buf1 = hessian.encode({ + $class: 'com.alipay.test.Child', + $: { + foo: { + $class: 'java.lang.String', + $: 'bar', + }, + bar: { + $class: 'java.lang.String', + $: 'foo', + }, + }, + }, version); + const buf2 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf2); + + const buf3 = encode(obj, version, classMap); + assert.deepEqual(buf1, buf3); + }); + }); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index b7d13ae..28c3b64 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,273 +1,262 @@ 'use strict'; -const is = require('is-type-of'); -const stream = require('stream'); -const utils = require('./utils'); -const assert = require('assert'); const protocol = require('../'); -const pedding = require('pedding'); -const utility = require('utility'); -const decode = require('../lib/protocol/dubbo').decode; +const assert = require('assert'); +const urlparse = require('url').parse; +const awaitEvent = require('await-event'); +const PassThrough = require('stream').PassThrough; describe('test/index.test.js', () => { - describe('exchange', () => { - it('should create decoder ok', () => { - let decoder = protocol.decoder('dubbo://127.0.0.1'); - assert(decoder); - assert(decoder instanceof stream.Writable); - decoder.end(); - decoder = protocol.decoder('exchange://127.0.0.1'); - assert(decoder); - decoder.end(); + + it('should create encoder ok', () => { + const sentReqs = new Map(); + const address = urlparse('dubbo://127.0.0.1:12200?serialization=hessian', true); + let encoder = protocol.encoder({ address, sentReqs }); + assert(encoder.protocolType === 'dubbo'); + assert(encoder.codecType === 'hessian'); + + encoder = protocol.encoder({ sentReqs }); + assert(encoder.protocolType === 'dubbo'); + assert(encoder.codecType === 'hessian2'); + + encoder = protocol.encoder({ sentReqs, codecType: 'protobuf' }); + assert(encoder.protocolType === 'dubbo'); + assert(encoder.codecType === 'protobuf'); + + encoder.codecType = 'hessian2'; + assert(encoder.codecType === 'hessian2'); + }); + + const reqSample = { + args: [ 1, 2 ], + serverSignature: 'com.alipay.test.TestService:1.0', + methodName: 'plus', + requestProps: { + foo: 'bar', + }, + timeout: 3000, + }; + const resSample = { + isError: false, + errorMsg: null, + appResponse: { + $class: 'java.lang.Integer', + $: 3, + }, + }; + + it('should encode request', async function() { + const codecType = 'hessian2'; + const protocolType = 'dubbo'; + const address = urlparse('dubbo://127.0.0.1:12200?serialization=hessian2'); + const sentReqs = new Map(); + const socket = new PassThrough(); + const encoder = protocol.encoder({ sentReqs, address }); + const decoder = protocol.decoder({ sentReqs }); + encoder.pipe(socket).pipe(decoder); + + setImmediate(() => { + encoder.writeRequest(1, Object.assign({}, reqSample)); }); - it('should create decoder failed', () => { - assert.throws(() => { - protocol.decoder('xxx://127.0.0.1'); - }, '[dubbo-remoting] unsupport protocol => xxx'); + let req = await awaitEvent(decoder, 'request'); + assert(req.packetId === 1); + assert(req.packetType === 'request'); + assert(req.data && req.data.methodName === reqSample.methodName); + assert(req.data.serverSignature === reqSample.serverSignature); + assert.deepEqual(req.data.args, reqSample.args); + assert.deepEqual(req.data.requestProps, { dubbo: '5.3.0', path: 'com.alipay.test.TestService', version: '1.0', foo: 'bar' }); + assert(req.options && req.options.protocolType === protocolType); + assert(req.options.codecType === codecType); + assert(req.meta); + assert(req.meta.size > 0); + assert(req.meta.start > 0); + assert(req.meta.rt >= 0); + + setImmediate(() => { + encoder.writeRequest(2, Object.assign({}, reqSample)); }); - it('should decode single packet ok', done => { - const decoder = protocol.decoder('exchange://127.0.0.1'); - decoder.on('packet', packet => { - assert(packet.id === 1); - assert(packet.version === '2.0.0'); - assert(packet.isTwoWay); - assert(!packet.isEvent); - assert(!packet.isBroken); - assert.deepEqual(packet.data, { - login: { - application: 'xxx', - dubbo: '2.5.3', - password: null, - username: null, - }, - }); - done(); - }); - utils.createReadStream('login').pipe(decoder); + req = await awaitEvent(decoder, 'request'); + assert(req.packetId === 2); + assert(req.packetType === 'request'); + assert(req.data && req.data.methodName === reqSample.methodName); + assert(req.data.serverSignature === reqSample.serverSignature); + assert.deepEqual(req.data.args, reqSample.args); + assert.deepEqual(req.data.requestProps, { dubbo: '5.3.0', path: 'com.alipay.test.TestService', version: '1.0', foo: 'bar' }); + assert(req.options && req.options.protocolType === protocolType); + assert(req.options.codecType === codecType); + assert(req.meta); + assert(req.meta.size > 0); + assert(req.meta.start > 0); + assert(req.meta.rt >= 0); + + setImmediate(() => { + encoder.writeResponse(req, resSample); }); - it('should decode multiple packets ok', done => { - done = pedding(done, 2); - const decoder = protocol.decoder('exchange://127.0.0.1'); - decoder.on('packet', packet => { - if (packet.id === 1) { - assert.deepEqual(packet.data, { - login: { - application: 'xxx', - dubbo: '2.5.3', - password: null, - username: null, - }, - }); - } else if (packet.id === 2) { - assert.deepEqual(packet.data, { - register: 'consumer://127.0.0.1?application=test&dubbo=2.5.3&check=false&pid=90972&protocol=dubbo&revision=1.0.0×tamp=1487836024465&category=consumer&methods=*&side=consumer&interface=com.gxc.demo.DemoService&version=1.0.0', - }); - } else { - done(new Error(`error packet id => ${packet.id}`)); - } - done(); + const res = await awaitEvent(decoder, 'response'); + assert(res.packetId === 2); + assert(res.packetType === 'response'); + assert.deepEqual(res.data, { error: null, appResponse: 3 }); + assert(res.options && res.options.protocolType === protocolType); + assert(res.options.codecType === codecType); + assert(res.meta); + assert(res.meta.size > 0); + assert(res.meta.start > 0); + assert(res.meta.rt >= 0); + }); + + it('should encode error response', async function() { + const codecType = 'hessian2'; + const protocolType = 'dubbo'; + const address = urlparse('dubbo://127.0.0.1:12200?serialization=hessian2'); + const sentReqs = new Map(); + const socket = new PassThrough(); + const encoder = protocol.encoder({ sentReqs, address }); + const decoder = protocol.decoder({ sentReqs }); + encoder.pipe(socket).pipe(decoder); + + setImmediate(() => { + encoder.writeRequest(1, { + args: [ 1, 2 ], + serverSignature: 'com.alipay.test.TestService:1.0', + methodName: 'plus', + requestProps: null, + timeout: 3000, + }, err => { + err && console.log(err); }); - utils.createReadStream('multiple').pipe(decoder); }); - it('should work with partial data', done => { - const socket = new stream.Transform({ - writableObjectMode: true, - transform(chunk, encoding, callback) { - callback(null, chunk); - }, - }); - const decoder = protocol.decoder('exchange://127.0.0.1'); - decoder.on('packet', packet => { - assert(packet.id === 1); - assert(packet.version === '2.0.0'); - assert(packet.isTwoWay); - assert(!packet.isEvent); - assert(!packet.isBroken); - assert.deepEqual(packet.data, { - login: { - application: 'xxx', - dubbo: '2.5.3', - password: null, - username: null, - }, - }); - done(); + const req = await awaitEvent(decoder, 'request'); + + setImmediate(() => { + encoder.writeResponse(req, { + isError: true, + errorMsg: 'mock error message', + appResponse: null, }); - socket.pipe(decoder); - const buf = utils.bytes('login'); - const length = buf.length; - const index = utility.random(length); + }); + let res = await awaitEvent(decoder, 'response'); - socket.write(buf.slice(0, index)); + assert(res.packetId === 1); + assert(res.packetType === 'response'); + assert(res.options && res.options.protocolType === protocolType); + assert(res.options.codecType === codecType); + assert(res.data && res.data.error); + assert(!res.data.appResponse); + assert(res.data.error.message.includes('mock error message')); - setTimeout(() => { - socket.write(buf.slice(index, length)); - }, 1000); - }); + req.options.protocolType = 'dubbo'; + req.options.codecType = 'hessian2'; - it('should emit error if decode failed', done => { - const socket = new stream.Transform({ - writableObjectMode: true, - transform(chunk, encoding, callback) { - callback(null, chunk); - }, + setImmediate(() => { + encoder.writeResponse(req, { + isError: true, + errorMsg: 'xxx error', + appResponse: null, }); - const decoder = protocol.decoder('exchange://127.0.0.1'); - decoder.on('error', err => { - assert(err && err.message === '[dubbo-remoting] invalid packet with magic => 102'); - done(); - }); - socket.pipe(decoder); - socket.write(new Buffer('fake data')); }); + res = await awaitEvent(decoder, 'response'); + assert(res.packetId === 1); + assert(res.packetType === 'response'); + assert(res.options && res.options.protocolType === 'dubbo'); + assert(res.options.codecType === 'hessian2'); + assert(res.data && res.data.error); + assert(!res.data.appResponse); + assert(res.data.error.message.includes('xxx error')); - it('should auto generate id', () => { - const req = new protocol.Request(); - assert(is.number(req.id)); + setImmediate(() => { + encoder.writeResponse(req, { + isError: true, + errorMsg: null, + appResponse: null, + }); }); + res = await awaitEvent(decoder, 'response'); + assert(res.packetId === 1); + assert(res.packetType === 'response'); + assert(res.options && res.options.protocolType === 'dubbo'); + assert(res.options.codecType === 'hessian2'); + assert(res.data && res.data.error); + assert(!res.data.appResponse); + assert(res.data.error.message.includes('Exception caught in invocation')); }); - describe('dubbo', () => { - it('should encode & decode dubbo request', () => { - const req = new protocol.Request(1); - req.data = new protocol.Invocation({ - methodName: 'test-method', - args: [ - 1, true, 1.1, new Date(1487940805137), new Buffer('buffer'), [ 1, 2, 3 ], { a: 'a' }, - ], - attachments: { - dubbo: '5.3.0', - path: 'com.test.TestService', - version: '1.0.0', - }, + it('should encode biz error response', async function() { + const codecType = 'hessian2'; + const protocolType = 'dubbo'; + const address = urlparse('dubbo://127.0.0.1:12200?serialization=hessian2'); + const sentReqs = new Map(); + const socket = new PassThrough(); + const encoder = protocol.encoder({ sentReqs, address }); + const decoder = protocol.decoder({ sentReqs }); + encoder.pipe(socket).pipe(decoder); + + setImmediate(() => { + encoder.writeRequest(1, { + args: [ 1, 2 ], + serverSignature: 'com.alipay.test.TestService:1.0', + methodName: 'plus', + requestProps: null, + uniformContextHeaders: null, + timeout: 3000, }); - assert(!req.isResponse); - const buf = req.encode(); - assert.deepEqual(buf, utils.bytes('dubbo_req')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, req); }); + const req = await awaitEvent(decoder, 'request'); - it('should encode & decode java class', () => { - const req = new protocol.Request(1); - req.data = new protocol.Invocation({ - methodName: 'test-method', - args: [{ - $class: 'com.test.TestClass', - $: { - a: 1, - b: 'str', - c: true, - d: [ 'a', 'b', 'c' ], - e: { foo: 'bar' }, - }, - }], - attachments: { - dubbo: '5.3.0', - path: 'com.test.TestService', - version: '1.0.0', + setImmediate(() => { + encoder.writeResponse(req, { + isError: false, + errorMsg: null, + appResponse: { + $class: 'java.lang.Exception', + $: new Error('mock error'), }, }); - const buf = req.encode(); - assert.deepEqual(buf, utils.bytes('dubbo_req_java_class')); - const result = decode(buf); - assert(result.total === buf.length); - assert(result.packet.id === 1); - const inv = result.packet.data; - assert(inv.methodName === 'test-method'); - assert.deepEqual(inv.attachments, { - dubbo: '5.3.0', - path: 'com.test.TestService', - version: '1.0.0', - }); - assert.deepEqual(inv.args, [{ - a: 1, - b: 'str', - c: true, - d: [ 'a', 'b', 'c' ], - e: { foo: 'bar' }, - }]); }); + const res = await awaitEvent(decoder, 'response'); - it('should encode & decode dubbo response', () => { - const res = new protocol.Response(1); - res.data = new protocol.Result({ value: 1.22 }); - assert(res.isSuccess); - assert(res.isResponse); - assert(!res.event); - const buf = res.encode(); - assert.deepEqual(buf, utils.bytes('dubbo_res')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, res); - }); + assert(res.packetId === 1); + assert(res.packetType === 'response'); + assert(res.options && res.options.protocolType === protocolType); + assert(res.options.codecType === codecType); + assert(res.data && res.data.error); + assert(!res.data.appResponse); + assert(res.data.error.message.includes('mock error')); + }); - it('should encode & decode dubbo exception response', () => { - const res = new protocol.Response(1); - res.data = new protocol.Result({ error: new Error('mock error') }); - assert(res.isResponse); - const buf = res.encode(); - const result = decode(buf); - assert(result.total === buf.length); - assert(result.packet.data.value == null); - assert(result.packet.data.error instanceof Error); - assert(result.packet.data.error.message.includes('mock error')); - }); + describe('heartbeat', () => { + it('should encode heartbeat', async function() { + const codecType = 'hessian2'; + const protocolType = 'dubbo'; + const address = urlparse('dubbo://127.0.0.1:12200?serialization=hessian2'); + const sentReqs = new Map(); + const socket = new PassThrough(); + const encoder = protocol.encoder({ sentReqs, address }); + const decoder = protocol.decoder({ sentReqs }); + encoder.pipe(socket).pipe(decoder); - it('should encode & decode return value => null', () => { - const res = new protocol.Response(1); - res.data = new protocol.Result({ value: null }); - assert(res.isResponse); - const buf = res.encode(); - assert.deepEqual(buf, utils.bytes('dubbo_res_with_null')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, res); - }); + setImmediate(() => { + encoder.writeHeartbeat(1, { clientUrl: 'xxx' }); + }); + const hb = await awaitEvent(decoder, 'heartbeat'); - it('should encode & decode packet with sys err', () => { - const res = new protocol.Response(1); - res.status = 70; - res.errorMsg = 'sys error'; - assert(res.isResponse); - const buf = res.encode(); - assert.deepEqual(buf, utils.bytes('dubbo_res_with_sys_error')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, res); - }); - }); + assert(hb.packetId === 1); + assert(hb.packetType === 'heartbeat'); + assert(hb.options && hb.options.protocolType === protocolType); + assert(hb.options.codecType === codecType); - describe('heartbeat', () => { - it('should decode & encode heartbeat req', () => { - const req = new protocol.Request(1); - req.event = null; - assert(!req.isResponse); - assert(req.isHeartbeat); - assert(req.event == null); - const buf = req.encode(); - assert.deepEqual(buf, utils.bytes('heartbeat_req')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, req); - }); + setImmediate(() => { + encoder.writeHeartbeatAck(hb); + }); - it('should decode & encode heartbeat res', () => { - const res = new protocol.Response(1); - res.event = null; - assert(res.isResponse); - assert(res.isHeartbeat); - assert(res.event == null); - const buf = res.encode(); - assert.deepEqual(buf, utils.bytes('heartbeat_res')); - const result = decode(buf); - assert(result.total === buf.length); - assert.deepEqual(result.packet, res); + const hbAck = await awaitEvent(decoder, 'heartbeat_ack'); + assert(hbAck.packetId === 1); + assert(hbAck.packetType === 'heartbeat_ack'); + assert(hbAck.options && hbAck.options.protocolType === protocolType); + assert(hbAck.options.codecType === codecType); }); }); }); diff --git a/test/protocol.test.js b/test/protocol.test.js new file mode 100644 index 0000000..0d730c5 --- /dev/null +++ b/test/protocol.test.js @@ -0,0 +1,166 @@ +'use strict'; + +const assert = require('assert'); +const protocol = require('../lib/protocol'); +const classMap = require('./fixtures/class_map'); + +describe('test/protocol.test.js', () => { + it('should requestEncode ok', () => { + const buf = protocol.requestEncode(1, { + args: [{ + $class: 'java.lang.String', + $: 'test', + }], + serverSignature: 'com.test.TestService:1.0', + methodName: 'test', + timeout: 300000, + }); + assert.deepEqual(Buffer.from('dabbc20000000000000000010000007005352e332e3014636f6d2e746573742e546573745365727669636503312e300474657374124c6a6176612f6c616e672f537472696e673b04746573744805647562626f05352e332e30047061746814636f6d2e746573742e54657374536572766963650776657273696f6e03312e305a', 'hex'), buf); + + const req = protocol.decode(buf); + assert(req.meta && req.meta.size === 128); + delete req.meta; + assert.deepEqual({ + packetId: 1, + packetType: 'request', + data: { + methodName: 'test', + serverSignature: 'com.test.TestService:1.0', + args: [ 'test' ], + methodArgSigs: [ 'java.lang.String' ], + requestProps: { dubbo: '5.3.0', path: 'com.test.TestService', version: '1.0' }, + }, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: undefined, + }, + }, req); + + }); + + it('should responseEncode ok', () => { + const buf = protocol.responseEncode(1, { + isError: false, + appResponse: 'ok', + }); + assert.deepEqual(Buffer.from('dabb021400000000000000010000000491026f6b', 'hex'), buf); + + const res = protocol.decode(buf); + assert(res.meta && res.meta.size === 20); + delete res.meta; + assert.deepEqual({ + packetId: 1, + packetType: 'response', + data: { appResponse: 'ok', error: null }, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: undefined, + }, + }, res); + }); + + it('should heartbeatEncode ok', () => { + const buf = protocol.heartbeatEncode(1); + assert.deepEqual(Buffer.from('dabbe2000000000000000001000000014e', 'hex'), buf); + + const hb = protocol.decode(buf); + assert(hb.meta && hb.meta.size === 17); + delete hb.meta; + assert.deepEqual({ + packetId: 1, + packetType: 'heartbeat', + data: null, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: undefined, + }, + }, hb); + + }); + + it('should heartbeatAckEncode ok', () => { + const buf = protocol.heartbeatAckEncode(1); + assert.deepEqual(Buffer.from('dabb22140000000000000001000000014e', 'hex'), buf); + + const hbAck = protocol.decode(buf); + assert(hbAck.meta && hbAck.meta.size === 17); + delete hbAck.meta; + assert.deepEqual({ + packetId: 1, + packetType: 'heartbeat_ack', + data: null, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap: undefined, + }, + }, hbAck); + }); + + const obj = { + b: true, + name: 'testname', + field: 'xxxxx', + testObj2: { name: 'xxx', finalField: 'xxx' }, + testEnum: { name: 'B' }, + testEnum2: [{ name: 'B' }, { name: 'C' }], + bs: new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), + list1: [{ name: 'A' }, { name: 'B' }], + list2: [ 2017, 2016 ], + list3: [{ name: 'aaa', finalField: 'xxx' }, + { name: 'bbb', finalField: 'xxx' }, + ], + list4: [ 'xxx', 'yyy' ], + list5: [ new Buffer([ 0x02, 0x00, 0x01, 0x07 ]), new Buffer([ 0x02, 0x00, 0x01, 0x06 ]) ], + map1: { 2017: { name: 'B' } }, + map2: { + 2107: 2106, + }, + map3: {}, + map4: { xxx: 'yyy' }, + map5: { 2017: new Buffer([ 0x02, 0x00, 0x01, 0x06 ]) }, + }; + + it('should encode complex object', () => { + const options = { classMap }; + const buf = protocol.requestEncode(1, { + args: [{ + $class: 'com.alipay.test.TestObj', + $: obj, + }], + serverSignature: 'com.alipay.test.TestService:1.0', + methodName: 'echoObj', + requestProps: {}, + timeout: 3000, + }, options); + const expect = Buffer.from('dabbc2000000000000000001000001ff05352e332e301b636f6d2e616c697061792e746573742e546573745365727669636503312e30076563686f4f626a194c636f6d2f616c697061792f746573742f546573744f626a3b4317636f6d2e616c697061792e746573742e546573744f626aa1016208746573744f626a32046e616d65056669656c640874657374456e756d0974657374456e756d32026273056c69737431056c69737432056c69737433056c69737434056c69737435046d617031046d617032046d617033046d617034046d6170356054431c636f6d2e616c697061792e746573742e7375622e546573744f626a3292046e616d650a66696e616c4669656c6461037878780378787808746573746e616d650578787878784318636f6d2e616c697061792e746573742e54657374456e756d91046e616d6562014272195b636f6d2e616c697061792e746573742e54657374456e756d6201426201432402000107720e6a6176612e7574696c2e4c6973746201416201427291cfe1cfe072916103616161037878786103626262037878787291037878780379797972912402000107240200010648ffe16201425a48d4083bd4083a5a485a4803787878037979795a48043230313724020001065a4805647562626f05352e332e3004706174681b636f6d2e616c697061792e746573742e54657374536572766963650776657273696f6e03312e305a', 'hex'); + assert.deepEqual(expect, buf); + + const req = protocol.decode(buf, options); + assert(req.meta && req.meta.size === 527); + delete req.meta; + assert.deepEqual({ + packetId: 1, + packetType: 'request', + data: { + methodName: 'echoObj', + serverSignature: 'com.alipay.test.TestService:1.0', + args: [ obj ], + methodArgSigs: [ 'com.alipay.test.TestObj' ], + requestProps: { + dubbo: '5.3.0', + path: 'com.alipay.test.TestService', + version: '1.0', + }, + }, + options: { + protocolType: 'dubbo', + codecType: 'hessian2', + classMap, + }, + }, req); + }); +}); diff --git a/test/utils.test.js b/test/utils.test.js index 2eee08b..63bfa29 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -51,6 +51,6 @@ describe('test/utils.test.js', () => { assert.deepEqual(utils.desc2classArray('S'), [ 'short' ]); assert.throws(() => { utils.desc2classArray('A'); - }, '[double-remoting] unknown class type => A'); + }, /\[double-remoting\] unknown class type => A/); }); });