起初第一眼看到题主的问题,我是有点匪夷所思的。。。主要是下面这段话
写接口,固定字段返回,看起来都很正常,那解决方案不就是
- 列表接口返回对象
UserListVO
,里面有3个字段 - 详情接口返回对象
UserDetailVO
,里面有4个字段
这跟jackson
配置null
是否返回,毫无关系啊,jackson
只是一个序列化工具,但是不同接口返回不同字段这是业务逻辑,就需要创建类才可以解决啊。
创建两个类真的就是很难了么。。。那为啥非要只用一个UserVO
对象来表示两个不同接口的返回呢?假如以后新增还有其他和User
相关的接口,是不是要把再在UserVO
类中塞满一堆字段,然后不同接口返回不同个数的字段呢,这样索性不要UserVO
得了,直接实体对象User
返回算了嘛。
诚然,就算是按照myskies的方式做了分组,这是一种解决题主问题的方案,但是但是,如果是实际公司业务开发,我想没有哪个兄弟愿意接手在一个大而全的VO
对象去找他们需要的分组叭。。。起码要是我,我是要哭的,字段少还好,字段多了。。。@JsonView
中各种取值是要看花了的哈
不过吐槽归吐槽,上面只是我作为同为程序员角色,站在同一角度提出的小意见,但是如果作为思否中问题和回答问题的角色,也就是相当于甲方乙方的角度来说的话,我还是要来想办法解决题主的问题
首先还是先捋一下当前的思路,以及需要解决的问题
假设现在就是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
,那还不无法无天,想怎么写就怎么写咯,当然这里用到了CGLB
的BeanMap
,spring
自己把CGLIB
也打入到项目中了,所以可以直接用Spring
的BeanMap
,用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
支持处理
- 返回数组,例如
UserVO[]
,对应的handler
是ReturnTypeCollectionHandler
- 返回集合,例如
List<UserVO>
,对应的handler
是ReturnTypeArrayHandler
- 返回单个
POJO
对象,例如UserVO
,对应的handler
是ReturnTypePOJOHandler
如果你以后还想支持Map
也可以再去添加一个ReturnTypeHandler
实现类即可。不过你需要对于Java
的java.lang.reflect.Type
以及Spring
的ResolvableType
也一些了解才比较好写。
所有实现类都在这里github上,你可以慢慢看
当然当然,不知道刚刚第一次提到注解FieldLimit
,我给予注解的@Retention
是RetentionPolicy.RUNTIME
,也就是运行时也保留,因此我下面的方案才是Spring AOP
+注解,
但是我也在FieldLimit
中提到,其实也可以是RetentionPolicy.SOURCE
,也就是注解是在编译时才有效。那最终的方案就不是Spring AOP
+注解,毕竟编译时那还没有Spring
啥事呢。那就走我们的APT(Annotation Processing Tool)
。就是Lombok
类似,不过甲方的这里的情况要比Lombok
的场景稍微难点(个人感觉),毕竟Lombok
已经是偷偷修改了AST
(抽象语法树),但是修改也是加方法而已,是增量,但是甲方这里的需求是直接需要修改以前的类的返回,自己以前也仅仅是做过新增类,就是auto-constants,它是根据POJO
对象新增一个专门存放其属性名的类,这样就不用老写了死代码了,如果修改了属性,对应的地方不修改,是要报错的,这都还没有做到修改AST
,但是给我的感觉是可以做的,不过估计要比我提供的Spring AOP
+注解还要麻烦点,算是提供一种思路了
不过最后说下来,题主也可以看到,其实明明新增两个对应业务POJO
对象就解决问题了,但是这个也太懒了叭。。。