1. 程式人生 > >仿照SpringMVC實現字串與方法對映

仿照SpringMVC實現字串與方法對映

預備知識:註解,反射,spring基礎,SpringContextAware,SpringMVC實現原理

需求

輸入一個字串(稱因子名)陣列,陣列的長度不固定,每個字串代表一個業務單元(稱因子),因子是高度可擴充套件的。
輸出所有因子名與因子輸出的鍵值對。
輸入引數示例:

{
    “input” : [
        “element1”,
        “element2”,
        “element3”,
        …
    ]
    …
}

輸出引數示例:

{
    “element1”:”result1”,
    “element2”:”result2”,
    “element3”:”result3”,
    …
}

需求整理

下面是個簡單的系統內的分層關係圖
分層
1.Controller只做請求的轉發,把不同種類的請求轉發到不同的service,我們目前的需求只涉及到一個service。
2.Service的實現,這部分的功能包括,請求資料的解析與拆分->請求資訊因子名與因子的對應->因子實現的呼叫與執行結果的收集->結果返回上層。
3.灰色部分就是因子的實現部分。因子是一個輸入輸出相對固定的小單元,在設計時要注意要利於擴充套件。

期望

這樣的一個需求要設計成什麼樣子才是最好的呢?在系統設計的初期制定一個最佳的期望是有必要的,有了目標所有的系統設計就具有了方向。
參考上圖,這個系統的痛點就是在灰色部分,此係統設計好了以後我們以後的絕大多數工作將是對因子進行擴充套件。所以在設計是就要考慮儘量要在擴充套件因子時變的具有獨立性

非侵入性
何為獨立性非侵入性
獨立性即在增加因子時除了因子本身的新增與刪除,不需要改動如Service層等其他程式碼模組。
非侵入性即在擴充套件時除了輸入輸出因子的內部實現不需要遵循特定的規範,因子的內部實現是自由的。

方案設計

方案1 使用條件表示式

在service層內使用條件表示式來區分不同的因子名,從而執行不同的方法。
優點:誰都可以想到。
缺點:因子的實現與service層嚴重耦合,在新增因子時要同步改變service,而且在因子不斷增多時條件表示式會不斷變長,當有上百個因子時service層的程式碼已是不敢想象了。

方案2 使用靜態代理

宣告一個因子介面,所有每一個因子的實現都要繼承這個介面,即每個因子就是一個類。在service層維護一個map,存放因子名與因子實現類例項的鍵值對,執行時只要根據鍵從map裡邊拿到因子例項,執行介面方法即可得到結果。
優點:靜態代理的方法與1比較明顯規矩了不少,耦合性名優1那麼高。
缺點:還是存在獨立性

問題,在新增因子是必須要修改service維護的map才行。而且每一個類都是一個因子,但因子變多了以後會產生很多的類,所有的類都是平行的關係,無法對因子進行分組管理,而且即使實現十分相近的兩個因子也必須寫兩個類,程式碼不好複用。

方案3 使用反射

與2一樣在service層內維護一個map,鍵是因子名,值是因子的例項與方法的陣列,這樣每個因子是一個方法,一個類裡邊可以維護多個因子。執行時根據因子名到map中拿到因子對應的方法與例項(引數列表固定),反射得出結果。
優點:有利於因子的分組程式碼複用。
缺點:增加因子需要修改service層程式碼,map很難維護。

方案4 仿照SpringMVC實現字串與方法對映

這個其實與方案3很像,繼承了方案3的優點,克服方案3的缺點即通過註解實現把map的維護交給spring容器來管理,這樣在增刪因子時不需要修改其他程式碼。
請求流程
請求流程
現在的問題就是
1)該怎樣在spring容器初始化時自動的把因子名與因子類/因子方法填裝在一個map中。
2)怎樣讓Service得到這個map。
為了解決以上問題需要一下幾步
使用註解標註類與方法,程式碼如下:
註解宣告

/**
 * Usage:
 * 因子類註解
 * Description:
 * User: fuxinpeng
 * Date: 2017-12-25
 * Time: 下午3:52
 */
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ElementHandler {
    String value() default "";
}

這裡ElementHandler一定要繼承註解@Component,這樣可以使@ElementHandler標註的類在spring初始化的時候,自動例項化到applicationContext中。詳細瞭解可以去這篇部落格深入Spring:自定義註解載入和使用

/**
 * Usage:
 * 因子方法註解
 * Description:
 * User: fuxinpeng
 * Date: 2017-12-25
 * Time: 下午3:54
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ElementMapping {
    String value() default "";
}

註解使用

/**
 * Usage:
 * 使用註解標註因子類與方法
 * Description:
 * User: fuxinpeng
 * Date: 2018-01-18
 * Time: 上午11:05
 */
@SuppressWarnings("unused")
@ElementHandler
public class ProductTypeElements {

    @ElementMapping("element_1")
    public Object element(JSONObject param) {
        //內部實現
        retrun "I am return data";
    }
}

因子探測
填裝map(裝有因子名與因子類/方法的鍵值對,稱ElementContext)
使外部可見

/**
 * Usage:
 * <p>
 * Description:
 * User: fuxinpeng
 * Date: 2017-12-25
 * Time: 下午3:57
 */
@Component
@Log4j2
//繼承ApplicationContextAware來獲得SpringContext
public class DetectElementMapping implements ApplicationContextAware{
    private ApplicationContext applicationContext;
    //稱elementContext
    public HashMap<String, Object[]> elementMapping;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
    //從這裡開始
    //spring類初始化後開始初始化elementContext,
    @PostConstruct
    public void initElementContext(){
        detectElements();
    }

    private void detectElements() {
        HashMap<String, Object[]> elementMapping = Maps.newHashMap();
        //從applicationContext中獲得標識有ElementHandler註解的例項
        Map<String, Object> beans = applicationContext.getBeansWithAnnotation(ElementHandler.class);
        //遍歷所有ElementHandler的class
        for (Map.Entry bean : beans.entrySet()) {
            Class<?> clazz = bean.getValue().getClass();
            Method[] declaredMethods = clazz.getDeclaredMethods();
            //遍歷所有方法查詢被ElementMapping註解標識的方法
            for (Method method:declaredMethods){
                if(!method.isAnnotationPresent(ElementMapping.class)){
                    continue;
                }
                ElementMapping annotation = method.getAnnotation(ElementMapping.class);
                String elementName = annotation.value();
                //用來執行反射的要素(args固定即可不需要關注)
                Object[] instanceMethodMapping = {bean.getValue(),method};
//把因子名和該因子的反射要素放到map裡
                elementMapping.put(elementName,instanceMethodMapping);
                this.elementMapping=elementMapping;

            }
            //獲取springContext中繼承了ElementContextAware介面的類
            Map<String, ElementContextAware> elementContextAware = applicationContext.getBeansOfType(ElementContextAware.class);
            //遍歷所有類呼叫set方法,把初始化完成的ElementContext傳遞出去
            for(ElementContextAware e:elementContextAware.values()){
                e.setElementContext(elementMapping);
            }
        }
    }
}

//該介面的作用與ApplicationContextAware異曲同工

/**
 * Usage:
 * 繼承介面即可獲知ElementContext
 * Description:
 * User: fuxinpeng
 * Date: 2017-12-25
 * Time: 下午5:55
 */
public interface ElementContextAware {
    void setElementContext(Map<String,Object[]> elementContext);
}

這樣我們的service只要繼承ElementContextAware即可得到裝有因子對應關係的map(ElementContext)

@Log4j2
@Service
public class ElementService implements ElementContextAware{
    private Map<String, Object[]> elementContext;

    /**
     * 計算出所有因子
     * @param params
     * @return
     */
    public Map<String, Object> getElements(JSONObject input) {
            List<String> elementNames = (List<String>)input.get("input");
        HashMap<String, Object> elementsResultMap = new HashMap<>();
        for(String elementName : elementNames){
            Object elementResult = getElementResult(elementName);
            elementsResultMap.put(elementName,elementResult);
        }
        return elementsResultMap;
    }



    /**
     * 通過反射得到結果
     * @param elementEntry
     * @param context
     * @return
     */
    private Object getElementResult(String elementName) {
        if(!elementContext.containsKey(elementName)){
            log.fatal("因子沒有對應實現:"+elementName);
            return null;
        }
        List<String> elementIds = elementEntry.getValue();
        Object[] instanceAndMethod = elementContext.get(elementEntry.getKey());
        JSONObject param = null;//param
        Object instance = instanceAndMethod[0];
        Method method = (Method)instanceAndMethod[1];
        Object result = null;
        try {
            result = method.invoke(instance,param);
        } catch (Exception e) {
            log.error("因子執行錯誤:",e);
        }
        return result;
    }

    @Override
    public void setElementContext(Map<String, Object[]> elementContext) {
        this.elementContext = elementContext;
    }
}