- 还是16年年底,开发 CRM 时设计的 api 开发规范。
- 配置有一点繁琐。
- 配置好后,就可以专注于业务逻辑。
- 适合中型团队多人协作。
- 小团队不建议这么操作。
- 该规范大量用到反射、接口、多态、工厂模式,新人不太容易理解。
- 小团队可以没有接口和反射,但是可以有、并且建议合理使用多态和工厂模式。
一. api开发总则
- 遵循单一职责原则,一个类只做一件(类)事。
- 数据库表与业务类是 1:N 的关系。
- 简单业务表建议只有一个业务类,复杂业务表,建议有多个业务类。
二. 类、接口命名规范
根据业务规则,首字母大写,如,针对表:crmSysApp,业务命名:SysAppService,接口、实现类、抽象类,都是在这个命名的基础上进一步命名的,规则如下。
- 接口,在前面加 I,如:ISysAppService。
- 抽象类(非必须),在前面A,如:ASysAppService。
- 实现类,在后面加Impl,如:SysAppServiceImpl,需要加@Service注解。
- 处理类,在后面加Handler,如:SysAppServiceHandler。
- 处理类仅仅是业务实现类供消费方(client)进行消费的桥梁,不处理具体的业务。声明业务处理对象,必须也只能用接口来声明,如:ISysAppService sysAppService,且加上@Autowired注解。
1
2
3
4
5
6
7
8
9
10public class ShopServiceHandler extends AapiHandler
{
IShopService shopService;
public List<ShopInfoView> getShopInfoView(RequestParametersView requestParametersView)
{
return shopService.getShopInfoView(requestParametersView);
}
}
三. 开发步骤
- 接口。在com.maile360.crm.service.facade.api下,创建interface,参考:ISysAppService;抽象类为非必须,如果有需要,在同样的目录下创建即可。参数,统一为:RequestParametersView requestParametersView。
- 实现类。在com.maile360.crm.service.impl.api下,创建实现类,参考:SysAppServiceImpl
- 处理类。在com.maile360.crm.service.facade.handler下,创建处理类,参考:SysAppServiceHandler。处理类须 extends AapiHandler。
- 处理类配置。
- 是一个 xml 文件:提供 api 调用时的 apiMethod 值, 作用是关联 apiMethod 与接口名。
- 在 restful 模块的资源文件夹下,handlers 子目录下,添加对应的配置 xml 文件,文件名保持与处理类同名(便于维护和查找),如:SysAppServiceHandler.xml。
- 在该文件配置具体的 apiMethod(调用参数)与对应接口名。apiMethod 命名规则,需要继续往下看,看完第四项再回过头来操作。
- 好处:维护人员方便查找对应处理方法,并且 api 会自动由配置对应到接口,而接口会有具体的实现,接口到实现类的调用,由 spring boot 启动时根据 serviceConfig.xml 配置自动装载。具体调用哪个方法,则用到了 java 的反射。
配置处理类列表。修改 restful 模块的资源文件夹下的配置文件:serviceConfig.xml,把上面的 handler 配置到列表中。spring boot 启动时根据 serviceConfig.xml 配置自动装载各个 handler。
这里的配置,除了让 spring boot 启动时自动装载 handler 类,还有一个重要的目的是让系统根据请求的 apiMethod 自动匹配到 handler 类,然后 handler 类根据其配置的方法名自动去调用业务接口。如:
1
<entry key="crm.shop_config.get.shop.config.view" value="getShopConfigView"/>
配置处理类列表 key 命名规则: crm_ + 下面第四项针对 apiMethod 命名规则中的 Y + 下划线 + 版本号。
- 如:crm_sys_sms_template_1.0,其中 Y = sys_sms_template, 版本号 = 1.0
- 处理类的 key 只用下划线分隔。
四. Handler xml 配置文件规则
总则:一个 Service 对应一个 Handler,一个 Handler 对应一个 xml 配置文件,存放到模块 restful 下,resources/handlers 目录,命名规则:XServiceHandler.xml,其中 X 为对应Service的名字,如:ShopConfigServiceHandler.xml
- 为了讲解 xml 配置文件的 key 和 value 命名规则,需要先了解与配置文件相关的接口和处理类。
- 所有处理类,即 Handler 都要继承的抽象类:AApiHandler。抽象类中定义了 Map 类型的变量 methods。
1
2
3
4
5
6
7
8
9public abstract class AApiHandler
{
public Map<String, String> methods;
public void setMethods(Map<String, String> methods)
{
this.methods = methods;
}
}
以 ShopConfigService 为例,先看实际例子。
接口定义 IShopConfigService.java
1
2
3
4
5
6
7
8
9
10
11package 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);
}处理类实例 ShopConfigServiceHandler.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23package 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
{
IShopConfigService shopConfigService;
public int addOrUpdate(RequestParametersView requestParametersView)
{
return shopConfigService.addOrUpdate(requestParametersView);
}
public ShopConfigView getShopConfigView(RequestParametersView requestParametersView)
{
return shopConfigService.getShopConfigView(requestParametersView);
}
}Handler 配置: ShopConfigServiceHandler.xml。其中 property 属性中的 methods 就是来自抽象类 AApiHandler 的 Map 类型的变量,这里的配置,spring boot 自动装载时会自动初始化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>该处理类配置,在 serviceConfig.xml 文件中的配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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 命名规则
- 分三部分,这里分别用大写的 X,Y,Z 来表示,用英文的点”.”来连接,得到:X.Y.Z,其中每一部分的英语字母全部用小写。
- X 为前缀,固定:crm, 即 X = crm
- Y 为服务名,英语字母加下划线,可以参考业务对应的表名,如上面的例子,Y = shop_config
- Z 为具体的业务方法名标志,最好能通过命名看出意义来,中间以”.”隔开,不限制长度,如上面的例子,Z = get.shop.config.view
- 将上面的 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 仅仅是中转调用,自身并不处理业务逻辑。
crm.shop_config.get.shop.config.view 调用下面的方法之后,requestParametersView.apiServiceName = crm.shop_config
1
2
3
4
5
6private 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);
}然后,通过 apiServiceName 和版本号,就可以找到匹配的处理类 Handler 了。接着上面的,应该得到:crm_shop_config_1.0
1
2
3
4private String generateServerKey(String apiServiceName, VersionEnum version)
{
return apiServiceName.replace(".", "_") + "_" + version.getVersion();
}
五. 返回数据类型
- 不建议返回类型为Map。在返回类型是确定的情况下,比如是一个int型的数字,或者某个确定的pojo以及某个pojo的列表,则直接返回这个确定的类型。
- 如果返回的数据直接是数据库表的记录,则返回对应的实体即可。
- 如果返回的数据需要加工整合,则在 com.maile360.crm.view.out 下,创建对应的view类,类名须以View结束。如:ShopInfoView,字段属性为private,需要setter和getter。
- 如果返回的数据除了基本类型,还包含实体,即定义在com.maile360.crm.dal.entity下的pojo(非Example类),而crm.view项目下又无法引用这部分pojo,那么,这种情况下,需要在com.maile360.crm.dal.view包下增加返回view的pojo。参考:OrderSmsOptionByTradeView、TopTmcMessageQueueView
- 如果需要返回多个结果集,定义一个pojo来组装,参考上面。
六. 入参
- 不建议使用Map当作mapper的参数类型。入参多于两个,建议添加pojo来接收。如果在程序内部,也建议通过pojo来组装查询参数。
如果入参只有一个,比如仅为id,则可以这样获取:
1
2
3
4int 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());添加pojo来接收参数时的规则:类名须以View结束。如:ShopSearchConditionView。放在 com.maile360.crm.view.in 包下,字段属性建议直接设置为public,这样,就只需要设置setter而不需要getter。入参接收需在自己的实现类定义和初始化:
1
ShopSearchConditionView shopSearchConditionView = JSONHelper.json2Object(requestParametersView.apiParas, ShopSearchConditionView.class);
七. mapper扩展
- 在com.maile360.crm.dal.ext下,创建相应的mapper,扩展自己需要的业务方法。不要继续com.maile360.crm.dal.mapper下的类,会引起装配冲突。
- com.maile360.crm.dal.mapper、com.maile360.crm.dal.entity,这两个包下的文件,禁止修改。这两个包下的文件是自动生成的,随时可能会被替换。
八. 数据库交互方式
以下三种方式均可,对于比较简单的crud,推荐基于注解的方式(下面的前两种方式),对于比较复杂的sql,建议使用更加灵活的xml配置方式。
注解方式一,参考: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
42public 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();
}
}注解方式二,参考: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
24public 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;
}
}方式三:基于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
<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 | public ResponsePagingView<SysApp> getAppInfoAll(RequestParametersView requestParametersView) |
十. 以下为php端调用参数示例
1 | public function getAppAll() |
错误列表
- 实现类需要添加注解:@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.