从 Android 开发视角学习后端项目
从 Android 开发视角学习后端项目本文是一次从 Android 开发转向理解 Java 后端项目的学习记录。文中已隐藏真实公司、项目、模块、接口域名、内网地址、账号密码和版权信息仅保留通用技术结构与学习方法。背景我原本主要做 Android 开发日常更熟悉 Activity、Fragment、ViewModel、Retrofit、OkHttp、Room、Gson 等移动端技术。最近接触到一个与 Android 项目配套的 Java 后端工程希望能看懂接口是如何从 Android 请求一路走到数据库再把结果返回给 App。刚开始看后端项目时最困惑的是目录很多、模块很多、注解很多不知道应该从哪里入手。后来我发现不应该一开始就试图理解全部后端知识而是应该先抓住一条真实接口链路。项目技术栈概览这个后端项目是一个典型的 Java 企业后端工程主要技术栈包括Java 8Maven 多模块工程Spring BootSpring MVCSpring SecurityJWT TokenMyBatis / MyBatis-PlusMySQLRedisDruid 数据库连接池Swagger / 接口文档工具PageHelper 分页Undertow / Web 容器Lombok从 Android 视角类比后端技术作用Android 类比Maven管理依赖和模块GradleSpring Boot启动和组织后端应用Android Application 框架基础设施Controller提供 HTTP 接口Retrofit 接口的服务端实现Service业务逻辑层Repository / UseCaseMapper数据库访问接口DAOMyBatis XMLSQL 查询文件Room SQL / SQLite 查询DTO / VO参数和返回数据模型Request / Response BeanAjaxResult统一接口返回格式ApiResponseRedis缓存、登录态、Token 用户信息MMKV / DataStore 的服务端类比Security / JWT登录鉴权OkHttp Header Token 机制第一阶段先看懂一个接口学习后端最有效的入口不是配置文件也不是所有模块而是一条 Android 正在调用的接口。我选择了一条成绩查询接口作为入口它的后端链路大概是Android Retrofit - Controller - Request 对象 - Service - ServiceImpl - Mapper - MyBatis XML - MySQL - DTO - AjaxResult - Android Response Bean这条链路可以理解为App 发起 HTTP 请求 后端 Controller 接收请求 Service 处理业务 Mapper 执行 SQL 数据库返回数据 后端封装 JSON App 解析响应URL 是怎么拼出来的后端接口 URL 通常由三部分组成服务地址 应用上下文路径 Controller 路径 方法路径例如http://host:port/app-context/module/action在 Spring Boot 项目里端口和上下文路径一般来自配置文件server:port:8080servlet:context-path:/app-contextController 上通常有RestControllerRequestMapping(/module)publicclassDemoController{GetMapping(/action)publicAjaxResultaction(){returnAjaxResult.success();}}最终接口路径就是GET /app-context/module/actionAndroid Retrofit 中如果baseUrl已经包含http://host:port/那么接口路径一般写GET(app-context/module/action)GET、POST 和参数传递后端常见的参数接收方式有几类。GET 普通对象参数后端GetMapping(/list)publicAjaxResultlist(QueryReqreq){returnAjaxResult.success(service.list(req));}AndroidGET(app-context/module/list)CallResultBeanlist(Query(userId)longuserId,Query(type)Stringtype);请求类似GET /list?userId1type2Spring 会自动把 query 参数绑定到QueryReq对象里。GET RequestParam后端GetMapping(/detail)publicAjaxResultdetail(RequestParam(id)Longid){returnAjaxResult.success(service.detail(id));}AndroidGET(app-context/module/detail)CallResultBeandetail(Query(id)longid);这种适合参数比较少的查询接口。POST RequestBody后端PostMapping(/save)publicAjaxResultsave(RequestBodySaveReqreq){returnAjaxResult.success(service.save(req));}AndroidPOST(app-context/module/save)CallResultBeansave(BodySaveReqreq);请求体是 JSON{userId:1,score:95}这种适合新增、保存、提交复杂对象。Multipart 文件上传后端PostMapping(/upload)publicAjaxResultupload(RequestParam(file)MultipartFilefile){returnAjaxResult.success();}AndroidMultipartPOST(app-context/module/upload)CallResultBeanupload(PartMultipartBody.Partfile);适合上传图片、视频、文件。PathVariable 路径参数后端GetMapping(/user/{userId})publicAjaxResultuser(PathVariable(userId)LonguserId){returnAjaxResult.success(service.user(userId));}AndroidGET(app-context/module/user/{userId})CallResultBeanuser(Path(userId)longuserId);请求路径GET /user/1这里的1是路径的一部分不是 query 参数。Controller、Service、Mapper 的分工后端项目里最重要的是分层。Controller 管 HTTP Service 管业务 Mapper 管数据库 XML 管 SQLController 不应该直接写 SQL。它应该负责接收参数、调用 Service、返回统一结果。Service 负责业务逻辑比如参数校验业务规则判断默认值处理调用多个 Mapper计算排名或分数组装返回数据抛出业务异常Mapper 负责数据库访问。它的 Java 方法会和 MyBatis XML 里的 SQL 对应。例如publicinterfaceDemoMapper{ListScoreDTOlist(QueryDTOquery);}对应 XMLselectidlistresultTypecom.example.ScoreDTOSELECT id AS scoreId, user_id AS userId, score FROM score_table WHERE user_id #{userId}/select方法名list和 XML 中的idlist对应。Request、DTO、VO 的区别一开始我很困惑为什么 Controller 接收一个对象转手又 copy 成另一个对象后来理解为Req接口层请求对象面向 Android / 前端 DTO业务层传输对象面向 Service / Mapper VO返回给前端或 Android 的展示对象例如publicAjaxResultlist(QueryReqreq){QueryDTOdtoBeanUtils.copy(req,QueryDTO.class);ListScoreVOlistservice.list(dto);returnAjaxResult.success(list);}这样做的好处是解耦外部接口参数变化不一定影响内部业务对象 内部业务字段变化也不一定暴露给外部这和 Android 中把网络 Response 转成 UI Model 的思路很像。MyBatis 参数是怎么进入 SQL 的XML 中经常看到#{userId}它的值来自 Mapper 方法传入的参数对象。例如ListScoreDTOlist(QueryDTOquery);XMLselectidlistparameterTypecom.example.QueryDTOSELECT * FROM score_tablewhereiftestuserId ! nullAND user_id #{userId}/if/where/select这里的#{userId}本质是query.getUserId()if是动态 SQL表示有值才拼接条件。MyBatis 返回结果怎么变成 Java 对象MyBatis 常见两种返回映射方式resultTypeselectidlistresultTypecom.example.ScoreDTOSELECT id AS scoreId, user_id AS userId, score FROM score_table/selectresultType表示 SQL 每一行结果自动封装成一个 Java 对象。一行 - ScoreDTO 多行 - ListScoreDTOresultMapresultMapidScoreMaptypecom.example.ScoreDTOidcolumnidpropertyscoreId/resultcolumnuser_idpropertyuserId/resultcolumnscorepropertyscore//resultMapselectidlistresultMapScoreMapSELECT id, user_id, score FROM score_table/selectresultMap是手动告诉 MyBatis数据库字段 - Java 字段比如user_id - userId如果项目没有开启下划线转驼峰配置那么 SQL 中最好写别名user_idASuserId否则user_id不一定能自动进入 Java 的userId字段。统一返回格式后端接口一般不会直接返回业务对象而是包一层统一结构。例如{code:200,msg:操作成功,time:2026-05-07 10:00:00,data:{}}Android 端通常应该有类似publicclassApiResultT{publicintcode;publicStringmsg;publicStringtime;publicTdata;}判断时不要只看 HTTP 是否成功还要看业务codeif(response.body()!nullresponse.body().code200){// 业务成功}else{// 显示 msg}异常处理后端里常见两种错误返回方式returnAjaxResult.error(操作失败);或者thrownewServiceException(业务异常);如果项目配置了全局异常处理器那么 Controller 一般不需要每个接口都手动 try-catch。业务层抛出的异常会被统一捕获然后转换成统一 JSON 返回给 Android。理解方式Service 抛异常 - 全局异常处理器捕获 - 统一封装 code/msg - Android 显示 msg登录接口的两种情况这次学习中我发现后端项目中可能同时存在两类登录。业务登录有些 Android 端登录接口只是校验账号密码然后返回userId。流程Android 传 username/password - 后端查询用户 - 后端校验密码 - 返回 userId - Android 用 userId 继续查用户信息或提交业务数据这种接口不一定返回 token也不一定使用 Authorization 请求头。JWT 登录另一类是标准 token 登录。流程登录成功 - 后端生成 token - Android 保存 token - 后续请求添加 Authorization 请求头 - 后端过滤器校验 token - 后端识别当前登录用户Android 请求头类似Authorization: Bearer xxx后端通常通过过滤器从请求头读取 token然后把登录用户放入安全上下文。Android baseUrl 和后端端口Android 项目中 Retrofit 一般配置newRetrofit.Builder().baseUrl(http://host:port/).addConverterFactory(GsonConverterFactory.create()).build();后端本地配置的端口不一定和 Android 访问端口一致。可能存在Nginx 转发网关Docker 端口映射测试环境和正式环境端口不同所以看到Android 访问端口 A 后端配置端口 B不一定矛盾可能是部署层做了转发。如果我要新增一个接口以后如果要新增一个后端接口我会按这个顺序做1. 明确需求URL、GET/POST、入参、返回值 2. 定义 Req / DTO / VO 3. 写 Controller 方法 4. 在 Service 接口加方法 5. 在 ServiceImpl 写业务逻辑 6. 在 Mapper 接口加方法 7. 在 MyBatis XML 写 SQL 8. Android Retrofit 增加接口 9. Android 增加 Result Bean 10. 联调并检查 code/msg/data一个简单查询接口可能是GetMapping(/latest)publicAjaxResultlatest(RequestParam(userId)LonguserId){returnAjaxResult.success(scoreService.latest(userId));}SQL 中为了避免映射问题尽量写清楚别名SELECTidASscoreId,user_idASuserId,score,create_timeAScreateTimeFROMscore_tableWHEREuser_id#{userId}ORDERBYcreate_timeDESCLIMIT1AndroidGET(app-context/score/latest)CallScoreResultlatest(Query(userId)longuserId);学习收获这次学习让我最有收获的是后端不是一堆陌生注解而是一条可以追踪的链路。从 Android 视角看后端可以按下面的问题一步步拆1. Android 调的是哪个 URL 2. 后端哪个 Controller 接收 3. 参数是 Query、Path、Body 还是 Multipart 4. Controller 调哪个 Service 5. Service 有没有业务处理 6. Mapper 方法名对应哪个 XML select/update 7. SQL 查哪张表 8. SQL 结果怎么映射成 DTO 9. AjaxResult 的 data 是什么 10. Android 的 Result Bean 是否和后端返回一致只要能回答这些问题就能看懂大部分业务接口。后续计划接下来我准备继续做两件事选一个 Android 页面从点击按钮开始完整追踪到后端 SQL。尝试自己写一个简单查询接口并在 Android 中完成联调。这样比单独看教程更有效因为它直接连接了我熟悉的 Android 项目和正在学习的后端项目。