在jackson中@JsonAnySetter实现不固定属性序列化和多层级属性序列化实战

举报
夜郎king 发表于 2025/08/19 09:02:59 2025/08/19
【摘要】 本文即详细介绍Jackson中@JsonAnySetter注解的使用来实现不固定属性列的序列化知识,通过讲解单一层级的JSON数据处理和多层级的嵌套JSON数据处理,让大家不仅能处理单层动态数据,也能处理多层动态数据。

目录


前言

一、需求场景介绍

1、相关场景简介

2、实际案例说明

3、关于Jackson和@JsonAnySetter

二、单层JSON序列化

1、实体类定义

2、序列化支持

3、结果过程

三、多层嵌套序列化

1、可能存在的问题

2、相关实体类定义

3、序列化支持

4、结果输出

四、总结



前言

        在当今数字化飞速发展的时代,数据的交互与处理已成为软件开发中至关重要的环节。而 Jackson 作为一款功能强大、广泛应用于 Java 领域的 JSON 处理库,在数据序列化与反序列化过程中发挥着关键作用。


1.jpg

在实际开发场景中,我们常常会遇到各种复杂多变的数据结构。一方面,可能会遇到一些具有不固定属性的对象。这些对象的属性数量和名称并不是在编译时就确定好的,而是可能随着不同的业务场景、数据来源或用户操作而动态变化。另一方面,多层级属性的序列化也是一个常见的痛点。很多数据结构并不是扁平的,而是具有父 - 子 - 孙子等多层级嵌套关系。例如,一个复杂的业务对象可能包含多个子对象,每个子对象又包含一系列的属性和子对象,如此层层嵌套下去。在将这种多层级的数据结构进行序列化转化为 JSON 格式,或者从 JSON 反序列化回 Java 对象时,很容易出现数据丢失格式、错误或者无法正确映射的问题,尤其是在层级较多或层级结构比较复杂的情况下。为了解决这些问题,Jackson 提供了诸如@JsonAnySetter 这样强大的注解工具。@JsonAnySetter 注解的出现,为我们处理不固定属性和多层级属性的序列化提供了灵活而高效的解决方案。通过深入学习和掌握@JsonAnySetter 的使用方法,并结合实际的项目实战演练,不仅能大大提高我们在复杂数据交互场景下的开发效率,还能确保数据的准确性和完整性,使我们的应用程序能够更加从容地应对各种复杂多变的数据挑战,为用户带来更加稳定可靠、功能强大的软件产品和服务,这也是我们开展此次在 Jackson 中利用@JsonAnySetter 实现不固定属性序列化和多层级属性序列化实战研究的重要意义所在。

        本文即详细介绍Jackson中@JsonAnySetter注解的使用来实现不固定属性列的序列化知识,通过讲解单一层级的JSON数据处理和多层级的嵌套JSON数据处理,让大家不仅能处理单层动态数据,也能处理多层动态数据,如果大家在项目中有碰到以上的需求,不妨来这里看看。

一、需求场景介绍

        本节将重点介绍一些可能会碰到的技术使用场景,通过场景的介绍和实际案例的说明,让大家对需求的产生背景有所了解。

1、相关场景简介

        在软件开发中,序列化是指将对象转换为可以存储或传输的格式(通常是字节序列或文本格式如JSON、XML等),而反序列化则是相反的过程。在Jackson库中,@JsonAnySetter注解可以用于处理不固定属性的序列化。不固定属性序列化通常用于处理具有动态属性的对象。这些属性在编译时无法预知,可能在运行时根据业务需求动态添加。例如,处理一些第三方API返回的数据时,这些数据可能包含额外的、未提前定义的属性,其响应格式可能不够规范和稳定,有时候会包含额外的、未提前定义的属性;或者在某些个性化配置的场景下,用户可以自定义各种不同的属性来满足自身需求,这就导致了我们无法使用传统的固定属性的 Java 类来进行精确的映射和序列化、反序列化操作。

2、实际案例说明

        我们以一个模拟接口为例,通过接口API。调用客户端可以放回以下信息:

String json = "{\"name\":\"Bob\",\"age\":30,\"city\":\"Paris\",\"active\":true}";

 那么在上述的这个JSON数据中,也许我们明确的两个字段就是姓名和年龄。其它的额外信息可能会根据处理的逻辑而有所不同,那么如何将明确的属性进行自动映射,其它的属性则自动的保存到一个属性列,比如一个HashMap中呢?下面的内容也将从这个案例作为突破口来进行展开。

3、关于Jackson和@JsonAnySetter

        为了让大家对Jackson和@JsonAnySetter有一个基本的印象,这里我们对其相关知识进行一个简单的介绍。Jackson 是一款功能强大的 Java 库,广泛用于处理 JSON 数据。它提供了灵活高效的工具,用于将 Java 对象转换为 JSON 格式(序列化),以及将 JSON 数据转换回 Java 对象(反序列化)。

        主要特点:

  • 灵活的数据绑定 :Jackson 能将 JSON 数据映射到 Java 对象,支持复杂数据结构,提高开发效率。

  • 高效的性能 :采用 Streaming API 进行低层数据处理,快速解析和生成 JSON 数据,资源占用少,适合大数据量处理。

  • 丰富的注解支持 :提供注解定制序列化和反序列化行为,如@JsonProperty 注解指定 JSON 属性名,@JsonIgnore 注解忽略属性,使数据处理更灵活。

        @JsonAnySetter 注解

        @JsonAnySetter 是 Jackson 提供的注解,用于处理 JSON 对象中未映射到 Java 类字段的动态属性。当 JSON 数据包含不确定的额外属性时,Jackson 可自动将这些属性存储到指定的 Map 中,通常标注在 Map 类型的属性的 setter 方法上。

        使用场景

  • 处理第三方 API 返回数据 :实际开发中常需处理第三方 API 返回的结构不固定 JSON 数据。@JsonAnySetter 可捕获未知属性,避免解析错误,保证数据完整性和灵活性。

  • 处理用户自定义数据 :在需用户自定义属性的场景中,如用户配置文件或表单数据,用户可添加自定义属性。@JsonAnySetter 能动态接收这些属性,提升应用扩展性和用户体验。

        到这里,相信大家对Jackson的相关知识有了基本的了解,接下来将从代码示例的角度来进行相关的介绍。

二、单层JSON序列化

        单层JSON数据的处理比较简单,单层JSON顾名思义就是对于JSON结构来说,只有一层,处理之外没有其它的情况。而对于固定对象,需要一些固定的属性进行业务处理,而更多的则是辅助信息,仅需要保留到HashMap对象当中即可。关于单层JSON的样例已经在前面一节中有所提及,这里不再进行赘述。

1、实体类定义

        对于单层JSON的处理其实比较简单,如果各位朋友们是使用SpringBoot的环境后,其依赖的JackSonb版本是直接可以使用的,这个通过Maven的依赖分析来获取相关知识。其依赖关系如下所示:


1.jpg

在上图中可以明显看到,系统项目引用了SpringBoot之后,就不用额外的引入jackjson相关资源包了。正是由于有不固定属性列的情况,因此在定义实体类的时候尤其需要注意,实体类的定义方法如下:


package com.yelang.project.jsoncase;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.HashMap;
import java.util.Map;
public class JacksonUser {
	private String name;
    private int age;
    private final Map<String, Object> unknownFields = new HashMap<>();
    // 标准 getter/setter
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public int getAge() { return age; }
    public void setAge(int age) { this.age = age; }
    // 处理所有未知键
    @JsonAnySetter
    public void setUnknownField(String key, Object value) {
        unknownFields.put(key, value);
    }
    public Map<String, Object> getUnknownFields() {
        return unknownFields;
    }
}

2、序列化支持

        在完成了上述的实体类的开发之后,接下来我们需要常见一个常见的main函数来进行相关序列化的支持和实现。关键代码如下:

public static void main(String[] args) throws Exception {
    String json = "{\"name\":\"Bob\",\"age\":30,\"city\":\"Paris\",\"active\":true}";
    ObjectMapper mapper = new ObjectMapper();
    JacksonUser user = mapper.readValue(json, JacksonUser.class);
    System.out.println(user.getUnknownFields());
    System.out.println(user.getName());
    System.out.println(user.getAge());
    // 输出: {city=Paris, active=true}
    System.out.println("----------------------------------------------------");
    String s = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
    System.out.println(s);
}

3、结果过程

        在Eclipse中运行上面的代码后,可以在程序控制台中看到以下输出:

{city=Paris, active=true}
Bob
30
----------------------------------------------------
{
  "name" : "Bob",
  "age" : 30,
  "unknownFields" : {
    "city" : "Paris",
    "active" : true
  }
}

三、多层嵌套序列化

        与上面固定的单层JSON结构相比,还有一些情况下,对于接口返回的数据可能存在多层级的情况。那么针对返回的数据有多层级情况下又应该如何处理呢?对于嵌套的 JSON 子属性,Jackson 提供了多种处理方式。当使用 @JsonAnySetter 时,它仅能捕获当前层级的未知属性,无法自动处理嵌套结构的动态属性。也就是说,在遇到多层的嵌套时,如果只在实体定义时申明一个Map属性,且标记为@JsonAnySetter,那么就会在序列化时出现无法映射的情况。

1、可能存在的问题

        这里举一种例子,用于说明上面的问题。下面是实体类定义的关键代码:

package com.yelang.project.jsoncase;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
public class DynamicEntity {
	private Long id;
	private String name;
	private final Map<String, Object> properties = new HashMap<>();
	// 标准getter/setter
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	// 处理所有未知属性(包括嵌套对象)
	@JsonAnySetter
	public void addProperty(String key, Object value) {
		// 如果值是Map类型(表示嵌套对象),则转换为DynamicEntity
		if (value instanceof Map) {
			DynamicEntity nestedEntity = new DynamicEntity();
			Map<?, ?> mapValue = (Map<?, ?>) value;
			mapValue.forEach((k, v) -> {
				if (k instanceof String) {
					nestedEntity.addProperty((String) k, v);
				}
			});
			properties.put(key, nestedEntity);
		} else {
			properties.put(key, value);
		}
	}
	// 序列化时将所有属性作为顶级字段
	@JsonAnyGetter
	public Map<String, Object> getProperties() {
		return properties;
	}
}

使用上述的对象定义来创建对象以及生成序列化信息的关键的代码如下:

package com.yelang.project.jsoncase;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
 * - 这种方案不太好,会有不该有的字段出现
 * @author Administrator
 */
public class DynamicNestedProperties {
	public static void main(String[] args) throws Exception {
        String json = "{\n" +
                "  \"id\": 123,\n" +
                "  \"name\": \"Dynamic Object\",\n" +
                "  \"attributes\": {\n" +
                "    \"color\": \"blue\",\n" +
                "    \"size\": \"large\",\n" +
                "    \"dimensions\": {\n" +
                "      \"width\": 100,\n" +
                "      \"height\": 200,\n" +
                "      \"depth\": {\n" +
                "        \"front\": 50,\n" +
                "        \"back\": 60\n" +
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"metadata\": {\n" +
                "    \"createdAt\": \"2023-01-01\",\n" +
                "    \"tags\": [\"tag1\", \"tag2\"],\n" +
                "    \"details\": {\n" +
                "      \"priority\": \"high\",\n" +
                "      \"notes\": \"Important object\"\n" +
                "    }\n" +
                "  }\n" +
                "}";
        ObjectMapper mapper = new ObjectMapper();
        DynamicEntity entity = mapper.readValue(json, DynamicEntity.class);
        System.out.println("基本属性:");
        System.out.println("ID: " + entity.getId());
        System.out.println("Name: " + entity.getName());
        System.out.println("\n动态属性:");
        entity.getProperties().forEach((key, value) -> {
            System.out.println(key + " = " + 
                (value instanceof DynamicEntity ? "[嵌套对象]" : value));
        });
        System.out.println("\n嵌套属性访问:");
        // 访问多层嵌套属性
        DynamicEntity attributes = (DynamicEntity) entity.getProperties().get("attributes");
        DynamicEntity dimensions = (DynamicEntity) attributes.getProperties().get("dimensions");
        DynamicEntity depth = (DynamicEntity) dimensions.getProperties().get("depth");
        System.out.println("Color: " + attributes.getProperties().get("color"));
        System.out.println("Width: " + dimensions.getProperties().get("width"));
        System.out.println("Front depth: " + depth.getProperties().get("front"));
        System.out.println("\n完整JSON结构:");
   System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entity));
    }
}

 运行之后你会发现以下输出:


1.jpg

相信细心的你一定发现了,在嵌套的层级中,id和name也是重复了,同时每个层级似乎都有这种情况出现。 

2、相关实体类定义

        那么如何来避免出现这种问题呢?这里分享一种实例的创建办法,是一种多层嵌套动态属性的通用方案。需要一个能够处理任意嵌套层级的动态属性的类。我们将使用递归的方式:当发现某个属性的值是一个Map(表示嵌套对象)时,将其转换为另一个动态实体对象(DynamicEntity)。这样,每个嵌套层级都可以有自己的动态属性。实现思路如下:

设计思路:

1. 使用一个Map来存储所有动态属性。

2. 在`@JsonAnySetter`方法中,当接收到的value是一个Map时,我们将其转换为DynamicEntity对象(该对象同样具有处理动态属性的能力,包括嵌套)。

3. 这样,嵌套的Map会被递归地转换为DynamicEntity,从而形成树形结构。

注意:Jackson在反序列化时,对于JSON对象,会将其转换为`LinkedHashMap`。因此,我们需要检查value是否为`Map`类型。我们将创建DynamicEntity类,它包含:一个Map<String, Object> properties,用于存储属性。 一个`@JsonAnySetter`方法,用于添加属性,并在遇到Map类型的值时进行转换。同时,为了能够正确序列化,我们还需要一个`@JsonAnyGetter`方法,将properties作为JSON的属性源。但是注意:在反序列化过程中,如果遇到嵌套对象,Jackson默认会将其转为Map。因此,我们需要在setter中递归处理这些Map,将它们也转为DynamicEntity,以便保持结构一致。

3、序列化支持

        支持多层嵌套的序列化关键代码如下,这里使用内部类的方式类创建,大家可以根据需要将类抽取出来设计成独立的类也是可以的。

package com.yelang.project.jsoncase;

import com.fasterxml.jackson.annotation.*;
import com.fasterxml.jackson.databind.*;
import java.util.*;

public class UniversalDynamicNestedProperties {

    public static void main(String[] args) throws Exception {
        // 测试 JSON 包含不同类型字段
        String json = "{\n" +
                "  \"id\": 123,\n" +
                "  \"name\": \"Dynamic Object\",\n" +
                "  \"timestamp\": \"2023-01-01T12:00:00Z\",\n" +
                "  \"attributes\": {\n" +
                "    \"color\": \"blue\",\n" +
                "    \"size\": \"large\",\n" +
                "    \"dimensions\": {\n" +
                "      \"width\": 100,\n" +
                "      \"height\": 200,\n" +
                "      \"depth\": {\n" +
                "        \"front\": 50,\n" +
                "        \"back\": 60,\n" +
                "        \"material\": \"wood\"\n" +  // 嵌套对象中的额外字段
                "      }\n" +
                "    }\n" +
                "  },\n" +
                "  \"metadata\": {\n" +
                "    \"createdAt\": \"2023-01-01\",\n" +
                "    \"tags\": [\"tag1\", \"tag2\"],\n" +
                "    \"details\": {\n" +
                "      \"priority\": \"high\",\n" +
                "      \"notes\": \"Important object\"\n" +
                "    }\n" +
                "  },\n" +
                "  \"customFields\": {\n" +  // 根对象中的额外字段
                "    \"field1\": \"value1\",\n" +
                "    \"field2\": 42\n" +
                "  }\n" +
                "}";

        ObjectMapper mapper = new ObjectMapper();
        DynamicEntity entity = mapper.readValue(json, DynamicEntity.class);
        
        System.out.println("=== 序列化输出 ===");
        System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(entity));
        
        System.out.println("\n=== 动态访问示例 ===");
        System.out.println("Timestamp: " + entity.getProperty("timestamp"));
        System.out.println("Custom field1: " + entity.getProperty("customFields.field1"));
        System.out.println("metadata: " + entity.getProperty("customFields.field1"));
    
       // DynamicNode node = entity.getNode("attributes");
       // System.out.println(node);
        System.out.println(entity.getDynamicProperties());
        Map<String,Object> map = entity.getDynamicProperties();
        // 遍历键集合
        for (String key : map.keySet()) {
            // 根据键获取值
            //String value = map.get(key);
        	Object value = map.get(key);
            System.out.println("Key: " + key + ", Value: " + value);
        }
        System.out.println("dfdfd===>"+entity.getProperty("customFields.field1", String.class));
        System.out.println(entity.getPath("customFields.field1"));
        System.out.println(entity.getPath("attributes.dimensions.depth.material"));
        System.out.println(entity.getPath("attributes.dimensions.depth.back"));
        System.out.println("******************************************");
        System.out.println("类访问:");
        System.out.println(entity.getId());
        System.out.println(entity.getName());
        System.out.println(entity.getTimestamp());
    }

    // 动态节点基类
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static abstract class DynamicNode {
        protected final Map<String, Object> dynamicProperties = new HashMap<>();
        private int depth = 0;
        private static final int MAX_DEPTH = 20;

        @JsonAnySetter
        public void addDynamicProperty(String key, Object value) {
            addDynamicProperty(key, value, depth);
        }

        private void addDynamicProperty(String key, Object value, int currentDepth) {
            if (currentDepth > MAX_DEPTH) {
                dynamicProperties.put(key, value);
                return;
            }
            
            if (value instanceof Map) {
                DynamicNode nestedNode = createChildNode();
                nestedNode.depth = currentDepth + 1; // 增加深度
                
                Map<?, ?> mapValue = (Map<?, ?>) value;
                mapValue.forEach((k, v) -> {
                    if (k instanceof String) {
                        nestedNode.addDynamicProperty((String) k, v, currentDepth + 1);
                    }
                });
                
                dynamicProperties.put(key, nestedNode);
            } 
            else if (value instanceof List) {
                List<?> list = (List<?>) value;
                List<Object> processedList = new ArrayList<>();
                
                for (Object item : list) {
                    if (item instanceof Map) {
                        DynamicNode nestedNode = createChildNode();
                        nestedNode.depth = currentDepth + 1;
                        
                        ((Map<?, ?>) item).forEach((k, v) -> {
                            if (k instanceof String) {
                                nestedNode.addDynamicProperty((String) k, v, currentDepth + 1);
                            }
                        });
                        
                        processedList.add(nestedNode);
                    } else {
                        processedList.add(item);
                    }
                }
                
                dynamicProperties.put(key, processedList);
            } 
            else {
                dynamicProperties.put(key, value);
            }
        }

        @JsonAnyGetter
        public Map<String, Object> getDynamicProperties() {
            return dynamicProperties;
        }
        
        // 工厂方法创建子节点
        protected abstract DynamicNode createChildNode();
        
        // 类型安全的属性访问
        public Object getProperty(String key) {
            return dynamicProperties.get(key);
        }
        
        @SuppressWarnings("unchecked")
        public <T> T getProperty(String key, Class<T> type) {
            Object value = dynamicProperties.get(key);
            return (value != null && type.isInstance(value)) ? type.cast(value) : null;
        }
        
        // 获取嵌套节点
        public DynamicNode getNode(String key) {
            Object value = dynamicProperties.get(key);
            return (value instanceof DynamicNode) ? (DynamicNode) value : null;
        }
        
        // 路径访问
        public Object getPath(String path) {
            String[] parts = path.split("\\.");
            Object current = this;
            
            for (String part : parts) {
                if (current instanceof DynamicNode) {
                    current = ((DynamicNode) current).getProperty(part);
                } else if (current instanceof Map) {
                    current = ((Map<?, ?>) current).get(part);
                } else {
                    return null;
                }
                
                if (current == null) return null;
            }
            
            return current;
        }
    }

    // 根对象类 - 包含特定字段
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static class DynamicEntity extends DynamicNode {
        // 根对象特定字段
        private Long id;
        private String name;
        
        // 可以添加任意数量的其他字段
        private String timestamp;
        private Boolean active;
        
        // 根对象的字段访问器
        public Long getId() { return id; }
        public void setId(Long id) { this.id = id; }
        
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        
        public String getTimestamp() { return timestamp; }
        public void setTimestamp(String timestamp) { this.timestamp = timestamp; }
        
        public Boolean getActive() { return active; }
        public void setActive(Boolean active) { this.active = active; }
        
        // 确保根对象字段不会被当作动态属性
        @JsonIgnore
        @Override
        public Map<String, Object> getDynamicProperties() {
            return super.getDynamicProperties();
        }
        
        // 序列化时包含根对象字段
        @Override
        protected DynamicNode createChildNode() {
            return new GenericDynamicNode();
        }
    }

    // 通用嵌套节点 - 没有预定义字段
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public static class GenericDynamicNode extends DynamicNode {
        @Override
        protected DynamicNode createChildNode() {
            return new GenericDynamicNode();
        }
    }
}

4、结果输出

        在IDE编辑器中运行上述程序可以看到以下输出,说明成功的进行了属性的设置。能可以实现动态多层级的属性读取,满足我们的设计需求。

1.jpg

在读取属性数据时,还设置了一个按照路径的读取方法,实现方法如下:

// 路径访问
public Object getPath(String path) {
    String[] parts = path.split("\\.");
    Object current = this;
    for (String part : parts) {
         if (current instanceof DynamicNode) {
              current = ((DynamicNode) current).getProperty(part);
         } else if (current instanceof Map) {
              current = ((Map<?, ?>) current).get(part);
         } else {
              return null;
         }
                
         if (current == null) return null;
     }    
     return current;
   }
}

主要是为了从不同的层级的Map中去获取对应的key对应的value值,并最终返回给调用方。 

四、总结

        以上就是本文的主要内容,本文即详细介绍Jackson中@JsonAnySetter注解的使用来实现不固定属性列的序列化知识,通过讲解单一层级的JSON数据处理和多层级的嵌套JSON数据处理,让大家不仅能处理单层动态数据,也能处理多层动态数据。为了解决这些问题,Jackson 提供了诸如@JsonAnySetter 这样强大的注解工具。@JsonAnySetter 注解的出现,为我们处理不固定属性和多层级属性的序列化提供了灵活而高效的解决方案。通过深入学习和掌握@JsonAnySetter 的使用方法,并结合实际的项目实战演练,不仅能大大提高我们在复杂数据交互场景下的开发效率,还能确保数据的准确性和完整性,使我们的应用程序能够更加从容地应对各种复杂多变的数据挑战,为用户带来更加稳定可靠、功能强大的软件产品和服务,这也是我们开展此次在 Jackson 中利用@JsonAnySetter 实现不固定属性序列化和多层级属性序列化实战研究的重要意义所在。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。