案例:图书管理(SpringBoot+Thymeleaf+SpringData-JPA)
添加图书:图书基本信息及封面图片的上传及入库
图书详细:图书基本信息和封面图片显示
- SpringData JPA 使用
- 上传页面的三个必须要求
- 图片上传接收和处理
- 资源映射(图片回显)
- 全局异常处理
- CREATE DATABASE wdzldb`
-
- USE `wdzldb`;
-
- DROP TABLE IF EXISTS `book`;
-
- CREATE TABLE `book` (
- `bookid` int(11) NOT NULL AUTO_INCREMENT,
- `bookName` varchar(120) DEFAULT NULL,
- `price` float DEFAULT NULL,
- `pubDate` date DEFAULT NULL,
- `author` varchar(20) DEFAULT NULL,
- `version` int(11) DEFAULT '0',
- `state` int(11) DEFAULT NULL,
- `pic` varchar(50) DEFAULT NULL,
- PRIMARY KEY (`bookid`)
- ) ENGINE=InnoDB AUTO_INCREMENT=157 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-
- /*Data for the table `book` */
-
- insert into `book`(`bookid`,`bookName`,`price`,`pubDate`,`author`,`version`,`state`,`pic`) values
- (22,'Java实战开发3',34,'2021-07-28','王磊',1,1,NULL),
- (53,'Java实战开发666',120,'2021-07-24','诸葛亮',0,1,NULL),
- (61,'Java实战开发1',39,'2021-07-29','王磊',0,1,NULL),
- (62,'Java实战开发1',39,'2021-07-29','王磊',0,0,NULL),
- (66,'Java实战开发1',39,'2021-07-29','王磊',0,0,NULL),
- (67,'SpringCloud微服务实战',45,'2021-08-11','王帆',0,0,NULL),
- (68,'SPringBoot整合JDBC',56,'2021-08-11','周瑜',0,1,NULL),
- (70,'SpringBoot入门与提高',78,'2021-08-11','曹操',0,1,NULL),
- (71,'Java实战开发5',100,'2021-07-23','诸葛亮',0,0,NULL),
- (72,'Java虚拟机深入',23,'2021-08-11','赵紫阳',0,1,NULL),
- (73,'深入学习Java虚拟机',69,'2021-08-05','黄盖',0,0,NULL),
- (74,'JSP开发技术',34,'2021-08-12','王超',0,1,NULL)
先搭建基本框架,完成业务层、DAO 层、pojo 等模块编写,并调试通过 springdata 正常使用。
web 启动器、thymeleaf 模板启动器、springdata 启动器及数据库驱动等
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-data-jpaartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-thymeleafartifactId>
- dependency>
- <dependency>
- <groupId>org.springframework.bootgroupId>
- <artifactId>spring-boot-starter-webartifactId>
- dependency>
-
- <dependency>
- <groupId>mysqlgroupId>
- <artifactId>mysql-connector-javaartifactId>
- <scope>runtimescope>
- dependency>
- <dependency>
- <groupId>org.projectlombokgroupId>
- <artifactId>lombokartifactId>
- <optional>trueoptional>
- dependency>
application.yml 数据源的基本信息配置、jpa 是否显示 sql 及 下划线等
- spring:
- datasource:
- username: root
- password: root
- url: jdbc:mysql://127.0.0.1:3306/wdzldb?allowMultiQueries=true&useUnicode=true&allowPublicKeyRetrieval=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
- driver-class-name: com.mysql.cj.jdbc.Driver
- jpa:
- show-sql: true
- hibernate:
- naming:
- physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
application.properties
下面多数是在使用阿里的初始化工具时,自动生成的
除了下面的上传保存的路径是自定义的
- # 应用名称
- spring.application.name=springboot_other
- # 应用服务 WEB 访问端口
- server.port=8080
- # THYMELEAF (ThymeleafAutoConfiguration)
- # 开启模板缓存(默认值: true )
- spring.thymeleaf.cache=false
- # 检查模板是否存在,然后再呈现
- spring.thymeleaf.check-template=true
- # 检查模板位置是否正确(默认值 :true )
- spring.thymeleaf.check-template-location=true
- #Content-Type 的值(默认值: text/html )
- spring.thymeleaf.content-type=text/html
- # 开启 MVC Thymeleaf 视图解析(默认值: true )
- spring.thymeleaf.enabled=true
- # 模板编码
- spring.thymeleaf.encoding=UTF-8
- # 要被排除在解析之外的视图名称列表,⽤逗号分隔
- spring.thymeleaf.excluded-view-names=
- # 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers( 默认值: HTML5)
- spring.thymeleaf.mode=HTML
- # 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ )
- spring.thymeleaf.prefix=classpath:/templates/
- # 在构建 URL 时添加到视图名称后的后缀(默认值: .html )
- spring.thymeleaf.suffix=.html
-
- #上传的绝对路径
- file.upload.path=d://save/images/
- #绝对路径下的相对路径
- file.upload.relativePath=/images/
- #文件上传大小限制
- spring.servlet.multipart.max-file-size=2048000
注意:日期和图片处理
- @Data
- @Entity(name = "book") //要求必须有@Id 也
- @ApiModel(value = "图书实体", description = "图书中明细属性")
- public class Book {
-
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY) //注意:默认的是序列,针对Oracle的
- private Integer bookId;
-
- @ApiModelProperty(value = "图书名")
- private String bookName;
-
- @ApiModelProperty(value = "图书作者")
- private String author;
-
- private Float price;
-
- @Column(name = "pic")
- private String picpath; // 封面
-
- @DateTimeFormat(pattern = "YYYY-MM-dd")
- private Date pubDate; //出版日期
- }
dao 接口直接使用 springdata 提供的统一接口 JpaRepository ,其中已经包含了基本的操作。也可以按约定规则自己定义其他方法
注意:继承接口时需要指定泛型
- public interface IBookDao extends JpaRepository
{ - List
queryBooksByBookName(String bookName); - List
findBooksByPriceBetween(Float min, Float max); - List
findBooksByBookNameLike(String bookName); -
- List
findAllByPriceOrderByPrice(float price); -
- @Query(
- "select bookId,bookName,price,author from Book where bookName like :bookname"
- )
- Object[] queryBook(@Param("bookname") String bookName);
-
- //HQL 语句 select book from Book book where ...
- @Query("from Book where bookName like :bookname")
- List
queryBooks(@Param("bookname") String bookName); - }
接口和实现类
- public interface IBookService {
- void add(Book book);
- void delete(Integer bookId);
- Book detail(Integer bookId);
- List
queryAll() ; - void update(Book book);
- }
- package com.wdzl.service.impl;
-
- import com.wdzl.dao.IBookDao;
- import com.wdzl.pojo.Book;
- import com.wdzl.service.IBookService;
- import java.util.List;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.stereotype.Service;
-
- /**
- * @Author: zhang
- * @Date:2022/8/12
- * @Description:
- */
- @Service
- public class BookService implements IBookService {
-
- @Autowired
- private IBookDao bookDao;
-
- @Override
- public void add(Book book) {
- System.out.println(book.getBookId());
- bookDao.save(book); // 注意: 如果对象在数据库中存在的,执行修改。
- System.out.println(book.getBookId());
- }
-
- @Override
- public void delete(Integer bookId) {
- bookDao.deleteById(bookId);
- }
-
- @Override
- public Book detail(Integer bookId) {
- return bookDao.findById(bookId).get();
- }
-
- @Override
- public List<Book> queryAll() {
- return bookDao.findAll();
- }
-
- @Override
- public void update(Book book) {
- //如果对象是存在时,就是修改操作,如果不存在则插入操作
- bookDao.save(book);
- }
- }
到这里,就可以使用单元测试来测试 springdata 是否能正常使用了。
页面必须
1.method必须是post 2.enctype="multipart/form-data" 必须 3.<input type="file" /> 必须
static 目录 下的 add.html:
- html>
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <title>Titletitle>
- head>
- <body>
-
-
- <h2>添加图书h2>
- <form action="add" method="post" enctype="multipart/form-data">
- <p>图书名字:<input name="bookName" />p>
- <p>图书价格:<input name="price" />p>
- <p>图书作者:<input name="author" />p>
- <p>出版日期:<input name="pubDate" type="date" />p>
- <p>图书封面:<input name="pic" type="file" />p>
- <p><input type="submit" value="保存" />p>
- form>
- body>
- html>
注意文件上传处理:单独上传、文件名重命名、保存路径的配置等
- package com.wdzl.controller;
-
- import com.wdzl.pojo.Book;
- import com.wdzl.service.IBookService;
- import com.wdzl.util.FileUtils;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.util.List;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.stereotype.Controller;
- import org.springframework.ui.ModelMap;
- import org.springframework.web.bind.annotation.ExceptionHandler;
- import org.springframework.web.bind.annotation.GetMapping;
- import org.springframework.web.bind.annotation.PostMapping;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.multipart.MultipartFile;
-
- /**
- * @Author: zhang
- * @Date:2022/8/12
- * @Description:
- *
- */
- @Controller
- public class BookController {
-
- @Autowired
- private IBookService bookService;
-
- @Value("${file.upload.savepath}")
- private String savePath; //保存文件根目录
-
- @Value("${file.upload.relativePath}")
- private String relativePath;
-
- /**
- * 日期处理
- * 文件上传
- * @param book
- * @return
- */
- @PostMapping("add")
- public String add(Book book, MultipartFile pic) {
- System.out.println("============add()========");
-
- String oriName = pic.getOriginalFilename(); //原始文件命名
-
- //判断目录是否存在并创建
- File rootDir = new File(savePath);
- if (rootDir.exists() == false) {
- rootDir.mkdirs();
- }
- if (!pic.isEmpty()) {
- //文件名
- String fileName = FileUtils.rename(oriName);
-
- File saveFile = new File(rootDir, fileName);
- //转存到指定文件中
- try {
- pic.transferTo(saveFile);
- System.out.println(">>>>>>文件保存在:" + saveFile.getAbsolutePath());
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- // 文件相对路径 用来入库和回显
- fileName = relativePath + fileName;
- book.setPicpath(fileName);
- }
-
- //入库
- bookService.add(book);
- return "redirect:list"; // /list
- }
-
- @GetMapping("list")
- public String list(ModelMap modelMap) {
- List
list = bookService.queryAll(); - modelMap.put("booklist", list);
- return "list"; ///templates/list.html
- }
-
- @GetMapping("detail")
- public String detail(Integer bookId, ModelMap modelMap) {
- Book book = bookService.detail(bookId);
- modelMap.put("book", book);
- return "detail";
- }
- }
添加成功后,跳转到列表页面显示,下面使用的 thymeleaf 遍历显示
注意:下面图片路径、链接等处理
templates 下的 list.html
- html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <head>
- <meta charset="UTF-8" />
- <title>图书列表title>
- <style type="text/css" rel="stylesheet">
- div {
- margin: 30px;
- text-align: center;
- }
- style>
- head>
- <body>
- <a href="add.html">添加图书a>
- <hr />
- <div>
- <table width="75%">
- <thead>
- <tr bgcolor="#556b2f">
- <th>序号th>
- <th>书名th>
- <th>作者th>
- <th>价格th>
- <th>出版日期th>
- <th>操作th>
- tr>
- thead>
- <tbody>
- <tr th:each="book,st:${booklist}" th:bgcolor="${st.even?'#aac':''}">
- <td th:text="${st.count}">td>
- <td>
- <a
- th:href="${'detail?bookId='+book.bookId}"
- th:text="${book.bookName}"
- >a>
- td>
- <td th:text="${book.author}">td>
- <td th:text="${book.price}">td>
- <td th:text="${#dates.format(book.pubDate,'yyyy年MM月dd日')}">td>
- <td>
- <a th:href="${'del?bookId='+book.bookId}">删除a>
- td>
- tr>
- tbody>
- table>
- div>
- body>
- html>
到此,可以通过前端的 add.html 来实现添加和上传操作了。
注意:默认文件上传大小是 1M,大于 1M 的会 500 异常。
可以通过配置修改默认文件大小限制:
#文件上传大小限制 spring.servlet.multipart.max-file-size=2048000
在项目发布运行中,不希望直接显示 500 异常页面时,可以配置全局异常解析器来进行处理
异常处理在 springboot 中有多种方式,下面介绍两种
位置:在启动类同包及子包下定义类
- @ControllerAdvice
- public class GlobalExceptionHander {
-
- @ExceptionHandler(Exception.class)
- public String doException(Exception ex) { // 不能使用model 无法传参到页面显示
- System.out.println(">>>>==异常了:" + ex.toString());
- return "error"; // 转发到 error.html 页面
- }
- }
上面的 @ControllerAdvice 注解中已经包含 @Component 注解,所以直接会被 spring 扫描加入容器中。
- /**
- * @Author: zhang
- */
- @Configuration
- public class ApplicationConfig {
-
- /**
- * 全局异常配置
- * 页面可以通过 exception对象来获取
- */
- @Bean
- public SimpleMappingExceptionResolver doException() {
- SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
- Properties properties = new Properties();
- properties.setProperty("java.lang.Exception", "error"); //映射异常类型和转发的页面对应关系
- resolver.setExceptionMappings(properties);
- System.out.println("===========异常配置======");
- return resolver;
- }
- }
上面是一个 @Configuration 标注的配置类。异常对象会被转发到页面。
在完成上面的文件上传和图书信息添加之后,跳转到图书列表页面,可以通过图书名链接打开图书详细信息。
但在显示图片静态资源时,路径问题导致图片无法正常显示。
下面来再来处理下文件上传和回显下载或显示问题
先来回顾上传时对于图片保存路径的设置
首先我们先在配置文件中自定义了两个路径:绝对路径和相对路径
- #上传的绝对路径, 文件保存的真正的目录位置
- file.upload.path=d://save/images/
- #绝对路径下的相对路径 用于前端请求调用的逻辑地址,默认是不能直接使用的
- file.upload.relativePath=/images/
在控制器保存图片时,使用上面地址保存图片和入库记录
- @PostMapping("add") //注意这里 pic 需要单独和页面元素对应,实体类中只是不同名的字符串用来存地址
- public String add(Book book, @ApiParam(value = "图片文件", required = true)MultipartFile pic){
- System.out.println(book+"===="+pic);
- String fileName = pic.getOriginalFilename();
- //重命名
- fileName = FileUtils.rename(fileName);
- // 保存到磁盘
- File saveDir = new File(savePath); //====================这里是真实保存目录
- if(saveDir.exists()==false){
- saveDir.mkdirs();//创建保存图片的目录
- }
- File saveFile = new File(saveDir,fileName);
- try {
- pic.transferTo(saveFile);
- System.out.println("图片保存在:"+saveFile.getAbsolutePath());
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- //保存的相对路径
- String picPath = relativePath + fileName; //==========这里逻辑目录 虚拟的
- System.out.println(">>>入库路径:"+picPath);
- book.setPicpath(picPath); //保存到实体类 入库
-
-
- //入库
- bookService.add(book);
- return "redirect:list";
- }
首先从上面代码中可以看到,保存的磁盘的目录和入库的路径是不同的,默认是不对应不能访问的。
页面中使用的路径为数据库中的相对逻辑路径
<img th:src="${book.picpath}" width="200" />
- html>
- <html lang="en" xmlns:th="http://www.thymeleaf.org">
- <head>
- <meta charset="UTF-8" />
- <title>图书明细title>
- head>
- <body>
- <div>
- <p>
- <img th:src="${book.picpath}" width="200" />
- p>
- <p>书名:<span th:text="${book.bookName}">span>p>
- <p>作者:<span th:text="${book.author}">span>p>
- <p>价格:<span th:text="${book.price}">span>p>
- <p>
- 出版日期:<span
- th:text="${#dates.format(book.pubDate,'yyyy-MM-dd')}"
- >span>
- p>
- div>
- body>
- html>
如果需要能正常的访问,则使用下面的配置进行映射
实现 WebMvcConfigurer 同时 标注 @Configuration
- /**
- * @Author: zhang
- * @Date:2022/8/11
- * @Description:
- */
- @Configuration
- public class ApplicationConfig implements WebMvcConfigurer {
-
- @Value("${file.upload.path}")
- private String savePath;
-
- @Value("${file.upload.relativePath}")
- private String relativePath;
-
- /**
- * 资源路径映射
- * 注意:路径前加 "file:/"
- * @param registry
- */
- @Override
- public void addResourceHandlers(ResourceHandlerRegistry registry) {
- System.out.println(relativePath + "==============" + savePath);
- registry
- .addResourceHandler(relativePath + "/**")
- .addResourceLocations("file:/" + savePath);
- }
- }
通过上面的配置后,再去访问就可以正常显示图片了。