問題引入
首先這篇博文是我在上直播課學習到的,受益匪淺,它教會我們用架構師的思維去解決問題,代碼擴展性強,於是乎記錄下來。首先遇到的是這麼一個問題(簡化版本),就是有兩張表:一張用戶表user表,另外一張是訂單表order表,其結構如下
就是很簡單的兩張表,其中用戶表的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,希望新型肺炎早點消除,還我們一個祥和的世界。