如何一个VO实体在多个接口中返回的字段不一样呢?

发布于 2022-09-12 22:05:32 字数 600 浏览 20 评论 0

假设现在有一张user表,包含id、name、pwd、area四个字段,有两个接口:

  • 用户列表
  • 用户详情
  1. 在用户列表中,只展示\`id\`/\`name\`/\`area\`三个字段;
  2. 在详情中,展示全部字段。

环境:springboot项目。

现在我配置了\`spring.jackson.default-property-inclusion=NON\_NULL\`。

P1:在列表接口中,一旦\`area\`字段为空,返回的字段就参差不齐,有的返回2个字段,有的返回三个字段,请问如何解决这样的问题?

P2:在详情接口中,也会出现这样的问题,当\`area\`为\`null\`时,详情返回3个字段;否则返回4个字段。


Q:请问如何让接口返回固定的字段,例如,列表接口中,固定返回3个字段,即使\`area\`为空,也要保留该字段。同理,详情接口中,固定返回4个字段,即使有字段为空也要保留该字段。

问题出现的环境背景及自己尝试过哪些方法

我尝试改配置\`spring.jackson.default-property-inclusion\`,
但是改了好多个都没有达到我的要求。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

乱了心跳 2022-09-19 22:05:32

起初第一眼看到题主的问题,我是有点匪夷所思的。。。主要是下面这段话
image.png

写接口,固定字段返回,看起来都很正常,那解决方案不就是

  1. 列表接口返回对象UserListVO,里面有3个字段
  2. 详情接口返回对象UserDetailVO,里面有4个字段

这跟jackson配置null是否返回,毫无关系啊,jackson只是一个序列化工具,但是不同接口返回不同字段这是业务逻辑,就需要创建类才可以解决啊。

创建两个类真的就是很难了么。。。那为啥非要只用一个UserVO对象来表示两个不同接口的返回呢?假如以后新增还有其他和User相关的接口,是不是要把再在UserVO类中塞满一堆字段,然后不同接口返回不同个数的字段呢,这样索性不要UserVO得了,直接实体对象User返回算了嘛。

诚然,就算是按照myskies的方式做了分组,这是一种解决题主问题的方案,但是但是,如果是实际公司业务开发,我想没有哪个兄弟愿意接手在一个大而全的VO对象去找他们需要的分组叭。。。起码要是我,我是要哭的,字段少还好,字段多了。。。@JsonView中各种取值是要看花了的哈

不过吐槽归吐槽,上面只是我作为同为程序员角色,站在同一角度提出的小意见,但是如果作为思否中问题和回答问题的角色,也就是相当于甲方乙方的角度来说的话,我还是要来想办法解决题主的问题
image

首先还是先捋一下当前的思路,以及需要解决的问题

假设现在就是UserService中的两个方法返回

public interface UserService {
    // 希望返回id,name,area
    List<UserVO> list();
    // 希望返回id,name,area,pwd
    UserVO detail();
}

虽然是两个方法,但是都是返回的同一个关联类UserVO的对象,然而又需要不同字段的返回结果。同一个类,但是不同方法返回不同的字段。你细品。。。不对啊,这不可能啊,字段是跟Class相关的,而Class本身就是独一份,咋能说运行时变来变去呢,毕竟UserVO的对象可是需要Class来实例化的啊

所以如果按照上面那种写法是根本不可能的,所以必须是list()detail()要返回不同的类,但是之前已经感受到了甲方的需求了,就是不想多创建两个类,那咋办?

当然就是回到Map啦,万能Map,毕竟Map的数据结构跟POJO的对象很类似嘛,都是键值对。
因此UserService就变成了

public interface UserService {
    // 希望返回id,name,area
    List<Map> list();
    // 希望返回id,name,area,pwd
    Map detail();
}

啊~多么朴素的写法啊(谁在我小组这么写,我就要杀人了啊!!)

那再看看实现,都这么返回Map,那还不无法无天,想怎么写就怎么写咯,当然这里用到了CGLBBeanMapspring自己把CGLIB也打入到项目中了,所以可以直接用SpringBeanMap,用BeanMap直接把UserVO转换成Map

所以UserService的实现类UserServiceImpl可能是这样写的

@Service
public class UserServiceImpl implements UserService {

    @Override
    public List<Map> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        List<Map> map = userVOList.stream()
                                  .map(BeanMap::create)
                                  .peek(beanMap -> beanMap.remove("pwd"))
                                  .collect(Collectors.toList());
        return map;
    }
    
    @Override
    public Map detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        BeanMap beanMap = BeanMap.create(userVO);
        return beanMap;
    }
}

虽然这种方案确实能解决问题,但是估计甲方会把我头打爆,这让甲方改多少东西,多少逻辑,而且如果需要扩展的话,比如用户详情detail()中突然不返回id了,岂不是还要在detail()方法体中增加remove的操作,实在太不开闭原则了

因此回归甲方需求的本身,那就是希望在detail()list()方法主体不进行太大改动的基础上实现功能,好歹也是什么方法返回什么字段应该是可配置的吧,这样后续修改也很容易,不用改方法主体,改配置就可以了

所以,Spring AOP+注解,上。

题主UserService的实现类UserServiceImpl估计是这样写的

@Service
public class UserServiceImpl implements UserService {

    @Override
    public UserVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    public List<UserVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

现在要控制不同方法返回的UserVO字段不一样,先不说咋实现,咱们先把咋配置整好,直接整一个注解配置上。所以注解FieldLimit应运而生,就一个属性,配置需要返回哪些字段的名称

// 注:选择实现方案不同,这里的Retention取值可以是RetentionPolicy.SOURCE
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FieldLimit {
    // 默认{}就是返回所有的字段
    String[] value() default {};
}

FieldLimit把我们的方法装饰一下

@Service
public class UserServiceImpl implements UserService {

    // 不填写任何字段就是返回所有的字段
    @Override
    @FieldLimit
    public UserVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    @FieldLimit({"id", "name", "area"})
    public List<UserVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

那接下来就是怎么实现了,显然刚才提到了AOP,但是AOP只是一个桥梁,是一个把处理不同字段的逻辑移到切面的一个桥梁,真正到底怎么实现不同方法返回不同的字段,仅仅靠AOP是解决不了,就像最开始我提到的,这个本质就是不同的业务,其实是需要两个不同返回对象来处理的,但是但是呢,甲方懒了亿点点,所以需要我们来用一些好工具帮他实现

即:我们需要帮甲方生成两个类,两个类的字段是有限制的,限制来源于FieldLimit(当然由于现在题主提到的需求中,detail()方法是返回所有字段的,所以准确来说只是生成一个类)

咋运行时生成类呢,当然用咱们的字节码编程啦。借助偏友好点API的用Javassist嘛(主要前段时间才接触过,哈哈哈)

由于本质上不同方法返回的类肯定是不一样的,所以就不能写返回UserVO了,得改成一个通用的,可以用Object,但是那也太抽象了,所以干脆做一个标记接口,返回接口,然后我们生成的新类去实现这个接口就可以了

public interface IBaseVO {
}

有了这个接口,这时候UserService就变成了

public interface UserService {

    IBaseVO detail();
    List<IBaseVO> list();
}

由于接口发生了变化,当然实现类也要做修改,不过不用改动太大,只是改改返回的类型,然后记得给UserVO实现接口IBaseVO即可

@Service
public class UserServiceImpl implements UserService {

    // 不填写任何字段就是返回所有的字段
    @Override
    @FieldLimit
    public IBaseVO detail() {
        UserVO userVO = new UserVO(1l, "小李", "123456", "四川");
        return userVO;
    }

    @Override
    @FieldLimit({"id", "name", "area"})
    public List<IBaseVO> list() {
        List<UserVO> userVOList = Arrays.asList(
                new UserVO(1l, "小李", "123456", "四川"),
                new UserVO(2l, "小张", "654321", "重庆"));
        return userVOList;
    }
}

那接下来就是最关键的地方了,怎么用切面实现生成新类的效果,直接上代码,切面类FieldLimitAspect

@Aspect
@Component
public class FieldLimitAspect {

    @Autowired
    private List<ReturnTypeHandler> handlers;

    @Pointcut(value = "@annotation(fieldLimit)")
    public void pointcut(FieldLimit fieldLimit) {
    }

    @Around(value = "pointcut(fieldLimit)")
    public Object around(ProceedingJoinPoint joinPoint, FieldLimit fieldLimit) throws Throwable {
        // 获取该方法返回结果
        Object result = joinPoint.proceed();
        // 若没有限制的字段,则直接返回原结果
        String[] fieldLimitNameArray = fieldLimit.value();
        if (fieldLimitNameArray.length == 0) return result;

        // 获取执行方法的返回类型
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        Type genericReturnType = method.getGenericReturnType();
        ResolvableType resolvableType = ResolvableType.forType(genericReturnType);
        Optional<ReturnTypeHandler> returnTypeHandlerOptional = handlers.stream()
                .filter(handler -> handler.isNeedProcess(resolvableType)).findFirst();
        // 没有处理的Handler,则直接返回
        if (!returnTypeHandlerOptional.isPresent()) return result;
        ReturnTypeHandler returnTypeHandler = returnTypeHandlerOptional.get();

        // 若方法返回为空,就不用处理了
        if (returnTypeHandler.isEmpty(result)) return result;

        String classPath = this.buildClassName(method);
        Class<?> resultClass = returnTypeHandler.getRawClass(result);
        // 这是需要返回的字段set
        Set<String> fieldLimitNameSet = Arrays.stream(fieldLimitNameArray).collect(Collectors.toSet());
        // 这是返回的原始实体类应该有的字段在经过了需要返回的set过滤
        List<Field> fieldLimitFields = Arrays.stream(resultClass.getDeclaredFields())
                .filter(field -> fieldLimitNameSet.contains(field.getName()))
                .collect(Collectors.toList());
        // 如果过滤后没有任何一个字段,则直接返回原结果
        if (fieldLimitFields.isEmpty()) return result;
        Class newClass = NewClassBuilder.getNewClass(classPath, fieldLimitFields);

        result = returnTypeHandler.newInstance(newClass, result);
        return result;
    }

    private String buildClassName(Method method) {
        String declaringClassName = method.getDeclaringClass().getSimpleName();
        String methodName = method.getName();
        String returnTypeClassName = method.getReturnType().getSimpleName();
        Class<?>[] parameterTypes = method.getParameterTypes();
        String className = Stream.concat(Stream.of(declaringClassName, methodName, returnTypeClassName),
                Arrays.stream(parameterTypes).map(Class::getSimpleName)).collect(Collectors.joining("$"));
        return className;
    }
}

emmm,由于我不想写死很多东西,所以FieldLimitAspect并没有把所有的逻辑包含进去,仅仅是包含了如何对方法返回的处理结果做类型判断和对注解FieldLimit的值的处理的主干过程,具体怎样构造新的Class,怎样创建新的对象,都在其中的handler中,也就是接口ReturnTypeHandler。方便后续的可插拔式处理。

现在ReturnTypeHandler支持处理

  1. 返回数组,例如UserVO[],对应的handlerReturnTypeCollectionHandler
  2. 返回集合,例如List<UserVO>,对应的handlerReturnTypeArrayHandler
  3. 返回单个POJO对象,例如UserVO,对应的handlerReturnTypePOJOHandler

如果你以后还想支持Map也可以再去添加一个ReturnTypeHandler实现类即可。不过你需要对于Javajava.lang.reflect.Type以及SpringResolvableType也一些了解才比较好写。

所有实现类都在这里github上,你可以慢慢看

当然当然,不知道刚刚第一次提到注解FieldLimit,我给予注解的@RetentionRetentionPolicy.RUNTIME,也就是运行时也保留,因此我下面的方案才是Spring AOP+注解,

但是我也在FieldLimit中提到,其实也可以是RetentionPolicy.SOURCE,也就是注解是在编译时才有效。那最终的方案就不是Spring AOP+注解,毕竟编译时那还没有Spring啥事呢。那就走我们的APT(Annotation Processing Tool)。就是Lombok类似,不过甲方的这里的情况要比Lombok的场景稍微难点(个人感觉),毕竟Lombok已经是偷偷修改了AST(抽象语法树),但是修改也是加方法而已,是增量,但是甲方这里的需求是直接需要修改以前的类的返回,自己以前也仅仅是做过新增类,就是auto-constants,它是根据POJO对象新增一个专门存放其属性名的类,这样就不用老写了死代码了,如果修改了属性,对应的地方不修改,是要报错的,这都还没有做到修改AST,但是给我的感觉是可以做的,不过估计要比我提供的Spring AOP+注解还要麻烦点,算是提供一种思路了

不过最后说下来,题主也可以看到,其实明明新增两个对应业务POJO对象就解决问题了,但是这个也太懒了叭。。。

陈甜 2022-09-19 22:05:32

image.png
ResponseBodyAdvice这个接口很强大,在你转json之前,会回调beforeBodyWrite这方法,你可以修改接口返回的数据,问题主要是在哪勒,程序不知道你这次调用到底需要哪些字段,这个得调用接口的时候,就传入相关参数过来

忆伤 2022-09-19 22:05:32

Spring提供了JsonView来解决当前问题:

package club.yunzhi.questionnaire.Entity;

import com.fasterxml.jackson.annotation.JsonView;

public class Test {
    @JsonView({AllJsonView.class, IdJsonView.class})
    private Long id;

    @JsonView({AllJsonView.class, NameJsonView.class})
    private String name;

    @JsonView({AllJsonView.class, PwdJsonView.class})
    private String pwd;

    @JsonView({AllJsonView.class, AreaJsonView.class})
    private String area;

    // 省略setter\getter
    
    interface AllJsonView {
    }

    interface IdJsonView {
    }

    interface NameJsonView {
    }

    interface PwdJsonView {
    }

    interface AreaJsonView {
    }
}

控制器:

package club.yunzhi.questionnaire.Entity;

import com.fasterxml.jackson.annotation.JsonView;

import java.util.List;

public class TestController {

    @JsonView(Test.AllJsonView.class)
    List<Test> getAll() {
    }

    @JsonView(GetByIdJsonView.class)
    Test getById() {
    }

    private interface GetByIdJsonView extends
            Test.IdJsonView,
            Test.NameJsonView,
            Test.AreaJsonView {
    }
}

希望能解决你的当前问题。


以下是关于评论中的问题。

空对象问题,假设B中有A,此时A如果为null,则将返回null。首先我们说这样设计当然是没有任何问题的。但如果你不想主上他返回null,则可以在B中重写下getA():

class B {
    private A a;
    
    public A getA() {
        if (null == this.a) {
            return new A();
        }
        
        return this.a;
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文