A Go implementation of the Hessian 2.0 serialization protocol, enabling seamless cross-language communication between Go and Java applications. Primarily used by Apache Dubbo-Go for RPC serialization.
- Features
- Installation
- Quick Start
- Core Concepts
- Type Mapping (Java <-> Go)
- API Reference
- Usage Examples
- Encoding and Decoding Custom Objects
- Customizing Field Names with Tags
- Decoding Field Name Matching Rules
- Using a Custom Tag Identifier
- Specifying Java Parameter Types (Inheritance)
- Working with Java Collections
- Working with Java Enums
- Custom Serializer
- Strict Mode
- Reusing Encoder/Decoder (Object Pool)
- Struct Inheritance
- Tools
- Reference
- Full Hessian 2.0 protocol implementation (encode/decode)
- Circular reference and object graph support
- All JDK exceptions (88+ exception types)
- Java wrapper types (Integer, Long, Boolean, etc.)
- Java BigDecimal / BigInteger
- Java Date & Time (java.util.Date, java.sql.Date/Time)
- Java 8 Time API (LocalDate, LocalDateTime, ZonedDateTime, Instant, Duration, etc.)
- Java UUID
- Java collections (HashSet, HashMap, etc.)
- Java inheritance / extends
- Field alias via struct tags
- Generic invocation
- Dubbo attachments
- Skipping unregistered POJOs
- Emoji support in strings
- Custom serializer support
- Strict mode for decoding validation
Note: From v1.6.0+, the decoder skips non-existent fields (matching Java hessian behavior). Versions before v1.6.0 returned errors for non-existent fields.
go get github.com/apache/dubbo-go-hessian2Requires Go 1.21+.
// Define a struct and implement the POJO interface
type User struct {
Name string
Age int32
}
func (User) JavaClassName() string {
return "com.example.User"
}
// Register the type before encoding/decoding
hessian.RegisterPOJO(&User{})
// Encode
user := &User{Name: "Alice", Age: 30}
encoder := hessian.NewEncoder()
encoder.Encode(user)
data := encoder.Buffer()
// Decode
obj, _ := hessian.NewDecoder(data).Decode()
decoded := obj.(*User)Any Go struct that needs to be serialized as a Java object must implement the POJO interface:
type POJO interface {
JavaClassName() string // Returns the fully qualified Java class name
}This establishes the mapping between your Go struct and the corresponding Java class.
Before encoding or decoding custom types, you must register them. This is typically done in an init() function:
func init() {
// Register a single type
hessian.RegisterPOJO(&MyStruct{})
// Register multiple types at once
hessian.RegisterPOJOs(&TypeA{}, &TypeB{}, &TypeC{})
// Register with a custom Java class name (overrides JavaClassName())
hessian.RegisterPOJOMapping("com.example.CustomName", &MyStruct{})
}If a type is not registered, the decoder will:
- Default mode: Decode unknown objects as
map[interface{}]interface{} - Strict mode: Return an error
| Hessian Type | Java Type | Go Type |
|---|---|---|
| null | null | nil |
| binary | byte[] | []byte |
| boolean | boolean | bool |
| date | java.util.Date | time.Time |
| double | double | float64 |
| int | int | int32 |
| long | long | int64 |
| string | java.lang.String | string |
| list | java.util.List | slice |
| map | java.util.Map | map |
| object | custom class | struct |
| Java Type | Go Type |
|---|---|
| java.lang.Integer | *int32 |
| java.lang.Long | *int64 |
| java.lang.Boolean | *bool |
| java.lang.Short | *int16 |
| java.lang.Byte | *uint8 |
| java.lang.Float | *float32 |
| java.lang.Double | *float64 |
| java.lang.Character | *hessian.Rune |
| Java Type | Go Type / Package |
|---|---|
| java.math.BigDecimal | github.com/dubbogo/gost/math/big Decimal |
| java.math.BigInteger | github.com/dubbogo/gost/math/big Integer |
| java.sql.Date | github.com/apache/dubbo-go-hessian2/java_sql_time Date |
| java.sql.Time | github.com/apache/dubbo-go-hessian2/java_sql_time Time |
| java.util.UUID | github.com/apache/dubbo-go-hessian2/java_util UUID |
| java.util.Locale | github.com/apache/dubbo-go-hessian2/java_util Locale |
| Java 8 time types (LocalDate, LocalTime, LocalDateTime, ZonedDateTime, Instant, Duration, Period, etc.) | github.com/apache/dubbo-go-hessian2/java8_time |
Tip: Avoid defining objects that only exist in one language. Use error codes/messages instead of Java exceptions for cross-language communication.
// Create a new encoder
encoder := hessian.NewEncoder()
// Encode a value (primitives, slices, maps, structs, etc.)
err := encoder.Encode(value)
// Get the encoded bytes
data := encoder.Buffer()
// Reset encoder state for reuse
encoder.Clean()
// Reset encoder state but reuse the underlying buffer
encoder.ReuseBufferClean()Special encoding methods:
// Encode a map as a typed Java object using "_class" key in the map
encoder.EncodeMapClass(map[string]interface{}{"_class": "com.example.Foo", "name": "bar"})
// Encode a map as a specific Java class
encoder.EncodeMapAsClass("com.example.Foo", map[string]interface{}{"name": "bar"})// Standard decoder
decoder := hessian.NewDecoder(data)
// Strict mode - returns error for unregistered types
decoder := hessian.NewStrictDecoder(data)
// Decode the next value
obj, err := decoder.Decode()
// Reset decoder with new data (for reuse with object pools)
decoder.Reset(newData)Decoder modes:
| Constructor | Behavior |
|---|---|
NewDecoder(data) |
Decodes unknown objects as maps |
NewStrictDecoder(data) |
Returns error for unregistered objects |
NewDecoderWithSkip(data) |
Skips non-existent fields |
NewCheapDecoderWithSkip(data) |
Poolable decoder, use with Reset() |
hessian.RegisterPOJO(&MyStruct{}) // Register a POJO type
hessian.RegisterPOJOs(&A{}, &B{}) // Register multiple POJOs
hessian.RegisterPOJOMapping("com.example.Name", &Struct{}) // Register with custom Java class name
hessian.RegisterJavaEnum(&MyEnum{}) // Register a Java enum type
hessian.UnRegisterPOJOs(&A{}, &B{}) // Unregister POJOs
hessian.SetCollectionSerialize(&MyHashSet{}) // Register a Java collection type
hessian.SetSerializer("com.example.Foo", &FooSerializer{}) // Register a custom serializer// Change the struct tag used for field name mapping (default: "hessian")
hessian.SetTagIdentifier("json")
// Look up a registered custom serializer
serializer, ok := hessian.GetSerializer("com.example.Foo")
// Find class info in the decoder
classInfo := hessian.FindClassInfo("com.example.Foo")type Circular struct {
Value
Previous *Circular
Next *Circular
}
type Value struct {
Num int
}
func (Circular) JavaClassName() string {
return "com.company.Circular"
}
func init() {
hessian.RegisterPOJO(&Circular{})
}
// Encode
c := &Circular{}
c.Num = 12345
c.Previous = c // circular reference - handled automatically
c.Next = c
e := hessian.NewEncoder()
if err := e.Encode(c); err != nil {
panic(err)
}
data := e.Buffer()
// Decode
obj, err := hessian.NewDecoder(data).Decode()
if err != nil {
panic(err)
}
circular := obj.(*Circular)
fmt.Println(circular.Num) // 12345The encoder converts Go field names to lowerCamelCase by default. Use the hessian tag to override:
type MyUser struct {
UserFullName string `hessian:"user_full_name"` // encoded as "user_full_name"
FamilyPhoneNumber string // encoded as "familyPhoneNumber" (default)
}
func (MyUser) JavaClassName() string {
return "com.company.myuser"
}When decoding, fields are matched in the following order:
- Tag match - matches the
hessiantag value - lowerCamelCase - e.g.,
mobilePhonematchesMobilePhone - Exact case - e.g.,
MobilePhonematchesMobilePhone - Lowercase - e.g.,
mobilephonematchesMobilePhone
type MyUser struct {
MobilePhone string `hessian:"mobile-phone"`
}
// Incoming field "mobile-phone" -> matched via tag (rule 1)
// Incoming field "mobilePhone" -> matched via lowerCamelCase (rule 2)
// Incoming field "MobilePhone" -> matched via exact case (rule 3)
// Incoming field "mobilephone" -> matched via lowercase (rule 4)Use SetTagIdentifier to read field names from a different struct tag (e.g., json):
hessian.SetTagIdentifier("json")
type MyUser struct {
UserFullName string `json:"user_full_name"`
FamilyPhoneNumber string
}
func (MyUser) JavaClassName() string {
return "com.company.myuser"
}When a Java method expects a parent class but you send a subclass, implement the Param interface:
Java side:
public abstract class User {}
public class MyUser extends User implements Serializable {
private String userFullName;
private String familyPhoneNumber;
}
public interface UserProvider {
String GetUser(User user); // accepts parent type
}Go side:
type MyUser struct {
UserFullName string `hessian:"userFullName"`
FamilyPhoneNumber string
}
func (m *MyUser) JavaClassName() string {
return "com.company.MyUser"
}
// JavaParamName tells the encoder to use the parent class name in the method signature
func (m *MyUser) JavaParamName() string {
return "com.company.User"
}Map a Java collection class (e.g., HashSet) to a Go struct:
type JavaHashSet struct {
value []interface{}
}
func (j *JavaHashSet) Get() []interface{} { return j.value }
func (j *JavaHashSet) Set(v []interface{}) { j.value = v }
func (j *JavaHashSet) JavaClassName() string { return "java.util.HashSet" }
func init() {
hessian.SetCollectionSerialize(&JavaHashSet{})
}Without this registration, Java collections are decoded as []interface{}.
type Color int32
const (
RED Color = 0
GREEN Color = 1
BLUE Color = 2
)
var colorNames = map[Color]string{
RED: "RED", GREEN: "GREEN", BLUE: "BLUE",
}
var colorValues = map[string]Color{
"RED": RED, "GREEN": GREEN, "BLUE": BLUE,
}
func (c Color) JavaClassName() string { return "com.example.Color" }
func (c Color) String() string { return colorNames[c] }
func (c Color) EnumValue(s string) hessian.JavaEnum {
return hessian.JavaEnum(colorValues[s])
}
func init() {
hessian.RegisterJavaEnum(RED)
}Implement the Serializer interface for full control over encoding/decoding:
type MySerializer struct{}
func (s *MySerializer) EncObject(encoder *hessian.Encoder, obj hessian.POJO) error {
// Custom encoding logic
return nil
}
func (s *MySerializer) DecObject(decoder *hessian.Decoder, typ reflect.Type, cls *hessian.ClassInfo) (interface{}, error) {
// Custom decoding logic
return nil, nil
}
func init() {
hessian.SetSerializer("com.example.MyClass", &MySerializer{})
}By default, unregistered objects are decoded as maps. Use strict mode to get errors instead:
decoder := hessian.NewDecoder(data)
decoder.Strict = true // returns error for unregistered types
// Or use the convenience constructor:
decoder = hessian.NewStrictDecoder(data)For high-performance scenarios, reuse encoder/decoder instances:
// Encoder reuse
encoder := hessian.NewEncoder()
encoder.Encode(obj1)
data1 := encoder.Buffer()
encoder.Clean() // or encoder.ReuseBufferClean() to keep the buffer
encoder.Encode(obj2)
data2 := encoder.Buffer()
// Decoder reuse (poolable decoder)
decoder := hessian.NewCheapDecoderWithSkip(data1)
obj1, _ := decoder.Decode()
decoder.Reset(data2) // reuse with new data
obj2, _ := decoder.Decode()Go struct embedding is supported for modeling Java inheritance:
type Animal struct {
Name string
}
func (Animal) JavaClassName() string { return "com.example.Animal" }
type Dog struct {
Animal // embedded parent struct
Breed string
}
func (Dog) JavaClassName() string { return "com.example.Dog" }Avoid these patterns:
-
Duplicate field names across parents - ambiguous field resolution:
type A struct { Name string } type B struct { Name string } type C struct { A; B } // which Name?
-
Pointer embedding - nil parent at initialization, not supported:
type Dog struct { *Animal // will be nil in Dog{}, not supported }
A code generation tool for creating Go enum types compatible with Java enums. See tools/gen-go-enum/README.md for details.
