编辑 | blame | 历史 | 原始文档

1. 简介

dobbinfw使用统一接口方式,即只有一个api地址,由系统级别参数进行路由。这种设计也很常见。得利于这种模式,我们完全不用写Controller,只需要写服务即可。

2. 基本原理(可跳过)

一般的,一个服务由接口和实现组成,例如 UserService 和 UserServiceImpl。

一般的,UserServiceImpl 会加上@Service注解,表示将这个类初始化一个实例,并加入IoC。

一般的,Service前面加上Controller,例如UserController,用于对外提供Web服务。例如提供 @PostMapping("/login")来给外部一个访问的URL。

UserController可以省略,条件是我们得有其他的依据来进行路由。

所以我们定义一个注解 @HttpOpenApi 表示,这个服务是需要暴露的服务,并且,给其一个属性 group。表示它的一级路由分组。

@HttpOpenApi(group = "user", description = "用户服务")
public interface UserService {

有了user这层group,就可以定位到服务的类,接下来还要定位到方法。

所以我们定义另一个注解@HttpMethod,被这个注解的方法表示需要暴露出去的方法。

@HttpMethod(description = "用户注销")
public String logout(
        @NotNull @HttpParam(name = Const.USER_ACCESS_TOKEN, type = HttpParamType.HEADER, description = "用户访问") String accessToken,
        @NotNull @HttpParam(name = "userId", type = HttpParamType.USER_ID, description = "用户Id") Long userId) throws ServiceException;

这样我们用 user.logout 就可以定位到一个唯一的方法。

有了必要的条件,我们就可以进行路由了。

前文提到,所有的Service都在IoC中,接下来我们只需要在IoC初始化之后,将所有Service都筛出来,并将其接口的类找出来。

拿到接口的类之后,就可以反射获取它所有的方法,并通过是否包含HttpMethod注解来进行过滤。就找出来所有需要暴露的方法。

接下来,通过group和方法名两个字段分组,映射,最后可以得到这样一个数据结构 Map<String, Map<String, Method>> methodMap

接下来,我们只需要从请求中获取到系统级别参数 group、method、再将应用参数按照方法签名拼装好。

最后: methodMap.get(group).get(method).invoke(serviceObj, args)

以上就是路由的大致原理了。详情请参考ApiController。

3. 基本使用

3.1. 首先定义一个HelloWordService试试水。

在 demo-app-api 模块创建com.dobbinsoft.demo.app.api.hello包。并创建好HelloService接口。

@HttpOpenApi(group = "hello", description = "Hello服务")
public interface HelloService {

}

使用@HttpOpenApi注解在类上来标记,这个接口方法是需要暴露的。并指定好服务的分组。

接着定义具体的方法。

@HttpOpenApi(group = "hello", description = "Hello服务")
public interface HelloService { 

    @HttpMethod(description = "测试hello接口")    
    public String say() throws ServiceException;
    
}

需要对此方法加上HttpMethod注解,表示此方法是要暴露的的方法。这样就定义好一个最简单的OpenApi了 ^_^

但是到目前为止,还没有写对应的实现类。在同包下写一个HelloServiceImpl类来实现HelloService。并且此Service实例需要放入IoC中。

@Service
public class HelloServiceImpl implements HelloService {    
    @Override    
    public String say() throws ServiceException {       
        return "hello world";   
    }
}

这样就完成了自己写的一个Api。起动项目。注意起动的是 DemoRunnerApplication 。

3.2. 访问自己写的API

在浏览器中输入 http://localhost:8801/m.api?_gp=hello&_mt=say

将会得到回复

{"data":"hello world", "errmsg": "成功", "errno": 200, "timestamp": 1234567890}

3.3. 总结

至此,您已经写好一个无参的API。您会发现该框架下,您无需写任何Controller的代码。所以请面向服务为核心开发。

4. 带参数API

在《Hello World》文档中,我们写了一个无参,返回字符串的接口;hello.say 在此文档中,我们将写带有参数,参数校验,登录校验的接口。

4.1. echo back

同样在HelloService里面写一个接口,这次我们添加三个不同类型的参数,分别是String,Inteager,自定义模型。

@HttpOpenApi(group = "hello", description = "Hello服务")
public interface HelloService {    

    @HttpMethod(description = "带参数Hello")    
    public String sayWithParam(
        @HttpParam(name = "content", type = HttpParamType.COMMON, description = "内容") String content,
        @HttpParam(name = "number", type = HttpParamType.COMMON, description = "数值") Integer number,
        @NotNull  @HttpParam(name = "model", type = HttpParamType.COMMON, description = "模型") HelloServiceImpl.Model model) throws ServiceException;
        
}

当我们在前端请求时,需要将 model 字段的对象以JSON的形式传递。

return "say: " + content + "  ;number:" + number + "  ;model:" + JSONObject.toJSONString(model);

![](https://tcs-devops.aliyuncs.com/storage/112301f025cf6fc6988cb1d1da7deafd8bb7?Signature=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBcHBJRCI6IjVlNzQ4MmQ2MjE1MjJiZDVjN2Y5YjMzNSIsIl9hcHBJZCI6IjVlNzQ4MmQ2MjE1MjJiZDVjN2Y5YjMzNSIsIl9vcmdhbml6YXRpb25JZCI6IiIsImV4cCI6MTYzOTI5NTA5MiwiaWF0IjoxNjM4NjkwMjkyLCJyZXNvdXJjZSI6Ii9zdG9yYWdlLzExMjMwMWYwMjVjZjZmYzY5ODhjYjFkMWRhN2RlYWZkOGJiNyJ9.vbfGDcE6dPibzWmzl84XdePv3c_rGzJxg9kFazEx2d4&download=6.png "")

后端开发时无需关注这一点,但是请悉知,防止在用Postman测试时不知道怎么构建报文。

4.2. 身份校验

有的接口需要用户登录时才能访问,例如更新用户的基本信息接口。

需要用户登录的接口,我们只需要在接口中添加一个USER_ID字段即可。

@NotNull @HttpParam(name = "userId", type = HttpParamType.USER_ID, description = "用户ID") Long userId
@HttpMethod(description = "同步用户信息")
public String syncUserInfo(
        @HttpParam(name = "nickname", type = HttpParamType.COMMON, description = "用户昵称") String nickname,
        @HttpParam(name = "avatarUrl", type = HttpParamType.COMMON, description = "用户头像url") String avatarUrl,
        @HttpParam(name = "gender", type = HttpParamType.COMMON, description = "性别0未知1男2女") Integer gender,
        @HttpParam(name = "birthday", type = HttpParamType.COMMON, description = "用户生日") Long birthday,
        @HttpParam(name = Const.USER_ACCESS_TOKEN, type = HttpParamType.HEADER, description = "访问令牌") String accessToken,
        @NotNull @HttpParam(name = "userId", type = HttpParamType.USER_ID, description = "用户ID") Long userId) throws ServiceException;

如果用户未登录,将进不syncUserInfo这个方法中,框架会拦截这个请求,并返回异常报文:

{"errmsg":"用户尚未登录","errno":10001,"timestamp":1616124309732}

若一个接口即可登录用户访问,又可非登录用户访问,则在 userId 前面不要加@NotNull注解即可。举例:商品详情接口,若用户已登录,则返回用户是否收藏该商品字段。

如果需要管理员登录,请

@NotNull @HttpParam(name = "adminId", type = HttpParamType.ADMIN_ID, description = "管理员ID") Long adminId)

规范👉:请将 adminId, userId 放在一个方法的最后,才能让其他人一眼看到。必须登录的接口,一定要加NotNull。

4.3. 获取Session域对象

只能获取已登录用户ID,难免单调,且力不从心。

如果您需要获取当前用户的其他字段,可使用SessionUtil。该对象已经放入IoC,您可以通过将服务继承 BaseService<UserDTO, AdminDTO>来获取,BaseService中有个 protect权限的sessionUtil 对象。或者您在非Service中,想要获取,可通过@Autowired 注入进来。

UserDTO user = sessionUtil.getUser();

请参操UserService.syncUserInfo接口。

4.4. 泛型擦除的坑

Java编译后就会擦除泛型,所以,当您注入一个例如

List 的参数时,Java 会将其认为时 List。最后通过fastJson反序列化回来的List变成了List 所以您会看到一个强转失败的异常。

所以,当您需要注入一个List<>对象时,将泛型类加入到HttpParam注解中加入 arrayClass 字段

@NotNull @HttpParam(name = "userList", type = HttpParamType.COMMON, arrayClass="UserDO.class", description = "用户列表") List<UserDO> userList)

提示💡:只有List支持arrayClass,在平常的使用中,几乎也只有List入参会用到泛型。如果您自己定义了需要用到泛型的模型,该字段就使用String类型传入,然后自己手动使用json工具进行反序列化。

4.5. 总结

框架提供了参数注入、返回值自动封装、参数校验、身份校验等功能。

public enum HttpParamType {
    HEADER,
    COMMON,
    USER_ID,
    ADMIN_ID,
    IP;

}

除了提到的外,还可以注入IP,HEADER中的值等类型,请以当前版本框架为准,说不定以后有需要会增加。

5. 使用粗粒度接口 与 参数校验

以录入一本书的接口为例:

boolean saveBook(String title, String author, Long adminId);

boolean saveBook(BookAddDTO addDTO, Long adminId);

可以有这两种方式,框架对这两种接口都做了参数校验的处理。

系统中并不限制您用哪种风格写接口,您可以按照个人习惯书写,但是推荐使用细粒度接口,对前端更加友善。

对于例如商品创建等结构复杂的请求,可以偏向于使用粗粒度接口。

5.1. 细粒度接口(APP-API推荐)

5.1.1. 判空

前文应该已经了解到 @NotNull 注解。此注解可注解于Service接口上的入参中。

若要返回指定信息,在NotNull注解中加入message字段

@NotNull(message="手机号不能为空") @HttpParam(name="phone", type =  HttpParamType.COMMON, description = "手机号") String phone,
5.1.2. 文本格式

@TextFormat 内容太多,自己看吧

5.1.3. 数字取值范围

@Range 包含 min 和 max,并非两个都要填。

例如页码可用min = 1。注意Range并不会判空,需要判空,额外添加NotNull

以上注解均支持 message 字段

5.2. 粗粒度接口

以上三个注解同样可以注解到属性上,用于对请求参数进行参数校验。

@Data
public class HelloDTO {

    @NotNull(message = "昵称不能为空", respScope = true)
    private String nickname;

    @Range(min = 0, max = 100, message = "年龄只能0到100")
    private Integer age;

    @NotNull(message = "haList不能为空")
    private List<H> haList;

    @NotNull(message = "哈不能为空")
    private H ha;

    @Data
    public static class H {

        @NotNull(message = "haha不能为空")
        private String haha;

    }

}

提示💡:List中的对象 每个元素都会校验。

          粗粒度中,并不会对对象本身进行判空,若需要对对象本身判空,需要在接口上加上@NotNull。以下代码hello参数可传空。
@HttpMethod(description = "你好")
public HelloDTO hello(
        @HttpParam(name = "hello", type = HttpParamType.COMMON, description = "你好对象") HelloDTO hello) throws ServiceException;

5.3. 总结

利用框架可对参数进行基本校验。减少枯燥的校验代码。