一场67万行代码的应用重构


来源:阿里云开发者


阿里妹导读

本文分享了作者对一个架构模糊,拥有67万行代码、46个module的超级应用重构过程中面临的一些问题,还探讨了重构的价值以及技术方案,最后对综合效果进行了分析。

一、背景

时值阿里巴巴集团客服技术对外商业化。工单系统是客服产品里面非常重要的系统,之前服务于集团内部各部门的客服业务,显然基于微服务的架构是比较合适的,应用分拆比较细,开发运维更加独立、灵活、高效。继承原来这套工单系统优秀的基础技术和产品设计,为客服系统商业化进程打下了坚实的基础。但随之也带来了一些问题:微服务的复杂性对于更关注资源成本且开发人手有限的toB业务团队,运维起来比较厚重。为了节省部署资源和降低运维成本,我们在代码层面进行了一次合并,经过2年的迭代,堆积大量的需求,原本清晰的架构体系,演变成了一个架构模糊,拥有67万行代码、46个module的超级应用。

工单系统商业化过程的演变

二、面临问题(我们迭代2年之后的代码版本)

  • 代码多,需求开发运维成本高,排查问题难度大。

  • 编译打包的jar包文件大,加载许多无用的组件和类,引了不少启动需要预热的富客户端,特别是对一些富客户端组件的依赖。构建、部署性能低。

  • 链路冗长、吃资源、单次调用各种重复查询。

  • 配置太多,迁移、部署成本高。

  • 顶层不同商业场景的业务逻辑,耦合在主干逻辑里面。

三、重构的价值

  • 简化开发运维成本;重新设计架构、分层,开发一套极致简洁且高内聚、低耦合的代码。

  • 提升开发人效(最大价值点)。大幅减少梳理源代码时间,大幅度提升部署速度(之前部署一次25-30分钟,现在3分钟之内)。

  • 降低资源成本。提升查询性能,降低对DB的压力;减少一些不必要的中间件的使用(比如之前所有动作记录都放redis);减少加载的组件类,降低整体对内存的消耗。

  • 提升系统稳定性。架构简单清晰,链路清爽简短,代码极简有序,这是保障稳定性的根基。

  • 清晰的代码架构和层次,方便后续对扩展能力的优化(扩展能力是工单最重要的能力)。

  • 以扩展点的形式解耦电商版和钉版等商业场景的代码逻辑,加强系统稳定性。

四、技术方案

1、总体架构(代码)

工单系统与电商类系统相比,它业务逻辑复杂,但业务流程不复杂,它复杂之处在于数据组装,多样的数据视图。不像电商,会有各种商品、价格、库存、营销、交易等联动行为,逻辑和流程都比较复杂。因此工单从分层设计来讲3层(service、manager、dao)就足够了。

新工单代码架构 v1.0


2、适配层(adapter)

老hsf接口和新的hsf接口映射层,无实际业务逻辑,纯粹的接口映射和出入参转换。

3、商业能力层(service)

商业能力层,是客户每一个操作直接调用到的服务,能真正产生经济效益。这个层面的代码只会调用manager模块提供的域能力接口或外部服务接口(外部服务集线器)。工单系统service层可以分为几个部分:

  • MTOP接口

工作台前端页面调用,重构之后重新划分为18个细分业务场景,对应18个接口。也可以归纳成几个大场景:工单活动执行、工单配置态、工单列表查询、工单详情展示、移动工单、访客工单、工单中心配置、工单评价、touch读写。

  • OpenApi接口

开放给客户调用的,通过网关发布到公网,这块服务之前不少接口没有充分收敛,这次重构全部收敛到域能力,或者直接复用mtop接口。

  • 电商版工单接口

这块是电商版工单场景下新写的接口,重构之后把电商相关服务(包括定时任务)放到独立的package。

  • 工单活动

这里的工单活动模块是在HSF服务层之下,将所有工单活动核心逻辑抽象到一个个xxxActicity类里面。如:CaseCreateActivity、CaseTransferActivity等等。在这里设计了一个活动模板,如何设计的?直接上代码:




































































































public abstract class BaseActivity<P extends ActivityCtx, R extends BaseResult> {
   protected static final EventProducer eventProducer = ofBean("eventProducer");
   protected static final EventBusProvider eventBusProvider = ofBean(EventBusProvider.class);
   protected static final TaskProcessor taskProcessor = ofBean(TaskProcessor.class);
   protected static final ActionRecordManager actionRecordManager = ofBean(ActionRecordManager.class);
   protected P activityCtx;
   protected static T ofBean(Class clazz) {        return SpringContext.getBean(clazz);    }
   protected static T ofBean(String beanId) {        return SpringContext.getBean(beanId);    }
   public R run() {        R result;        try {            CheckResult checkResult = checkExtensionPermission();            if (Objects.nonNull(checkResult) && BooleanUtils.isFalse(checkResult.isPermission)) {                return (R)new BaseResult().setSuccess(false).setWarningMsg(checkResult.warningMsg);            }            checkParams();            preExecute();            ActivityExtManager.getPreExeExt(                this.bizId(),                this.getClass()            ).preExecute(activityCtx);            result = execute();            setTaskStatus2Processing();            postExecute();            ActivityExtManager.getPostExeExt(                this.bizId(),                this.getClass()            ).postExecute(activityCtx);            if (BooleanUtils.isTrue(activityCtx.getIfRecordAction())) {                recordAction();            }            sendEvent();        } catch (NormalInterruptException e) {            return (R)new BaseResult().setSuccess(false).setWarningMsg(e.getWarningMsg());        } finally {            this.activityCtx = null;        }        return result;
   }
   protected abstract String bizId();
   protected abstract void checkParams();
   protected abstract void preExecute();
   protected abstract R execute();
   protected abstract void postExecute();
   protected abstract void recordAction();
   protected abstract void sendEvent();
   public static , P extends ActivityCtx, R extends BaseResult> A of(Class clazz, P baseParam) {        A instance;        try {            instance = clazz.newInstance();        } catch (InstantiationException | IllegalAccessException e) {            throw new RuntimeException(e);        }        instance.activityCtx = baseParam;        return instance;    }
   private CheckResult checkExtensionPermission() {        return null;    }
   private static class CheckResult {        public Boolean isPermission;        public String warningMsg;
   }
   private void setTaskStatus2Processing() {        TaskStatusToProcessing processing = this.getClass().getAnnotation(TaskStatusToProcessing.class);        if (processing == null) {            return;        }        TppTaskIdDO tppTaskIdDO = activityCtx.getTaskDO();        tppTaskIdDO.setTaskStatus(PROCESSING.getCode());        taskProcessor.doSetProcessing(tppTaskIdDO.getId(), activityCtx.getUserParam().getUserId());    }
}
























































@Datapublic class ActivityCtx<Ctx extends ActivityCtx> {
   private UserParam userParam;
   private Map ext;
   private String reqSource;
   //是否记录动作记录    private Boolean ifRecordAction = Boolean.TRUE;
   private final Map, BaseDO> clazz2Domain = new ConcurrentHashMap<>();
   public T getEntity(Class tClass) {        T t = (T)clazz2Domain.get(tClass);        if (t == null) {            throw new RuntimeException(String.format("cannot find entity %s", tClass.getSimpleName()));        }        return t;    }
   public Ctx addEntity(BaseDO domain) {        if (domain == null) {            throw new RuntimeException("domain cannot be null");        }        clazz2Domain.put(domain.getClass(), domain);        return (Ctx)this;    }
   public Ctx addEntities(BaseDO... domains) {        for (BaseDO domain : domains) {            if (domain == null) {                throw new RuntimeException("domain cannot be null");            }            clazz2Domain.put(domain.getClass(), domain);        }        return (Ctx)this;    }
   public OspTppCaseDO getCaseDO() {        OspTppCaseDO caseDO = (OspTppCaseDO)clazz2Domain.get(OspTppCaseDO.class);        return caseDO;    }
   public TppTaskIdDO getTaskDO() {        if (clazz2Domain.containsKey(TppTaskIdDO.class)) {            return (TppTaskIdDO)clazz2Domain.get(TppTaskIdDO.class);        }        if (clazz2Domain.containsKey(TppTaskCaseDO.class)) {            return (TppTaskCaseDO)clazz2Domain.get(TppTaskCaseDO.class);        }        return null;    }
}





































































































































public class CaseCreateActivity extends BaseActivity<CaseCreateCtx, Table2<OspTppCaseDO, TppTaskIdDO>> {
   private static final CaseInstanceManager caseInstanceManager = ofBean(CaseInstanceManager.class);
   private static final StateMachineManager stateMachineManager = ofBean(StateMachineManager.class);
   private static final TaskInstanceManager taskInstanceManager = ofBean(TaskInstanceManager.class);
   private static final CaseTemplateManager caseTemplateManager = ofBean(CaseTemplateManager.class);
   private static final AccountServiceHub accountServiceHub = ofBean(AccountServiceHub.class);
   private static final BizKeyUtils bizKeyUtils = ofBean(BizKeyUtils.class);
   @Override    protected String bizId() {        return bizKeyUtils.buildBizKey(activityCtx.getUserParam().getBuId());
   }
   @Override    protected void checkParams() {        Preconditions.checkArgument(activityCtx.getCaseParam() != null);        Preconditions.checkArgument(activityCtx.getUserParam() != null);        Preconditions.checkArgument(activityCtx.getEntity(TppCaseTypeDO.class) != null);        Preconditions.checkArgument(activityCtx.getEntity(StateMachineDO.class) != null);        Preconditions.checkArgument(activityCtx.getEntity(SrTypeDO.class) != null);        Preconditions.checkArgument(activityCtx.getActionParam() != null);
   }
   @Override    protected void preExecute() {    }
   @Override    protected Table2 execute() {
       OspTppCaseDO ospCaseDO = buildCaseDO(activityCtx.getUserParam(), activityCtx.getCaseParam(), activityCtx.getEntity(TppCaseTypeDO.class),            activityCtx.getEntity(StateMachineDO.class));        caseInstanceManager.saveCase(ospCaseDO);        activityCtx.addEntity(ospCaseDO);
       TppTaskIdDO taskIdDO = buildTaskDO(activityCtx.getUserParam(), ospCaseDO.getId(), activityCtx.getTaskParam(),            activityCtx.getEntity(TppCaseTypeDO.class));        taskInstanceManager.saveTask(taskIdDO);        activityCtx.addEntity(taskIdDO);
       return new Table2().setT1(ospCaseDO).setT2(taskIdDO);    }
   @Override    protected void postExecute() {
   }
   @Override    protected void recordAction() {        if (activityCtx.getCaseParam().isFinish()) {            activityCtx.getActionParam().setActionCode(ActionCodeEnum.CASE_FINISH.getCode());        } else {            activityCtx.getActionParam().setActionCode(ActionCodeEnum.CASE_CREATE.getCode());        }        com.xixikf.caze.utils.ActionMemoBuilder memoBuilder = activityCtx.getActionParam().getActionMemoBuilder();        memoBuilder.addOperatorNick(activityCtx.getUserParam().getUserName());        memoBuilder.addAcceptorNick(activityCtx.getUserParam().getMemberName());
       JSONObject obj = new JSONObject();        obj.put("commonQueueId", activityCtx.getTaskParam().getCommonQueueId());        obj.put("sopCateId", activityCtx.getCaseParam().getSopCateId());        obj.put("srType", activityCtx.getCaseParam().getSrType());        memoBuilder.put("$custom", obj);        memoBuilder.addActionKeyMemo(            ActionMemoExposer.buildActionKeyMemo(activityCtx.getEntity(SrTypeDO.class).getFormCode(), activityCtx.getCaseParam().getFormData(),                emptyIfNull(activityCtx.getCaseParam().getFormBizData()))        );        LtppActionDO actionDO = actionRecordManager.saveAction(activityCtx.getActionParam().getActionCode(), memoBuilder.toJSONString(),            activityCtx.getEntity(OspTppCaseDO.class),            activityCtx.getEntity(TppTaskIdDO.class),            activityCtx.getUserParam().getUserId());        activityCtx.addEntity(actionDO);    }
   @Override    protected void sendEvent() {        OspTppCaseDO ospTppCaseDO = activityCtx.getEntity(OspTppCaseDO.class);        SrTypeDO srTypeDO = activityCtx.getEntity(SrTypeDO.class);        /*         * 工单事件消息         */        CaseCreateEvent caseCreateEvent = new CaseCreateEvent(ospTppCaseDO, srTypeDO, activityCtx.getUserParam());        eventProducer.sendEvent(caseCreateEvent);        eventBusProvider.sendEvent(caseCreateEvent);        List eventDOList = JSONArray.parseArray(activityCtx.getEntity(StateMachineDO.class).getEventSchema(), EventDO.class);        Optional optional = eventDOList.stream()            .filter(eventDO -> eventDO.getEventStateType() == EventDO.caseEventType.START.code()).findFirst();        CaseStateInitEvent caseStateInitEvent = new CaseStateInitEvent(ospTppCaseDO, srTypeDO,            optional.orElseThrow(() -> new RuntimeException("status[START] not found")), activityCtx.getUserParam());        eventBusProvider.sendEvent(caseStateInitEvent);        /*         * 任务事件消息         */        TaskCreateEvent taskCreateEvent = new TaskCreateEvent(activityCtx.getEntity(TppTaskIdDO.class), ospTppCaseDO, null);        eventProducer.sendEvent(taskCreateEvent);        eventBusProvider.sendEvent(taskCreateEvent);        /*         * 动作记录消息         */        ActionCreateEvent actionCreateEvent = new ActionCreateEvent(activityCtx.getEntity(LtppActionDO.class));        eventBusProvider.sendEvent(actionCreateEvent);        //创建子工单消息
       //todo for 自动外呼        if (!Arrays.asList(226410, 226411, 226412).contains(activityCtx.getCaseParam().getCaseType())) {            if (activityCtx.getCaseParam().getParentCaseId() != null && activityCtx.getCaseParam().getParentCaseId() > 0) {                OspTppCaseDO parentCaseDO = caseInstanceManager.loadCaseDO(activityCtx.getCaseParam().getParentCaseId());                SrTypeDO parentCaseTemplateDO = caseTemplateManager.getCaseTemplate(parentCaseDO.getSrType());                CaseChildCreateEvent caseChildCreateEvent = new CaseChildCreateEvent(parentCaseDO, parentCaseTemplateDO, activityCtx.getUserParam());                eventBusProvider.sendEvent(caseChildCreateEvent);            }        }
       //发送“创建工单”活动被执行消息        if (StringUtils.isNotBlank(activityCtx.getCaseParam().getChannelTouchId()) && StringUtils.isNumeric(activityCtx.getCaseParam().getChannelTouchId())) {            OspTppCaseDO caseDO = caseInstanceManager.loadCaseDO(Long.parseLong(activityCtx.getCaseParam().getChannelTouchId()));            SrTypeDO caseTemplate = caseTemplateManager.getCaseTemplate(caseDO.getSrType());            CaseCreate4ChannelEvent caseCreate4ChannelEvent = new CaseCreate4ChannelEvent(caseDO, caseTemplate, activityCtx.getUserParam());            eventBusProvider.sendEvent(caseCreate4ChannelEvent);        }    }

}



Table2 createResult = BaseActivity.of(CaseCreateActivity.class,            new CaseCreateCtx(userParam, caseParam, taskParam, actionParam, extParam).addEntities(srTypeDO, caseTypeDO, stateMachineDO)        ).run();

扩展点的设计是为了在主干代码中,解耦电商版和钉版工单这些不同业务场景的逻辑,提高系统的稳定性和可扩展性。考虑到扩展点的实现逻辑性质上是最顶层、最具体的业务逻辑,所以把扩展点的实现也放到了商业能力层,代码组织和扩展点实现示例如下:
先看一下具体怎么用的:
接下来介绍下扩展点框架怎么设计的,啥也别说,code first:













public interface MonoExtPoint<P, R> {    R execExt(P param);
}public interface BiExtPoint<P1, P2, R> {    R execExt(P1 param1, P2 param2);
}public interface TriExtPoint<P1, P2, P3, R> {    R execExt(P1 param1, P2 param2, P3 param3);
}



































































































































@Component@Slf4jpublic class CommonExtManager {    private static final Map<String, MonoExtPoint> monoExtPoints = new ConcurrentHashMap<>();    private static final Map<String, BiExtPoint> biExtPoints = new ConcurrentHashMap<>();    private static final Map<String, TriExtPoint> triExtPoints = new ConcurrentHashMap<>();
   @Autowired    public void setMonoExtPoints(Collection monoExtList) {        for (MonoExtPoint monoExt : emptyIfNull(monoExtList)) {            CommRouter router = monoExt.getClass().getAnnotation(CommRouter.class);            if (Objects.isNull(router)) {                throw new RuntimeException("router annotation cannot be null");            }            if (Strings.isBlank(router.key())) {                throw new RuntimeException("router key cannot be null");            }            String key = buildRouterKey(router.key(), router.scene());            if (monoExtPoints.containsKey(key)) {                throw new RuntimeException(String.format("key[%s] is duplicate", router.key()));            }            monoExtPoints.put(key, monoExt);            log.warn("MonoExtPoint:{}", key);        }    }
   @Autowired    public void setBiExtPoints(Collection biExtList) {        for (BiExtPoint biExt : emptyIfNull(biExtList)) {            CommRouter router = biExt.getClass().getAnnotation(CommRouter.class);            if (Objects.isNull(router)) {                throw new RuntimeException("router annotation cannot be null");            }            if (Strings.isBlank(router.key())) {                throw new RuntimeException("router key cannot be null");            }            String key = buildRouterKey(router.key(), router.scene());            if (biExtPoints.containsKey(key)) {                throw new RuntimeException(String.format("key[%s] is duplicate", router.key()));            }            biExtPoints.put(key, biExt);            log.warn("BiExtPoint:{}", key);        }    }
   @Autowired    public void setTriExtPoints(Collection triExtList) {        for (TriExtPoint triExt : emptyIfNull(triExtList)) {            CommRouter router = triExt.getClass().getAnnotation(CommRouter.class);            if (Objects.isNull(router)) {                throw new RuntimeException("router annotation cannot be null");            }            if (Strings.isBlank(router.key())) {                throw new RuntimeException("router key cannot be null");            }            String key = buildRouterKey(router.key(), router.scene());            if (triExtPoints.containsKey(key)) {                throw new RuntimeException(String.format("key[%s] is duplicate", router.key()));            }            triExtPoints.put(key, triExt);            log.warn("TriExtPoint:{}", key);        }    }
   public static MonoExtPoint getMonoExtPoint(String key, RouterScene scene, MonoExtPoint defaultFuc) {        if (Strings.isBlank(key) || Objects.isNull(scene)) {            return defaultFuc;        }        String _key = buildRouterKey(key, scene);        return (param) -> {            try {                MonoExtPoint monoExtPoint = monoExtPoints.get(_key);                if (monoExtPoint != null) {                    return monoExtPoint.execExt(param);                } else {                    log.warn(String.format("key:[%s] not find implement", _key));                }            } catch (Throwable e) {                log.error(String.format("key:[%s] execute error", _key), e);                return null;            }            return defaultFuc.execExt(param);        };    }
   public static BiExtPoint getBiExtPoint(String key, RouterScene scene, BiExtPoint defaultFuc) {        if (Strings.isBlank(key) || Objects.isNull(scene)) {            return defaultFuc;        }        String _key = buildRouterKey(key, scene);        return (param1, param2) -> {            try {                BiExtPoint biExtPoint = biExtPoints.get(_key);                if (biExtPoint != null) {                    return biExtPoint.execExt(param1, param2);                } else {                    log.warn(String.format("key:[%s] not find implement", _key));                }            } catch (Throwable e) {                log.error(String.format("key:[%s] execute error", _key), e);                return null;            }            return defaultFuc.execExt(param1, param2);        };    }
   public static TriExtPoint getTriExtPoint(String key, RouterScene scene, TriExtPoint defaultFunc) {        if (Strings.isBlank(key) || Objects.isNull(scene)) {            return defaultFunc;        }        String _key = buildRouterKey(key, scene);        return (param1, param2, param3) -> {            try {                TriExtPoint triExtPoint = triExtPoints.get(_key);                if (triExtPoint != null) {                    return triExtPoint.execExt(param1, param2, param3);                } else {                    log.warn(String.format("key:[%s] not find implement", _key));                }            } catch (Throwable e) {                log.error(String.format("key:[%s] execute error", _key), e);                return null;            }            return defaultFunc.execExt(param1, param2, param3);        };    }
   private static String buildRouterKey(String key, RouterScene scene) {        return String.format("%s_%s", scene.name(), key);    }
}























public enum RouterScene {    CaseCard("touch工单卡片展示"),    CaseCreate("工单创建"),    DisplayColumn("工单列表展示字段"),    SearchResult("工单列表返回结果"),    BasicInfoView("工单基本信息卡片"),    ResultConvert("列表查询结果转换"),    AutoTaskCondition("自动任务条件"),    DefaultActivities("默认活动列表"),    RoleAndAuth("角色列表权限"),    TransferRelations("工单转交关系"),    ConfigRule("自动任务配置规则"),    Protocol("协议"),    ViewConf("视图配置"),    CaseTypeList("工单类型列表");
   @Getter    private final String desc;
   RouterScene(String desc) {        this.desc = desc;    }}

工具类没啥好说的,都是这些年个人总结的好用工具方法,多用工具类,无论是自己写的还是其他三方的如guava。工具类最大的好处是使代码更简洁,增强代码的可读性。不然一堆与核心业务逻辑无关的判断、校验、转换等逻辑充斥在代码里,使得代码重点不突出,影响开发者理解代码的效率。

4、域能力层(manager)

域能力层是将工单这个大的业务域拆分成若干子域,每个子域所具备的核心能力(从另一个视角来看它应该是商业能力层会多次复用的能力,具备一定通用性特征),用于支持和实现其业务目标也就是商业能力。工单系统manager层可以分为几个部分:

本次重构是将工单域拆分为30个子业务域,拆分规则基本上是基于产品模型和实体模型综合考量。一般只考虑产品模型可能会拆的太粗,只考虑实体模型可能会拆的太细,所以先取两者之间的一个平衡,然后在开发过程中根据细节反馈调整。域能力的拆分是没有固定标准的,全看个人理解和经验。在本文中30个子域具体具备的能力就不一一展开了,有兴趣了解的可以去看看代码,全是细节。

数据同步是整个工单系统中处理数据的异步链路,他虽然没有被商业能力层直接同步调用,但是它异步加工处理的数据,也会直接作用到商业能力层,因此无可厚非,这部分能力可以归到域能力层。这4条消费链路也是重新设计了流程和代码,下面看个核心case和task数据同步ES的链路流程设计:

这块是将工单依赖的外部服务按照提供者应用名称(一个应用对应一个类)对外部服务方法进行了收敛,封装了一层,入参基本不变,出参去掉了Result包装。这样做的目的:其一,可以统一监控外部服务的状况(当前是打印了简要的出入参日志);其二,方便后续工作中各类梳理评估,比如后面纪元替换xform。一条请求链路我们最关心的几个节点:网络请求、DB访问,一般链路追踪重点监控的主要也是远程服务和DB访问,因此监控好外部服务的调用情况,对于日常运维是非常有帮助的。

支撑整个manager层的运转抽象出来的脱离具体业务场景的通用能力,包括:搜索参数构建器、消息生产/消费模板、缓存工具类、通用工具类。

  1. 流转:这块原来是一个模块来承载它的代码,经过分析发现里面有很多流程是我们不需要的,状态机流转引擎主要干了三件事:持久化变化后的新状态、发送流程结束消息、发送活动事件消息。以此反推重写了状态机流转引擎代码,精简后的代码放在一个package里面,可以看到代码并不多。

  2. 发布:工单状态机整体是用一个大json去存储的,里面涉及多个实体对象的结构,然后实体对象转换成特定的schema存储,原来这块逻辑是写在一个大方法里面,逻辑主线不清晰,基本是看不懂。重构后的主线逻辑就很清晰了。

5、数据访问层(dao)

这就比较简单了,就是提供各个实体的mapper方法。原来应用里面是淘宝比较老版本的自己写的一套ORM框架,用起来是非常费劲,一次查询下来无论是看编译时还是运行时连访问的哪个表都不知道。这次是整体换成了mybatis,并且对其能力进行了增强,这里可以着重介绍下增强的逻辑。












@Table("ltpp_action")public interface LtppActionMapper extends BaseMapper {
   //自己通过xml写的sql    Long countBizId(Map<String, Object> where);    //自己通过xml写的sql    Long countByCondition(Map<String, Object> where);    //自己通过xml写的sql    List queryByCondition(Map<String, Object> where);
}

6、重新设计自动任务

自动任务是工单系统里面比较独立的模块,本质上就是消费工单产生的事件消息,通过校验,执行动作。因此它是一个典型的监听者模式。
自动任务新流程设计
重构前后的变化:

以上只是对比了几个比较明显方便展示出来的点,重构之后每一行代码都不一样,主要还是更加精简了,主线更清晰。自动任务这种异步链路,排查问题的效率非常重要,所以需要代码写的比较清晰简洁易于维护和理解。

五、综合效果分析

1、代码缩减的原因

(1)域能力和工具类的高度内聚,不存在同样的能力有多套代码实现。

(2)删除了所有无用的开关逻辑、特殊业务场景、参数检验、灰度逻辑、无效封装、异常捕获、无效日志代码,特别是Result的封装。

(3)重写了大量弯弯绕绕的业务逻辑。

(4)去掉原来自己开发的DB访问框架,统一为mybatis,并增加增强逻辑。
2、fatjar包大小缩减的原因

(1)去除了无用的pom依赖,全删了,然后一个个根据需要加。

(2)对比较重的pom依赖,打印出mvn tree,一个个把间接依赖排掉,然后测试看看影响,这一步比较耗时。
3、部署&启动速度提升的原因。

(1)fatjar包大小缩减和pom依赖的减少,会大幅减少jar包下载、上传的时间。

(2)去除了对codePlatform富客户端的依赖,这个客户端会在启动的时候去加载各种数据完成初始化,用jstack命令观察这个点耗时5min中左右,最高到7、8分钟,偶尔会超时导致启动失败。

(3)去掉了很多不需要注册spring的组件。
4、nacos配置缩减的原因。

(1)去掉了没用的配置,全删了一个个根据需求加回来。

(2)有些万年不变的配置可以回归到代码里面。

(3)一些控制前端渲染结构的配置(一般是大json),这种其实本质就是代码,基本不会改动,在工程Resource目录里面以json文件的形式保存。

六、上线方案(简单说明)

1、所有消息的topic切换成新的,避免新旧应用消息串扰带来不可控的问题。

2、通过流量统计工具统计的有流量的服务接口,查漏补缺。

4、灰度环境beta部分pod(比如搞5个pod,其中1个pod部署新应用,新应用的流量大约就是1/5),并逐步调整比例,直到全覆盖。

5、生产环境beta部分pod,并逐步调整比例,直到全覆盖。

6、所有异常日志推送钉钉群,实时感知,实时解。
7、随时关注数据库性能监控,关注慢sql,关注监控告警,特别是核心接口的失败率。
8、灰度过程中如发现异常情况随时准备kill掉新应用pod。

9、对于一些应用场景复杂、测试用例无法全面覆盖的接口服务(比如定制逻辑调的服务),如何最大程度减少上线故障的发生,我们的思路是在新代码和老代码同时部署在生产环境灰度发布的时候:

这样可以保证在新代码里面执行的逻辑最大程度不会出问题,告警出来可以及时发现问题。为此开发了一个切面框架,不用侵入业务代码。操作也不复杂,打一个注解就行了(如下图)。后续其他应用重构上线也可以复用这个工具。

七、对代码质量的思考

“梳理”是我们日常工作最消耗时间的动作,需求开发、私有化部署、技术改造......第一步都是要梳理应用代码,费时且容易遗漏。所谓梳理2小时,写码5分钟。清晰简洁的代码梳理成本是极低的。摆脱沉重的梳理只有提升代码架构和质量,科学技术是第一生产力。
如果把整个公司的协同看做一条依赖链路的话,开发和测试是基础服务,他的RT(耗时)会扩散到各个链路节点。现在市场,公司之间的竞争已经到了深水区,基本上你能做的别人也能做,不太可能再有一方拥有压倒性的优势(特别是toB,比如说不太可能出现销售能力很强,反正能拉到单子,光靠这个就建立了势不可挡的壁垒),一定就是在长时间的你追我赶过程中,看谁能在各方面多做好一点点,效率高一点点,在持续的时间累计下积累优势。从这些来看,好的架构和代码质量时间复利是比较大的。
1、业务形态的区别,注定了toC的代码直接用于toB业务是不合适的

2、对开发的意义:摆脱烟冲式开发(烟冲式就是每次都要从头来一遍,每次都要关注全局,梳理全局,如果扩展点设计的好那就可以只关注扩展点,如果分层很合理,那就只关注对应的层面的问题,不用牵一发动全身)。像平时工作过程中,要求沉淀文档,及时总结,都是摆脱烟冲式的工作的思路,不用每次问题来了,要重新回忆,重新思考,重新到处问。
3、总结几个标准:

4、重构的核心不是一次性的代码改造,更为重要的是它所传达的写精致代码的理念,要形成一个对每一行代码讲究的,写到让自己满意的意识和氛围。不然开发的人多了,马上打回原形。...... 今天又想了想,感觉在紧密的业务开发节奏下,每个人的意识和水平不一样,做到这些不太现实,还是得靠个人的强行干预,你们说呢?
5、不要认为一些小的问题不重要,大的点注意就行。比如命名很随意,无用的代码不删除,代码不格式化,风格规范不统一,idea的黄色提示不关注。。真实的规律可能是讲究的都讲究,不讲究的都不讲究。
6、最后一句话:如果你发现一个问题的解决方案搞的很复杂,那一定是方案有问题,写代码也是一样,简单才是王道。“简单点,代码的方式简单点”。

八、未来规划

工单是扩展定制需求最旺盛的系统,目前工单已有的扩展方式比较重,不够灵活轻量,未来会基于重构后的工单,对扩展能力做一次升级,主要往低代码、灵活、丰富和加强配套工具的方向去思考。最大程度提升定开效率,降低边际成本。

请使用浏览器的分享功能分享到微信等