第一部分:图片展示问题(图片展示前半段请看上一期文章)
1.1 上传与展示的区别
⭐老师强调:上传和展示是两个独立步骤——上传只是把文件存进去,展示是后续从存储中读取并呈现,不能以为传完就自动显示。
| 步骤 | 说明 |
|---|---|
| 上传 | 把文件存到磁盘(如E:/upload/) |
| 展示 | 从存储中读取并渲染到页面 |
问题现象:
- 用户上传图片后,前端页面没有变化
- 后端接口仅返回了success状态,没有返回图片具体信息
- 前端接收到数据后,缺少处理逻辑,无法展示图片
1.2 图片的特殊性
⭐老师强调:图片较特殊,必须显式引入页面才能展示;而word、pdf等一般文件不需要引。
<!-- 图片必须用img标签引入 --> <img src="cebbcb019f2f469ca6330d7fb5dbb431.jpg">1.3 路径问题
⭐老师强调:不能直接写文件路径。因为网页默认用http协议加载,直接写本地路径对应的是file协议,两协议不同,浏览器不允许这样加载资源。
| 协议 | 说明 |
|---|---|
| http协议 | 网页默认使用 |
| file协议 | 本地文件路径使用 |
⭐老师强调:传统Tomcat部署目录有文件夹,可直接通过路径访问静态资源;但Spring Boot内嵌Tomcat无法操作,文件在磁盘某处也访问不了。
第二部分:资源映射配置(WebConfig)
2.1 解决思路
⭐老师强调:写一个普通Java工具类,实现Spring MVC提供的扩展配置接口。该接口能在不修改Spring Boot配置的前提下改变Spring MVC行为。
WebMvcConfigurer接口:
- 可拦截静态资源
- 可处理跨域
- 接口内含多个方法,按需求实现即可
2.2 WebConfig代码
package com.qcby.util; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 资源处理器映射 @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 访问地址:OIP-C.jpg registry.addResourceHandler("/showImg/**") .addResourceLocations("file:E:/upload/"); } }2.3 映射规则
⭐老师强调:访问前缀与本地目录的绑定关系,且配置需成对修改保持一致。
| 部分 | 作用 | 示例 |
|---|---|---|
/showImg/** | 访问前缀(可随意改名) | xxx.jpg |
file:E:/upload/ | 本地磁盘目录 | E:xxx.jpg |
映射效果:
请求 OIP-C.jpg │ ▼ 映射到 file:E:OIP-C.jpg │ ▼ 返回图片给浏览器⭐老师强调:
- 这类似给网址起"外号"指向真实文件夹
- 前缀像门牌,后段像房号,拼起来才找到文件
- 代码两处要同步改,只改一处会导致映射失效
第三部分:统一返回格式
3.1 问题背景
⭐老师强调:后端接口查单个返回的是对象,查所有返回的是数组,两者格式不统一。前端拿到对象只能"点属性"直接用,拿到数组却要加循环依次套用,得先判断查的是"一个"还是"所有"。
| 接口 | 返回类型 | 前端处理 |
|---|---|---|
| 查全部 | List | 需要循环 |
| 按id查 | Student对象 | 直接点属性 |
⭐老师强调:这种对象与数组混用的方式不合理,应该像之前Servlet那样统一返回格式,避免前端额外区分。
3.2 统一返回模板
⭐老师强调:不管查询方式是查全部还是按id查,也不管结果有几条,返回结构必须统一——业务数据都放在data字段里。
返回模板结构:
{ "code": 200, "msg": "提示信息", "data": "业务数据" }3.3 ResponseResult代码
package com.qcby.model;(放这) import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { private Integer code; private String msg; private T data; public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }3.4 泛型 vs Object
⭐老师强调:
Object可作为最大父类型来兜底承载数据,但更推荐用泛型,由调用方传入具体类型,灵活且类型安全。
| 对比 | Object | 泛型<T> |
|---|---|---|
| 类型安全 | 需强制转换 | 编译期检查 |
| 灵活性 | 低 | 高 |
| 推荐度 | ❌ | ✅ |
3.5 构造方法
⭐老师强调:至少可定义三个构造方法,也可按需简化。
// 完整构造(含msg) public ResponseResult(Integer code, String msg, T data) { ... } // 简化构造(不含msg) public ResponseResult(Integer code, T data) { ... }⭐老师强调:根据传参决定调用哪个构造方法,比如传200和list时,只返回code和data,不带message。
3.6 @JsonInclude注解
⭐老师强调:未赋值的引用类型字段(如message设为null)默认也会输出。若想空值不展示,可使用
@JsonInclude注解。
放在ResponseResult上 @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { // message为空时不输出 }效果对比:
| 设置 | 返回结果 |
|---|---|
| 无注解 | {"code":200,"msg":null,"data":[...]} |
| 有注解 | {"code":200,"data":[...]} |
⭐老师强调:区分"不赋值"和"不输出"——不赋值是数据状态,不输出靠注解控制序列化行为,二者不可混淆。
第四部分:Controller改造
4.1 改造前
@RequestMapping("search") @ResponseBody public List<Student> findAll() { List<Student> list = studentService.findAll(); return list; // 直接返回List } @RequestMapping("searchById") @ResponseBody public Student searchById(Integer id) { Student student = studentService.findById(id); return student; // 直接返回对象 }4.2 改造后(统一格式)
@RequestMapping("search") @ResponseBody public ResponseResult findAll() { System.out.println("controller层查找"); List<Student> list = studentService.findAll(); return new ResponseResult(200, list); // 包进Result } @RequestMapping("searchById") @ResponseBody public ResponseResult searchById(Integer id) { Student student = studentService.findById(id); return new ResponseResult(200, student); // 包进Result }⭐老师强调:
- 别直接返裸数据,要"绕一下"包进Result
- 所有数据都放进data字段,保持结构统一
- 前端拿到的永远是
value.data,不用区分对象还是数组
附录一:完整代码汇总
WebConfig.java(资源映射)
package com.qcby.util; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/showImg/**") .addResourceLocations("file:E:/upload/"); } }ResponseResult.java(统一返回模板)
package com.qcby.model; import com.fasterxml.jackson.annotation.JsonInclude; @JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { private Integer code; private String msg; private T data; public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } }StudentController.java
package com.qcby.controller; import com.qcby.entity.Student; import com.qcby.model.ResponseResult; import com.qcby.service.StudentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; import java.util.List; @Controller public class StudentController { @Autowired StudentService studentService; @RequestMapping("search") @ResponseBody public ResponseResult findAll() { System.out.println("controller层查找"); List<Student> list = studentService.findAll(); return new ResponseResult(200, list); } @RequestMapping("searchById") @ResponseBody public ResponseResult searchById(Integer id) { Student student = studentService.findById(id); return new ResponseResult(200, student); } @RequestMapping("delete") @ResponseBody public int delete(Integer id) { int code = studentService.delete(id); return code; } }index.html(图片展示)
<!-- 上传表单 --> <form class="filebox"> 选择文件:<input type="file" class="file" name="file"> <input type="button" class="btn" value="上传"> </form> <!-- 图片展示:路径指向映射前缀 --> <img src="cebbcb019f2f469ca6330d7fb5dbb431.jpg"> <!-- 文件下载 --> <h3>文件下载</h3> <div class="block"> <div>服务器保存文件名(saveFileName):</div> <input type="text" class="downFileName" placeholder="上传成功后复制saveFileName填入"> <button class="内部工具Btn">下载文件</button> </div>