序列化方式四——Hessian

news2024/9/30 19:11:19

介绍

Hessian是一个轻量级的、基于HTTP的RPC(远程过程调用)框架,由Resin开源提供。它使用一个简单的、基于二进制的协议来序列化对象,并通过HTTP进行传输。Hessian的设计目标是提供一种高效、可靠且易于使用的远程服务调用机制。

优缺点

优点

  1. 轻量级:Hessian协议简单,不依赖于特定的Java类库或API,使得它非常适合用于资源受限的环境,如移动设备。

  2. 高效性:Hessian使用二进制协议进行数据传输,相比基于文本的协议(如XML或JSON),它在序列化和反序列化方面具有更高的性能。

  3. 跨语言支持:虽然Hessian主要由Java实现,但它的协议是跨语言的,意味着其他语言也可以实现Hessian客户端或服务器。

  4. 与HTTP集成:Hessian基于HTTP协议,因此可以轻松地与现有的Web基础设施和工具集成,如Web服务器、代理服务器和缓存系统。

  5. 易于使用:Hessian的API简洁明了,使得开发者能够快速地构建和部署RPC服务。

缺点

  1. HTTP开销:尽管Hessian使用二进制协议进行数据传输,但它仍然基于HTTP协议。这意味着每次调用都可能伴随着HTTP请求/响应的开销,包括连接建立、头部传输等。

  2. 不支持异步调用:Hessian原生不支持异步调用模式,这可能限制了它在某些高性能或响应性要求较高的场景中的应用。

  3. 安全性考虑:由于Hessian基于HTTP,因此可能需要考虑额外的安全措施来保护传输的数据,如使用HTTPS进行加密传输。

原理

远程服务的工作原理

  1. 定义服务接口:开发者首先定义远程服务的接口,这些接口将用于客户端和服务端之间的通信。

  2. 实现服务端:服务端实现这些接口,并提供具体的业务逻辑。Hessian服务端通常部署在Web服务器上,作为Servlet来处理客户端的请求。

  3. 生成客户端代理:客户端使用Hessian提供的代理工厂类(如HessianProxyFactory)来生成服务接口的代理对象。这个代理对象负责将方法调用转换为网络请求,并发送给服务端。

  4. 请求传输与响应:客户端通过HTTP请求将序列化后的方法调用参数发送给服务端。服务端接收到请求后,反序列化参数,执行相应的方法,并将结果序列化后返回给客户端。客户端再反序列化响应数据,得到方法调用的结果。

序列化&反序列化的工作原理

Hessian的序列化

Hessian的序列化过程主要依赖Hessian2Output类。当需要将一个对象序列化为二进制流时,可以通过以下步骤:

  1. 创建一个Hessian2Output对象,传入一个OutputStream,例如ByteArrayOutputStream

  2. 调用writeObject方法,将要序列化的对象作为参数传入。

  3. 调用flush方法,确保所有缓冲的数据都被写入到OutputStream中。

  4. 获取OutputStream中的二进制流数据。

Hessian的反序列化

Hessian的反序列化过程主要依赖Hessian2Input类。当需要从二进制流中反序列化一个对象时,可以通过以下步骤:

  1. 创建一个Hessian2Input对象,传入一个InputStream,例如ByteArrayInputStream

  2. 调用readObject方法,从InputStream中读取并反序列化对象。

序列化和反序列化的原理

Hessian的序列化和反序列化过程基于Field机制,这意味着它会根据对象的类结构,将对象的每个字段序列化为二进制数据,并存储在OutputStream中。在反序列化时,它会根据类结构从InputStream中读取数据,并将数据设置到对象的字段中。

序列化&反序列化demo

实体类

package com.zhz.test.serialization.entity;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Student implements Serializable {

    private String name;
    private String sex;
    private int age;
    private Date birthday;
}

测试用例


    @Test
    public void test() {
        Student student = Student.builder()
                .age(18)
                .birthday(new Date())
                .name("zhouhengzhe")
                .sex("男")
                .build();
        byte[] bytes;
        try {
            //hessian序列化
            bytes = serialize(student);
            System.out.println(bytes);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //反序列化
        try {
            Object deserialize = deserialize(bytes);
            System.out.println(deserialize);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    public static byte[] serialize(Object obj) throws Exception {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Hessian2Output out = new Hessian2Output(bos);
        out.writeObject(obj);
        out.flush();
        return bos.toByteArray();
    }

    public static Object deserialize(byte[] bytes) throws Exception {
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        Hessian2Input in = new Hessian2Input(bis);
        return in.readObject();
    }

在这里插入图片描述

远程服务demo

使用说明

  1. 实现 Serializable 接口:

    • 要进行 Hessian 序列化,对象需要实现 java.io.Serializable 接口。如果不实现此接口,尝试序列化该对象时将会抛出 java.io.NotSerializableException 异常。
  2. serialVersionUID:

    • 虽然实现了 Serializable 接口的类可以有一个 serialVersionUID 字段,但在 Hessian 序列化中,serialVersionUID 实际上不是必须的。即使存在 serialVersionUID,修改其值也不会影响 Hessian 的反序列化过程。这是因为 Hessian 并不使用 Java 的序列化机制,而是基于自己的协议。
  3. 序列化过程:

    • 在 Hessian 中,复杂对象的所有属性都被存储在一个 Map 中进行序列化。这意味着如果父类和子类有同名的成员变量,那么在序列化时,Hessian 会先序列化子类,然后序列化父类。这意味着反序列化时,子类的同名成员变量将被父类的值覆盖。
  4. 注意事项:

    • 如果你的对象有大量的属性或嵌套对象,可能需要考虑性能问题。虽然 Hessian 是轻量级的,但大量的数据仍然可能导致性能下降。

    • 由于 Hessian 序列化时使用的是二进制格式,因此在反序列化时,接收方需要知道对象的类结构,以便正确解析数据。

  5. 使用示例:

    • 在客户端,你可以使用 HessianProxyFactory 类来创建远程服务接口的代理对象。

    • 在服务端,你可以使用 HessianServlet 来暴露服务,或者自己实现 Hessian 的序列化/反序列化逻辑。

源码解析

首先分析Hessian2Output的初始化过程,源码如下:

/** 输出字节流对象*/
// 这一行注释说明`_os`是一个用于将序列化后的数据写入到输出流中的对象。
protected OutputStream _os;

/**
 * 通过构造函数初始化Hessian2Output对象,并传入一个OutputStream对象,该对象用于在后续操作中写入序列化的数据。
 */
public Hessian2Output(OutputStream os){
    init(os);
}

/** 初始化*/
// 这一行注释说明`init`方法用于初始化Hessian2Output对象,包括重置其内部状态并设置`_os`。
public void init(OutputStream os){
    reset();
    _os = os;
}

/** 重置所有的指针和引用*/
// 这一行注释说明`reset`方法用于重置Hessian2Output对象的内部状态,清除所有已存储的引用和指针,以确保每次序列化操作都从一个清晰的状态开始。
public void reset(){
    if (_refs != null) {
        _refs.clear();  // 如果_refs不为null,则清除其所有元素。
        _refCount = 0;  // 将引用计数设置为0。
    }

    _classRefs.clear();  // 清除已序列化的类引用。
    _typeRefs = null;   // 将类型引用设置为null。
    _offset = 0;        // 初始化偏移量。
    _isPacket = false;  // 初始化是否处理数据包标志。
    _isUnshared = false; // 初始化是否共享对象标志。
}

Hessian2Output内部包含一个OutputStream属性,该属性用于将对象序列化后的字节流写入到该对象中。在创建Hessian2Output对象时,其构造方法主要执行两个操作:

  • 1、重置所有与序列化相关的方法和属性,确保在序列化的过程中不会出现混乱或遗留状态;

  • 2、初始化字节流对象,为后续的序列化操作做好准备。

writeObejct方法解析(序列化源码解析)

public void writeObject(Object object)throws IOException{
    /** 1.如果对象为空,则写入空对象 */
    if (object == null) {
        writeNull();
        return;
    }
    /** 2.根据对象的Class来获取序列化器 */
    Serializer serializer = findSerializerFactory().getObjectSerializer(object.getClass());
    /** 3.调用序列化器的writeObject进行对象序列化 */
    serializer.writeObject(object, this);
}

该方法相对简单,逻辑清晰。当传入的对象为空时,它会直接写入空数据;而当对象不为空时,它会根据对象的Class信息来构造一个特定的序列化器对象,随后直接调用该序列化器的writeObject方法进行序列化操作。

当对象为空时

/**
*定义了一个常量,表示字节数组缓存的大小,即8 * 1024字节,也就是8KB。
*/
public final static int SIZE = 8 * 1024;
/** 字节数组缓存,用于缓存序列化数据 */  
private final byte []_buffer = new byte[SIZE];
/** 字节数组偏移量,表示当前在_buffer中的偏移量,即下一个要写入的位置 */
private int _offset;

/** 
*写入空对象
*<p/>
* 详细解析:用于将空对象写入缓存。首先检查是否有足够的空间来写入一个表示“空对象”的标识符(这里是字符'N')。
* 如果空间不足,调用flushBuffer方法将缓存中的数据写入输出流,并清空缓存。
* 然后,在_buffer中写入'N'字符,并更新_offset。
*/
public void writeNull() throws IOException{
    int offset = _offset;
    byte []buffer = _buffer;

    if (SIZE <= offset + 16) {
        /** 如果字节数组缓存不足,则将缓存数据写入到OutputStream中并清除缓存*/
        flushBuffer();
        offset = _offset;
    }
    /** 写入字符串N表示当前的对象为空对象 */
    buffer[offset++] = 'N';
    /** 更新偏移量*/
    _offset = offset;
}

/**
* 清除缓存,并将缓存数据写入输出字节流中
*
* <p/>
* 这个方法用于清除缓存,并将缓存中的数据写入输出流。
* 如果_isPacket为false且_offset大于0,表示有数据需要写入输出流,然后重置_offset为0,并将数据写入输出流。
* 如果_isPacket为true且_offset大于4,表示需要处理数据包格式,并在数据前添加4字节的长度信息。然后,将数据写入输出流,并重置_offset为4。
* 最后,清除数据包的标识(将前三字节设置为0x00、0x56、0x56)。
*
*/
public final void flushBuffer()
        throws IOException{
    int offset = _offset;
    OutputStream os = _os;

    if (! _isPacket && offset > 0) {
        _offset = 0;
        if (os != null)
            os.write(_buffer, 0, offset);
    }
    else if (_isPacket && offset > 4) {
        int len = offset - 4;

        _buffer[0] |= (byte) 0x80;
        _buffer[1] = (byte) (0x7e);
        _buffer[2] = (byte) (len >> 8);
        _buffer[3] = (byte) (len);
        _offset = 4;

        if (os != null)
            os.write(_buffer, 0, offset);

        _buffer[0] = (byte) 0x00;
        _buffer[1] = (byte) 0x56;
        _buffer[2] = (byte) 0x56;
        _buffer[3] = (byte) 0x56;
    }
}

writeNull方法逻辑相对简单,主要就是在_buffer字节数组中写入字符’N’来标识空对象。Hessian2Output对象内部有一个字节数组缓存_buffer和一个数组写入偏移量_offset变量。

该字节数组缓存的大小为8KB(即8 * 1024个字节),用于临时存储序列化的字节流。在序列化过程中,数据首先写入缓存中。当缓存的容量不足以容纳更多数据时,会调用flushBuffer方法将缓存中的数据写入输出流,并清除缓存。这种机制确保了数据的分批次处理和输出,同时也确保了内存的有效利用。

当对象不为空时

当对象不为空时,序列化的方式需要根据对象的类型来选择。不同的对象类型,如StringList以及用户自定义的实体类,通常会有不同的序列化方式。为了支持这些不同的序列化方式,定义了一个Serializer接口,该接口应包含相应类型对象序列化的具体实现。

例如,String类型可能需要一个将字符串转换为字节序列的序列化方法,而List类型可能需要一个将列表元素逐个序列化并合并的序列化方法。对于用户自定义的实体类,也需要提供相应的序列化方法。

通过实现Serializer接口,可以为不同的对象类型提供合适的序列化方式,确保在序列化过程中能够正确地将对象转换为可存储或传输的格式。

对象序列化的接口为Serializer,定义如下:

public interface Serializer {
   public void writeObject(Object obj, AbstractHessianOutput out)throws IOException;
}

具体的实现类比较多,针对不同的类型有不同的实现子类,

image

基本数据类型序列化

对于常用的基本数据类型及其数组类型,Hessian框架提供了BasicSerializer这一序列化器,用于这些类型的序列化。这个序列化器在ContextSerializerFactory类中被初始化并注册。

ContextSerializerFactory类中,通过相应的代码,基本数据类型的序列化器被注册到序列化器工厂中,以便在序列化过程中能够正确地识别和序列化这些基本数据类型及其数组类型。

addBasic(void.class, "void", BasicSerializer.NULL);

addBasic(Boolean.class, "boolean", BasicSerializer.BOOLEAN);
addBasic(Byte.class, "byte", BasicSerializer.BYTE);
addBasic(Short.class, "short", BasicSerializer.SHORT);
addBasic(Integer.class, "int", BasicSerializer.INTEGER);
addBasic(Long.class, "long", BasicSerializer.LONG);
addBasic(Float.class, "float", BasicSerializer.FLOAT);
addBasic(Double.class, "double", BasicSerializer.DOUBLE);
addBasic(Character.class, "char", BasicSerializer.CHARACTER_OBJECT);
addBasic(String.class, "string", BasicSerializer.STRING);
addBasic(Object.class, "object", BasicSerializer.OBJECT);
addBasic(java.util.Date.class, "date", BasicSerializer.DATE);

addBasic(boolean.class, "boolean", BasicSerializer.BOOLEAN);
addBasic(byte.class, "byte", BasicSerializer.BYTE);
addBasic(short.class, "short", BasicSerializer.SHORT);
addBasic(int.class, "int", BasicSerializer.INTEGER);
addBasic(long.class, "long", BasicSerializer.LONG);
addBasic(float.class, "float", BasicSerializer.FLOAT);
addBasic(double.class, "double", BasicSerializer.DOUBLE);
addBasic(char.class, "char", BasicSerializer.CHARACTER);

addBasic(boolean[].class, "[boolean", BasicSerializer.BOOLEAN_ARRAY);
addBasic(byte[].class, "[byte", BasicSerializer.BYTE_ARRAY);
_staticSerializerMap.put(byte[].class.getName(), ByteArraySerializer.SER);
addBasic(short[].class, "[short", BasicSerializer.SHORT_ARRAY);
addBasic(int[].class, "[int", BasicSerializer.INTEGER_ARRAY);
addBasic(long[].class, "[long", BasicSerializer.LONG_ARRAY);
addBasic(float[].class, "[float", BasicSerializer.FLOAT_ARRAY);
addBasic(double[].class, "[double", BasicSerializer.DOUBLE_ARRAY);
addBasic(char[].class, "[char", BasicSerializer.CHARACTER_ARRAY);
addBasic(String[].class, "[string", BasicSerializer.STRING_ARRAY);
addBasic(Object[].class, "[object", BasicSerializer.OBJECT_ARRAY);

虽然基本数据类型都是通过BasicSerializer进行序列化,但是不同的类型都是有不同的type的,BasicSerializer根据不同的type进行不同的序列化逻辑处理。具体的序列化逻辑如下:

/** 这是一个处理各种数据类型序列化的方法,专门用于Hessian序列化库。 */
public void writeObject(Object obj, AbstractHessianOutput out)
throws IOException {
    // 根据对象的类型,执行不同的序列化操作
    switch (_code) {
        // 处理布尔类型
        case BOOLEAN:
            out.writeBoolean(((Boolean) obj).booleanValue());
            break;
        
        // 处理byte, short, 和 int类型
        case BYTE:
        case SHORT:
        case INTEGER:
            out.writeInt(((Number) obj).intValue());
            break;
        
        // 处理long类型
        case LONG:
            out.writeLong(((Number) obj).longValue());
            break;
        
        // 处理float和double类型
        case FLOAT:
        case DOUBLE:
            out.writeDouble(((Number) obj).doubleValue());
            break;
        
        // 处理char和Character类型
        case CHARACTER:
        case CHARACTER_OBJECT:
            out.writeString(String.valueOf(obj));
            break;
        
        // 处理String类型
        case STRING:
            out.writeString((String) obj);
            break;
        
        // 处理StringBuilder类型
        case STRING_BUILDER:
            out.writeString(((StringBuilder) obj).toString());
            break;
        
        // 处理Date类型
        case DATE:
            out.writeUTCDate(((Date) obj).getTime());
            break;
        
        // 处理布尔数组类型
        case BOOLEAN_ARRAY:
            // 如果对象已经存在于输出流中,则不再序列化
            if (out.addRef(obj)) return;
            
            boolean []data = (boolean []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[boolean");
            for (int i = 0; i < data.length; i++)
                out.writeBoolean(data[i]);
            
            // 结束列表
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理byte数组类型
        case BYTE_ARRAY:
            byte []data = (byte []) obj;
            out.writeBytes(data, 0, data.length);
            break;
        
        // 处理short数组类型
        case SHORT_ARRAY:
            if (out.addRef(obj)) return;
            
            short []data = (short []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[short");
            for (int i = 0; i < data.length; i++)
                out.writeInt(data[i]);
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理int数组类型
        case INTEGER_ARRAY:
            if (out.addRef(obj)) return;
            
            int []data = (int []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[int");
            for (int i = 0; i < data.length; i++)
                out.writeInt(data[i]);
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理long数组类型
        case LONG_ARRAY:
            if (out.addRef(obj)) return;
            
            long []data = (long []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[long");
            for (int i = 0; i < data.length; i++)
                out.writeLong(data[i]);
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理float数组类型
        case FLOAT_ARRAY:
            if (out.addRef(obj)) return;
            
            float []data = (float []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[float");
            for (int i = 0; i < data.length; i++)
                out.writeDouble(data[i]);
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理double数组类型
        case DOUBLE_ARRAY:
            if (out.addRef(obj)) return;
            
            double []data = (double []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[double");
            for (int i = 0; i < data.length; i++)
                out.writeDouble(data[i]);
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理String数组类型
        case STRING_ARRAY:
            if (out.addRef(obj)) return;
            
            String []data = (String []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[string");
            for (int i = 0; i < data.length; i++) {
                out.writeString(data[i]);
            }
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理char数组类型
        case CHARACTER_ARRAY:
            char []data = (char []) obj;
            out.writeString(data, 0, data.length);
            break;
        
        // 处理Object数组类型
        case OBJECT_ARRAY:
            if (out.addRef(obj)) return;
            
            Object []data = (Object []) obj;
            boolean hasEnd = out.writeListBegin(data.length, "[object");
            for (int i = 0; i < data.length; i++) {
                out.writeObject(data[i]);
            }
            
            if (hasEnd)
                out.writeListEnd();
            break;
        
        // 处理null类型
        case NULL:
            out.writeNull();
            break;
        
        // 处理Object类型
        case OBJECT:
            ObjectHandleSerializer.SER.writeObject(obj, out);
            break;
        
        // 处理特殊的字节、短整型、浮点数字符句柄类型
        case BYTE_HANDLE:
            out.writeObject(new ByteHandle((Byte) obj));
            break;
        
        case SHORT_HANDLE:
            out.writeObject(new ShortHandle((Short) obj));
            break;
        
        case FLOAT_HANDLE:
            out.writeObject(new FloatHandle((Float) obj));
            break;
        
        // 如果对象的类型不在上述列表中,则抛出异常
        default:
            throw new RuntimeException(_code + " unknown code for " + obj.getClass());
    }
}

代码逻辑清晰,对于不同类型的对象,通过类型判断调用对应的write方法。对于数组类型,它会在写入数组数据之前和之后分别调用writeListBeginwriteListEnd方法,以标记数组的起始和结束。

如字符串的序列化方法writeString源码如下:

/**
 * 序列化字符串的函数
 * @param value 需要序列化的字符串
 * @throws IOException 如果在序列化过程中发生I/O错误,则抛出此异常
 */
public void writeString(String value) throws IOException {
    int offset = _offset; // 当前的偏移量
    byte []buffer = _buffer; // 当前的缓冲区

    /**
     * 如果缓冲区已满(即,剩余空间不足以容纳接下来的数据),则清空缓冲区,并更新偏移量
     */
    if (SIZE <= offset + 16) {
        flushBuffer();
        offset = _offset;
    }

    /**
     * 如果要序列化的字符串为空,则写入一个'N'标记表示空字符串
     */
    if (value == null) {
        buffer[offset++] = (byte) 'N';
        _offset = offset;
    } else {
        int length = value.length(); // 字符串的长度
        int strOffset = 0; // 字符串的起始偏移量

        /**
         * 如果字符串的长度超过32K,则将其拆分成多个32K大小的字符串进行序列化
         */
        while (length > 0x8000) {
            int sublen = 0x8000; // 子字符串的长度

            // 如果缓冲区已满,则清空缓冲区,并更新偏移量
            if (SIZE <= offset + 16) {
                flushBuffer();
                offset = _offset;
            }

            // 检查最后一个字符是否为高代理字符,如果是,则减少子字符串的长度
            char tail = value.charAt(strOffset + sublen - 1);
            if (0xd800 <= tail && tail <= 0xdbff)
                sublen--;

            /**
             * 写入子字符串的标记('R')、长度和具体的数据
             */
            buffer[offset + 0] = (byte) BC_STRING_CHUNK;
            buffer[offset + 1] = (byte) (sublen >> 8);
            buffer[offset + 2] = (byte) (sublen);

            _offset = offset + 3;
            printString(value, strOffset, sublen); // 写入子字符串的数据

            length -= sublen; // 更新剩余长度
            strOffset += sublen; // 更新偏移量
        }

        // 如果字符串的长度小于32K,则直接写入
        offset = _offset;

        // 如果缓冲区已满,则清空缓冲区,并更新偏移量
        if (SIZE <= offset + 16) {
            flushBuffer();
            offset = _offset;
        }

        if (length <= STRING_DIRECT_MAX) {
            buffer[offset++] = (byte) (BC_STRING_DIRECT + length);
        } else if (length <= STRING_SHORT_MAX) {
            buffer[offset++] = (byte) (BC_STRING_SHORT + (length >> 8));
            buffer[offset++] = (byte) (length);
        } else {
            buffer[offset++] = (byte) ('S');
            buffer[offset++] = (byte) (length >> 8);
            buffer[offset++] = (byte) (length);
        }

        _offset = offset;

        printString(value, strOffset, length); // 写入剩余的字符串数据
    }
}

在写入字符串时,首先会写入一个标记,表示字符串的类型。如果字符串过长,会将其拆分成多个子字符串,并使用“R”标记表示子字符串,而非子字符串则使用“S”标记。接着,会写入字符串的长度。最后,使用printString方法将字符串的实际数据写入字节数组中。

printString方法的逻辑是遍历字符串的每一位,将每个字符依次写入字节数组中。源码如下:

public void printString(String v, int strOffset, int length) throws IOException {
    int offset = _offset;  // 获取当前的偏移量
    byte []buffer = _buffer;  // 获取当前的缓冲区

    for (int i = 0; i < length; i++) {  // 遍历字符串中的每个字符

        // 如果缓冲区已满,则清空缓冲区,并更新偏移量
        if (SIZE <= offset + 16) {
            _offset = offset;
            flushBuffer();  // 清空缓冲区
            offset = _offset;  // 更新偏移量
        }

        char ch = v.charAt(i + strOffset);  // 获取当前字符

        // 如果字符的Unicode值小于0x80,则直接将该字符的字节值放入缓冲区
        if (ch < 0x80) {
            buffer[offset++] = (byte) ch;
        }
        // 如果字符的Unicode值在0x80到0x7ff之间,则使用两个字节的UTF-8编码表示
        else if (ch < 0x800) {
            buffer[offset++] = (byte) (0xc0 + ((ch >> 6) & 0x1f));
            buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
        }
        // 如果字符的Unicode值大于0x7ff,则使用三个字节的UTF-8编码表示
        else {
            buffer[offset++] = (byte) (0xe0 + ((ch >> 12) & 0xf));
            buffer[offset++] = (byte) (0x80 + ((ch >> 6) & 0x3f));
            buffer[offset++] = (byte) (0x80 + (ch & 0x3f));
        }
    }
    _offset = offset;  // 更新偏移量
}

解释:

  • 该方法接收一个字符串v,一个偏移量strOffset和一个长度length

  • 它遍历字符串中的每个字符,并使用UTF-8编码将其转换为字节。

  • 如果缓冲区已满(即,无法再容纳16个字节),它会清空缓冲区并更新偏移量。

  • 最后,它更新偏移量以反映已写入的字节数。

对于其他基本数据类型,其写入逻辑与字符串相似,都是先写入标记,然后直接写入数据。例如,Long类型使用"L"作为标记,Double类型使用"D"作为标记,而Int类型则使用"I"作为标记**。**这种序列化方式与JDK的序列化方法相比,在数据类型方面显著减少了数据量。Hessian通过仅使用一个字符来表示基本数据类型,而JDK序列化则需要序列化类的全路径。例如,对于String类型,Hessian只需写入"S"作为标记,而JDK序列化则需要写入完整的类路径"java.lang.String"

自定义数据类型序列化

自定义数据类型的序列化是通过UnsafeSerializer进行的序列化,源码如下:

public void writeObject(Object obj, AbstractHessianOutput out)throws IOException {
     if (out.addRef(obj)) {
         return;
     }

     Class<?> cl = obj.getClass();
     /** 写入Object类型开始标记 将类名存入Map中,并记录该类的引用次数 */
     int ref = out.writeObjectBegin(cl.getName());

     /** 如果引用次数大于0则表示已经被引用过*/
     if (ref >= 0) {
         /** 直接写入实例 */
         writeInstance(obj, out);
     }
     /** 值为-1表示第一次引用 */
     else if (ref == -1) {
         /** 写入类的定义,依次写入属性个数和所有属性的名称 */
         writeDefinition20(out);
         /** 写入类的名称 */
         out.writeObjectBegin(cl.getName());
         /** 写入实例 */
         writeInstance(obj, out);
     }
     else {
         writeObject10(obj, out);
     }
 }

主要逻辑为:

  1. 获取Class的引用次数:首先,检查对象所属的类是否已经被序列化过。这通常是通过检查输出流中的引用计数映射来完成的。

  2. 判断是否需要重新解析:如果类已经被引用过,那么不需要重新解析类的定义,直接写入对象的实例即可。

  3. 写入类的定义(如果需要):如果类没有被引用过,那么需要先写入类的定义,包括属性的个数和所有属性的名称。

  4. 写入类的名称:无论类是否已经被引用过,都需要写入类的名称。

  5. 写入实例对象:最后,执行writeInstance方法将实例对象写入输出流。

writeDefinition20方法源码如下:

/**
 * 写入类的定义
 *
 * @param outHessian输出流对象,用于将类的定义写入
 * @throws IOException可能抛出的异常
 */
private void writeDefinition20(AbstractHessianOutput out) throws IOException {

    /**
     * 写入类的属性个数
     *
     * @param length属性个数
     */
    out.writeClassFieldLength(_fields.length); // 写入属性个数到输出流中

    /**
     * 依次写入属性的名称
     *
     * @param field当前处理的属性
     */
    for (int i = 0; i < _fields.length; i++) {
        Field field = _fields[i]; // 获取当前处理的属性
        out.writeString(field.getName()); // 将属性的名称写入输出流中
    }
}

writeObjectBegin方法源码如下:

/** 开始写入对象*/
public int writeObjectBegin(String type) throws IOException {
  // 获取类的引用次数
  int newRef = _classRefs.size();
  int ref = _classRefs.put(type, newRef, false);

  // 如果引用次数发生了变化
  if (newRef != ref) {
      // 如果缓冲区已满,先清空缓冲区
      if (SIZE < _offset + 32)
           flushBuffer();

       // 如果引用次数小于等于直接对象最大数
       if (ref <= OBJECT_DIRECT_MAX) {
           // 写入直接对象的标记和引用次数
           _buffer[_offset++] = (byte) (BC_OBJECT_DIRECT + ref);
       } else {
           // 写入对象引用类型的标记
           _buffer[_offset++] = (byte) 'O';
           // 写入引用次数
           writeInt(ref);
       }
       // 返回引用次数
       return ref;
   } else {
       // 如果缓冲区已满,先清空缓冲区
       if (SIZE < _offset + 32)
           flushBuffer();

       // 写入类类型的标记
       _buffer[_offset++] = (byte) 'C';

       // 写入类名
       writeString(type);

       // 返回-1,表示这是一个新创建的类引用
       return -1;
   }
}

writeInstance方法源码如下:

/** 写入对象实例 */
final public void writeInstance(Object obj, AbstractHessianOutput out) throws IOException {
    try {
        /** 获取所有属性序列化器对象 */
        FieldSerializer []fieldSerializers = _fieldSerializers;
        int length = fieldSerializers.length;
        // 遍历所有属性序列化器,并对每个属性执行序列化方法
        for (int i = 0; i < length; i++) {
            fieldSerializers[i].serialize(out, obj);
         }
     } catch (RuntimeException e) {
         // 如果捕获到RuntimeException,则抛出一个新的RuntimeException,其中包含原始异常信息、类名和对象信息
         throw new RuntimeException(e.getMessage() + "\n class: " + obj.getClass().getName() + " (object=" + obj + ")", e);
     } catch (IOException e) {
         // 如果捕获到IOException,则抛出一个新的IOExceptionWrapper,其中包含原始异常信息、类名和对象信息
         throw new IOExceptionWrapper(e.getMessage() + "\n class: " + obj.getClass().getName() + " (object=" + obj + ")", e);
     }
 }

写入实例的整体逻辑相当直接且简洁。针对每个字段,我们都有一个专门的序列化器。通过遍历所有属性,我们调用与之对应的序列化器的序列化方法。如果某个属性是自定义类型,那么我们会继续遍历该属性的类,直到我们处理到的是基本数据类型属性为止。这样的方法确保了所有属性都能被适当地序列化。

反序列化源码解析

和序列化相反,反序列化是通过输入流Hessian2Input对象的readObject方法来实现的,源码如下:

/** 反序列化对象*/
public Object readObject() throws IOException
{
   /** 调用read()方法读取字节,第一次就读取第一个字节 */
   int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();

   /** 判断字节对应的标记类型,执行对应的解析方法 */
   switch (tag) {
       case 'N':
           return null;

       case 'T':
           return Boolean.valueOf(true);

       case 'F':
           return Boolean.valueOf(false);

       // direct integer
       case 0x80: case 0x81: case 0x82: case 0x83:
       case 0x84: case 0x85: case 0x86: case 0x87:
       case 0x88: case 0x89: case 0x8a: case 0x8b:
       case 0x8c: case 0x8d: case 0x8e: case 0x8f:

       case 0x90: case 0x91: case 0x92: case 0x93:
       case 0x94: case 0x95: case 0x96: case 0x97:
       case 0x98: case 0x99: case 0x9a: case 0x9b:
       case 0x9c: case 0x9d: case 0x9e: case 0x9f:

       case 0xa0: case 0xa1: case 0xa2: case 0xa3:
       case 0xa4: case 0xa5: case 0xa6: case 0xa7:
       case 0xa8: case 0xa9: case 0xaa: case 0xab:
       case 0xac: case 0xad: case 0xae: case 0xaf:

       case 0xb0: case 0xb1: case 0xb2: case 0xb3:
       case 0xb4: case 0xb5: case 0xb6: case 0xb7:
       case 0xb8: case 0xb9: case 0xba: case 0xbb:
       case 0xbc: case 0xbd: case 0xbe: case 0xbf:
           return Integer.valueOf(tag - BC_INT_ZERO);

       /* byte int */
       case 0xc0: case 0xc1: case 0xc2: case 0xc3:
       case 0xc4: case 0xc5: case 0xc6: case 0xc7:
       case 0xc8: case 0xc9: case 0xca: case 0xcb:
       case 0xcc: case 0xcd: case 0xce: case 0xcf:
           return Integer.valueOf(((tag - BC_INT_BYTE_ZERO) << 8) + read());

       /* short int */
       case 0xd0: case 0xd1: case 0xd2: case 0xd3:
       case 0xd4: case 0xd5: case 0xd6: case 0xd7:
           return Integer.valueOf(((tag - BC_INT_SHORT_ZERO) << 16)
                   + 256 * read() + read());

       case 'I':
           return Integer.valueOf(parseInt());

       // direct long
       case 0xd8: case 0xd9: case 0xda: case 0xdb:
       case 0xdc: case 0xdd: case 0xde: case 0xdf:

       case 0xe0: case 0xe1: case 0xe2: case 0xe3:
       case 0xe4: case 0xe5: case 0xe6: case 0xe7:
       case 0xe8: case 0xe9: case 0xea: case 0xeb:
       case 0xec: case 0xed: case 0xee: case 0xef:
           return Long.valueOf(tag - BC_LONG_ZERO);

       /* byte long */
       case 0xf0: case 0xf1: case 0xf2: case 0xf3:
       case 0xf4: case 0xf5: case 0xf6: case 0xf7:
       case 0xf8: case 0xf9: case 0xfa: case 0xfb:
       case 0xfc: case 0xfd: case 0xfe: case 0xff:
           return Long.valueOf(((tag - BC_LONG_BYTE_ZERO) << 8) + read());

       /* short long */
       case 0x38: case 0x39: case 0x3a: case 0x3b:
       case 0x3c: case 0x3d: case 0x3e: case 0x3f:
           return Long.valueOf(((tag - BC_LONG_SHORT_ZERO) << 16) + 256 * read() + read());

       case BC_LONG_INT:
           return Long.valueOf(parseInt());

       case 'L':
           return Long.valueOf(parseLong());

       case BC_DOUBLE_ZERO:
           return Double.valueOf(0);

       case BC_DOUBLE_ONE:
           return Double.valueOf(1);

       case BC_DOUBLE_BYTE:
           return Double.valueOf((byte) read());

       case BC_DOUBLE_SHORT:
           return Double.valueOf((short) (256 * read() + read()));

       case BC_DOUBLE_MILL:
       {
           int mills = parseInt();

           return Double.valueOf(0.001 * mills);
       }

       case 'D':
           return Double.valueOf(parseDouble());

       case BC_DATE:
           return new Date(parseLong());

       case BC_DATE_MINUTE:
           return new Date(parseInt() * 60000L);

       case BC_STRING_CHUNK:
       case 'S':
       {
           _isLastChunk = tag == 'S';
           _chunkLength = (read() << 8) + read();

           _sbuf.setLength(0);

           parseString(_sbuf);

           return _sbuf.toString();
       }

       case 0x00: case 0x01: case 0x02: case 0x03:
       case 0x04: case 0x05: case 0x06: case 0x07:
       case 0x08: case 0x09: case 0x0a: case 0x0b:
       case 0x0c: case 0x0d: case 0x0e: case 0x0f:

       case 0x10: case 0x11: case 0x12: case 0x13:
       case 0x14: case 0x15: case 0x16: case 0x17:
       case 0x18: case 0x19: case 0x1a: case 0x1b:
       case 0x1c: case 0x1d: case 0x1e: case 0x1f:
       {
           _isLastChunk = true;
           _chunkLength = tag - 0x00;

           int data;
           _sbuf.setLength(0);

           parseString(_sbuf);

           return _sbuf.toString();
       }

       case 0x30: case 0x31: case 0x32: case 0x33:
       {
           _isLastChunk = true;
           _chunkLength = (tag - 0x30) * 256 + read();

           _sbuf.setLength(0);

           parseString(_sbuf);

           return _sbuf.toString();
       }

       case BC_BINARY_CHUNK:
       case 'B':
       {
           _isLastChunk = tag == 'B';
           _chunkLength = (read() << 8) + read();

           int data;
           ByteArrayOutputStream bos = new ByteArrayOutputStream();

           while ((data = parseByte()) >= 0)
               bos.write(data);

           return bos.toByteArray();
       }

       case 0x20: case 0x21: case 0x22: case 0x23:
       case 0x24: case 0x25: case 0x26: case 0x27:
       case 0x28: case 0x29: case 0x2a: case 0x2b:
       case 0x2c: case 0x2d: case 0x2e: case 0x2f:
       {
           _isLastChunk = true;
           int len = tag - 0x20;
           _chunkLength = 0;

           byte []data = new byte[len];

           for (int i = 0; i < len; i++)
               data[i] = (byte) read();

           return data;
       }

       case 0x34: case 0x35: case 0x36: case 0x37:
       {
           _isLastChunk = true;
           int len = (tag - 0x34) * 256 + read();
           _chunkLength = 0;

           byte []buffer = new byte[len];

           for (int i = 0; i < len; i++) {
               buffer[i] = (byte) read();
           }

           return buffer;
       }

       case BC_LIST_VARIABLE:
       {
           // variable length list
           String type = readType();

           return findSerializerFactory().readList(this, -1, type);
       }

       case BC_LIST_VARIABLE_UNTYPED:
       {
           return findSerializerFactory().readList(this, -1, null);
       }

       case BC_LIST_FIXED:
       {
           // fixed length lists
           String type = readType();
           int length = readInt();

           Deserializer reader;
           reader = findSerializerFactory().getListDeserializer(type, null);

           return reader.readLengthList(this, length);
       }

       case BC_LIST_FIXED_UNTYPED:
       {
           // fixed length lists
           int length = readInt();

           Deserializer reader;
           reader = findSerializerFactory().getListDeserializer(null, null);

           return reader.readLengthList(this, length);
       }

       // compact fixed list
       case 0x70: case 0x71: case 0x72: case 0x73:
       case 0x74: case 0x75: case 0x76: case 0x77:
       {
           // fixed length lists
           String type = readType();
           int length = tag - 0x70;

           Deserializer reader;
           reader = findSerializerFactory().getListDeserializer(type, null);

           return reader.readLengthList(this, length);
       }

       // compact fixed untyped list
       case 0x78: case 0x79: case 0x7a: case 0x7b:
       case 0x7c: case 0x7d: case 0x7e: case 0x7f:
       {
           // fixed length lists
           int length = tag - 0x78;

           Deserializer reader;
           reader = findSerializerFactory().getListDeserializer(null, null);

           return reader.readLengthList(this, length);
       }

       case 'H':
       {
           return findSerializerFactory().readMap(this, null);
       }

       case 'M':
       {
           String type = readType();

           return findSerializerFactory().readMap(this, type);
       }

       case 'C':
       {
           readObjectDefinition(null);

           return readObject();
       }

       case 0x60: case 0x61: case 0x62: case 0x63:
       case 0x64: case 0x65: case 0x66: case 0x67:
       case 0x68: case 0x69: case 0x6a: case 0x6b:
       case 0x6c: case 0x6d: case 0x6e: case 0x6f:
       {
           int ref = tag - 0x60;

           if (_classDefs.size() <= ref)
               throw error("No classes defined at reference '"
                       + Integer.toHexString(tag) + "'");

           ObjectDefinition def = _classDefs.get(ref);
           //读取实例对象
           return readObjectInstance(null, def);
       }

       case 'O':
       {
           int ref = readInt();

           if (_classDefs.size() <= ref)
               throw error("Illegal object reference #" + ref);

           ObjectDefinition def = _classDefs.get(ref);

           return readObjectInstance(null, def);
       }

       case BC_REF:
       {
           int ref = readInt();

           return _refs.get(ref);
       }

       default:
           if (tag < 0)
               throw new EOFException("readObject: unexpected end of file");
           else
               throw error("readObject: unknown code " + codeName(tag));
   }
}

主要逻辑是先读取标记,随后依据这些标记判断其对应的数据类型。一旦确定了类型,例如,若解析到的类型为’C’,则表示该数据为对象类型,于是系统会进一步调用readObjectDefinition方法进行类定义的反解析,完成后,再通过readObject方法递归地解析后续内容。整个流程是依据数据类型进行条件判断,并执行相应的反解析操作。

readObejctDefintion方法源码如下:

/**
 * 读取类的定义
 */
private void readObjectDefinition(Class<?> cl) throws IOException {
    /**
     * 获取类型
     */
    String type = readString(); // 读取类型字符串
    int len = readInt(); // 读取属性数量

    SerializerFactory factory = findSerializerFactory(); // 找到序列化工厂对象

    // 获取对应类型的反序列化器
    Deserializer reader = factory.getObjectDeserializer(type, null);
    // 创建属性数组,用于存储反序列化后的属性值
    Object[] fields = reader.createFields(len);
    // 创建属性名称数组
    String[] fieldNames = new String[len];

    // 依次读取每个属性的名称和值
    for (int i = 0; i < len; i++) {
        String name = readString(); // 读取属性名称
        fields[i] = reader.createField(name); // 根据属性名称创建对应的属性值对象
        fieldNames[i] = name; // 将属性名称存储到数组中
    }
    // 根据读取到的类型、反序列化器、属性值和属性名称,构造ObjectDefinition对象
    ObjectDefinition def = new ObjectDefinition(type, reader, fields, fieldNames);
    _classDefs.add(def); // 将构造好的ObjectDefinition对象添加到_classDefs列表中
}

主要功能是读取并反序列化一个类的定义。它首先读取类型和属性的数量,然后获取相应的反序列化器,并创建用于存储属性和属性名称的数组。接下来,它循环读取每个属性的名称,并使用反序列化器为每个属性创建对应的属性值对象。最后,它使用读取到的信息构造一个ObjectDefinition对象,并将其添加到一个列表中。

接着会执行到readObjectInstance方法,源码如下:

/**
 * 根据给定的类和对象定义反序列化一个对象实例。
 *
 * @param cl       要反序列化对象的类
 * @param def      对象定义,包含反序列化所需的信息
 * @return         反序列化后的对象实例
 * @throws IOException 如果在反序列化过程中发生I/O错误
 */
private Object readObjectInstance(Class<?> cl, ObjectDefinition def) throws IOException {
    // 从对象定义中获取对象的类型
    String type = def.getType();

    // 从对象定义中获取反序列化器
    Deserializer reader = def.getReader();

    // 从对象定义中获取字段信息(可能是字段的值或者其他相关信息)
    Object[] fields = def.getFields();

    // 查找序列化工厂,该工厂可能提供特定的反序列化器
    SerializerFactory factory = findSerializerFactory();

    // 检查给定的类与反序列化器期望的类是否匹配
    if (cl != reader.getType() && cl != null) {
        // 如果不匹配且给定的类不为null,则从工厂中获取一个新的反序列化器
        reader = factory.getObjectDeserializer(type, cl);

        // 使用新的反序列化器执行反序列化操作,并传入字段名数组以获取正确的字段顺序
        return reader.readObject(this, def.getFieldNames());
    } else {
        // 如果匹配或给定的类为null,则直接使用当前的反序列化器执行反序列化操作,并传入字段信息数组
        return reader.readObject(this, fields);
    }
}

首先获取对象定义中的类型、反序列化器和字段信息。然后,它查找一个序列化工厂,该工厂可能用于提供特定类型的反序列化器。接下来,方法检查给定的类是否与反序列化器期望的类匹配。如果不匹配(且给定的类不为null),则从工厂中获取一个新的反序列化器,并使用它来执行反序列化操作。如果匹配或给定的类为null,则直接使用当前的反序列化器执行反序列化操作。最后,方法返回反序列化后的对象实例。

然后执行反序列化器的readObejct方法,执行的是UnsafeDeserializer的readObject方法,源码如下:

/**
 * 读取并反序列化一个对象。
 *
 * @param in         Hessian输入流,用于读取对象数据
 * @param fieldNames 字段名数组,指定要反序列化的字段顺序
 * @return           反序列化后的对象
 * @throws IOException 如果在反序列化过程中发生I/O错误或其他异常
 */
public Object readObject(AbstractHessianInput in, String[] fieldNames) throws IOException {
    try {
        // 通过Unsafe或其他机制实例化对象,不调用构造函数
        Object obj = instantiate();
        
        // 给对象的所有属性赋值,使用Hessian输入流和字段名数组
        return readObject(in, obj, fieldNames);
    } catch (IOException e) {
        // 如果是I/O异常,直接抛出
        throw e;
    } catch (RuntimeException e) {
        // 如果是运行时异常,直接抛出
        throw e;
    } catch (Exception e) {
        // 如果是其他类型的异常,封装为IOExceptionWrapper并抛出
        // 这里_type可能是当前反序列化对象的类型,用于提供更详细的错误信息
        throw new IOExceptionWrapper(_type.getName() + ":" + e.getMessage(), e);
    }
}

解析:

1、调用了Unsafe的allocationInstance构建了一个对象

protected Object instantiate()
  throws Exception {
  return _unsafe.allocateInstance(_type);
}

2、给该对象的所有属性进行赋值,源码如下:

/**
 * 读取Hessian输入流中的数据,并反序列化到指定的对象实例中,为对象的字段赋值。
 *
 * @param in        Hessian输入流,提供反序列化所需的数据
 * @param obj       已存在的对象实例,将对其字段进行赋值
 * @param fieldNames 需要赋值的字段名数组
 * @return          完成字段赋值后的对象,可能是经过resolve处理的新对象
 * @throws IOException 如果在反序列化过程中发生I/O错误或其他异常
 */
public Object readObject(AbstractHessianInput in, Object obj, String[] fieldNames) throws IOException {
    try {
        // 在输入流中添加对当前对象的引用,返回引用ID
        int ref = in.addRef(obj);

        // 遍历所有需要赋值的字段
        for (String fieldName : fieldNames) {
            // 从字段映射中获取对应字段名的反序列化器
            FieldDeserializer reader = _fieldMap.get(fieldName);
            if (reader != null) {
                // 如果找到了反序列化器,则使用该反序列化器从输入流中读取数据,并为对象的字段赋值
                reader.deserialize(in, obj);
            } else {
                // 如果没有找到对应的反序列化器,则直接从输入流中读取并丢弃该字段的数据
                // 这可能是为了兼容旧版本的数据格式,或者是处理不需要的字段
                in.readObject();
            }
        }

        // 对对象进行可能的resolve处理,例如处理代理对象或进行其他后处理
        Object resolve = resolve(in, obj);

        // 如果resolve处理返回了新的对象,则在输入流中更新对该对象的引用
        if (obj != resolve) {
            in.setRef(ref, resolve);
        }

        // 返回完成字段赋值(和可能的resolve处理)后的对象
        return resolve;
    } catch (IOException e) {
        // 如果是I/O异常,直接抛出
        throw e;
    } catch (Exception e) {
        // 如果是其他类型的异常,封装为IOExceptionWrapper并抛出,同时提供发生异常的对象的类型信息
        throw new IOExceptionWrapper(obj.getClass().getName() + ":" + e, e);
    }
}

其中FieldDeserializer有很多的子类,不同的类型有不同的反序列化方式,比如字符串属性,赋值的逻辑如下:

void deserialize(AbstractHessianInput in, Object obj)
   throws IOException {
   String value = null;
   
   try {
     value = in.readString();

     _unsafe.putObject(obj, _offset, value);
   } catch (Exception e) {
     logDeserializeError(_field, obj, value, e);
   }
 }

先是读取一个字符串的值,然后还是调用UnSafe的putObejct方法进行赋值给此对象。

如果属性也是对象类型,那么就递归执行直到所有的属性都为基本数据类型并解析成功为止。

打个号外

本人新搞的个人项目,有意者可到 DDD用户中台 这里购买

可以学习到的体系

  • 项目完全从0到1开始架构,包含前端,后端,架构,服务器,技术管理相关运维知识!

    • 最佳包名设计,项目分层
  • 破冰CRUD,手撕中间件!

    • 基于MybatisPlus封装属于自己的DDD ORM框架

    • 基于Easyexcel封装属于自己的导入导出组件

    • oss对象存储脚手架(阿里云,minio,腾讯云,七牛云等)

    • 邮件脚手架

    • completefuture脚手架

    • redis脚手架

    • xxl-job脚手架

    • 短信脚手架

    • 常用工具类等

  • 传统MVC代码架构弊端的解决方案

    • DDD+CQRS+ES最难架构
  • 结合实际代码的业务场景

    • 多租户单点登录中心

    • 用户中台

    • 消息中心

    • 配置中心

    • 监控设计

  • 程序员的职业规划,人生规划

    • 打工永远没有出路!

    • 打破程序员的35岁魔咒

    • 技术带给你的优势和竞争力【启发】

    • 万物互联网的淘金之路!

技术以外的赚钱路子

可以一起沟通

具体的文章目录

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

购买链接

DDD用户中台

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2180460.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

算法分析——《二分查找》

&#x1f6e9;《二分查找》 &#x1f3a8;题目描述&#xff1a; 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 …

厦门大龄自闭症寄宿学校:为孩子们提供全方位关怀和教育

在自闭症儿童的教育与关怀领域&#xff0c;厦门大龄自闭症寄宿学校以其专业性和全面性&#xff0c;为众多家庭提供了宝贵的支持与帮助。而在中国南方的广州&#xff0c;也有一所同样致力于自闭症儿童全方位关怀与教育的机构——星贝育园自闭症儿童寄宿制学校。星贝育园以其独特…

为什么要自定义异常

背景 我们在阅读各类源码时&#xff0c;会注意到几乎各个功能包中&#xff0c;都包含有自定义的异常&#xff1b;那有没有想过&#xff0c;JDK的源码中&#xff0c;已经定义了各种异常体系&#xff0c;为啥后续的一些依赖Jar包中&#xff0c;还要自定义异常呢&#xff1f; 下面…

Java应用程序的服务器有哪些?

1.Tomcat、Jetty 和 JBoss 区别&#xff1f; Apache Tomcat、Jetty 和 JBoss都是用于部署Java应用程序的服务器&#xff0c;它们都支持Servlet、JSP和其他Java EE&#xff08;现在称为Jakarta EE&#xff09;技术。尽管它们有一些相似的功能&#xff0c;但它们之间还是存在一些…

二叉树相关oj题(Java)

一. 检查两颗树是否相同。OJ链接 这里我们考虑两种情况: 1.结构上 2.节点值上 当上面两种情况同时遍历时: 1.如果两颗树的节点都不为空,就判断值 2.如果两棵树种一棵树的节点为空另一棵树的节点不为空,则这两颗肯定不是相同的树 整体来看:要判断两棵树是否相同,得判断根,然后判…

SQLite数据库迁移与备份技术详解

目录 引言 SQLite数据库迁移 迁移概述 迁移步骤 1. 创建目标数据库系统 2. 导出SQLite数据库数据 3. 导入数据到目标数据库 4. 验证数据迁移 迁移注意事项 SQLite数据库定期备份 备份的重要性 备份方法 1. 使用VACUUM命令 2. 使用ATTACH DATABASE和PRAGMA语句 3.…

这些211热度不高,毕业年薪20-40万!自动化考研择校

这些211学校&#xff0c;今年热度不高&#xff0c;就业还意外的好&#xff0c;一定不要错过&#xff01;搭配历年数据&#xff0c;供大家参考~ 目录 ① 华东理工大学 ② 东华大学 ③ 江南大学 ④ 安徽大学 ① 华东理工大学 复试线招生人数 控制学科等级为B&#xff0c;上…

Qt QIntValidator详解

一、介绍 QIntValidator是Qt框架中用于验证整数输入的验证器类。它可以限制用户输入的整数范围&#xff0c;确保输入的整数在指定的范围内。通过QIntValidator&#xff0c;可以轻松地实现整数输入的有效性和范围限制。 二、 常用方法 QIntValidator(QObject *parent Q_NULLPT…

Python编码系列—Python设计模式的选择与权衡:打造高效代码架构

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

SigmaStudio中部分滤波器算法有效性频谱分析

一、各类滤波器参数如下图设置 1.1、输入源白噪音经过如下算法处理后Notch\Band Pass\Band Stop&#xff0c;如下频谱分析图 1.2、输入源白噪音经过low pass后处理前后的频谱分析如如下 二、Notch滤波器配置图&#xff0c;如下 2.1、两串联、五个串联和未串联的Notch对白噪音…

券商股大涨,至少17家券商已入局AI人工智能金融大模型

大家好&#xff0c;我是Shelly&#xff0c;一个专注于输出AI工具和科技前沿内容的AI应用教练&#xff0c;体验过300款以上的AI应用工具。关注科技及大模型领域对社会的影响10年。关注我一起驾驭AI工具&#xff0c;拥抱AI时代的到来。 最近&#xff0c;券商股价的大涨成为了财经…

鲜花配送小程序开发制作方案

鲜花配送小程序系统通过用户端、商家端和配送员端的协同工作&#xff0c;确保鲜花能够在指定时间内送达&#xff0c;提升用户满意度和忠诚度。 目标用户 个人消费者&#xff1a;生日、纪念日、节日等特殊场合送花的人群。 企业客户&#xff1a;需要定期为办公场所或活动提供花…

低至1元/小时:国庆七天,30元通关《黑神话:悟空》!

目录 前言 一、云游戏解决方案的引入 二、服务的核心亮点及性价比 1、高清画质体验 2、广泛的手柄支持 3、隐私与安全性 4、直播推流优势 5、游戏快速启动 6、价格优惠 &#xff08;1&#xff09;新用户专享&#xff0c;低至1元/时&#xff01; &#xff08;2&#…

利用C++优化Chrome浏览器的启动速度

Chrome浏览器是全球使用最广泛的网络浏览器之一&#xff0c;但有时候它的启动速度可能会让人感到不满。为了解决这个问题&#xff0c;我们可以使用C进行一些优化。本文将介绍如何通过关闭预加载功能、设置允许弹出窗口和为网页添加快捷方式来加快Chrome浏览器的启动速度。 &am…

谨防火灾!电瓶车检测算法助力城市/小区/园区多场景安全管理精细化、智能化

随着人工智能技术的快速发展&#xff0c;AI智能分析网关V4在电瓶车检测领域的应用日益广泛。这一技术通过深度学习、计算机视觉等先进算法&#xff0c;实现了对电瓶车及其相关行为的智能识别和分析&#xff0c;为电瓶车的管理和应用提供了强大的技术支持。 一、电瓶车检测算法…

python为姓名注音实战案例

有如下数据&#xff0c;需要对名字注音。 数据样例&#xff1a;&#x1f447; 一、实现过程 前提条件&#xff1a;由于会用到pypinyin库&#xff0c;所以一定得提前安装。 pip install pypinyin1、详细代码&#xff1a; from pypinyin import pinyin, Style# 输入数据 names…

Unreal 对象、属性同步流程

文章目录 类型同步初始化创建 FObjectReplicator创建 FRepLayout、Cmd、ShadowOffset创建 FRepChangedPropertyTracker、FRepState创建 FReplicationChangelistMgr、FRepChangelistState、ShadowBuffer 属性同步属性变化检测查找变化属性&#xff0c;写入ShadowMemory发送数据 …

如何在Code::Blocks中集成EasyX库?

EasyX库是一个轻量级的C图形库&#xff0c;专为Windows平台设计&#xff0c;适用于图形编程初学者&#xff0c;特别是少儿编程或编程启蒙阶段。使用EasyX库&#xff0c;用户可以在C环境下快速绘制简单的图形、动画等视觉效果。本文将详细介绍如何在Code::Blocks中集成EasyX库&a…

Python兼职接单,30天收益近16.5K,分享详细方法!

写在开篇 随着大数据和人工智能的兴起。各行业对爬虫类和数分类服务的需求量呈指数级的暴增。传统行业受经济下行的影响&#xff0c;近段时间失业找不到工作的朋友&#xff0c;后台咨询技术变现、兼职接单问题也越来越多。 当然也有网友向我发来喜报&#xff0c;告诉我他上半年…

JVM 垃圾回收算法细节

目录 前言 GC Root 可达性分析 根节点枚举 安全点 安全区域 记忆集与卡表 写屏障 并行的可达性分析 前言 学习了几种垃圾收集算法之后&#xff0c; 我们再来看看它们在具体实现上有什么细节之处&#xff0c;我们所能看到的理论很简单&#xff0c;但是实现起来那…