[解锁新姿势] 封装通用返回值

前言

最近入坑了一家坑爹的公司,巨型单体,那代码,脑壳痛。

💩山遍地是,竟没有一片净土!

差点被同化了

致敬那些在屎山开发的程序猿

谁说站在光里的才算英雄 ?

你们才是!

场景

如果

我们的表,没有冗余数据

且实时查询时对应值时,都需要手动的 查询对应值,并赋值,如下代码所示:

public CardVO query(QueryDTO dto) {
  
  Card card = CardRepository.findById(dto.getId());
  
  CardVO cardVO = new CardVO();
  cardVO.setId(card.getId());
  //... 省略set 步骤
  
  User user = userRepository.findById(dto.getUserId());
  cardVO.setUsername(user.getName());
}

此时查询评论接口也需要赋值 username

public CommentVO query(QueryDTO dto) {
  
  Comment comment = CommentRepository.findById(dto.getId());
  
  CommentVO commentVO = new CommentVO();
  commentVO.setId(card.getId());
  //... 省略set 步骤
  
  User user = userRepository.findById(dto.getUserId());
  commentVO.setUsername(user.getName());
}

这时候,另外接口也需要…

那么这段代码不出意外的话会被 copy 到其他接口。

这种事情,我们怎么允许发生呢?

简单优化

public interface UsernameWrapObject {

    Long getUserId();

    void setUsername(String userName);
}

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UsernameWrap {

    private final UserRepository userRepository;

    public void wrap(UsernameWrapObject object) {
        User user = userRepository.findById(object.getUserId());
        object.setUsername(user.getName());
    }
}

@Data													 // 实现对应接口 
public class CardVO implements UsernameWrapObject {

    @ApiModelProperty("打卡id")
    private Long id;

    @ApiModelProperty("打卡人")
    private String username;
  
  	... 省略其他参数
}

提取 赋值 到 UsernameWrap,简单封装一个通用 wrap 方法

入参为 UsernameWrapObject 接口(便于后续其他对象赋值使用,所以抽取成接口)

传入所需 UsernameWrapObject 的对象,就可以赋值 username 值。

然后对应调用改为,如下代码所示:

public CardVO query(QueryDTO dto) {
  
  Card card = CardRepository.findById(dto.getId());
  
  CardVO cardVO = new CardVO();
  cardVO.setId(card.getId());
  //... 省略set 步骤
  
  // User user = userRepository.findById(dto.getUserId());
  // cardVO.setUsername(user.getName());
 
  // 替换为 wrap 通用类
  usernameWrap.wrap(cardVO);
}

至此,优化结束,相信很多人就心满意足,开始愉快的摸鱼了。

🐟

🐟

🐟

但是,回过头来想,每次都需要手动调用 UsernameWrap 进行赋值,难免有些重复(主要还是因为懒,连手动调用都不想)。

你说有没有什么拦截器,可以让我们在返回参数前,对返回值进行拦截,并通过反射赋值。

一顿 Goolge+必应+百度,三连后 (暗示三连 :)

发现利器 -> ResponseBodyAdvice,通过 ResponseBodyAdvice,实现最终想要效果,让我们先来看看:

优化2

@Component
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserInfoWrap implements IWrap<UsernameWrapObject> {

    private final UserInfoRepository userInfoRepository;

    @Override
    public void wrap(UsernameWrapObject object) {
        User user = userInfoRepository.findById(object.getUserId());
        object.setUsername(user.getName());
    }
}

public interface UsernameWrapObject {

    Long getUserId();

    void setUsername(String userName);
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface Wrap {

    Class<? extends IWrap<?>>[] value();
}


// 给类添加需要 Wrap注解
@Wrap(UserInfoWrap.class)
@Data													 // 实现对应接口 
public class CardVO implements UsernameWrapObject {

    @ApiModelProperty("打卡id")
    private Long id;

    @ApiModelProperty("打卡人")
    private String username;
  
  	... 省略其他参数
}

只需在需要赋值对象添加 @Wrap 注解,声明需要注入 Class,即可实现自动赋值 username

原理

ResponseBodyAdvice 说明
public interface ResponseBodyAdvice<T> {

	/**
	 * Whether this component supports the given controller method return type
	 * and the selected {@code HttpMessageConverter} type.
	 * @param returnType the return type
	 * @param converterType the selected converter type
	 * @return {@code true} if {@link #beforeBodyWrite} should be invoked;
	 * {@code false} otherwise
	 */
	boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

	/**
	 * Invoked after an {@code HttpMessageConverter} is selected and just before
	 * its write method is invoked.
	 * @param body the body to be written
	 * @param returnType the return type of the controller method
	 * @param selectedContentType the content type selected through content negotiation
	 * @param selectedConverterType the converter type selected to write to the response
	 * @param request the current request
	 * @param response the current response
	 * @return the body that was passed in or a modified (possibly new) instance
	 */
	@Nullable
	T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
			Class<? extends HttpMessageConverter<?>> selectedConverterType,
			ServerHttpRequest request, ServerHttpResponse response);

}

ResponseBodyAdvice 相当于拦截器,可以在注解@ResponseBody将返回值处理成相应格式之前,操作返回值。该接口提供两个方法

  • supports:判断是否支持需要返回参数类型,返回 True,则进入后续 beforeBodyWrite 方法。
  • beforeBodyWrite:在这里可以对返回值,做一些特殊处理。

刚好符合我们的需求,我们在 VO 返回之前,需要对 返回值进行 set 赋值

PS:这时候有个"聪明"的小朋友可以会问,我的 Controller 没有加 @ResponseBody 注解啊,怎么会拦截呢?

早期项目 Controller 是需要在每个 Contoller 加入 @ResponseBody 注解,后续 Springboot 为简化这流程,组合 注解 @Controller@ResponseBody 注解 为 @RestController

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody  
public @interface RestController {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 * @since 4.0.1
	 */
	@AliasFor(annotation = Controller.class)
	String value() default "";

}
实战
public interface IWrap<T> {

    void wrap(T object);
}

@Component
public class WrapStore {

    private final Map<Class<?>, IWrap<?>> wrapMap;

    @Autowired
    public WrapStore(List<IWrap<?>> wraps) {
        wrapMap = new HashMap<>();
        wraps.forEach(wrap -> wrapMap.put(wrap.getClass(), wrap));
    }

    @SuppressWarnings("unchecked")
    public <V> void wrap(Class<?> clazz, Object object) {
        IWrap<V> iWrap = (IWrap<V>) wrapMap.get(clazz);
        iWrap.wrap((V) object);
    }
}

创建一个 WrapStore 类,主要是管理 IWrap 实现类,通过 Spring 构造函数注入对应 IWrap 实现类,然后根据 Class 转化成对应的 Map,便于后续 wrap 处理。

@ControllerAdvice
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class WrapResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private final WrapStore wrapStore;

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return returnType.getParameterType().equals(Response.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
      
        Response<?> result = (Response<?>) body;

        Object data = result.getData();
        if (data != null) {
            wrapCurrentClass(data);
        }
        return body;
    }

    private void wrapCurrentClass(Object data) {
        Class<?> aClass = data.getClass();
        Wrap wrap = aClass.getAnnotation(Wrap.class);
        
        if (wrap == null) {
           return;
        }
      
        Class<?>[] wrapClasses = wrap.value();
        for (Class<?> clazz : wrapClasses) {
            wrapStore.wrap(clazz, data);
        }
    }
}

创建一个ResponseBodyAdvice 接口实现类—— WrapResponseBodyAdvice

  1. supports ,过滤出需要处理的 Response 类型 返回值。

  2. 然后 beforeBodyWrite方法,通过返回值 Class,获取对应 @Wrap 注解。

  3. 然后调用对应 wrapStore的 wrap,即可赋值。

注意

WrapResponseBodyAdvice 只展示了简单的功能,后续如果返回值是 Response<PageVO>,需要自行适配。

适合的场景,写适合的代码,切勿拿着锤子看那都是钉子。

提问环节

每次写一些优化代码文章,总有的人会问到:

为了省 2行代码,加入一堆类,值得吗?

有这时间摸鱼不香吗?

这是过度设计,走火入魔!!!

封装这一会确实可以摸好多鱼,没毛病,但是你点进文章就是你的不对了 :)

最后

以上就是全部内容,希望能帮助到你~

如有不妥,欢迎指出,大家一起交流学习。

感谢阅读,下次再见。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值