背景
在开发过程中,你肯定会遇到这样一个场景:
- 获取订单列表,需要显示订单id,下单人
member_id
,下单人姓名member_name
。 - 数据库订单表只有
member_id
字段,member_name
字段在用户会员表中。
这时候你肯定会使用连表查询
代码语言:javascript复制select o.id,o.member_id,m.name from order o,member m where o.member_id = m.id;
一行SQL搞定,很简单嘛。
但是随着时间推移,系统越来越大,单体服务会拆分为微服务。每个服务有自己的库。
这时候你就不好做跨库查询了吧。
不过你用的Spring Cloud框架,这套框架可以将远程调用变得和本地调用一样简单。
你可能会有如下代码:
其中订单实体Order
(作了简化,只保留本次案例用的字段)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
/**
* 订单id
*/
private String id;
/**
* 下单用户id
*/
private String memberId;
}
OrderVo
为:
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class OrderVo {
private String id;
private String memberId;
/**
* 获取订单用户name
*/
private String memberName;
}
用户会员实体Member
为:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Member implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private String id;
private String name;
}
测试结果为:
在listOrderVo()
方法中,我们只有两步:
- 从数据库中查询Order
- 从用户服务中获取用户名
其中第2步的memberService.getById(memberId)
是远程跨库调用。
你发现就算不是以前的连表查询,也不难嘛。
关于上面跨库查询的思考
上面的场景我也经历过,看了代码,我思考:步骤2在这个方法listOrderVo()
中是否显得多余呢。我只是需要订单信息,却有大段的代码是如何获取memberName
。这里只是订单信息要显示memberName
,那么物流信息可能也要显示memberName
。还有其他的业务比如:后台要展示用户签到记录也需要显示memberName
。都要写步骤2的代码,太麻烦了。
“可不可以将步骤2封装呢?”
思来想去,发现使用注解可以优雅的解决这个问题。
注解封装
我们先来定义几个注解:
NeedSetValue
代码语言:javascript复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NeedSetValue {
Class<?> beanClass();
String param();
String method();
String targetFiled();
}
NeedSetValue
注解用于字段上,下面来解释下这几个属性的含义:
- beanClass:表示获取值的方法在哪个类上
- param:表示字段值的方法的传入参数
- method:表示获取字段值的方法
- targetFiled:表示方法的返回的某个字段值赋给当前字段
NeedSetValueField
代码语言:javascript复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NeedSetValueField {
}
作用与方法上,表示该方法的返回值会通过注解处理获得。
我们来开发NeedSetValue
注解的功能
@Component
@Aspect
public class SetFieldValueAspect {
@Autowired
BeanUtil beanUtil;
@Around("@annotation(com.lvshen.demo.annotation.NeedSetValueField)")
public Object doSetFieldValue(ProceedingJoinPoint point) throws Throwable {
Object o = point.proceed();
if (o instanceof Collection) {
this.beanUtil.setFieldValueForCol(((Collection) o));
} else {
//返回结果不是集合的逻辑
}
return o;
}
}
使用AOP,执行方法后会执行后置处理this.beanUtil.setFieldValueForCol(((Collection) o));
这里我只写了方法返回是集合的处理。
setFieldValueForCol
方法如下:
由于代码过长,这里我转成图片了。这个方法主要做几件事:
“1.获取有
NeedSetValue
注解的字段 2.反射获取注解上的参数,方法,返回值 3.执行注解上的方法,获取返回值 4.将返回的这个值赋给标注注解的这个字段 ”
如果上面的代码没有看明白,那先看看我下面的使用例子吧
首先OrderVo
代码如下:
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class OrderVo{
private String id;
private String memberId;
/**
* 获取订单用户name
*/
@NeedSetValue(beanClass = MemberService.class, param = "memberId", method = "getById", targetFiled = "name")
private String memberName;
}
上面的注解表示:memberName
的值是通过MemberService#getById
方法获取的,该方法的参数为字段memberId
,并将方法值的name
赋给memberName
。
我们刚才获取订单展示的方法改造如下:
代码语言:javascript复制@NeedSetValueField
public List<OrderVo> listOrderVoByAnnotation() {
//1.从数据库中查询Order
List<Order> orderList = this.listOrder();
log.info("数据库中的数据orderList:{}",orderList);
//list之间的转换
return OrderConverter.INSTANCE.listentity2Vo(orderList);
}
只需要添加@NeedSetValueField
注解即可。省了步骤2。
测试结果如下:
你看,我没有写步骤2,一样获取到了memberName
。
有的开发认为写注解是多此一举,但是步骤2实际上是违反了设计原则:单一职责原则和开闭原则。
如果想要看源码的可以访问下面地址:
“https://github.com/lvshen9/demo/blob/lvshen-dev/src/main/java/com/lvshen/demo/annotation”
欢迎大家完善