从 Android 开发视角学习后端项目
本文是一次从 Android 开发转向理解 Java 后端项目的学习记录。文中已隐藏真实公司、项目、模块、接口域名、内网地址、账号密码和版权信息,仅保留通用技术结构与学习方法。
背景
我原本主要做 Android 开发,日常更熟悉 Activity、Fragment、ViewModel、Retrofit、OkHttp、Room、Gson 等移动端技术。最近接触到一个与 Android 项目配套的 Java 后端工程,希望能看懂接口是如何从 Android 请求一路走到数据库,再把结果返回给 App。
刚开始看后端项目时,最困惑的是目录很多、模块很多、注解很多,不知道应该从哪里入手。后来我发现,不应该一开始就试图理解全部后端知识,而是应该先抓住一条真实接口链路。
项目技术栈概览
这个后端项目是一个典型的 Java 企业后端工程,主要技术栈包括:
- Java 8
- Maven 多模块工程
- Spring Boot
- Spring MVC
- Spring Security
- JWT Token
- MyBatis / MyBatis-Plus
- MySQL
- Redis
- Druid 数据库连接池
- Swagger / 接口文档工具
- PageHelper 分页
- Undertow / Web 容器
- Lombok
从 Android 视角类比:
| 后端技术 | 作用 | Android 类比 |
|---|---|---|
| Maven | 管理依赖和模块 | Gradle |
| Spring Boot | 启动和组织后端应用 | Android Application + 框架基础设施 |
| Controller | 提供 HTTP 接口 | Retrofit 接口的服务端实现 |
| Service | 业务逻辑层 | Repository / UseCase |
| Mapper | 数据库访问接口 | DAO |
| MyBatis XML | SQL 查询文件 | Room SQL / SQLite 查询 |
| DTO / VO | 参数和返回数据模型 | Request / Response Bean |
| AjaxResult | 统一接口返回格式 | ApiResponse |
| Redis | 缓存、登录态、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 上通常有:
@RestController@RequestMapping("/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));}Android:
@GET("app-context/module/list")Call<ResultBean>list(@Query("userId")longuserId,@Query("type")Stringtype);请求类似:
GET /list?userId=1&type=2Spring 会自动把 query 参数绑定到QueryReq对象里。
GET + @RequestParam
后端:
@GetMapping("/detail")publicAjaxResultdetail(@RequestParam("id")Longid){returnAjaxResult.success(service.detail(id));}Android:
@GET("app-context/module/detail")Call<ResultBean>detail(@Query("id")longid);这种适合参数比较少的查询接口。
POST + @RequestBody
后端:
@PostMapping("/save")publicAjaxResultsave(@RequestBodySaveReqreq){returnAjaxResult.success(service.save(req));}Android:
@POST("app-context/module/save")Call<ResultBean>save(@BodySaveReqreq);请求体是 JSON:
{"userId":1,"score":95}这种适合新增、保存、提交复杂对象。
Multipart 文件上传
后端:
@PostMapping("/upload")publicAjaxResultupload(@RequestParam("file")MultipartFilefile){returnAjaxResult.success();}Android:
@Multipart@POST("app-context/module/upload")Call<ResultBean>upload(@PartMultipartBody.Partfile);适合上传图片、视频、文件。
@PathVariable 路径参数
后端:
@GetMapping("/user/{userId}")publicAjaxResultuser(@PathVariable("userId")LonguserId){returnAjaxResult.success(service.user(userId));}Android:
@GET("app-context/module/user/{userId}")Call<ResultBean>user(@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{List<ScoreDTO>list(QueryDTOquery);}对应 XML:
<selectid="list"resultType="com.example.ScoreDTO">SELECT id AS scoreId, user_id AS userId, score FROM score_table WHERE user_id = #{userId}</select>方法名list和 XML 中的id="list"对应。
Request、DTO、VO 的区别
一开始我很困惑:为什么 Controller 接收一个对象,转手又 copy 成另一个对象?
后来理解为:
Req:接口层请求对象,面向 Android / 前端 DTO:业务层传输对象,面向 Service / Mapper VO:返回给前端或 Android 的展示对象例如:
publicAjaxResultlist(QueryReqreq){QueryDTOdto=BeanUtils.copy(req,QueryDTO.class);List<ScoreVO>list=service.list(dto);returnAjaxResult.success(list);}这样做的好处是解耦:
外部接口参数变化,不一定影响内部业务对象 内部业务字段变化,也不一定暴露给外部这和 Android 中把网络 Response 转成 UI Model 的思路很像。
MyBatis 参数是怎么进入 SQL 的
XML 中经常看到:
#{userId}它的值来自 Mapper 方法传入的参数对象。
例如:
List<ScoreDTO>list(QueryDTOquery);XML:
<selectid="list"parameterType="com.example.QueryDTO">SELECT * FROM score_table<where><iftest="userId != null">AND user_id = #{userId}</if></where></select>这里的:
#{userId}本质是:
query.getUserId()<if>是动态 SQL,表示有值才拼接条件。
MyBatis 返回结果怎么变成 Java 对象
MyBatis 常见两种返回映射方式:
resultType
<selectid="list"resultType="com.example.ScoreDTO">SELECT id AS scoreId, user_id AS userId, score FROM score_table</select>resultType表示 SQL 每一行结果自动封装成一个 Java 对象。
一行 -> ScoreDTO 多行 -> List<ScoreDTO>resultMap
<resultMapid="ScoreMap"type="com.example.ScoreDTO"><idcolumn="id"property="scoreId"/><resultcolumn="user_id"property="userId"/><resultcolumn="score"property="score"/></resultMap><selectid="list"resultMap="ScoreMap">SELECT id, user_id, score FROM score_table</select>resultMap是手动告诉 MyBatis:
数据库字段 -> Java 字段比如:
user_id -> userId如果项目没有开启下划线转驼峰配置,那么 SQL 中最好写别名:
user_idASuserId否则user_id不一定能自动进入 Java 的userId字段。
统一返回格式
后端接口一般不会直接返回业务对象,而是包一层统一结构。
例如:
{"code":200,"msg":"操作成功","time":"2026-05-07 10:00:00","data":{}}Android 端通常应该有类似:
publicclassApiResult<T>{publicintcode;publicStringmsg;publicStringtime;publicTdata;}判断时不要只看 HTTP 是否成功,还要看业务code:
if(response.body()!=null&&response.body().code==200){// 业务成功}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_timeDESCLIMIT1Android:
@GET("app-context/score/latest")Call<ScoreResult>latest(@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 项目和正在学习的后端项目。