预备知识:注解,反射,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;
}
}