Day 7 全接口贯通我把/book忘了改成/books编译器一声没吭今天是 Day 7任务叫全接口贯通。说人话就是把图书那几个接口从/book/list、/book/save这种动词式URL 改成 REST 风格再补一个注册接口。说是 4 个新文件 5 个改动实际抄起来不到三小时。抄完我自己觉得挺顺。IDEA 没飘红跑了一下编译cd D:\mywebapps\book-system-boot mvn-o-q compile# EXIT0编译通过。然后我打开配套的api/day7.http按顺序点绿色 ▶。前四个注册三次 登录都正常从第五个开始全是一样的错。一、我一开始改错了哪里问题出在BookController的类头上RestControllerRequestMapping(/book)// ← 是这里publicclassBookController{GetMapping// 原来是 GetMapping(/list)publicResultListBooklist(RequestParam(requiredfalse)Integeruid,RequestParam(requiredfalse)Integerstatus,RequestParam(requiredfalse)Stringkeyword){...}我把方法上的路径后缀都删了 ——/list没了、/save没了改成靠 HTTP 方法区分。这一步是对的但我忘了类上那个RequestMapping(/book)得跟着改成/books。结果就是api/day7.http里所有图书请求打的是/books而我的应用实际映射在/book。差了这一个字母。顺便把这次抄写里所有对不上的地方都列出来一共 4 处#答案纸要求我抄成了编译器能不能抓到1类上RequestMapping(/books)RequestMapping(/book)不能2regist(Validated RegistDTO dto)regist(Validated LoginDTO dto)不能3删掉import java.security.MessageDigest;留着没删不能只在 IDEA 里飘黄4public class BookServiceImplpublic class BookServiceImpl两个空格不能第 2 条我多解释一句我照着答案纸新建了dto/RegistDTO.java文件建好了但SysUserController里注册接口的参数类型我还是写的LoginDTO。也就是说这个新文件从头到尾一次都没被用过纯粹摆在那儿。两个类的字段现在一模一样所以功能上暂时看不出区别但答案纸想表达的每个接口一份自己的入参对象被我抄回了原型。这 4 处里有 3 处编译根本不报错。编译通过只能说明语法对它对这个类映射到哪个 URL这件事一无所知。二、改完的完整代码BookController整段重写类上换/books方法注解全部不带路径后缀packagecom.first.book.controller;importcom.first.book.common.Result;importcom.first.book.entity.Book;importcom.first.book.service.BookService;importorg.springframework.validation.annotation.Validated;importorg.springframework.web.bind.annotation.*;importjava.util.List;RestControllerRequestMapping(/books)publicclassBookController{privatefinalBookServicebookService;publicBookController(BookServicebookService){this.bookServicebookService;}// 列表条件查询GET /books?uid1status1keywordxxGetMappingpublicResultListBooklist(RequestParam(requiredfalse)Integeruid,RequestParam(requiredfalse)Integerstatus,RequestParam(requiredfalse)Stringkeyword){ListBookbooksbookService.listBooks(uid,status,keyword);returnResult.success(books);}// 详情GET /books/13GetMapping(/{bid})publicResultBookdetail(PathVariableIntegerbid){BookbookbookService.getBookById(bid);returnResult.success(book);}// 新增POST /booksPostMappingpublicResultBooksave(RequestBodyValidatedBookbook){bookService.saveBook(book);returnResult.success(book);// 插入后 bid 被 MP 回填了顺手返回}// 修改PUT /books/13PutMapping(/{bid})publicResultVoidupdate(PathVariableIntegerbid,RequestBodyValidatedBookbook){book.setBid(bid);// 以 URL 里的 bid 为准bookService.updateBook(book);returnResult.success(null);}// 删除DELETE /books/13DeleteMapping(/{bid})publicResultVoiddelete(PathVariableIntegerbid){bookService.deleteBook(bid);returnResult.success(null);}}注册接口的参数类型改成RegistDTOPostMapping(/regist)publicResultVoidregist(RequestBodyValidatedRegistDTOdto){sysUserService.regist(dto.getUsername(),dto.getUserPwd());returnResult.success(null);}SysUserServiceImpl里把已经用不到的 import 删掉 —— 私有md5()方法搬进Md5Util之后java.security.MessageDigest这个类在文件里已经没人用了// import java.security.MessageDigest; ← 删掉这一行三、我当时没想通的三个点1. 为什么编译器一个错都不报/book和/books在编译期都只是两个普通的字符串常量。Java 编译器不知道 Spring 那套 URL 映射的规矩也不关心。URL 对不对是运行期才成立的事。所以mvn compile通过和接口能通之间隔着十万八千里。我以后不再拿编译通过当作抄对了的信号。2. 为什么地址打错了报的不是 404而是「异常繁忙请稍后重试」这条最让我懵。我带着有效 token 去请求那个不存在的地址拿到的是{code:0,data:null,message:异常繁忙请稍后重试}HTTP 状态码是200。不是 404。我把两个地址对比着打了一遍真实结果是这样请求都带 token实际返回GET /bookcode1data 是一整个图书数组bid 13 测试书、9 道德经、8 基督山伯爵……GET /bookscode0“异常繁忙请稍后重试”GET /book/99999code0“图书不存在”原因在路径找不到对应 Controller 方法的时候请求会落到兜底的静态资源处理器上它映射的是/**资源找不到就抛异常。而我项目里的GlobalExceptionHandler上挂着一个ExceptionHandler(Exception.class)的兜底方法ExceptionHandler(Exception.class)publicResultVoidhandleException(Exceptione){log.error(系统异常,e);returnResult.error(异常繁忙请稍后重试);}它把这个资源找不到的异常也一起接走了返回了系统繁忙的文案。说白了是我自己写的那段兜底把 404 换成了 200 一句误导人的话。答案纸的报错对照表里写的是请求老地址报 404实测不符 —— 因为答案纸那版假设全局异常处理器没兜这么宽。这事提醒我ExceptionHandler(Exception.class)这种兜底写起来省事但它会把真正有用的信息到底是 404 还是真的系统异常盖掉。排查的时候日志里那行系统异常后面的堆栈才是唯一线索。3. 不带 token 的时候两个地址返回一模一样我第一次测的时候忘了带 token/book和/books返回的都是{code:0,data:null,message:未登录请先登录}因为拦截器挂的是addPathPatterns(/**)registry.addInterceptor(loginInterceptor).addPathPatterns(/**).excludePathPatterns(/user/login,/user/regist,/hello,/error);静态资源那个/**处理器也算匹配到了一个 handler所以拦截器照样跑先把我拦下来了。我一个从未登录这条错里根本看不出 URL 到底是对是错。我一开始还以为白名单拼错了或者拦截器坏了折腾了一阵才反应过来得先跑 ④ 登录拿到 token才算真正测到 URL。四、怎么验证改完/books之后带上 token 重跑期望值我列在这儿一行一行对#请求期望结果①POST /user/registcode1success②重复注册同名用户名已存在③注册空 body{}密码不能为空用户名不能为空⑤GET /books 不带 token未登录请先登录⑥GET /books 带 tokencode1图书数组⑦GET /books/13我库里 bid13 是测试书⑧GET /books/99999图书不存在⑫改了之后再查title / price 是改后的值⑮删掉之后再查图书不存在闭环我用 curl 直接打、已经验证过的几个GET /books/99999→图书不存在DELETE /books/99999→图书不存在删除接口用rows 0判断是安全的因为删除没有值变没变的歧义GET /user/info带 token →{uid:1,message:token 有效已通过拦截器}五、报错对照表现象真实原因GET /books返回 200 “异常繁忙请稍后重试”路径没匹配到 handler异常被ExceptionHandler(Exception.class)兜底吞了。根因是类上RequestMapping(/book)没改GET /books不带 token 也报未登录请先登录拦截器匹配/**静态资源处理器也算匹配到 handler所以先被拦。从这条错里看不出 URL 对不对mvn -o compile通过但接口全不通编译期没有 URL 契约检查POST /user/regist返回密码不能为空用户名不能为空校验器逐字段拼文案。以前我手写的 if 是一句用户名和密码不能为空行为变了但不是 bug改完代码行为还是老的没有热部署必须重启应用六、下一篇写什么Day 8 是 Knife4j 接口文档。前面几天接口一直是靠.http文件手点文档这东西迟早要接上正好那天轻松把扫描的活交给工具。另外今天留了三个尾巴记一下别忘了RegistDTO建了还没真正接线、SysUserServiceImpl里那个废弃 import、BookServiceImpl类名后面多打的一个空格。都不影响运行但下次开工前顺手清掉。