Spring mongo 只写字段

Spring mongo write only field

我有一个描述 mongo 文档的摘要 class。这个 class 可能有不同的实现需要覆盖抽象方法。这是一个简化的例子:

@Document
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
public abstract class Entity {

    @Id
    private ObjectId id;

    abstract String getSomething();
}

我希望 getSomething() 作为字符串字段写入文档。但是我不想读回去

我尝试使用 @AccessType 注释:

@AccessType(AccessType.Type.PROPERTY)
abstract String getSomething();

但是当我从数据库中读取此文档时,spring 抛出 UnsupportedOperationException: No accessor to set property。它试图为这个字段找到一个 setter,但我不想为它定义一个 setter - 该方法可能 return 一个计算值并且应该没有能力更改。尽管空 setter 可能有所帮助,但它看起来更像是一种解决方法,我会尽量避免它。

所以我想知道是否有办法在从数据库读取时跳过这个特定的 属性?与 @Transient 注释相反的东西。或者类似于 Jackson 库中的 @JsonIgnore

您可以尝试添加 @Transient 属性 以便在映射字段时忽略它。 参考this问题。

另一方面,如果 属性 只是在阅读时被忽略,那么您可以指定一个空的 setter 方法。这将避免 exception of no accessor method.

或者创建自定义注解,将所有需要空 setter 方法的属性放在单独的 class 中,并使用新注解对 class 进行注解。这个 class 然后可以在需要的地方扩展。您可以查看 lombok 项目,了解他们如何为 getter 和 setter 方法创建注释。

您遇到的问题是方法名称中包含单词 "get"。

通过 "get" 这个词,Spring 认为有一个名为 "something" 的属性,并以此检查它是否具有相应的 getter 和 setter方法。

将您的方法从 "getSomething" 重命名为 "calculatedInformation"。

令人惊讶的是,没有简单的解决方案可以使字段只写。

虽然创建一个空 setter 可以解决问题,但我觉得它破坏了 least surprise principle:如果你有一个 setter,你希望它设置一个值。

所以我决定创建自己的 @WriteOnly 注释并用它来忽略我不想从数据库中读取的字段:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD})
public @interface WriteOnly {
}

要使用它,您需要扩展 AbstractMongoEventListener:

@Service
public class WriteOnlyListener extends AbstractMongoEventListener<Entity> {

    @Override
    public void onAfterLoad(AfterLoadEvent<Entity> event) {
        Document doc = event.getDocument();
        if (doc == null) return;
        for (Field field: getWriteOnly(event.getType(), Class::getDeclaredFields)) {
            doc.remove(field.getName());
        }
        for (Method method: getWriteOnly(event.getType(), Class::getDeclaredMethods)) {
            doc.remove(getFieldName(method.getName()));
        }
    }

    private <T extends AccessibleObject> List<T> getWriteOnly(Class<?> type, 
                                                              Function<Class<?>, T[]> extractor) {
        List<T> list = new ArrayList<>();
        for (Class<?> c = type; c != null; c = c.getSuperclass()) {
            list.addAll(Arrays.stream(extractor.apply(c))
                    .filter(o -> o.isAnnotationPresent(WriteOnly.class))
                    .collect(Collectors.toList()));
        }
        return list;
    }

    private static String getFieldName(String methodName) {
        return Introspector.decapitalize(methodName.substring(methodName.startsWith("is") ? 2 : 
                    methodName.startsWith("get") ? 3 : 0));
    }

}