dobbinfw使用统一接口方式,即只有一个api地址,由系统级别参数进行路由。这种设计也很常见。得利于这种模式,我们完全不用写Controller,只需要写服务即可。
一般的,一个服务由接口和实现组成,例如 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。
在 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 。
在浏览器中输入 http://localhost:8801/m.api?_gp=hello&_mt=say
将会得到回复
{"data":"hello world", "errmsg": "成功", "errno": 200, "timestamp": 1234567890}
至此,您已经写好一个无参的API。您会发现该框架下,您无需写任何Controller的代码。所以请面向服务为核心开发。
在《Hello World》文档中,我们写了一个无参,返回字符串的接口;hello.say 在此文档中,我们将写带有参数,参数校验,登录校验的接口。
同样在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);
后端开发时无需关注这一点,但是请悉知,防止在用Postman测试时不知道怎么构建报文。
有的接口需要用户登录时才能访问,例如更新用户的基本信息接口。
需要用户登录的接口,我们只需要在接口中添加一个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。
只能获取已登录用户ID,难免单调,且力不从心。
如果您需要获取当前用户的其他字段,可使用SessionUtil。该对象已经放入IoC,您可以通过将服务继承 BaseService<UserDTO, AdminDTO>来获取,BaseService中有个 protect权限的sessionUtil 对象。或者您在非Service中,想要获取,可通过@Autowired 注入进来。
UserDTO user = sessionUtil.getUser();
请参操UserService.syncUserInfo接口。
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工具进行反序列化。
框架提供了参数注入、返回值自动封装、参数校验、身份校验等功能。
public enum HttpParamType {
HEADER,
COMMON,
USER_ID,
ADMIN_ID,
IP;
}
除了提到的外,还可以注入IP,HEADER中的值等类型,请以当前版本框架为准,说不定以后有需要会增加。
以录入一本书的接口为例:
boolean saveBook(String title, String author, Long adminId);
boolean saveBook(BookAddDTO addDTO, Long adminId);
可以有这两种方式,框架对这两种接口都做了参数校验的处理。
系统中并不限制您用哪种风格写接口,您可以按照个人习惯书写,但是推荐使用细粒度接口,对前端更加友善。
对于例如商品创建等结构复杂的请求,可以偏向于使用粗粒度接口。
前文应该已经了解到 @NotNull 注解。此注解可注解于Service接口上的入参中。
若要返回指定信息,在NotNull注解中加入message字段
@NotNull(message="手机号不能为空") @HttpParam(name="phone", type = HttpParamType.COMMON, description = "手机号") String phone,
@TextFormat 内容太多,自己看吧
@Range 包含 min 和 max,并非两个都要填。
例如页码可用min = 1。注意Range并不会判空,需要判空,额外添加NotNull
以上注解均支持 message 字段
以上三个注解同样可以注解到属性上,用于对请求参数进行参数校验。
@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;
利用框架可对参数进行基本校验。减少枯燥的校验代码。