1. 程式人生 > >簡單註解+AOP+反射實現特定功能

簡單註解+AOP+反射實現特定功能

先描述下完成功能的場景:
先查一個訂單表,想要取得使用者表的相關資訊,但由於某些原因使用者表不能進行關聯查詢,這個時候往往會想到冗餘使用者表字段,但這也會帶來一個問題,就是使用者表裡的欄位改變值後,比較但以維護(因為訂單表的欄位也需要同步修改)。
所以直接先查一遍訂單表再查使用者表,當然這樣資料庫效能肯定比較低。下面假設我們就要實現這個功能。
這是我們service實現這個功能的方法

    public List<Order> query(){
        
        //先查詢所有訂單
        List<Order> orderList = orderMapper.queryOrder();
        
        //侵入程式碼
        //根據訂單的使用者id去查詢使用者名稱
        for(Order order : orderList){
            String customerName = userMapper.queryNameById(order.getCustomerId());
            order.setCustomerName(customerName);
        }
        
        
        return orderList;
    }

很容易發現,如果對於其他類似功能,那麼我們這段類似的侵入程式碼要重複在多個方法內出現。
所以現在想要實現以下這麼一個功能:給物件的某些欄位自動去查詢資料庫注入值。
那麼問題的關鍵就是

  1. 要為哪個類設定屬性?
  2. 為哪些屬性設定值?
  3. 需要藉助哪個類去查詢資料庫?
  4. 藉助類的哪個具體方法查詢?

所以定義一個簡單的註解來取得這四個值

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SetValue {
    
    /**
     * 查詢類的類名
     * @return
     */
    Class<?> className();
    
    /**
     * 查詢的方法
     * @return
     */
    String methodName();
    
    /**
     * 藉助的欄位
     * @return
     */
    String paraNameForGet();
    
    
    /**
     * 設定的欄位
     * @return
     */
    String paraNameForSet();
}

這樣子在實體類中我們就可以通過使用該註解設定這些資訊

public class Order {
    /*  訂單id    */
    private Integer id;
    
    /*  下訂單的使用者id    */
    private Integer customerId;
    
    /*  下訂單的使用者名稱     */
    @SetValue(className = MUserMapper.class, methodName = "queryNameById", paraNameForGet = "customerId", paraNameForSet = "customerName")
    private String customerName;
    
    @SetValue(className = MUserMapper.class,methodName = "queryAgeById",paraNameForGet = "customerId",paraNameForSet = "customerAge")
    private Integer customerAge;
	。。。。。。    
}

獲得這些資訊後,接下來就是要執行上面侵入程式碼的邏輯,下面通過AOP來實現,在此之前再寫一個註解讓AOP知道哪些方法需要被增強

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NeedSetFieldValue {
}
@Component
@Aspect
public class SetValueAnnotationAspect {
    @Autowired
    private BeanUtil beanUtil;

    @Around("@annotation(com.ay.mybatis.annotion.annotation.NeedSetFieldValue)")
    public Object setValueAround(ProceedingJoinPoint joinPoint){
        System.out.println("-----------------環繞前置通知--------------------");
        try {
            Object o =joinPoint.proceed();
            //返回型別是個集合
            if(o instanceof Collection){
                boolean result = beanUtil.setFieldValue((Collection)o);
                if(!result){
                    System.out.println("設定Field值不成功");
                }
            }else{
                System.out.println("返回型別不是集合");
            }
            System.out.println("-----------------環繞後置通知--------------------");
            return o;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        
        return null;
    }
}

增強後置的功能封裝在BeanUtil中,那麼接下來的關鍵就是通過反射取得註解的四個值,結合特定業務功能實現邏輯。

@Component
public class BeanUtil implements ApplicationContextAware {
    
    private ApplicationContext applicationContext;
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    
    
    public boolean setFieldValue(Collection c) throws NoSuchMethodException, IllegalAccessException, InstantiationException {
        //得到需要設定欄位的類名
        Iterator iterator = c.iterator();
        Class cla  =iterator.next().getClass();
        Field[] fields = cla.getDeclaredFields();
        
        for(Field m : fields) {
            //得到註解
            SetValue s = m.getAnnotation(SetValue.class);
            if(s != null){
                //查詢類,介面無法例項化,通過springIOC容器取得例項!!!!!
                Object o = applicationContext.getBean(s.className());
                
                //引數型別一定要加!!! 不然沒有方法進行匹配
                //預設引數是空!!!!
                Class cla2 = s.className();
                
                //查詢field方法
                Method[] c2m = cla2.getMethods();
                Method queryMethod = null;
                // 無法知道要呼叫的引數型別,只好採用遍歷
                // cla2.getMethod(s.methodName(),Integer.class);
                for(Method m2 : c2m){
                    if(m2.getName().equals(s.methodName())){
                        queryMethod = m2;
                        break;
                    }
                }
                //get方法
                Method getMethod = cla.getMethod("get" + StringUtils.capitalize(s.paraNameForGet()));
                //set方法
                Method setMethod = null;
                Method[] mcla = cla.getMethods();
                for(Method m2: mcla){
                    if(m2.getName().equals("set" + StringUtils.capitalize(s.paraNameForSet()))){
                        setMethod = m2;
                        break;
                    }
                }
                iterator = c.iterator();
                while (iterator.hasNext()) {
                    Object object = iterator.next();
                    try {
                        //獲取值
                        Object getField = getMethod.invoke(object);
                        //查詢
                        Object field =  queryMethod.invoke(o, getField);
                        //設定值
                        setMethod.invoke(object, field);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                }
    
            }
        }
        return true;
    }
}

思路就是取得查詢類的例項物件,因為這邊使用的是mybatis介面,無法直接例項化,所以通過容器獲得例項。然後獲得要反射呼叫的三個方法,然後invoke呼叫三個方法實現即可,很簡單的反射邏輯。

這樣寫的優點主要是使得程式碼編寫變得規範了很多。下面我們實現相同功能的service方法裡就只要這樣寫就行了。

@NeedSetFieldValue  //需要自動注入引數值
    public List<Order> query(){
        
        List<Order> orderList = orderMapper.queryOrder();
        
        return orderList;
    }

當然這裡主要是想練習下手寫註解以及反射的相關知識。
具體程式碼碼雲