问题引入
首先这篇博文是我在上直播课学习到的,受益匪浅,它教会我们用架构师的思维去解决问题,代码扩展性强,于是乎记录下来。首先遇到的是这么一个问题(简化版本),就是有两张表:一张用户表user表,另外一张是订单表order表,其结构如下
![](https://i-blog.csdnimg.cn/blog_migrate/e443deaf1d380488f3efc23c7a1a2c71.png)
![](https://i-blog.csdnimg.cn/blog_migrate/39377d2fa7351549b009df58ba5e8090.png)
就是很简单的两张表,其中用户表的custId和订单表的custId相关联,现在要统计订单的信息,其中信息中包含用户姓名,于是乎sql语句就如下所示:
ELECT o.id,o.custId,u.name FROM `user` u,`order` o WHERE u.custId=o.custId
但是随着业务量的增长两张表已经不再一个数据库中,一个数据库已经容纳不了。像现在很多大型公司数据量动不动就过亿,对于数据库层面而言首先要做的就是分库分表,而且还有可能将不同种类的表拆分成各自的微服务,比如用户信息表可能就被客户信息平台所维护。那么既然出现了这样的问题也就意味着我们上面的sql的执行结果由之前的:
[{"id":1,"custId":3,"name":"jack"},{"id":2,"custId":4,"name":"hmm"},{"id":3,"custId":4,"name":"hmm"},{"id":4,"custId":1,"name":"tom"},{"id":5,"custId":2,"name":"jeery"}]
变成了如下(因为现在客户信息我们之前写的已经查不到了)
[{"id":1,"custId":3,"name":null},{"id":2,"custId":4,"name":null},{"id":3,"custId":4,"name":null},{"id":4,"custId":1,"name":null},{"id":5,"custId":2,"name":null}]
思考如何解决
首先我们第一时间想到就是既然之前写的sql已经查不到客户姓名了,那我们肯定要单独写一个查询。如果是一些高频或者经常用到的会存于redis,于是一顿操作猛如虎,在之前代码的基础上进行修改,如下:
@service
public class UserService {
@Autowired
UserDao userDao;
//旧代码
public List<Order> queryOrder(){
return userDao.queryOrderInfo();
}
}
@service
public class UserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
UserDao userDao;
//新代码
public List<Order> queryOrder(){
List<Order> orders = userDao.queryOrderInfo();
for (Order order : orders) {
if (StringUtils.isEmpty(order.getName())){
//先去查询缓存 这里的key就简单的以custId定义一下,实际生产请正确命名
String custName = stringRedisTemplate.opsForValue().get(order.getCustId());
if (StringUtils.isEmpty(custName)){
User user = userDao.queryUserInfo(order.getCustId());
order.setName(user.getName());
stringRedisTemplate.opsForValue().set(String.valueOf(order.getCustId()),user.getName());
}
}
}
return orders;
}
}
但这样做真的好吗?因为这里可能只是有一处需要修改,但是如果你这个service被其它地方引用或者说你的项目中不止一处也需要同样的属性或者不同的属性,对于这种典xxx需要xxx属性,那我们这么改真的好吗?如果多个地方有类似需求不仅改动量大,而且都是一些重复造轮子的工作,而且我们的设计模式要求的也是开闭原则,这些都不不符合。那我们怎么做呢?首先我们想到的肯定就是动态代理,利用Spring提供的aop注解去对返回结果进行处理。
理论上就是会返回一个结果
集合,其中集合中的每个对象都缺乏用户姓名或者其它属性信息,但是我们项目中有很多xxx需要xxx属性类似的操作,我们可能每个都建立一个切面逻辑,来进行代理增强,那么代码实现虽然也不难,但是明显不优雅,移植性也很差。那我们如何做呢?
首先我们要知道的就是某某某个类上徐少某某某属性,然后我们找到那些缺少的属性,然后逐个进行增强?显然也不合适,那我们可不可以通过一种手段找到我们缺少的属性,然后放在一个代理方法里进行增强呢?答案是可以的,这时候为了扩展代码的灵活性这时候就会用到注解,用自定义注解去标记哪个类上缺少啥属性,吧唧一下我们就知道了啥啥啥属性是缺失的。既然我们知道缺失的字段后,那我们怎么拿到需要的属性值呢?
聪明的人已经想到了反射,利用反射去拿到我们所需的客户姓名。比如我们这里有个查询客户姓名的方法:但是利反射method.invoke(bean,agrs)的前提是我们必须知道是哪个类要调用哪个方法,这个方法的入参又是啥,当我们执行外这个方法返回的user对象中我们又需要哪些字段呢?OK,可以确定的就是我们的注解必须知道4点
@Select("SELECT * FROM user u WHERE u.custId=#{custId}")
User queryUserInfo(@Param("custId") int custId);
开始撸代码
1.首先定义我们的注解类
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedSetValue {
//@NeedSetValue(beanClass = UserDao.class,param = "custId",method = "queryUserInfo",targetFiled = "name")
//method.invoke(bean,args) 通过传参的形式告诉
//该注解 哪个对象
Class<?> beanClass();
//该注解 哪个方法
String method();
//该注解 哪些入参 最好定义为数组 (int custId,String xx) 复杂点就数组
String param();
//该注解 传入的哪个值,这里也可以数组,因为返回的user对象可能不止一个属性
String targetFiled();
}
2.注解作用于订单类上
public class Order {
private int id;
private int custId;
//需要查询的信息
//beenClass:反射需要知道的类
//param:反射方法的入参 (因为我们要拿到方法,拿到方法除了知道方法名字外还需要知道方法的入参数类型)
//method:反射执行所需的方法名
//targetFiled:指的是dao层的返回user对象结果中的的所需字段名字,可以通过该名字拿到具体的值
@NeedSetValue(beanClass = UserDao.class,param = "custId",method = "queryUserInfo",targetFiled = "name")
private String name;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getCustId() {
return custId;
}
public void setCustId(int custId) {
this.custId = custId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Order{" +
"id=" + id +
", custId=" + custId +
", name='" + name + '\'' +
'}';
}
}
3.我们的dao 层方法,额外做的业务逻辑
@Mapper
public interface UserDao {
//旧的查询
@Select("SELECT o.id,o.custId,u.name FROM `user` u,`order` o WHERE u.custId=o.custId")
List<Order> queryTotalInfo();
//模拟 完成上述操作
@Select("SELECT o.id,o.custId FROM `order` o ")
List<Order> queryOrderInfo();
//额外业务逻辑
@Select("SELECT * FROM user u WHERE u.custId=#{custId}")
User queryUserInfo(@Param("custId") int custId);
}
写到这里我们不禁要停下来,因为因为我们的增强逻辑还没写,那我们梳理一下思路,具体的流程是怎样的
- 我们开始通过切面拿到原来之前的返回结果值,我这里暂时是一个List<Order>
- 遍历List<Order>的同时,通过于凌驾于字段之上的注解去拿到我们的key值,这里就看自身怎么定义了,反正要唯一,这里我是这样定义的做到业务唯一即可,key:操作的bean对象 + 方法名 + 客户号 比如下面,因为一个人对应多个订单,所以缓存好点com.cloud.ceres.rnp.Neek.dao.UserDao-queryUserInfo-1
- 这通过上面的key值,判断缓存中有没有我们所需user对象,有从缓存中去拿,没有则利用反射调用额外写的业务方法,去拿到我们所缺失的字段值
- 重新set值,顺便将其存入缓存
4.因为我们的切点是作用于方法的,很难通过表达是做到,所以我们还需要通过另外一个注解来标示哪些方法应该被代理增强
/**
* @author heian
* @create 2020-02-02-12:55 上午
* @description 定义切点:指明哪些类需要返回增强,如果范围较大可以使用Spring的el表达式Pointcut
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface NeedProxyEnhance {
}
@service
public class UserService {
@Autowired
UserDao userDao;
//旧代码
@NeedProxyEnhance//被代理增强的方法
public List<Order> queryOrder(){
return userDao.queryOrderInfo();
}
}
/**
* @author heian
* @create 2020-02-02-1:13 上午
* @description 切面:返回通知增强
*/
@Aspect
@Component
public class SetFieldValueAspect {
@Autowired
private BeanUtil beanUtil;
//返回类型任意,方法参数任意,方法名任意
@Pointcut(value = "@annotation(com.cloud.ceres.rnp.Neek.annotation.NeedProxyEnhance)")
public void myPointcut(){
}
@Around("myPointcut()")
public Object doSetFieldValue(ProceedingJoinPoint pjp) throws Throwable{
Object ret = pjp.proceed();//[{{id=1, custId=3, name=null},{xx}}]
//具体的增强逻辑
beanUtil.setNeedField((Collection) ret);
return ret;
}
}
5.最复杂的就是这里了,因为要用到大量的反射知识,必须对反射的api比较熟悉才行。
package com.cloud.ceres.rnp.Neek;
import com.cloud.ceres.rnp.Neek.annotation.NeedSetValue;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* @author heian
* @create 2020-02-02-1:21 上午
* @description
*/
@Component
public class BeanUtil implements ApplicationContextAware {
@Autowired
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (applicationContext == null){
this.applicationContext = applicationContext;
}
}
/**
* 通过返回执行结果 利用反射对其进行赋值
*/
public void setNeedField(Collection collection) throws Exception{
//1、拿到单个对象(order),然后根据单个对象某个字段(name)上 去拿到注解
Object retObj = collection.iterator().next();
Field[] fields = retObj.getClass().getDeclaredFields();//field.getFields();父类的变量,别搞错了
Map<String,Object> cacheMap = new HashMap<>();//假装自己是个redis 缓存 形式为:UserDao-queryUserInfo-name-custId一个
//一个大对象中字段 可能不止一个注解变量 逐个遍历
for (Field field : fields) {
NeedSetValue annotation = field.getAnnotation(NeedSetValue.class);
if (annotation == null){
continue;
}
field.setAccessible(true);
//2、取得注解后,再从容器中取得dao层实例,再取得该dao对用的方法Method
Object bean = applicationContext.getBean(annotation.beanClass());//拿到dao层的实例对象
//有了注解就可以拿到查询的方法 方法名 + 方法入参 --> userDao.queryUserInfo(int custId)
String methodName = annotation.method();
Field custIdField = retObj.getClass().getDeclaredField(annotation.param());
Class<?> parpmClassType = custIdField.getType();
Method method = bean.getClass().getMethod(methodName, parpmClassType);
//3、反射拿到结果值
custIdField.setAccessible(true);
boolean bool = !StringUtils.isEmpty(annotation.targetFiled());
// UserDao-queryUserInfo-name-(user类中的name字段)
String keyPrefix = annotation.beanClass() + "-" + annotation.method() + "-";
for (Object ret : collection) {
Object paramValue = custIdField.get(ret);//获取当前对象中当前Field的value 也可以通过反射getCustId()
if (paramValue == null)
continue;
//interface com.cloud.ceres.rnp.Neek.dao.UserDao-queryUserInfo-name-custId(1)
String key = keyPrefix + paramValue;//redis 中的kev
Object needValue = null;
if (cacheMap.containsKey(key)){
//假设缓存中存在custId,则利用反射去执行方法,拿到name
needValue = cacheMap.get(key);
}else {
//缓存不存在,则利用刚拿到的custId,反射去执行方法,拿到name
Object userRet = method.invoke(bean, paramValue);
//注解上必须标明要拿哪个对象的哪个值(这里指user对象的name),并且redis中必须存在该对象
if (bool && userRet != null){
Field userNameField = userRet.getClass().getDeclaredField(annotation.targetFiled());
userNameField.setAccessible(true);
needValue = userNameField.get(userRet);
cacheMap.put(key,needValue);
}
}
//4、对结果进行返回增强
Object currentNeedVlaue = field.get(ret);
if (currentNeedVlaue == null){
field.set(ret,needValue);
}
}
}
}
}
其大概的思路就是通过我们在对返回结果Order类上的注解拿到我们反射所需的参数,我在这里就简单枚举几个关键点:
-
NeedSetValue annotation = field.getAnnotation(NeedSetValue.class);筛选被我们标记的field
-
field.setAccessible(true);暴力拆解,因为我们后续要set/get值field.set(ret,needValue);必须可见
-
Object bean = applicationContext.getBean(annotation.beanClass());拿到dao层的实例对象
-
Method method = bean.getClass().getMethod(methodName, parpmClassType);拿到执行dao的执行方法
-
Field userNameField = userRet.getClass().getDeclaredField(annotation.targetFiled());拿到缺失的字段
-
Object userRet = method.invoke(bean, paramValue);反射执行方法拿到业务缺失结果
-
needValue = userNameField.get(userRet);拿到缺失字段具体的值
-
field.set(ret,needValue); 存值
总结:
上面代码虽然实现起来不是很难,但是因为涉及的面要求比较广(设计模式+自定义注解+aop+反射)所以还是挺综合的,关键是它的思路你必须搞明白,我们这样做是为了解决一个什么样的问题?这样做相比之前有什么好处?可以当作以后自己代码的一种提升,keep on 2020,希望新型肺炎早点消除,还我们一个祥和的世界。