### 1. 简介 > dobbinfw使用统一接口方式,即只有一个api地址,由系统级别参数进行路由。这种设计也很常见。得利于这种模式,我们完全不用写Controller,只需要写服务即可。 ### 2. 基本原理(可跳过) 一般的,一个服务由接口和实现组成,例如 UserService 和 UserServiceImpl。 一般的,UserServiceImpl 会加上@Service注解,表示将这个类初始化一个实例,并加入IoC。 一般的,Service前面加上Controller,例如UserController,用于对外提供Web服务。例如提供 @PostMapping("/login")来给外部一个访问的URL。 UserController可以省略,条件是我们得有其他的依据来进行路由。 所以我们定义一个注解 @HttpOpenApi 表示,这个服务是需要暴露的服务,并且,给其一个属性 group。表示它的一级路由分组。 ```java @HttpOpenApi(group = "user", description = "用户服务") public interface UserService { ``` 有了user这层group,就可以定位到服务的类,接下来还要定位到方法。 所以我们定义另一个注解@HttpMethod,被这个注解的方法表示需要暴露出去的方法。 ```java @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> 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接口。 ```java @HttpOpenApi(group = "hello", description = "Hello服务") public interface HelloService { } ``` 使用@HttpOpenApi注解在类上来标记,这个接口方法是需要暴露的。并指定好服务的分组。 接着定义具体的方法。 ```java @HttpOpenApi(group = "hello", description = "Hello服务") public interface HelloService { @HttpMethod(description = "测试hello接口") public String say() throws ServiceException; } ``` 需要对此方法加上HttpMethod注解,表示此方法是要暴露的的方法。这样就定义好一个最简单的OpenApi了 ^_^ 但是到目前为止,还没有写对应的实现类。在同包下写一个HelloServiceImpl类来实现HelloService。并且此Service实例需要放入IoC中。 ```java @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,自定义模型。 ```java @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的形式传递。 ```java 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字段即可。 ```java @NotNull @HttpParam(name = "userId", type = HttpParamType.USER_ID, description = "用户ID") Long userId ``` ```java @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注解即可。举例:商品详情接口,若用户已登录,则返回用户是否收藏该商品字段。 如果需要管理员登录,请 ```text @NotNull @HttpParam(name = "adminId", type = HttpParamType.ADMIN_ID, description = "管理员ID") Long adminId) ``` > 规范👉:请将 adminId, userId 放在一个方法的最后,才能让其他人一眼看到。必须登录的接口,一定要加NotNull。 #### 4.3. 获取Session域对象 > 只能获取已登录用户ID,难免单调,且力不从心。 如果您需要获取当前用户的其他字段,可使用SessionUtil。该对象已经放入IoC,您可以通过将服务继承 BaseService来获取,BaseService中有个 protect权限的sessionUtil 对象。或者您在非Service中,想要获取,可通过@Autowired 注入进来。 ```text UserDTO user = sessionUtil.getUser(); ``` 请参操UserService.syncUserInfo接口。 #### 4.4. 泛型擦除的坑 Java编译后就会擦除泛型,所以,当您注入一个例如 List 的参数时,Java 会将其认为时 List。最后通过fastJson反序列化回来的List变成了List 所以您会看到一个强转失败的异常。 所以,当您需要注入一个List<>对象时,将泛型类加入到HttpParam注解中加入 arrayClass 字段 ```java @NotNull @HttpParam(name = "userList", type = HttpParamType.COMMON, arrayClass="UserDO.class", description = "用户列表") List userList) ``` > 提示💡:只有List支持arrayClass,在平常的使用中,几乎也只有List入参会用到泛型。如果您自己定义了需要用到泛型的模型,该字段就使用String类型传入,然后自己手动使用json工具进行反序列化。 #### 4.5. 总结 框架提供了参数注入、返回值自动封装、参数校验、身份校验等功能。 ```java 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字段 ```text @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. 粗粒度接口 以上三个注解同样可以注解到属性上,用于对请求参数进行参数校验。 ```java @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 haList; @NotNull(message = "哈不能为空") private H ha; @Data public static class H { @NotNull(message = "haha不能为空") private String haha; } } ``` > 提示💡:List中的对象 每个元素都会校验。 > 粗粒度中,并不会对对象本身进行判空,若需要对对象本身判空,需要在接口上加上@NotNull。以下代码hello参数可传空。 ```java @HttpMethod(description = "你好") public HelloDTO hello( @HttpParam(name = "hello", type = HttpParamType.COMMON, description = "你好对象") HelloDTO hello) throws ServiceException; ``` #### 5.3. 总结 利用框架可对参数进行基本校验。减少枯燥的校验代码。