merge for the release of v1.6.0 (#195)
* add license checker (#175)
* add release note for v1.5.0 (#178)
Co-authored-by: Xin.Zh <dragoncharlie@foxmail.com>
* Imp: cache in reflection (#179)
* benchmark result
* use cache in findField
* encode benchmark
* call field() once
* remove version
* fix import sync
* cache in registerPOJO
* add json bench result
* prune unneccessary rtype.Field(index)
* cache comment
* rename cache
* switch to if
* remove return value name
* findFieldWithCache
* remove if check when fieldStruct is nil
Co-authored-by: 望哥 <gelnyang@163.com>
* update dependency
* rename serialize arg name
* Create .asf.yaml
* 优化hessian解码string性能,提升54%
* optimize code.
* optimize code.
* fix code review.
* optimize codes.
* optimize cods.
* optimize code.
* update license
* go.sum
* ci go version
* testify -> 1.4.0
* testcase
* travis.yml
* decode value before reflect find
* setvalue
* decode nilPtr to nilPtr
* fix get attachment lost nil key
* manually import package
* add ToMapStringString unit test
* rename test function name with issue
* setmap
* support for decode emoji.
* refactor code
* add unit test.
* add unit tests.
* refactor tests.
* Update travis/main.sh (#200)
- Remove duplicate key 'webhooks'
- Key 'matrix' is an alias for `jobs`, using `jobs`
- Specify the os and dist explicitly
* Mod: modify
* Code format (#199)
* .gitignore
* code clean
* code clean
* remove length check
* Fix: comments
* Fix: format package
* Fix #181: float32 accuracy issue (#196)
* Fix #181: float32 accuracy issue
* Fix go fmt failure
* Add the unit test case for Issue181
* Add encFloat32 in double.go to encode float32 type
- Call encFloat32 to encode float32 while encoding
- Add unit test case to test float32 encoding
* Improve encFloat32 of double.go
* Fix git fmt failure
* add release note for v1.6.0 (#202)
* add release note for v1.5.1
* add release note for v1.5.1
* add notice
* update notice
* =fix release note for v1.6.0
Co-authored-by: Joe Zou <joezou@apache.org>
Co-authored-by: Xin.Zh <dragoncharlie@foxmail.com>
Co-authored-by: huiren <zhrlnt@gmail.com>
Co-authored-by: Huang YunKun <htynkn@gmail.com>
Co-authored-by: zonghaishang <yiji@apache.org>
Co-authored-by: fangyincheng <fangyc666@gmail.com>
Co-authored-by: champly <champly@outlook.com>
Co-authored-by: wilson chen <willson.chenwx@gmail.com>
Co-authored-by: fangyincheng <fangyincheng@sina.com>
Co-authored-by: gaoxinge <gaoxx5@gmail.com>
diff --git a/.asf.yaml b/.asf.yaml
new file mode 100644
index 0000000..8d84e69
--- /dev/null
+++ b/.asf.yaml
@@ -0,0 +1,5 @@
+notifications:
+ commits: commits@dubbo.apache.org
+ issues: notifications@dubbo.apache.org
+ pullrequests: notifications@dubbo.apache.org
+ jira_options: link label link label
diff --git a/.gitignore b/.gitignore
index 1363720..fc62e8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
.idea
+vendor
coverage.txt
diff --git a/.travis.yml b/.travis.yml
index a711aab..5442ab1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
language: go
-matrix:
+jobs:
include:
- language: java
jdk: openjdk8
@@ -8,15 +8,23 @@
go:
- "1.12"
+os: linux
+
+dist: xenial
+
script:
- mvn clean package -f test_hessian/pom.xml
- mvn clean package -f test_dubbo/pom.xml
- go fmt && [[ -z `git status -s` ]]
+ - sh before_validate_license.sh
+ - chmod u+x /tmp/tools/license/license-header-checker
+ - /tmp/tools/license/license-header-checker -v -a -r -i vendor /tmp/tools/license/license.txt . go && [[ -z `git status -s` ]]
- GO111MODULE=on && go mod vendor && go test -race -v && go test -bench . -race -coverprofile=coverage.txt
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
- webhooks: https://oapi.dingtalk.com/robot/send?access_token=27a5eb4510c8cf913b67a72832549b123a8c44655483d20443515604669de0ae
- webhooks: https://oapi.dingtalk.com/robot/send?access_token=8250008579ed1defda3a44fb8608a38d81a55700fdfb15466315a90a7dd2045f
+ webhooks:
+ - https://oapi.dingtalk.com/robot/send?access_token=27a5eb4510c8cf913b67a72832549b123a8c44655483d20443515604669de0ae
+ - https://oapi.dingtalk.com/robot/send?access_token=8250008579ed1defda3a44fb8608a38d81a55700fdfb15466315a90a7dd2045f
diff --git a/CHANGE.md b/CHANGE.md
index a3f32e4..492220c 100644
--- a/CHANGE.md
+++ b/CHANGE.md
@@ -1,5 +1,18 @@
# Release Notes
+## v1.6.0
+
+### New Features
+- ignore non-exist fields when decoding. [#201](https://github.com/apache/dubbo-go-hessian2/pull/201)
+
+### Enhancement
+- add cache in reflection to improve performance. [#179](https://github.com/apache/dubbo-go-hessian2/pull/179)
+- string decode performance improvement. [#188](https://github.com/apache/dubbo-go-hessian2/pull/188)
+
+### Bugfixes
+- fix attachment lost for nil value. [#191](https://github.com/apache/dubbo-go-hessian2/pull/191)
+- fix float32 accuracy issue. [#196](https://github.com/apache/dubbo-go-hessian2/pull/196)
+
## v1.5.0
### New Features
diff --git a/LICENSE b/LICENSE
index 43bbf60..7dd1e2b 100755
--- a/LICENSE
+++ b/LICENSE
@@ -174,3 +174,28 @@
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index 9c12941..6bc9b78 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,15 @@
---
+> **Notice: When decoding, the java version of hessian will default skip and ignore non-exist fields.**
+> **From the version of v1.6.0 , dubbo-go-hessian2 will skip non-exist fields too, while that before v1.6.0 will return errors.**
+
It's a golang hessian library used by [Apache/dubbo-go](https://github.com/apache/dubbo-go).
+There is a big performance improvement, and some bugs fix for v1.6.0,
+thanks to [micln](https://github.com/micln), [pantianying](https://github.com/pantianying), [zonghaishang](https://github.com/zonghaishang),
+ [willson-chen](https://github.com/willson-chen), [champly](https://github.com/champly).
+
## Feature List
* [All JDK Exceptions](https://github.com/apache/dubbo-go-hessian2/issues/59)
diff --git a/before_validate_license.sh b/before_validate_license.sh
new file mode 100644
index 0000000..8fa6e38
--- /dev/null
+++ b/before_validate_license.sh
@@ -0,0 +1,26 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+remoteLicenseCheckerPath="https://github.com/dubbogo/resources/raw/master/tools/license"
+remoteLicenseCheckerName="license-header-checker"
+remoteLicenseCheckerURL="${remoteLicenseCheckerPath}/${remoteLicenseCheckerName}"
+remoteLicenseName="license.txt"
+remoteLicenseURL="${remoteLicenseCheckerPath}/${remoteLicenseName}"
+
+licensePath="/tmp/tools/license"
+mkdir -p ${licensePath}
+wget -P "${licensePath}" ${remoteLicenseCheckerURL}
+wget -P "${licensePath}" ${remoteLicenseURL}
diff --git a/binary_test.go b/binary_test.go
index dafda34..c145e13 100644
--- a/binary_test.go
+++ b/binary_test.go
@@ -20,8 +20,6 @@
import (
"bytes"
"fmt"
-
- // "fmt"
"testing"
)
diff --git a/decode.go b/decode.go
index c5aafac..f10456c 100644
--- a/decode.go
+++ b/decode.go
@@ -50,15 +50,54 @@
return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), typeRefs: &TypeRefs{records: map[string]bool{}}}
}
+// NewDecoder generate a decoder instance
+func NewDecoderSize(b []byte, size int) *Decoder {
+ return &Decoder{reader: bufio.NewReaderSize(bytes.NewReader(b), size), typeRefs: &TypeRefs{records: map[string]bool{}}}
+}
+
// NewDecoder generate a decoder instance with skip
func NewDecoderWithSkip(b []byte) *Decoder {
return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), typeRefs: &TypeRefs{records: map[string]bool{}}, isSkip: true}
}
+// NewCheapDecoderWithSkip generate a decoder instance with skip,
+// only for cache pool, before decode Reset should be called.
+// For example, with pooling use, will effectively improve performance
+//
+// var hessianPool = &sync.Pool{
+// New: func() interface{} {
+// return hessian.NewCheapDecoderWithSkip([]byte{})
+// },
+// }
+//
+// decoder := hessianPool.Get().(*hessian.Decoder)
+// fill decode data
+// decoder.Reset(data[:])
+// decode anything ...
+// hessianPool.Put(decoder)
+func NewCheapDecoderWithSkip(b []byte) *Decoder {
+ return &Decoder{reader: bufio.NewReader(bytes.NewReader(b)), isSkip: true}
+}
+
/////////////////////////////////////////
// utilities
/////////////////////////////////////////
+func (d *Decoder) Reset(b []byte) *Decoder {
+ // reuse reader buf, avoid allocate
+ d.reader.Reset(bytes.NewReader(b))
+ d.typeRefs = &TypeRefs{records: map[string]bool{}}
+
+ if d.refs != nil {
+ d.refs = nil
+ }
+ if d.classInfoList != nil {
+ d.classInfoList = nil
+ }
+
+ return d
+}
+
// peek a byte
func (d *Decoder) peekByte() byte {
return d.peek(1)[0]
@@ -150,6 +189,8 @@
return EnsureInterface(d.DecodeValue())
}
+func (d *Decoder) Buffered() int { return d.reader.Buffered() }
+
// DecodeValue parse hessian data, the return value maybe a reflection value when it's a map, list, object, or ref.
func (d *Decoder) DecodeValue() (interface{}, error) {
var (
diff --git a/decode_test.go b/decode_test.go
index f91aca0..3728fec 100644
--- a/decode_test.go
+++ b/decode_test.go
@@ -33,6 +33,7 @@
const (
hessianJar = "test_hessian/target/test_hessian-1.0.0.jar"
+ testString = "hello, world! 你好,世界!"
)
func isFileExist(file string) bool {
diff --git a/double.go b/double.go
index d70c682..376a1f4 100644
--- a/double.go
+++ b/double.go
@@ -20,6 +20,7 @@
import (
"encoding/binary"
"math"
+ "strconv"
)
import (
@@ -62,6 +63,38 @@
byte(bits>>32), byte(bits>>24), byte(bits>>16), byte(bits>>8), byte(bits))
}
+func encFloat32(b []byte, v float32) []byte {
+ fv := float32(int32(v))
+ if fv == v {
+ iv := int32(v)
+ switch iv {
+ case 0:
+ return encByte(b, BC_DOUBLE_ZERO)
+ case 1:
+ return encByte(b, BC_DOUBLE_ONE)
+ }
+ if iv >= -0x80 && iv < 0x80 {
+ return encByte(b, BC_DOUBLE_BYTE, byte(iv))
+ } else if iv >= -0x8000 && iv < 0x8000 {
+ return encByte(b, BC_DOUBLE_SHORT, byte(iv>>8), byte(iv))
+ }
+
+ goto END
+ }
+
+END:
+ if float32(int32(v*1000)) == v*1000 {
+ iv := int32(v * 1000)
+ return encByte(b, BC_DOUBLE_MILL, byte(iv>>24), byte(iv>>16), byte(iv>>8), byte(iv))
+ } else {
+ str := strconv.FormatFloat(float64(v), 'f', -1, 32)
+ d, _ := strconv.ParseFloat(str, 64)
+ bits := math.Float64bits(d)
+ return encByte(b, BC_DOUBLE, byte(bits>>56), byte(bits>>48), byte(bits>>40),
+ byte(bits>>32), byte(bits>>24), byte(bits>>16), byte(bits>>8), byte(bits))
+ }
+}
+
/////////////////////////////////////////
// Double
/////////////////////////////////////////
diff --git a/double_test.go b/double_test.go
index 111124b..43e9391 100644
--- a/double_test.go
+++ b/double_test.go
@@ -42,6 +42,33 @@
t.Logf("decode(%v) = %v, %v\n", v, res, err)
}
+func TestIssue181(t *testing.T) {
+ var (
+ v float32
+ err error
+ e *Encoder
+ d *Decoder
+ res interface{}
+ )
+
+ e = NewEncoder()
+ v = 99.8
+ e.Encode(v)
+ if len(e.Buffer()) == 0 {
+ t.Fail()
+ }
+
+ // res would be '99.800003' without patches in PR #196
+ d = NewDecoder(e.Buffer())
+ res, err = d.Decode()
+ f := res.(float64)
+ if float32(f) != v {
+ t.Errorf("decode(%v) = %v, %v\n", v, res, err)
+ return
+ }
+ t.Logf("decode(%v) = %v\n", v, res)
+}
+
func TestDouble(t *testing.T) {
testDecodeFramework(t, "replyDouble_0_0", 0.0)
testDecodeFramework(t, "replyDouble_0_001", 0.001)
diff --git a/encode.go b/encode.go
index fbeae99..ad9d5c6 100644
--- a/encode.go
+++ b/encode.go
@@ -112,7 +112,7 @@
}
case float32:
- e.buffer = encFloat(e.buffer, float64(val))
+ e.buffer = encFloat32(e.buffer, val)
case float64:
e.buffer = encFloat(e.buffer, val)
diff --git a/go.mod b/go.mod
index 8497c83..fb0a269 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,7 @@
module github.com/apache/dubbo-go-hessian2
require (
- github.com/dubbogo/gost v1.5.1
- github.com/pkg/errors v0.8.1
+ github.com/dubbogo/gost v1.9.0
+ github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.4.0
)
diff --git a/go.sum b/go.sum
index 6c93958..f799fe3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,9 +1,11 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dubbogo/gost v1.5.1 h1:oG5dzaWf1KYynBaBoUIOkgT+YD0niHV6xxI0Odq7hDg=
-github.com/dubbogo/gost v1.5.1/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8=
+github.com/dubbogo/gost v1.9.0 h1:UT+dWwvLyJiDotxJERO75jB3Yxgsdy10KztR5ycxRAk=
+github.com/dubbogo/gost v1.9.0/go.mod h1:pPTjVyoJan3aPxBPNUX0ADkXjPibLo+/Ib0/fADXSG8=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
diff --git a/int_test.go b/int_test.go
index 726a11f..082f399 100644
--- a/int_test.go
+++ b/int_test.go
@@ -18,10 +18,13 @@
package hessian
import (
- "github.com/stretchr/testify/assert"
"testing"
)
+import (
+ "github.com/stretchr/testify/assert"
+)
+
func TestEncInt32Len1B(t *testing.T) {
var (
v int32
diff --git a/object.go b/object.go
index 4025dac..e2ef7b3 100644
--- a/object.go
+++ b/object.go
@@ -21,6 +21,7 @@
"io"
"reflect"
"strings"
+ "sync"
)
import (
@@ -164,21 +165,23 @@
structs := []reflect.Value{vv}
for len(structs) > 0 {
vv := structs[0]
+ vvt := vv.Type()
num = vv.NumField()
for i = 0; i < num; i++ {
+ tf := vvt.Field(i)
// skip unexported anonymous field
- if vv.Type().Field(i).PkgPath != "" {
+ if tf.PkgPath != "" {
continue
}
// skip ignored field
- if tag, _ := vv.Type().Field(i).Tag.Lookup(tagIdentifier); tag == `-` {
+ if tag, _ := tf.Tag.Lookup(tagIdentifier); tag == `-` {
continue
}
field := vv.Field(i)
- if vv.Type().Field(i).Anonymous && field.Kind() == reflect.Struct {
- structs = append(structs, vv.Field(i))
+ if tf.Anonymous && field.Kind() == reflect.Struct {
+ structs = append(structs, field)
continue
}
@@ -292,36 +295,71 @@
return classInfo{javaName: clsName, fieldNameList: fieldList}, nil
}
-func findField(name string, typ reflect.Type) ([]int, error) {
+type fieldInfo struct {
+ indexes []int
+ field *reflect.StructField
+}
+
+// map[rType][fieldName]indexes
+var fieldIndexCache sync.Map
+
+func findFieldWithCache(name string, typ reflect.Type) ([]int, *reflect.StructField, error) {
+ typCache, _ := fieldIndexCache.Load(typ)
+ if typCache == nil {
+ typCache = &sync.Map{}
+ fieldIndexCache.Store(typ, typCache)
+ }
+
+ iindexes, existCache := typCache.(*sync.Map).Load(name)
+ if existCache && iindexes != nil {
+ finfo := iindexes.(*fieldInfo)
+ var err error
+ if len(finfo.indexes) == 0 {
+ err = perrors.Errorf("failed to find field %s", name)
+ }
+ return finfo.indexes, finfo.field, err
+ }
+
+ indexes, field, err := findField(name, typ)
+ typCache.(*sync.Map).Store(name, &fieldInfo{indexes: indexes, field: field})
+ return indexes, field, err
+}
+
+// findField find structField in rType
+//
+// return
+// indexes []int
+// field reflect.StructField
+// err error
+func findField(name string, typ reflect.Type) ([]int, *reflect.StructField, error) {
for i := 0; i < typ.NumField(); i++ {
// matching tag first, then lowerCamelCase, SameCase, lowerCase
typField := typ.Field(i)
- if val, has := typField.Tag.Lookup(tagIdentifier); has && strings.Compare(val, name) == 0 {
- return []int{i}, nil
- }
+ tagVal, hasTag := typField.Tag.Lookup(tagIdentifier)
fieldName := typField.Name
- switch {
- case strings.Compare(lowerCamelCase(fieldName), name) == 0:
- return []int{i}, nil
- case strings.Compare(fieldName, name) == 0:
- return []int{i}, nil
- case strings.Compare(strings.ToLower(fieldName), name) == 0:
- return []int{i}, nil
+ if hasTag && tagVal == name ||
+ fieldName == name ||
+ lowerCamelCase(fieldName) == name ||
+ strings.ToLower(fieldName) == name {
+
+ return []int{i}, &typField, nil
}
if typField.Anonymous && typField.Type.Kind() == reflect.Struct {
- next, _ := findField(name, typField.Type)
+ next, field, _ := findField(name, typField.Type)
if len(next) > 0 {
- pos := []int{i}
- return append(pos, next...), nil
+ indexes := []int{i}
+ indexes = append(indexes, next...)
+
+ return indexes, field, nil
}
}
}
- return []int{}, perrors.Errorf("failed to find field %s", name)
+ return []int{}, nil, perrors.Errorf("failed to find field %s", name)
}
func (d *Decoder) decInstance(typ reflect.Type, cls classInfo) (interface{}, error) {
@@ -337,13 +375,14 @@
for i := 0; i < len(cls.fieldNameList); i++ {
fieldName := cls.fieldNameList[i]
- index, err := findField(fieldName, typ)
+ index, fieldStruct, err := findFieldWithCache(fieldName, typ)
if err != nil {
- return nil, perrors.Errorf("can not find field %s", fieldName)
+ d.DecodeValue()
+ continue
}
// skip unexported anonymous field
- if vv.Type().FieldByIndex(index).PkgPath != "" {
+ if fieldStruct.PkgPath != "" {
continue
}
@@ -357,8 +396,8 @@
// unpack pointer to enable value setting
fldRawValue := UnpackPtrValue(field)
-
kind := fldTyp.Kind()
+
switch kind {
case reflect.String:
str, err := d.decString(TAG_READ)
@@ -483,7 +522,7 @@
}
default:
- return nil, perrors.Errorf("unknown struct member type: %v %v", kind, typ.Name()+"."+typ.FieldByIndex(index).Name)
+ return nil, perrors.Errorf("unknown struct member type: %v %v", kind, typ.Name()+"."+fieldStruct.Name)
}
} // end for
diff --git a/object_test.go b/object_test.go
index 2484d07..db66596 100644
--- a/object_test.go
+++ b/object_test.go
@@ -18,9 +18,15 @@
package hessian
import (
+ "encoding/json"
"math"
"reflect"
"testing"
+ "time"
+)
+
+import (
+ "github.com/stretchr/testify/assert"
)
type Department struct {
@@ -659,3 +665,106 @@
testJavaDecode(t, "customArgTypedFixed_Extends", dog)
}
+
+type Mix struct {
+ A int
+ B string
+ CA time.Time
+ CB int64
+ CC string
+ CD []float64
+ D map[string]interface{}
+}
+
+func (m Mix) JavaClassName() string {
+ return `test.mix`
+}
+
+func init() {
+ RegisterPOJO(new(Mix))
+}
+
+//
+// BenchmarkJsonEncode-8 217354 4799 ns/op 832 B/op 15 allocs/op
+func BenchmarkJsonEncode(b *testing.B) {
+ m := Mix{A: int('a'), B: `hello`}
+ m.CD = []float64{1, 2, 3}
+ m.D = map[string]interface{}{`floats`: m.CD, `A`: m.A, `m`: m}
+
+ for i := 0; i < b.N; i++ {
+ _, err := json.Marshal(&m)
+ if err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+//
+// BenchmarkEncode-8 211452 5560 ns/op 1771 B/op 51 allocs/op
+func BenchmarkEncode(b *testing.B) {
+ m := Mix{A: int('a'), B: `hello`}
+ m.CD = []float64{1, 2, 3}
+ m.D = map[string]interface{}{`floats`: m.CD, `A`: m.A, `m`: m}
+
+ for i := 0; i < b.N; i++ {
+ _, err := encodeTarget(&m)
+ if err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+//
+// BenchmarkJsonDecode-8 123922 8549 ns/op 1776 B/op 51 allocs/op
+func BenchmarkJsonDecode(b *testing.B) {
+ m := Mix{A: int('a'), B: `hello`}
+ m.CD = []float64{1, 2, 3}
+ m.D = map[string]interface{}{`floats`: m.CD, `A`: m.A, `m`: m}
+ bytes, err := json.Marshal(&m)
+ if err != nil {
+ b.Error(err)
+ }
+
+ for i := 0; i < b.N; i++ {
+ m := &Mix{}
+ err := json.Unmarshal(bytes, m)
+ if err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+//
+// BenchmarkDecode-8 104196 10924 ns/op 6424 B/op 98 allocs/op
+func BenchmarkDecode(b *testing.B) {
+ m := Mix{A: int('a'), B: `hello`}
+ m.CD = []float64{1, 2, 3}
+ m.D = map[string]interface{}{`floats`: m.CD, `A`: m.A, `m`: m}
+ bytes, err := encodeTarget(&m)
+ if err != nil {
+ b.Error(err)
+ }
+
+ for i := 0; i < b.N; i++ {
+ d := NewDecoder(bytes)
+ _, err := d.Decode()
+ if err != nil {
+ b.Error(err)
+ }
+ }
+}
+
+type Person183 struct {
+ Name string
+}
+
+func (Person183) JavaClassName() string {
+ return `test.Person183`
+}
+
+func TestIssue183_DecodeExcessStructField(t *testing.T) {
+ RegisterPOJO(&Person183{})
+ got, err := decodeJavaResponse(`customReplyPerson183`, ``, false)
+ assert.NoError(t, err)
+ t.Logf("%T %+v", got, got)
+}
diff --git a/pojo.go b/pojo.go
index 0c9f9aa..be74bac 100644
--- a/pojo.go
+++ b/pojo.go
@@ -120,6 +120,11 @@
pojoRegistry.Lock()
defer pojoRegistry.Unlock()
+ if goName, ok := pojoRegistry.j2g[o.JavaClassName()]; ok {
+ return pojoRegistry.registry[goName].index
+ }
+
+ // JavaClassName shouldn't equal to goName
if _, ok := pojoRegistry.registry[o.JavaClassName()]; ok {
return -1
}
diff --git a/request.go b/request.go
index 807f73f..020c789 100644
--- a/request.go
+++ b/request.go
@@ -337,6 +337,11 @@
dest := make(map[string]string)
for k, v := range origin {
if kv, ok := k.(string); ok {
+ if v == nil {
+ dest[kv] = ""
+ continue
+ }
+
if vv, ok := v.(string); ok {
dest[kv] = vv
}
diff --git a/request_test.go b/request_test.go
index c64e013..b5886db 100644
--- a/request_test.go
+++ b/request_test.go
@@ -18,6 +18,7 @@
package hessian
import (
+ "reflect"
"testing"
"time"
)
@@ -80,3 +81,37 @@
assert.Equal(t, "[Ljava/lang/String;", results[0])
assert.Equal(t, "[I", results[1])
}
+
+func TestIssue192(t *testing.T) {
+ type args struct {
+ origin map[interface{}]interface{}
+ }
+ tests := []struct {
+ name string
+ args args
+ want map[string]string
+ }{
+ {
+ name: "not null",
+ args: args{
+ origin: map[interface{}]interface{}{
+ "1": nil,
+ "2": "3",
+ "": "",
+ },
+ },
+ want: map[string]string{
+ "1": "",
+ "2": "3",
+ "": "",
+ },
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ if got := ToMapStringString(tt.args.origin); !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("ToMapStringString() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
diff --git a/serialize.go b/serialize.go
index bb5860f..2584d10 100644
--- a/serialize.go
+++ b/serialize.go
@@ -42,12 +42,12 @@
var serializerMap = make(map[string]Serializer, 16)
-func SetSerializer(key string, codec Serializer) {
- serializerMap[key] = codec
+func SetSerializer(javaClassName string, codec Serializer) {
+ serializerMap[javaClassName] = codec
}
-func GetSerializer(key string) (Serializer, bool) {
- codec, ok := serializerMap[key]
+func GetSerializer(javaClassName string) (Serializer, bool) {
+ codec, ok := serializerMap[javaClassName]
return codec, ok
}
diff --git a/string.go b/string.go
index f692438..a2a24ce 100644
--- a/string.go
+++ b/string.go
@@ -217,46 +217,46 @@
// ::= 'S' b1 b0 <utf8-data> # string of length 0-65535
// ::= [x00-x1f] <utf8-data> # string of length 0-31
// ::= [x30-x34] <utf8-data> # string of length 0-1023
-func (d *Decoder) getStringLength(tag byte) (int32, error) {
- var (
- err error
- buf [2]byte
- length int32
- )
+func (d *Decoder) getStringLength(tag byte) (int, error) {
+ var length int
switch {
case tag >= BC_STRING_DIRECT && tag <= STRING_DIRECT_MAX:
- return int32(tag - 0x00), nil
+ return int(tag - 0x00), nil
case tag >= 0x30 && tag <= 0x33:
- _, err = io.ReadFull(d.reader, buf[:1])
+ b, err := d.readByte()
if err != nil {
return -1, perrors.WithStack(err)
}
- length = int32(tag-0x30)<<8 + int32(buf[0])
+ length = int(tag-0x30)<<8 + int(b)
return length, nil
case tag == BC_STRING_CHUNK || tag == BC_STRING:
- _, err = io.ReadFull(d.reader, buf[:2])
+ b0, err := d.readByte()
if err != nil {
return -1, perrors.WithStack(err)
}
- length = int32(buf[0])<<8 + int32(buf[1])
+
+ b1, err := d.readByte()
+ if err != nil {
+ return -1, perrors.WithStack(err)
+ }
+
+ length = int(b0)<<8 + int(b1)
return length, nil
default:
- return -1, perrors.WithStack(err)
+ return -1, perrors.Errorf("string decode: unknown tag %b", tag)
}
}
func (d *Decoder) decString(flag int32) (string, error) {
var (
- tag byte
- charTotal int32
- last bool
- s string
- r rune
+ tag byte
+ last bool
+ s string
)
if flag != TAG_READ {
@@ -311,24 +311,18 @@
last = true
}
- l, err := d.getStringLength(tag)
+ chunkLen, err := d.getStringLength(tag)
if err != nil {
return s, perrors.WithStack(err)
}
- charTotal = l
- charCount := 0
-
- runeData := make([]rune, charTotal)
- runeIndex := 0
-
- byteCount := 0
- byteLen := 0
- charLen := 0
+ bytesBuf := make([]byte, chunkLen<<2)
+ offset := 0
for {
- if int32(charCount) == charTotal {
+ if chunkLen <= 0 {
if last {
- return string(runeData[:runeIndex]), nil
+ b := bytesBuf[:offset]
+ return *(*string)(unsafe.Pointer(&b)), nil
}
b, _ := d.readByte()
@@ -343,21 +337,184 @@
last = true
}
- l, err := d.getStringLength(b)
+ chunkLen, err = d.getStringLength(b)
if err != nil {
return s, perrors.WithStack(err)
}
- charTotal += l
- bs := make([]rune, charTotal)
- copy(bs, runeData)
- runeData = bs
-
+ remain, cap := len(bytesBuf)-offset, chunkLen<<2
+ if remain < cap {
+ grow := len(bytesBuf) + cap
+ bs := make([]byte, grow)
+ copy(bs, bytesBuf)
+ bytesBuf = bs
+ }
default:
return s, perrors.New("expect string tag")
}
}
- r, charLen, byteLen, err = decodeUcs4Rune(d.reader)
+ if chunkLen > 0 {
+ nread, err := d.next(bytesBuf[offset : offset+chunkLen])
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ return s, perrors.WithStack(err)
+ }
+
+ // quickly detect the actual number of bytes
+ prev, i := offset, offset
+ len := offset + nread
+ copied := false
+ for r, r1 := len-1, len-2; i < len; chunkLen-- {
+ ch := bytesBuf[offset]
+ if ch < 0x80 {
+ i++
+ offset++
+ } else if (ch & 0xe0) == 0xc0 {
+ i += 2
+ offset += 2
+ } else if (ch & 0xf0) == 0xe0 {
+ // handle the 3-byte right edge
+ // case:
+ // 1. Expect 3 bytes, but the current byte is on the right
+ // 2. Expect 3 bytes, but the current byte is second to last to the right
+ if i == r {
+ bytesBuf[i+1], err = d.reader.ReadByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ bytesBuf[i+2], err = d.reader.ReadByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ nread += 2
+ len += 2
+ } else if i == r1 {
+ bytesBuf[i+2], err = d.reader.ReadByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ nread++
+ len++
+ }
+
+ // we detect emoji first
+ c1 := ((uint32(ch) & 0x0f) << 12) + ((uint32(bytesBuf[i+1]) & 0x3f) << 6) + (uint32(bytesBuf[i+2]) & 0x3f)
+ if c1 >= 0xD800 && c1 <= 0xDBFF {
+
+ var (
+ c2 rune
+ n2 int
+ err error
+ ch0 byte
+ )
+
+ // more cache byte available
+ if i+3 < len {
+ ch0 = bytesBuf[i+3]
+ } else {
+ ch0, err = d.reader.ReadByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ // update accumulates read bytes,
+ // because it reads more than thunk bytes
+ nread++
+ len++
+ }
+
+ if ch0 < 0x80 {
+ c2, n2 = rune(ch0), 1
+ } else if (ch0 & 0xe0) == 0xc0 {
+ var ch1 byte
+ if i+4 < len {
+ ch1 = bytesBuf[i+4]
+ } else {
+ // out of the chunk byte data
+ bytesBuf[i+4], err = d.reader.ReadByte()
+ ch1 = bytesBuf[i+4]
+ nread++
+ len++
+ }
+ c2, n2 = rune(((uint32(ch0)&0x1f)<<6)+(uint32(ch1)&0x3f)), 2
+ } else if (ch0 & 0xf0) == 0xe0 {
+ var ch1, ch2 byte
+ if i+5 < len {
+ ch1 = bytesBuf[i+4]
+ ch2 = bytesBuf[i+5]
+ } else {
+ ch1, err = d.reader.ReadByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ ch2, err = d.reader.ReadByte()
+ len += 2
+ nread += 2
+ }
+ c := ((uint32(ch0) & 0x0f) << 12) + ((uint32(ch1) & 0x3f) << 6) + (uint32(ch2) & 0x3f)
+ c2, n2 = rune(c), 3
+ }
+
+ c := rune(c1-0xD800)<<10 + (c2 - 0xDC00) + 0x10000
+ n3 := utf8.EncodeRune(bytesBuf[i:], c)
+ if copied = n3 > 0 && n3 < /** front three byte */ 3+n2; copied {
+ // We need to move the bytes,
+ // for example, less bytes after decoding
+ offset = i + n3
+ copy(bytesBuf[offset:], bytesBuf[i+3+n2:len])
+ }
+
+ i += n2
+ chunkLen--
+ }
+ i += 3
+
+ // fix read the next byte index
+ if copied {
+ copied = false
+ continue
+ }
+
+ offset += 3
+ } else {
+ return s, perrors.Errorf("bad utf-8 encoding")
+ }
+ }
+
+ if remain := offset - prev - nread; remain > 0 {
+ if remain == 1 {
+ ch, err := d.readByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ bytesBuf[offset-1] = ch
+ } else {
+ var err error
+ if buffed := d.Buffered(); buffed < remain {
+ // trigger fill data if required
+ copy(bytesBuf[offset-remain:offset], d.peek(remain))
+ _, err = d.reader.Discard(remain)
+ } else {
+ // copy remaining bytes.
+ _, err = d.next(bytesBuf[offset-remain : offset])
+ }
+
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ }
+ }
+
+ // the expected length string has been processed.
+ if chunkLen <= 0 {
+ // we need to detect next chunk
+ continue
+ }
+ }
+
+ // decode byte
+ ch, err := d.readByte()
if err != nil {
if err == io.EOF {
break
@@ -365,14 +522,58 @@
return s, perrors.WithStack(err)
}
- runeData[runeIndex] = r
- runeIndex++
+ if ch < 0x80 {
+ bytesBuf[offset] = ch
+ offset++
+ } else if (ch & 0xe0) == 0xc0 {
+ ch1, err := d.readByte()
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+ bytesBuf[offset] = ch
+ bytesBuf[offset+1] = ch1
+ offset += 2
+ } else if (ch & 0xf0) == 0xe0 {
+ var err error
+ if buffed := d.Buffered(); buffed < 2 {
+ // trigger fill data if required
+ copy(bytesBuf[offset+1:offset+3], d.peek(2))
+ _, err = d.reader.Discard(2)
+ } else {
+ _, err = d.next(bytesBuf[offset+1 : offset+3])
+ }
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
- charCount += charLen
- byteCount += byteLen
+ bytesBuf[offset] = ch
+
+ // we detect emoji first
+ c1 := ((uint32(ch) & 0x0f) << 12) + ((uint32(bytesBuf[offset+1]) & 0x3f) << 6) + (uint32(bytesBuf[offset+2]) & 0x3f)
+ if c1 >= 0xD800 && c1 <= 0xDBFF {
+ c2, n2, err := decodeUcs2Rune(d.reader)
+ if err != nil {
+ return s, perrors.WithStack(err)
+ }
+
+ c := rune(c1-0xD800)<<10 + (c2 - 0xDC00) + 0x10000
+ utf8.EncodeRune(bytesBuf[offset:], c)
+
+ // update next rune
+ offset += n2
+ chunkLen--
+ }
+
+ offset += 3
+ } else {
+ return s, perrors.Errorf("bad utf-8 encoding, offset=%d\n", offset)
+ }
+
+ chunkLen--
}
- return string(runeData[:runeIndex]), nil
+ b := bytesBuf[:offset]
+ return *(*string)(unsafe.Pointer(&b)), nil
}
return s, perrors.Errorf("unknown string tag %#x\n", tag)
diff --git a/string_test.go b/string_test.go
index 5751332..ae8d5c1 100644
--- a/string_test.go
+++ b/string_test.go
@@ -19,6 +19,7 @@
import (
"fmt"
+ "sync"
"testing"
)
@@ -158,6 +159,34 @@
testJavaDecode(t, "argString_65536", s65560[:65536])
}
+var decodePool = &sync.Pool{
+ New: func() interface{} {
+ return NewCheapDecoderWithSkip([]byte{})
+ },
+}
+
+func TestStringWithPool(t *testing.T) {
+ e := NewEncoder()
+ e.Encode(testString)
+ buf := e.buffer
+
+ for i := 0; i < 3; i++ {
+ d := decodePool.Get().(*Decoder)
+ d.Reset(buf)
+
+ v, err := d.Decode()
+ if err != nil {
+ t.Errorf("err:%s", err.Error())
+ }
+ if v != testString {
+ t.Errorf("excpect decode %v, actual %v", testString, v)
+ }
+
+ decodePool.Put(d)
+ }
+
+}
+
func TestStringEmoji(t *testing.T) {
// see: test_hessian/src/main/java/test/TestString.java
s0 := "emoji🤣"
@@ -166,3 +195,11 @@
testDecodeFramework(t, "customReplyStringEmoji", s0)
testJavaDecode(t, "customArgString_emoji", s0)
}
+
+func TestStringComplex(t *testing.T) {
+ // see: test_hessian/src/main/java/test/TestString.java
+ s0 := "킐\u0088中国你好!\u0088\u0088\u0088\u0088\u0088\u0088"
+
+ testDecodeFramework(t, "customReplyComplexString", s0)
+ testJavaDecode(t, "customArgComplexString", s0)
+}
diff --git a/test_hessian/src/main/java/test/TestCustomDecode.java b/test_hessian/src/main/java/test/TestCustomDecode.java
index e2394f5..1c4c276 100644
--- a/test_hessian/src/main/java/test/TestCustomDecode.java
+++ b/test_hessian/src/main/java/test/TestCustomDecode.java
@@ -200,6 +200,11 @@
return TestString.getEmojiTestString().equals(o);
}
+ public Object customArgComplexString() throws Exception {
+ String o = (String) input.readObject();
+ return TestString.getComplexString().equals(o);
+ }
+
public Object customArgTypedFixedList_HashSet() throws Exception {
HashSet o = (HashSet) input.readObject();
return o.contains(0) && o.contains(1);
diff --git a/test_hessian/src/main/java/test/TestCustomReply.java b/test_hessian/src/main/java/test/TestCustomReply.java
index 813d7eb..12fc411 100644
--- a/test_hessian/src/main/java/test/TestCustomReply.java
+++ b/test_hessian/src/main/java/test/TestCustomReply.java
@@ -425,6 +425,23 @@
output.flush();
}
+ public void customReplyPerson183() throws Exception {
+ Person183 p = new Person183();
+ p.name = "pname";
+ p.age = 13;
+ InnerPerson innerPerson = new InnerPerson();
+ innerPerson.name = "pname2";
+ innerPerson.age = 132;
+ p.innerPerson = innerPerson;
+ output.writeObject(p);
+ output.flush();
+ }
+
+ public void customReplyComplexString() throws Exception {
+ output.writeObject(TestString.getComplexString());
+ output.flush();
+ }
+
public void customReplyExtendClass() throws Exception {
Dog dog = new Dog();
dog.name = "a dog";
@@ -486,3 +503,14 @@
}
}
+
+class Person183 implements Serializable {
+ public String name;
+ public Integer age;
+ public InnerPerson innerPerson;
+}
+
+class InnerPerson implements Serializable {
+ public String name;
+ public Integer age;
+}
diff --git a/test_hessian/src/main/java/test/TestString.java b/test_hessian/src/main/java/test/TestString.java
index 2ffbdf3..c026796 100644
--- a/test_hessian/src/main/java/test/TestString.java
+++ b/test_hessian/src/main/java/test/TestString.java
@@ -29,4 +29,9 @@
return s + ",max" + maxUnicode;
}
+
+ public static String getComplexString() {
+ String s = "킐\u0088中国你好!\u0088\u0088\u0088\u0088\u0088\u0088";
+ return s;
+ }
}