• 还是16年年底,开发 CRM 时设计的 api 开发规范。
    • 配置有一点繁琐。
    • 配置好后,就可以专注于业务逻辑。
    • 适合中型团队多人协作。
    • 小团队不建议这么操作。
  • 该规范大量用到反射、接口、多态、工厂模式,新人不太容易理解。
  • 小团队可以没有接口和反射,但是可以有、并且建议合理使用多态和工厂模式。

一. api开发总则

  1. 遵循单一职责原则,一个类只做一件(类)事。
  2. 数据库表与业务类是 1:N 的关系。
  3. 简单业务表建议只有一个业务类,复杂业务表,建议有多个业务类。

二. 类、接口命名规范

根据业务规则,首字母大写,如,针对表:crmSysApp,业务命名:SysAppService,接口、实现类、抽象类,都是在这个命名的基础上进一步命名的,规则如下。

  1. 接口,在前面加 I,如:ISysAppService。
  2. 抽象类(非必须),在前面A,如:ASysAppService。
  3. 实现类,在后面加Impl,如:SysAppServiceImpl,需要加@Service注解。
  4. 处理类,在后面加Handler,如:SysAppServiceHandler。
  5. 处理类仅仅是业务实现类供消费方(client)进行消费的桥梁,不处理具体的业务。声明业务处理对象,必须也只能用接口来声明,如:ISysAppService sysAppService,且加上@Autowired注解。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class ShopServiceHandler extends AapiHandler
    {
    @Autowired
    IShopService shopService;

    public List<ShopInfoView> getShopInfoView(RequestParametersView requestParametersView)
    {
    return shopService.getShopInfoView(requestParametersView);
    }
    }

三. 开发步骤

  1. 接口。在com.maile360.crm.service.facade.api下,创建interface,参考:ISysAppService;抽象类为非必须,如果有需要,在同样的目录下创建即可。参数,统一为:RequestParametersView requestParametersView。
  2. 实现类。在com.maile360.crm.service.impl.api下,创建实现类,参考:SysAppServiceImpl
  3. 处理类。在com.maile360.crm.service.facade.handler下,创建处理类,参考:SysAppServiceHandler。处理类须 extends AapiHandler。
  4. 处理类配置。
    1. 是一个 xml 文件:提供 api 调用时的 apiMethod 值, 作用是关联 apiMethod 与接口名。
    2. 在 restful 模块的资源文件夹下,handlers 子目录下,添加对应的配置 xml 文件,文件名保持与处理类同名(便于维护和查找),如:SysAppServiceHandler.xml。
    3. 在该文件配置具体的 apiMethod(调用参数)与对应接口名。apiMethod 命名规则,需要继续往下看,看完第四项再回过头来操作。
    4. 好处:维护人员方便查找对应处理方法,并且 api 会自动由配置对应到接口,而接口会有具体的实现,接口到实现类的调用,由 spring boot 启动时根据 serviceConfig.xml 配置自动装载。具体调用哪个方法,则用到了 java 的反射。
  5. 配置处理类列表。修改 restful 模块的资源文件夹下的配置文件:serviceConfig.xml,把上面的 handler 配置到列表中。spring boot 启动时根据 serviceConfig.xml 配置自动装载各个 handler。

    1. 这里的配置,除了让 spring boot 启动时自动装载 handler 类,还有一个重要的目的是让系统根据请求的 apiMethod 自动匹配到 handler 类,然后 handler 类根据其配置的方法名自动去调用业务接口。如:

      1
      <entry key="crm.shop_config.get.shop.config.view" value="getShopConfigView"/>
    2. 配置处理类列表 key 命名规则: crm_ + 下面第四项针对 apiMethod 命名规则中的 Y + 下划线 + 版本号。

      1. 如:crm_sys_sms_template_1.0,其中 Y = sys_sms_template, 版本号 = 1.0
      2. 处理类的 key 只用下划线分隔。

四. Handler xml 配置文件规则

总则:一个 Service 对应一个 Handler,一个 Handler 对应一个 xml 配置文件,存放到模块 restful 下,resources/handlers 目录,命名规则:XServiceHandler.xml,其中 X 为对应Service的名字,如:ShopConfigServiceHandler.xml

  1. 为了讲解 xml 配置文件的 key 和 value 命名规则,需要先了解与配置文件相关的接口和处理类。
  2. 所有处理类,即 Handler 都要继承的抽象类:AApiHandler。抽象类中定义了 Map 类型的变量 methods。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    public abstract class AApiHandler
    {
    public Map<String, String> methods;

    public void setMethods(Map<String, String> methods)
    {
    this.methods = methods;
    }
    }

以 ShopConfigService 为例,先看实际例子。

  1. 接口定义 IShopConfigService.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    package com.maile360.crm.service.facade.api;

    import crm.model.view.RequestParametersView;
    import crm.model.view.out.ShopConfigView;

    public interface IShopConfigService
    {
    int addOrUpdate(RequestParametersView requestParametersView);

    ShopConfigView getShopConfigView(RequestParametersView requestParametersView);
    }
  2. 处理类实例 ShopConfigServiceHandler.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    package com.maile360.crm.service.facade.handler;

    import org.springframework.beans.factory.annotation.Autowired;
    import com.maile360.crm.service.facade.api.IShopConfigService;
    import com.maile360.crm.service.facade.base.AApiHandler;
    import crm.model.view.RequestParametersView;
    import crm.model.view.out.ShopConfigView;

    public class ShopConfigServiceHandler extends AApiHandler
    {
    @Autowired
    IShopConfigService shopConfigService;

    public int addOrUpdate(RequestParametersView requestParametersView)
    {
    return shopConfigService.addOrUpdate(requestParametersView);
    }

    public ShopConfigView getShopConfigView(RequestParametersView requestParametersView)
    {
    return shopConfigService.getShopConfigView(requestParametersView);
    }
    }
  3. Handler 配置: ShopConfigServiceHandler.xml。其中 property 属性中的 methods 就是来自抽象类 AApiHandler 的 Map 类型的变量,这里的配置,spring boot 自动装载时会自动初始化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-autowire="byName">
    <bean name="shopConfigServiceHandler" class="com.maile360.crm.service.facade.handler.ShopConfigServiceHandler">
    <property name="methods">
    <map>
    <entry key="crm.shop_config.add.or.update" value="addOrUpdate"/>
    <entry key="crm.shop_config.get.shop.config.view" value="getShopConfigView"/>
    </map>
    </property>
    </bean>
    </beans>
  4. 该处理类配置,在 serviceConfig.xml 文件中的配置如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://www.springframework.org/schema/beans"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd"
    default-autowire="byName">
    <import resource="classpath:handlers/*.xml" />
    <import resource="classpath:dbSource.xml" />
    <bean name="restApiService" class="com.maile360.crm.service.impl.base.RestApiServiceImpl">
    <property name="handlers">
    <map>
    <entry key="crm_shop_config_1.0" value-ref="shopConfigServiceHandler" />
    </map>
    </property>
    </bean>
    </beans>

其实,就是初始化 com.maile360.crm.service.impl.base.RestApiServiceImpl 的 handlers(Map 类型)

Handler 配置文件中 key 命名规则

  1. 分三部分,这里分别用大写的 X,Y,Z 来表示,用英文的点”.”来连接,得到:X.Y.Z,其中每一部分的英语字母全部用小写。
  2. X 为前缀,固定:crm, 即 X = crm
  3. Y 为服务名,英语字母加下划线,可以参考业务对应的表名,如上面的例子,Y = shop_config
  4. Z 为具体的业务方法名标志,最好能通过命名看出意义来,中间以”.”隔开,不限制长度,如上面的例子,Z = get.shop.config.view
  5. 将上面的 X,Y,Z,用英文的点”.”来连接,就得到 apiMethod = crm.shop_config.get.shop.config.view

Handler 配置文件中 value 配置

这里的 value 跟接口名对应,建议将 key 中 Z 部分的”.”去掉,再将除第一个单词之外的单词首字母大写,到得接口名。如:get.shop.config.view –> getShopConfigView

对 key 的处理,从而得到对应的 Handler,再进一步就可以找到对应 Handler 的接口了 。

这里说接口,其实不恰当,其实是 Handler 普通方法调用了接口而已。不过,可以理解为接口,因为 Handler 仅仅是中转调用,自身并不处理业务逻辑。

  1. crm.shop_config.get.shop.config.view 调用下面的方法之后,requestParametersView.apiServiceName = crm.shop_config

    1
    2
    3
    4
    5
    6
    private void setRequestApiServiceName(RequestParametersView requestParametersView)
    {
    int endIndex = StringHelper.getCharacterPosition(requestParametersView.apiMethod, ".", 2);// getCharacterPosition -> 获取字符串中第N次出现的字符位置 没有出现N次,则返回-1
    AssertUtils.isTrue(endIndex > 0, ExceptionCodeEnum.notExistsService);
    requestParametersView.apiServiceName = requestParametersView.apiMethod.substring(0, endIndex);
    }
  2. 然后,通过 apiServiceName 和版本号,就可以找到匹配的处理类 Handler 了。接着上面的,应该得到:crm_shop_config_1.0

    1
    2
    3
    4
    private String generateServerKey(String apiServiceName, VersionEnum version)
    {
    return apiServiceName.replace(".", "_") + "_" + version.getVersion();
    }

五. 返回数据类型

  1. 不建议返回类型为Map。在返回类型是确定的情况下,比如是一个int型的数字,或者某个确定的pojo以及某个pojo的列表,则直接返回这个确定的类型。
  2. 如果返回的数据直接是数据库表的记录,则返回对应的实体即可。
  3. 如果返回的数据需要加工整合,则在 com.maile360.crm.view.out 下,创建对应的view类,类名须以View结束。如:ShopInfoView,字段属性为private,需要setter和getter。
  4. 如果返回的数据除了基本类型,还包含实体,即定义在com.maile360.crm.dal.entity下的pojo(非Example类),而crm.view项目下又无法引用这部分pojo,那么,这种情况下,需要在com.maile360.crm.dal.view包下增加返回view的pojo。参考:OrderSmsOptionByTradeView、TopTmcMessageQueueView
  5. 如果需要返回多个结果集,定义一个pojo来组装,参考上面。

六. 入参

  1. 不建议使用Map当作mapper的参数类型。入参多于两个,建议添加pojo来接收。如果在程序内部,也建议通过pojo来组装查询参数。
  2. 如果入参只有一个,比如仅为id,则可以这样获取:

    1
    2
    3
    4
    int id = (int)requestParametersView.objApiParas.get("id");
    //如果客户端传的是string类型,则上面的转换会报错:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
    //改为:
    int id = Integer.parseInt(requestParametersView.objApiParas.get("id").toString());
  3. 添加pojo来接收参数时的规则:类名须以View结束。如:ShopSearchConditionView。放在 com.maile360.crm.view.in 包下,字段属性建议直接设置为public,这样,就只需要设置setter而不需要getter。入参接收需在自己的实现类定义和初始化:

    1
    ShopSearchConditionView shopSearchConditionView = JSONHelper.json2Object(requestParametersView.apiParas, ShopSearchConditionView.class);

七. mapper扩展

  1. 在com.maile360.crm.dal.ext下,创建相应的mapper,扩展自己需要的业务方法。不要继续com.maile360.crm.dal.mapper下的类,会引起装配冲突。
  2. com.maile360.crm.dal.mapper、com.maile360.crm.dal.entity,这两个包下的文件,禁止修改。这两个包下的文件是自动生成的,随时可能会被替换。

八. 数据库交互方式

以下三种方式均可,对于比较简单的crud,推荐基于注解的方式(下面的前两种方式),对于比较复杂的sql,建议使用更加灵活的xml配置方式。

  1. 注解方式一,参考:com.maile360.crm.dal.ext.ShopMapperExt.getShopInfoViewByAnnotated

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    public class ShopSqlProviderExt
    {
    public String getShopInfoViewByAnnotated(ShopSearchConditionView shopSearchConditionView)
    {
    SQL sql = new SQL();

    sql.SELECT("a.appID, a.appName, a.appCode, sp.shopName, sp.sellerNickname, sp.secretKey");
    sql.FROM("crmSysApp a");
    sql.INNER_JOIN("crmShop sp on a.appID=sp.appID");

    if (shopSearchConditionView.shopName.length() > 0)
    {
    shopSearchConditionView.shopName = "%" + shopSearchConditionView.shopName + "%";
    sql.WHERE("sp.shopName like #{shopName,jdbcType=VARCHAR}");
    }

    if (shopSearchConditionView.sellerNickname.length() > 0)
    {
    sql.WHERE("sp.sellerNickname = #{sellerNickname,jdbcType=VARCHAR}");
    }

    if (shopSearchConditionView.shopID > 0)
    {
    sql.WHERE("sp.shopID = #{shopID,jdbcType=INTEGER}");
    }

    if (shopSearchConditionView.appID > 0)
    {
    sql.WHERE("sp.appID = #{appID,jdbcType=INTEGER}");
    }

    if (shopSearchConditionView.secretKey.length() > 0)
    {
    sql.OR();
    sql.WHERE("sp.secretKey = #{secretKey,jdbcType=VARCHAR}");
    }

    sql.ORDER_BY("sp.shopID");

    return sql.toString();
    }
    }
  2. 注解方式二,参考:com.maile360.crm.dal.ext.ShopOrderSmsConfigSqlProviderExt

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    public class ShopOrderSmsConfigSqlProviderExt
    {
    public String getShopOrderSmsConfigInfo(Map<String, Integer> map)//入参已不推荐使用map,上面已写
    {
    String sql = "select sost.orderSTID, sost.iconNameClose, sost.iconNameOpen, sost.smsTypeName, soss.isSwitchOn \n" +
    "from crmSysOrderSmsType sost \n" +
    "left join crmShopOrderSmsConfig soss on sost.orderSTID = soss.orderSTID and soss.shopID = #{shopID,jdbcType=INTEGER}\n" +
    "where sost.orderSTStatus = #{orderSTStatus,jdbcType=INTEGER}\n" +
    "order by sost.orderByIndex;";
    return sql;
    }

    public String getFilterConditionView(Map<String, Integer> map)//入参已不推荐使用map,上面已写
    {
    String sql = "select fc.filterPID, fc.filterEIDs, fc.isAll, fc.filterValueOne, fc.filterValueTwo, fp.propertyName, fp.dataTypeID\n" +
    "from crmFilterCondition fc\n" +
    "inner join crmSysFilterProperty fp on fc.filterPID = fp.filterPID\n" +
    "where fc.shopID = #{shopID,jdbcType=INTEGER} and fc.FCStatus = 1 " +
    "and fc.bizType = #{bizType,jdbcType=INTEGER} " +
    "and fc.bizTypeID = #{bizTypeID,jdbcType=INTEGER};\n";

    return sql;
    }
    }
  3. 方式三:基于xml配置方式(与注解方式等效),参考:com.maile360.crm.dal.ext.ShopMapperExt.getShopInfoView

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.maile360.crm.dal.ext.ShopMapperExt">
    <select id="getShopInfoView" parameterType="com.maile360.crm.view.in.ShopSearchConditionView"
    resultType="com.maile360.crm.view.out.ShopInfoView">
    select
    a.appID, a.appName, a.appCode, sp.shopName, sp.sellerNickname, sp.secretKey
    from crmSysApp a
    inner join crmShop sp on a.appID=sp.appID
    where 1=1
    <if test="shopName != null and shopName != ''">
    and sp.shopName like CONCAT(CONCAT('%', #{shopName, jdbcType=VARCHAR}),'%')
    </if>
    <if test="sellerNickname != null and sellerNickname != ''">
    and sp.sellerNickname = #{sellerNickname,jdbcType=VARCHAR}
    </if>
    <if test="shopID > 0">
    and sp.shopID = #{shopID,jdbcType=INTEGER}
    </if>
    <if test="appID > 0">
    and sp.appID = #{appID,jdbcType=INTEGER}
    </if>
    <if test="sellerNickname != null and sellerNickname != ''">
    or sp.secretKey = #{secretKey,jdbcType=VARCHAR}
    </if>
    order by sp.shopID
    </select>
    </mapper>

九. 分页查询示例:

1
2
3
4
5
6
7
8
9
10
11
public ResponsePagingView<SysApp> getAppInfoAll(RequestParametersView requestParametersView)
{
PageHelper.startPage(requestParametersView.pagingInfo.pageNum, requestParametersView.pagingInfo.pageSize);
List<SysApp> sysAppList = sysAppMapper.selectByExample(null);

ResponsePagingView responsePagingView = new ResponsePagingView();
responsePagingView.resultList = sysAppList;
responsePagingView.totalRecord = ((Page<?>) sysAppList).getTotal();

return responsePagingView;
}

十. 以下为php端调用参数示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public function getAppAll()
{
Doo::loadClass('ApiClient');
$c = new ApiClient();

$request = array();
$request['apiParas'] = array(
"id" => 3,
"pagingInfo" => array(
"pageNum" => 3,
"pageSize" => 10
)
);
$request['apiMethod'] = "crm.app.get.all";
$request['secretKey'] = $this->secret_key;
$request['shopID'] = $this->shop_id;

$resp = $c->execute($request);
print_r($resp);
}

错误列表

  1. 实现类需要添加注解:@Service,否则不能自动扫描并注入,启动应用时会报错:
    1
    Field specifyMobileSentBatchSchedulesService in com.maile360.crm.service.facade.handler.SpecifyMobileSentBatchSchedulesServiceHandler required a bean of type 'com.maile360.crm.service.facade.api.ISpecifyMobileSentBatchSchedulesService' that could not be found.