页面搭建动态对接web和provider框架

达芬奇密码2018-07-04 18:20
背景: 
1、16年底,活动落地页新模块持续增加,每次新增一个模块,都要ms前端、ms后端,h5前端、后端同步修改。并且后续出现些其他独立业务,需要在落地页加模块的场景。人力成本压力很大,急欲减少开发成本,提高效率。
基于此,落地页增加了一套模块库逻辑。把单模块配置抽离,每种模块一个唯一区分的moduleType,后台差异配置以json存储,这样后台增删改成接口基本不用修改;前台展示时,根据以moduleType/素材id作为url一部分,后端入口统一,以模板方法模式(公共部分提取),每种moduleType一种实现,以moduleType为依据调用不用实现,处理结果统一json格式返回给前端。
2、基于上述模板方法,扩张思路:其他业务方的模块(如试用模块,会员模块)的实现,是否可以交由具体也业务实现,且新增类型时,落地页工程不需要再次开发,就能实现功能呢? 基于此想法,调研实现了一种不引用api,动态服务发现、调用dubbo接口的方法。
3、17年页面搭建开工,设计之初就是为了替代落地页,提供更灵活、更强大功能。刚好把上述想法定制化应用进去

解决方案:
1、搭建定义桥接接口规范
2、各业务根据区块纬度,实现桥接接口,并注册为dubbo服务,不需要打包api
3、通过disconf配置区块类型、业务实现dubbo服务对应关系
4、服务发现:通过javassist动态创建业务方dubbo接口,基于spring,构建beanDefinition(设置为ReferenceBean.class) ,并通过BeanFactory初始化为dubbo消费者
5、构建区块type --》 业务dubbo接口映射关系
6、调用时,根据type找到具体实现,直接调用即可
代码实现:

/**
 * 桥接接口初始化类
 */
public class RemoteServiceCreator {

    private static Logger log = LoggerFactory.getLogger(RemoteServiceCreator.class);

    @Autowired
    private SpringIocUtil springIocUtil;
    @Autowired
    private BeanDynamicCreator beanDynamicCreator;
    @Autowired
    private DisconfigHolder disconfigHolder;

    //解析结果
    private final Map remoteProcessorMap = Maps.newHashMap();


    //基础接口全路径
    private static final String baseInterfaceName = "com.netease.kaola.pages.bridge.processor.PageBridgeProcessor";

    @Value("${dubbo.consumer.group}")
    private String remoteDubboGroup;

    /**
     * 定时任务,手动触发
     */
    public void refreshRemoteService() {
        log.info("kschedule task refreshRemoteService start");
        initRemoteService();
        log.info("kschedule task refreshRemoteService end");
    }

    /**
     * 解析配置,注册spring bean 及 dubbo消费者; 作为init-method,服务启动,bean初始化时执行
     */
    public void initRemoteService() {

        //解析配置信息, 配置第三方服务与区块type映射信息,包括类路径、dubbo group version等
        List remoteConfigModels = getRemoteServiceConfig();
        if (CollectionUtils.isEmpty(remoteConfigModels)) {
            log.info("配置信息为空");
            return;
        }

        long beginTime = System.currentTimeMillis();
        log.info("[RemoteServiceCreator] begin init remote service, beginTime :" + beginTime);

        for (RemoteConfigModel remoteConfigModel : remoteConfigModels) {
            //配置信息基本校验
            if (!checkConfig(remoteConfigModel)) {
                continue;
            }
            //兼容本地实现
            if (remoteConfigModel.isDefaultFlag()) {
                PageBridgeProcessor pageBridgeProcessor = (PageBridgeProcessor) springIocUtil.getBean("commonProcessor");

                if (pageBridgeProcessor == null) {
                    log.error("[RemoteServiceCreator] 默认实现不存在,请检查");
                } else {
                    bindProcess(remoteConfigModel.getRegionType(), pageBridgeProcessor);
                }
                continue;
            }
            
            String targetFullName = remoteConfigModel.getTargetFullName();
            if (springIocUtil.containsBean(targetFullName)) {
                //已经构建过,直接绑定,兼容不同type,调用同一实现
                PageBridgeProcessor pageBridgeProcessor = (PageBridgeProcessor) springIocUtil.getBean(targetFullName);
                bindProcess(remoteConfigModel.getRegionType(), pageBridgeProcessor);
                continue;
            }
            try {

                if (checkIsExistClass(targetFullName)) {
                    log.error("[RemoteServiceCreator] initRemoteService error, targetFullName already is class, " +
                            "targetFullName:" + targetFullName);
                }else {
                    //远程是否接口未引入,基于javassist,动态创建dubbo接口对应的api
                    parse(baseInterfaceName, targetFullName);
                }
                if (!checkIsExistClass(targetFullName)) {
                    log.info("[RemoteServiceCreator] initRemoteService error, targetFullName:" + targetFullName);
                    continue;
                }

                DefaultListableBeanFactory factory = beanDynamicCreator.getBeanFactory();

                if (factory.containsBean(targetFullName)) {
                    continue;
                }

                RootBeanDefinition beanDefinition = new RootBeanDefinition();
                beanDefinition.setBeanClass(ReferenceBean.class);//设置为ReferenceBean,getBean()时就会走到dubbo服务发现逻辑
                beanDefinition.setLazyInit(false);

                factory.registerBeanDefinition(targetFullName, beanDefinition);

                beanDefinition.getPropertyValues().addPropertyValue("id", targetFullName);
                beanDefinition.getPropertyValues().addPropertyValue("interface", targetFullName);
                //设置版本
                if (StringUtils.isNotBlank(remoteConfigModel.getDubboVersion())) {
                    beanDefinition.getPropertyValues().addPropertyValue("version", remoteConfigModel.getDubboVersion());
                }
                //设置分组
                if (StringUtils.isNotBlank(remoteConfigModel.getDubboGroup())) {
                    beanDefinition.getPropertyValues().addPropertyValue("group", remoteConfigModel.getDubboGroup());
                } else {
                    beanDefinition.getPropertyValues().addPropertyValue("group", remoteDubboGroup);
                }


                //getBean会实例化
                PageBridgeProcessor object = (PageBridgeProcessor) factory.getBean(targetFullName);
                log.info("[RemoteServiceCreator] init remote service " + targetFullName + " " + object);
                if (object != null) {
                    bindProcess(remoteConfigModel.getRegionType(), object);
                }
            } catch (Exception t) {
                log.info("[RemoteServiceCreator] initRemoteService error, targetFullName:" + targetFullName, t);
            }

            log.info("[RemoteServiceCreator] end init remote service, cost :" + (System.currentTimeMillis() -
                    beginTime));
        }
    }

    private List getRemoteServiceConfig() {
        List list = Lists.newArrayList();
        String json = disconfigHolder.getBridgeRemoteServiceConfig();
        if (StringUtils.isBlank(json)) {
            return list;
        }
        try {
            list = FastJsonUtil.parseList(json, RemoteConfigModel.class);
        } catch (Exception e) {
            log.error("[RemoteServiceCreator] 解析配置异常", e);
        }
        return list;
    }

    private boolean checkIsExistClass(String targetFullName) {
        try {
            Class clazz = Class.forName(targetFullName);
            return clazz != null;
        } catch (Exception e) {
            return false;
        }
    }

    //根据区块类型查询实现类
    public PageBridgeProcessor getRemoteProcessor(Integer regionType) {
        return remoteProcessorMap.get(regionType);
    }

    public Map getRemoteProcessorMap() {
        return remoteProcessorMap;
    }

    //绑定服务
    private void bindProcess(Integer regionType, PageBridgeProcessor processor) {
        remoteProcessorMap.put(regionType, processor);
    }


    /**
     * 动态创建接口,远程接口未引入
     *
     * @param baseFullName   基础接口全路径
     * @param targetFullName 目标接口
     * @throws Throwable
     */
    private void parse(String baseFullName, String targetFullName) throws Exception {

        ClassPool pool = ClassGenerator.getClassPool(ClassHelper.getCallerClassLoader(Wrapper.class));
        CtClass baseClazz = pool.get(baseFullName);
        //动态创建远程接口类,继承桥接基础接口
        pool.makeInterface(targetFullName, baseClazz).toClass();
    }

    //基本校验
    private boolean checkConfig(RemoteConfigModel remoteConfigModel) {
        if (remoteConfigModel == null) {
            return false;
        }
        if (!remoteConfigModel.isDefaultFlag() && StringUtils.isBlank(remoteConfigModel.getTargetFullName())) {
            log.error("[RemoteServiceCreator-checkConfig] 远程实现接口不能为空");
            return false;
        }
        if (!remoteConfigModel.isDefaultFlag() && remoteConfigModel.getTargetFullName().equals(baseInterfaceName)) {
            log.error("[RemoteServiceCreator-checkConfig] 远程实现接口不能直接使用基础接口");
            return false;
        }
        return true;
    }
接口入参:
public class FrontPageParam extends BaseParam{
    private static final long serialVersionUID = 1L;

    /**
     * 区块类型,用于判断走什么处理逻辑
     */
    private int regionType;

    /**
     * 区块资源id
     */
    private Long regionId;

    /**
     * 动态参数,具体业务自己定义,建议传json格式
     */
    private String dynamicParams ;

    /**
     * 预览标识, true: 预览, false: 非预览; 默认不是预览
     */
    private boolean previewFlag = false;

    /**
     * 请求来源,用于区分来自哪种页面
     */
    private Integer requestSource;

    /**
     * 模块id 预留
     */
    private Long moduleId;

    /**
     * 模块类型,预留
     */
    private int moduleType;


    /**
     * 页面id,预留
     */
    private Long pageId;
}
应用效果:
搭建系统
1、搭建呈现系统做成框架模式,可以作为一个桥接通道
2、动态增加模块,搭建的呈现系统可以不用上线
3、第三方业务的模块,可以让业务方自己逻辑,解放搭建人力
4、规范入参,出参格式,多区块可以并行处理(用dubbo异步调用)

存在的问题:
1、按搭建区块定制模式,无法复用到其他系统
2、动态服务发现,依赖disconf配置,有配置错误风险
3、服务发现后,无法动态删除(可优化)
本文来自网易实践者社区,经作者张晓单授权发布。