第一章 HelloWorld

1.0 idea工具破解

参考博客:https://www.cnblogs.com/sujw/p/14025767.html

  • 下载安装
  • 启动IDEA,首次启动,选择第二个选项,然后OK,选择免费试用,进入软件界面,创建一个新项目
  • 然后直接将刚才解压好的**jetbrains-agent.jar文件用鼠标拖进idea 界面,出现如下界面,点击Restart**
  • 重启成功后,会跳出如下界面,直接点击**为IDEA安装,继续点击**安装,破解就成功了

1.1创建项目

springboot入门,我们从一个hello world开始入门,首先我们新建一个springboot项目

  • 新建项目,选择Spring Initializer,SDK选择Java1.8,Service URL就使用默认的,配置项目信息
  • Group:包名 Artifact:项目名称;
  • 依赖部分:选择Web - Spring Web
  • 配置项目名称和路径

idea和maven版本问题

如果版本不一致,则可能无法使用maven下载依赖,我们可以使用idea提供的默认maven(会显示出版本号)

端口号被占用问题

可以使用如下命令查找被占用8080端口的pid,然后杀掉

1
2
lsof -i tcp:8080  查找8080端口
kill 999 或者 kill -9 999 杀掉查找出来的pid

手动修改端口号:

在Mac下占用8080端口的pid老是杀不掉,杀死了又重启,所以这里选择修改版本号,在application.properties中配置如下语句:

1
server.port=8081

1.2配置国内仓库

打开idea的设置maven,有如下三个设置

  • Maven home directory:maven路径
  • User setting file:配置文件,我们可以复制一份源文件,放在我们自己定义的目录,配置国内仓库也是修改这个文件
  • local repository:下载依赖本地存放路径,这个路径,我们最好自己定义

settings.xml里面如下配置:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

1.3HelloWorld

创建一个包controller,创建HelloController,代码如下:

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
public class HelloController {

@RequestMapping("/say")
public String sayHello(){
return "hello springboot";
}
}

创建kotlin版本

启动类

1
2
3
4
5
6
@SpringBootApplication
class SpringBootKotlinApplication

fun main(args: Array<String>) {
runApplication<SpringBootKotlinApplication>(*args)
}

controller

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/api")
class HelloController {

@RequestMapping("/say")
fun sayHello():String{
return "hello World"
}
}

第二章 请求响应

2.1注解讲解

在上面的例子中,我们使用了两个注解

@RestController: 这个注解相当于@ResponseBody + @Controller合在一起的作用,即两个作用:首先表示该类是一个controller,第二,它可以返回JSON数据

@RequestMapping:定义请求的URL路径,可以标识在controller上面,也可以标识在方法上面

在上面的例子中,如果我们要在浏览器里访问sayHello方法,请求的URL地址为:

1
http://127.0.0.1:8081/api/say

请求类型

@RequestMapping可以定义请求的类型,是GET请求,还是POST请求,PUT请求,DELETE请求,如果省略不写,则默认支持所有类型的请求,显示定义请求类型

1
@RequestMapping(value = ["/say"],method = [RequestMethod.GET])

简写形式:

1
2
@PostMapping("/say")  //POST请求
@GetMapping("/say") //GET请求

返回HTML

在上面的例子中,返回的是字符串或者说JSON格式的数据,如果是要返回一个HTML,应该怎么做呢?这里就需要用到thymeleaf

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

然后不能使用@RestController,应该使用@Controller,html文件在resources/templates下新建

1
2
3
4
5
6
7
8
9
10
11
12
13
@Controller
@RequestMapping("/api")
class BookListController {

@RequestMapping("/bookList")
fun getBookList():String{
return "bookList" //这里并不是返回bookList字符串,而是bookList.html
}

@RequestMapping("/getAll")
@ResponseBody //如果要返回字符串数据,则这里就要使用@ResponseBody
fun getAll():String = "get All" //这里返回的就是字符串数据
}

2.2参数处理

知识预览

在本小节,你将学习到如何获取get请求和post请求的参数,主要使用以下两个注解

  • @PathVariable:处理路径参数,如/{id}
  • @RequestParam:获取post请求表单数据,get请求?参数

PathVariable 案例一:获取请求参数

需求分析:如下是一个restful风格的请求,请求URL如下所示:

1
http://127.0.0.1:8081/api/v1/book/{id}  如:http://127.0.0.1:8081/api/v1/book/12

在后端我们需要解析出末尾值为12的id参数,需要使用@PathVariable这个注解,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/api/v1")
class BookController {

@GetMapping("/book/{id}")
fun getOne(@PathVariable id:Long):Map<String,String>{

var map = HashMap<String,String>()
map["name"] = "Flutter开发指南"
map["money"] = "13.14"
return map
}
}

需要注意的是参数路径中的名称需要和方法中的参数名称一致,否则无法解析,如果确实需要不一致,需要在注解中声明

1
2
3
4
@GetMapping("/book/{id}")
//这里因为id和ids不一致,所以必须在注解中声明
fun getOne(@PathVariable("id") ids:Long):Map<String,String>{
}

RequestParam 案例一

1
2
3
4
5
6
7
8
9
10
11
12
//创建一本书
@PostMapping("/book")
fun crateBook(@RequestParam book: String,
@RequestParam price: String,
@RequestParam isbn: String): Map<String, String> {

return mapOf(
"name" to book,
"price" to price,
"isbn" to isbn
).apply { println(this) }
}

在浏览器中输入:127.0.0.1:8080/api/v1/book,添加表单参数:book,price,isbn,请求成功,返回数据:

1
2
3
4
5
{
"name": "Flutter第一行代码",
"price": "68.0",
"isbn": "124646455612"
}

RequestParam 案例二

如何获取请求url后面跟着?的参数呢?如:?page=2&pageNum=10,请看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//获取书单列表
@GetMapping("/bookList")
fun getBookList(@RequestParam page: Int, @RequestParam pageNum:Int):Map<String,Any>{
val map = mapOf(
"name" to "Flutter第一行代码",
"price" to 68.0,
"isbn" to "124564545654"
)
val bookList = listOf<Map<String,Any>>(
map,
map
)
return mapOf(
"page" to page,
"pageNum" to pageNum,
"data" to bookList
)
}

在浏览器如何访问呢?输入:127.0.0.1:8080/api/v1/bookList?page=2&pageNum=10,得到 响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"page": 2,
"pageNum": 10,
"data": [
{
"name": "Flutter第一行代码",
"price": 68.0,
"isbn": "124564545654"
},
{
"name": "Flutter第一行代码",
"price": 68.0,
"isbn": "124564545654"
}
]
}

参数正则表达式**

需求:现在要求末尾请求参数为小写字母和下划线

1
2
正确的请求路径:http://127.0.0.1:8081/api/v1/book/12/ivan_
错误的请求路径:http://127.0.0.1:8081/api/v1/book/12/ivan_123

那么应该如何做呢?

路径参数的正则表达式规则:{路径参数名:正则表达式}

请看代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/api/v1")
class BookController {

//username使用正则表达式,限制为小写字母和下划线
@GetMapping("/book/{id}/{username:[a-z_]+}")
fun getOne(@PathVariable("id") id:Long,@PathVariable username:String):Map<String,String>{

var map = HashMap<String,String>()
map["name"] = "Flutter开发指南"
map["money"] = "13.14"
map["username"] = username
return map
}
}

第三章 配置文件

知识概要

在本章节,你将学习到如下内容:

  • application.properties,application.yml 两种配置文件
  • 常用配置属性
    • server.port server.context-path
    • logging.level.root logging.level.包名
    • logging.file
  • 自定义配置属性

3.1 yml

yml是一种新的配置文件,替代以前用的properties配置文件,主要使用key:(空格)value形式,yml文件放在resources目录下,一般命名为:application.yml

常用属性配置

  • server.port 端口号
  • server.servlet.context-path 配置应用根路径,如:/api/v1
  • logging.level.root 根级别日志 logging.level.包名 包级别日志,logging.file:定义log日志文件名和路径

自定义配置属性

步骤一:在yml文件中定义自定义属性

1
2
3
4
5
#自定义配置属性
book:
name: flutter第一行代码
price: 66.6
isbn: "${random.uuid}"

步骤二:在代码中通过@Value注解,引入自定义属性,注意在kotlin中,$是取变量的值,所以这里需要转义字符

1
2
3
4
5
6
7
8
@Value("\${book.name}")
private lateinit var name:String

@Value("\${book.price}")
lateinit var price:String

@Value("\${book.isbn}")
lateinit var isbn:String

然后就可以直接使用变量啦!

自定义配置对象

步骤一:同上,定义Yml配置属性

步骤二:新建实体类,使用注解@Component和@ConfigurationProperties(prefix = “xxx”),prefix对应yml配置属性

1
2
3
4
5
6
7
@Component
@ConfigurationProperties(prefix = "book")
class Book{
lateinit var name:String
lateinit var price:String
lateinit var isbn:String
}

步骤三:在代码中,使用Autowired注入对象

1
2
@Autowired
private lateinit var book:Book

3.2 yml多环境配置

一般我们会使用多个开发环境,如Dev开发环境,生产环境pro,那么我们如何通过yml来配置多环境呢?

第一步:首先我们需要定义各个环境的yml文件,如:

  • dev开发环境 application-dev.yml
  • pro开发环境 application-pro.yml

第二步:在application.yml中配置使用哪个开发环境

1
2
3
spring:
profiles:
active: dev 表示当前Dev开发环境处于激活状态

第四章 数据库

知识概要

在本章节,你将学习到以下内容

  • Springboot-Data-JPA
  • MySQL

4.1mysql

安装

安装版本:mysql-8.0.22-winx64

参考博客:https://blog.csdn.net/qq_44040327/article/details/110420405

Navicat连接mysql

连接失败,错误原因:mysql8 之前的版本中加密规则是mysql_native_password,而在mysql8之后,加密规则是caching_sha2_password,而我电脑上装的是最新版的mysql8

解决办法

方法1.升级navicat驱动;

方法2.把mysql用户登录密码加密规则还原成mysql_native_password.

这里我们选择第二种方式

第一步:mysql -u root -p,输入密码,进入mysql

第二步:进入如下操作:

1
2
3
**ALTER USER 'root'@'localhost' IDENTIFIED BY 'password' PASSWORD EXPIRE NEVER; #修改加密规则 (password就是你的密码)**
**ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password'; #更新一下用户的密码 password就是你的密码**
**FLUSH PRIVILEGES; #刷新权限**

参考博客:https://blog.csdn.net/seventopalsy/article/details/80195246?utm_medium=distribute.pc_relevant.none-task-blog-searchFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-searchFromBaidu-1.control

Navicat 创建数据库

字符集编码选择 utf-8 unicode,排序规则 utf8-bin

mysql时区问题

springboot启动报错,错误如下:

1
You must configure either the server or JDBC driver (via the serverTimezone conf)

错误原因:mysql版本太高,默认时区和当前电脑时区不一致,如何修改时区:

1
2
//serverTimezone=GMT%2B8 指定中国时区
url: jdbc:mysql://localhost:3306/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8

4.2 Sping-Data-JPA

pom.xml添加相关依赖

1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

注意,如果是用kotlin,还需要添加一些插件 jpa,all-open

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<configuration>
...
<compilerPlugins>
...
<plugin>jpa</plugin>
<plugin>all-open</plugin>
</compilerPlugins>
<pluginOptions>
<option>all-open:annotation=javax.persistence.Entity</option>
<option>all-open:annotation=javax.persistence.Embeddable</option>
<option>all-open:annotation=javax.persistence.MappedSuperclass</option>
</pluginOptions>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>

<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>

4.3数据库配置

1
2
3
4
5
6
7
8
9
10
11
#JPA配置,mysql数据库配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/book?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true

4.3实战增删改查

主要分为三层:

  • repository层 实际操作数据库
  • service层:连接controller和repository层
  • controller层:业务层

repository层

定义接口,继承JpaRepository,泛型为实体类,Long

1
interface BookRepository:JpaRepository<Book,Long>

service层

service层需要使用@Service标注,使用@Autowired注入BookRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
class BookService {

@Autowired
lateinit var bookRepository: BookRepository

fun add(book: Book): Book = bookRepository.save(book)

fun delete(id: Long) = bookRepository.deleteById(id)

//注意不要使用getOne,如果查询不到数据,会抛出异常
fun getOne(id:Long):Book? = bookRepository.findByIdOrNull(id)

fun findAll(): List<Book> = bookRepository.findAll()
}

controller层

新增一本书:使用post请求, @PostMapping(“/book”)

删除一本书:使用delete请求,@DeleteMapping(“/book/{id}”)

查询书单列表:使用get请求,@GetMapping(“/bookList”)

更新某本书:使用put请求,@PutMapping(“/update”)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@RestController
class BookController {

@Autowired
lateinit var service: BookService


//添加一本书
@PostMapping("/book")
fun add(book: Book):Book{
return service.add(book)
}

//删除某本书
@DeleteMapping("/book/{id}")
fun deleteById(@PathVariable id:Long){
service.delete(id)
}

@PutMapping("/update")
fun updateBook(book: Book){
service.add(book)
}

//查询所有书籍
@GetMapping("/bookList")
fun getBookList():List<Book>{
return service.findAll()
}

}

4.3复杂查询

如何查看Spring-Data-JPA查询关键词,在Spring.io官网,找Project,Spring-Data-JPA,点击查看想要查看版本的

Reference Doc.根据目录,找到Appendix C: Repository query keywords

Spring-Data-JPA默认提供的只有基于主键ID的增删改查,以及查询全部等方法,并没有提供POJO的其他属性的操作方法,需要自己手动书写,书写有一定的规则,一般有by,and等连接属性,idea会有语法提示,这个不用担心

1
2
//方法名必须按照规则写,参数名称不用和数据库字段一致,一定要写返回值类型
fun findByAuthorAndStatus(author:String,status:Int):List<Book>

And查询:findByAuthorAndStatus:根据author和status两个字段查询,都符合的才满足条件

EndsWith:以xxx结尾

1
2
//查询description以end结尾
fun findByDescriptionEndsWith(end:String):List<Book>

contain:包含

1
2
//查询描述中包含某个字符串
fun findByDescriptionContains(content:String):List<Book>

4.4 自定义查询

自定义查询适用于JPA提供的语法不能满足我们得需求,需要自定义SQL语句的查询,提供两种查询

  • JPQL查询
  • SQL查询

JPQL查询:这里使用的是类名,同时必须给类名取个别名

1
2
@Query("select item from Book item where length(item.name) >?1")
fun executeJPQL(len: Int): List<Book>

SQL查询:

注意即使是原生SQL语句,?后面也要跟占位符,1表示方法的第一个参数,如果有第二个参数,用2占位;原生SQL使用的是表名

1
2
3
4
5
@Query("select * from book where length(name) > ?1", nativeQuery = true)
fun executeSQL(len: Int): List<Book>

@Query("select u from User u where u.firstname like %?1")
fun findByFirstnameEndsWith(firstname:String):List<User>

4.5事务

本小节中,你将学习事务操作的两个注解

  • @Modifying
  • @Transactional

在spring-data-jpa中,自定义语句没有提供删除和更新的注解,仍然使用@Query来自定义,必须加上**@Modifying**

来支持更新和删除的语句,同时要支持事务,要添加@Transactional

案例一:根据id修改status属性

1
2
3
@Query("update Book b set b.status = ?2 where b.id = ?1")
@Modifying @Transactional
fun updateByJPQL(id: Long, status: Int):Int

注意:这里为什么没有使用jpa默认提供的save方法来更新,是因为save更新,会将实体类的每个属性都更新,而我们的需求只是根据id更新status属性

案例二:根据id删除该条数据

1
2
3
@Query("delete from Book b where b.id = ?1")
@Modifying @Transactional
fun deleteByJPQL(id:Long):Int

案例三:演示事务操作

在下面的例子中,我们执行了一条更新操作,一条删除操作,中间抛出了一个异常,但是加了事务注解,所以发生异常的时候,第一条更新操作会回滚

1
2
3
4
5
6
7
@Transactional
fun updateAndDeleteByJPQL(uId: Long, status: Int,dId:Long):Int{
val uCount = bookRepository.updateByJPQL(uId,status)
throw Exception()
val dCount = bookRepository.deleteByJPQL(dId)
return uCount + dCount
}

第五章 Thymeleaf

知识预览

在本章节,你将学习到如下内容:

  • Thymeleaf配置
  • Thymeleaf获取数据

5.1 配置thymeleaf

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

注意:默认springboot提供的是2版本,如果需要使用3版本,需要如下操作:

如果是SpringBoot1.x.x版本配置thymeleaf3

1
2
3
4
5
6
<properties>
...
//SpringBoot1.x.x版本配置thymeleaf
<thymeleaf.version>3.0.9.RELEASE</thymeleaf.version>
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
</properties>

如果是SpringBoot2.x.x版本配置thymeleaf3

1
2
3
4
5
6
<properties>
...
//SpringBoot2.x.x版本配置thymeleaf
<springboot-thymeleaf.version>3.0.9.RELEASE</springboot-thymeleaf.version>
<thymeleaf-layout-dialect.version>2.2.2</thymeleaf-layout-dialect.version>
</properties>

注意:我使用的springboot 2.4.0 ,当使用2.4.1时,找不到css

yml配置

1
2
3
spring:
thymeleaf:
model: HTML

5.2 thymeleaf取值

本小节你将学习到

  • thymeleaf传参:通过springMVC的model
  • thymeleaf获取数据:通过th:text

案例一:获取书单详情

调用service查询数据库,得到书单信息,添加到springMVC的model中

1
2
3
4
5
6
@GetMapping("/book/{id}")
fun detail(@PathVariable id:Long,model:Model):String{
val book: Book = service.getOne(id)
model.addAttribute("book",book)
return "book"
}

在book.html中如何获取数据呢?

1
2
3
4
<p><strong>书单:</strong><span th:text="${book.name}">西游记</span></p>
<p><strong>作者:</strong><span th:text="${book.author}">吴承恩</span></p>
<p><strong>描述:</strong><span th:text="${book.description}">这是一本中国古代神话小说</span></p>
<p><strong>状态:</strong><span th:text="${book.status}">0</span></p>

我们可以将数据在body引入,方便在页面各个地方引用,这里使用object

1
<body th:object="${book}">

使用的时候则用星号 *{}

1
th:text="*{name}"

注意:th需要导入头文件:xmlns:th=”http://www.w3.org/1999/xhtml"

5.3静态资源

本小节中,你将学习到

  • 引入bootstrap资源
  • thymeleaf引入静态资源

BootStrap

一:引入资源

下载bootstrap的zip包,用于生产环境的 Bootstrap,解压,拷贝css,js,fonts文件夹到项目的resources目录下

二:thymeleaf引入静态资源

header中引入:

注意

    1. href使用th:href,th:href引入资源只有在项目运行的时候才能有效
    2. 引入语法规则:@{/xxx} /表示资源的根路径,即static目录
1
2
3
4
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}"
href="../static/css/bootstrap.min.css">

body末尾引入:

1
2
<script src="https://cdn.jsdelivr.net/npm/jquery@1.12.4/dist/jquery.min.js"></script>
<script th:src="@{/js/bootstrap.min.js}" src="../static/js/bootstrap.min.js"></script>

5.4 判断和循环

Swtich case用法

1
2
3
4
5
<p th:switch="*{status}"><strong>状态:</strong>
<span th:case="0">想读</span>
<span th:case="1">在读</span>
<span th:case="2">已读</span>
</p>

If,unless

1
2
3
4
5
6
<div class="alert alert-success" th:unless="*{status == 0}">
<strong>不错</strong>,知识就是力量
</div>
<div class="alert alert-warning" th:if="*{status == 0}">
<strong>注意</strong>,你还没有行动
</div>

If:表示条件满足的时候显示 unless:表示条件不满足显示

Each:循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<table class="table table-bordered">
<thead>
<th>#</th><th>书名</th><th>作者</th><th>描述</th><th>状态</th>
</thead>

<tbody>
<tr th:each="book,iterStat:${books}">
<td th:text="${iterStat.count}">#</td>
<td th:text="${book.name}">书名</td>
<td th:text="${book.author}">作者</td>
<td th:text="${book.description}">描述</td>
<td th:text="${book.status}">状态</td>
</tr>
</tbody>
</table>

循环: th:each=”book,iterStat:${books},book表示每个item,iterStat:状态变量(itemStat是第二个参数,变量取名无所谓)

itemStat的属性有:

  • index:索引,从0开始
  • count:索引,从1开始
  • size:长度
  • first:当前循环是否第一个
  • last:当前循环是否最后一个

5.5 URL

案例一:从书单列表跳转到书单详情

1
<td><a th:text="${book.name}" href="#" th:href="@{'/book/'+${book.id}}">书名</a></td>

th:href=”@{‘/book/‘+${book.id}}” 这里使用的是字符串拼接+变量获取

5.6提交表单,重定向

1
<form action="#" th:action="@{/book/add}" method="post">

这里的@{/book/add} 表示提交到controller的一个方法,路径是/book/add

1
2
3
4
5
@PostMapping("/book/add")
fun add(book: Book):String{
service.add(book)
return "redirect:/books"
}

添加成功后,需要重定向到列表页面,重新查询数据

1
2
3
4
5
@GetMapping("/books")
fun bookList(model: Model):String{
service.findAll().apply { model.addAttribute("books",this) }
return "books"
}

注意以下几点:

  • 表单的name属性值要对应实体类的属性
  • controller中接收的实体类前不需要加@requestParam,基本类型属性才加

thymeleaf表单下拉框回显

1
2
3
4
5
<select name="status" id="statusId" class="form-control">
<option value="0" th:selected="*{status} == '0'">想读</option>
<option value="1" th:selected="*{status} == '1'">在读</option>
<option value="2" th:selected="*{status} == '2'">已读</option>
</select>

重定向带参数

使用RedirectAttributes,RedirectAttributes是Spring mvc 3.1版本之后出来的一个功能,专门用于重定向之后还能带参数跳转的

案例:新增书单后,重定向到列表页,携带参数message

1
2
3
4
5
6
7
//新增一本书单,重定向列表页
@PostMapping("/book/add")
fun add(book: Book,ra: RedirectAttributes):String{
service.add(book)
ra.addFlashAttribute("message","《${book.name}》更新成功")
return "redirect:/books"
}

5.7 使用内置对象

1
<p>对不起,你访问的资源[<code th:text="${#httpServletRequest.getAttribute('javax.servlet.error.request_uri')}"></code>]不存在!</p>

5.7 fragment

片段,即可以将公共部分抽取出来,如博客的导航栏部分,header部分

  • 使用th:fragment标识
  • 传参:这里需要传入参数title,使用th:replace替换标题
1
2
3
4
5
6
7
8
9
10
11
12
<head th:fragment="head(title)">
<meta charset="UTF-8">
<!--移动端-->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title th:replace="${title}">首页</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css">
<link rel="stylesheet" href="../static/css/typo.css" th:href="@{/css/typo.css}">
<link rel="stylesheet" href="../static/css/animate.css" th:href="@{/css/animate.css}">
<link rel="stylesheet" href="../static/lib/prism/prism.css" th:href="@{/lib/prism/prism.css}">
<link rel="stylesheet" href="../static/lib/tocbot/tocbot.css" th:href="@{/lib/tocbot/tocbot.css}">
<link rel="stylesheet" href="../static/css/me.css" th:href="@{/css/me.css}">
</head>

引入片段:th:replace=”_fragments :: head(~{::title})”,这里传参就是title标签的博客首页

1
2
3
4
5
6
7
8
<head th:replace="_fragments :: head(~{::title})">
<meta charset="UTF-8">
<!--移动端-->
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/semantic-ui/2.2.10/semantic.min.css">
<link rel="stylesheet" href="../static/css/me.css" >
<title>博客首页</title>
</head>

第六章 分页查询

知识预览

本章节,你将学习到以下内容

  • 分页 Pageable,PageRequest,Sort

6.1分页查询

分页查询主要使用以下对象, Pageable,PageRequest,Sort

案例一:查询所有书单

1
2
3
4
//Service层
fun findAllByPage(page: Pageable):Page<Book>{
return bookRepository.findAll(page)
}
1
2
3
4
5
6
7
//controller层 查询所有书单,提供默认值,防止不传参数报错,返回JSON数据
@GetMapping("/bookList")
fun getBookList(@RequestParam(defaultValue = "0") page: Int, @RequestParam(defaultValue = "5") size: Int): Page<Book> {
val sort = Sort.by(Sort.Direction.ASC, "id")
val pageAble: Pageable = PageRequest.of(page, size, sort)
return service.findAllByPage(pageAble)
}

如果是HTML页面:

1
2
3
4
5
6
7
8
9
10
11
//书单列表
@GetMapping("/books")
fun bookList(@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "5") size: Int,
model: Model):String{

val sort = Sort.by(Sort.Direction.ASC, "id")
val pageAble: Pageable = PageRequest.of(page, size, sort)
service.findAllByPage(pageAble).apply { model.addAttribute("page",this) }
return "books"
}

6.2 PageableDefault

如何避免page传-1报错,使用PageableDefault注解,当前端页面传递-1的时候,会默认传0到数据库

1
2
3
4
5
6
7
//书单列表
@GetMapping("/books")
fun bookList(@PageableDefault(size = 5,sort = ["id"],direction = Sort.Direction.ASC) page:Pageable,
model: Model):String{
service.findAllByPage(page).apply { model.addAttribute("page",this) }
return "books"
}

6.3删除书单

HTML部分

1
<a href="#" th:href="@{/books/{id}/delete(id=${book.id})}">删除</a>

controller层

1
2
3
4
5
6
7
//浏览器这里没法直接发起delete请求,所以这里用的GetMapping
@GetMapping("/books/{id}/delete")
fun delete(@PathVariable id:Long,ra:RedirectAttributes):String{
service.delete(id)
ra.addFlashAttribute("message","删除成功")
return "redirect:/books"
}

第七章 表单验证

7.1表单注册

新建实体类

1
2
3
4
5
6
7
8
@Entity
class User(
@Id @GeneratedValue var id: Long? = null,
var username: String? = null,
var password: String? = null,
var phone: String? = null,
var email: String? = null
)

注册controller

1
2
3
4
5
6
7
8
9
10
//注册
@PostMapping("/register")
fun register(@RequestParam username: String,
@RequestParam password: String,
@RequestParam email: String,
@RequestParam phone: String) :String{
val user = User(username = username,password = password,email = email,phone = phone.toInt())
repository.save(user)
return "redirect:/login"
}

数据传输

表单数据都会提交过来,包括确认密码,所以我们需要新建一个对象,UserForm接收表单数据,然后在User类中提供一个方法,拷贝需要的属性,再存入数据库

1
2
3
4
5
6
7
8
companion object{
fun convertUser(form:UserForm):User{
// val user = User()
// BeanUtils.copyProperties(form,user)
// return user
return User().apply { BeanUtils.copyProperties(form,this) }
}
}
1
2
3
4
...省略
val user = User.convertUser(userForm) //拷贝需要的属性
repository.save(user)
return "redirect:/login"

7.2 表单验证

表单验证这里主要是后台验证,使用validation注解处理,最新版本没有引入相关依赖,需要自己引入:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

遇到问题:SpringBoot Validation + Kotlin校验无效的解决方法

解决办法:使用 @field,@get

1
2
3
4
5
@field:Min(6,message = "密码必须大于6位数")
var password: String? = null,
@field:Pattern(regexp = PHONE_REG,message = "请输入正确手机号")
var phone: String? = null,
@get:Pattern(regexp = EMAIL_REG,message = "邮箱格式不对")

注意:@Email注解,没效果,所以用的正则表达式判断

7.3 错误数据返回

如果我们使用@Valid处理错误信息,那么当return “register”的时候,框架自动会将错误信息封装,不需要我们手动添加错误信息到model中返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//注册
@PostMapping("/register")
fun register(@Valid form:UserForm,result:BindingResult) :String{
if (!form.checkPassword()){
result.rejectValue("passwordAgain","confirmError","两次密码不一致")
}

if (result.hasErrors()){
return "register"
}

val user = User.convertUser(form)
repository.save(user)
return "redirect:/login"
}

前端页面:

1
2
3
4
5
6
7
<form th:action="@{/register}" action="/register" method="post">
<div class="form-group">
<label for="usernameId">用户名:</label>
<input type="text" th:field="*{username}" name="username" id="usernameId" class="form-control">
<p class="form-control-static text-danger" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">用户名不能为空</p>
</div>
</form>
1
2
3
判断是否显示错误:th:if="${#fields.hasErrors('username')}"

显示错误:th:errors="*{username}"

第八章:异常处理

知识预览

本章节,你将学习到如下内容:

  • 基于thymeleaf模板的异常处理
  • 基于RESTful API的异常处理

8.1定义错误页面

分两种情况,项目中是否使用了模板引擎

  • 未使用模板引擎,在static文件下,新建error文件夹,再新建类似404,403,500文件(没有成功,不知道为什么)

  • 使用了模板引擎,将error文件夹,移动到templates文件夹

    1
    <link rel="stylesheet" href="../../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}">

    案例一:404页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}">
<title>404</title>
</head>
<body>
<div class="container" style="max-width: 600px;margin-top: 50px">
<div class="jumbotron">
<h2>404</h2>
<p>对不起,你访问的资源[<code th:text="${#httpServletRequest.getAttribute('javax.servlet.error.request_uri')}"></code>]不存在! </p>
</div>
</div>
</body>
</html>

8.2 HTTP状态码处理异常

案例分析:在本小节中的案例中,根据ID查询数据库,没查询到数据,此时book为null,我们得需求是抛出一个自定义异常,BookNotFoundException,并且需要该异常对应到404页面,该如何处理呢?注意:如果我们不处理数据为空,thymeleaf也会抛出异常,最后到500异常页面,现在的需求就是知道是因为查询不到数据,应该是转到404页面

1
2
3
4
5
6
7
//书单详情
@GetMapping("/book/{id}")
fun detail(@PathVariable id: Long, model: Model): String {
val book = service.getOne(id)
model.addAttribute("book", book)
return "book"
}

service层:

1
fun getOne(id:Long):Book? = bookRepository.findByIdOrNull(id)?:throw BookNotFoundException("查找不到该书单")

当查询出来的book为null时,抛出自定义异常,并响应到404页面,给用户提示

1
2
3
//HttpStatus.NOT_FOUND 即404
@ResponseStatus(code = HttpStatus.NOT_FOUND)
class BookNotFoundException(message: String?) : RuntimeException(message)

8.3 全局异常处理

在上一章节,我们看到,我们可以在service里面处理异常,其实也可以在controller中处理异常,但是controller中,只能处理当前controller中的异常,如果我们处理全局异常,需要使用到ControllerAdvice

可以看到:

  • 类上面使用了@ControllerAdvice注解
  • 处理全局异常的方法使用 @ExceptionHandler(value = [Exception::class])注解,参数表示可以处理哪些异常
  • 参数:request: HttpServletRequest, e: Exception
  • 返回值:ModelAndView
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ControllerAdvice
class GlobalExceptionHandler {

private val logger: Logger by lazy { LoggerFactory.getLogger(this.javaClass) }

@ExceptionHandler(value = [Exception::class])
fun handleException(request: HttpServletRequest, e: Exception): ModelAndView {
logger.error("request url :{}, exception: {}", request.requestURL, e.message)

//如果有其他自定义异常,则走自定义逻辑,否则走全局异常处理
if (AnnotationUtils.findAnnotation(e.javaClass, ResponseStatus::class.java) != null) {
throw e
}
return ModelAndView().apply {
addObject("url",request.requestURL)
addObject("exception",e)
viewName = "error/error"
}
}
}

前端页面小技巧:将错误信息使用注释异常,用户无法直接看到,但是开发者可以通过查看HTML源码,查看错误信息

主要是使用了th:utext,它可以转义HTML标签

用th:text不会解析html,用th:utext会解析html,在页面中显示相应的样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="../../static/css/bootstrap.min.css" th:href="@{/css/bootstrap.min.css}">
<title>500</title>
</head>
<body>
<div class="container" style="max-width: 600px;margin-top: 50px">
<div class="jumbotron">
<h2>500</h2>
<p>对不起,服务异常,请联系管理员!</p>
</div>

<div th:utext="'<!--'" th:remove="tag"></div>
<div th:utext="'Failed request url : ' + ${url}" th:remove="tag"></div>
<div th:utext="'Exception message : ' + ${exception.message}" th:remove="tag"></div>
<ul th:remove="tag">
<li th:each="st:${exception.stackTrace}" th:remove="tag">
<span th:utext="${st}" th:remove="tag"></span>
</li>
</ul>

<div th:utext="'-->'" th:remove="tag"></div>

</div>
</body>
</html>

第九章 日志与AOP

9.1日志

日志级别

Springboot 默认使用LogBack来记录日志,日志级别从低到高为:

1
TRACE < DEBUG < INFO < WARN < ERROR

Springboot 默认的日志级别为info,即只会打印info及更高级别的日志,如warn,error

如何修改日志级别?

1
2
3
4
logging:
level:
root: debug
com.ivan.springboot: debug

修改为debug级别日志,则控制台只会输出debug及以上的日志

日志输出到文件

1
2
3
4
5
6
7
logging.file.path:只能指定路径,默认为spring.log
logging.file.name:可以指定路径和文件名,如果同时指定,则path无效,建议指定name

logging:
file:
path: ttt //同时指定,这个无效,只能指定目录,默认文件名为spring.log
name: ddd/ivan.log //这个生效,同时指定目录和文件名

默认日志输出文件大小为10M,超过10M的时候,会切分,如spring.0 spring.1等多个文件

9.2 自定义日志配置

如果我们要进行更多的精细化的日志配置,那么我们需要使用XML来进行配置

指定日志配置文件

1
2
3
4
5
6
logging:
config: classpath:logback-dev.xml
level:
root: debug
file:
name: devlog/my.log

通过logging.config 来指定日志配置文件

主要包含两部分:

  • 继承的默认配置
  • 需要自定义的配置:appender部分

在以下文件中,我们主要配置日志文件名和分割大小

  • 日志文件名:fileNamePattern来配置,加上年月日,方便我们查看日志
  • 日志切分大小:maxFileSize来配置,默认10兆,我们可以根据需要修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!--包含Spring boot对logback日志的默认配置-->
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<property name="LOG_FILE" value="${LOG_FILE:-${LOG_PATH:-${LOG_TEMP:-${java.io.tmpdir:-/tmp}}}/spring.log}"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml" />

<!--重写了Spring Boot框架 org/springframework/boot/logging/logback/file-appender.xml 配置-->
<appender name="TIME_FILE"
class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
</encoder>
<file>${LOG_FILE}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--LOG_FILE 对应我们在yml中配置的日志文件名-->
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i</fileNamePattern>
<!--保留历史日志一年的时间-->
<maxHistory>30</maxHistory>
<!--
Spring Boot默认情况下,日志文件10M时,会切分日志文件,这样设置日志文件会在100M时切分日志
-->
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>1MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>

</rollingPolicy>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="TIME_FILE" />
</root>

</configuration>
<!--
1、继承Spring boot logback设置(可以在appliaction.yml或者application.properties设置logging.*属性)
2、重写了默认配置,设置日志文件大小在100MB时,按日期切分日志,切分后目录:

my.2017-08-01.0 80MB
my.2017-08-01.1 10MB
my.2017-08-02.0 56MB
my.2017-08-03.0 53MB
......
-->

9.3 Spring AOP

引入AOP

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用AOP

和AOP相关的几个注解:

  • @Aspect 标识这是一个AOP
  • @Component
  • @Pointcut 定义切面
  • @Before 方法执行之前
  • @After 方法执行之后

切面定义规则:

@Pointcut(“execution(返回值 包名.类名.方法名(方法参数))”),使用* 表示任意值

如:

1
@Pointcut("execution(* com.qw.springbootdemo.springbootdemokt.controller.LogTestApi.log(..))")

如果是包下面的所有类的所有方法,则:

1
@Pointcut("execution(* com.qw.springbootdemo.springbootdemokt.controller.*.*(..))")

案例一:使用aop在方法执行前后打印日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
@Component
class LogAspect {

private val logger = LoggerFactory.getLogger(this.javaClass)

@Pointcut("execution(* com.qw.springbootdemo.springbootdemokt.controller.LogTestApi.log(..))")
fun log(){}

@Before("log()")
fun doBefore(){
logger.info("------doBefore----------")
}
@After("log()")
fun doAfter(){
logger.info("------doAfter----------")
}

@AfterReturning(pointcut = "log()",returning = "result")
fun doAfterReturning(result:Any){
logger.info("------doAfterReturning---------- 内容:{}",result)
}
}
1
2
3
4
5
6
7
8
9
10
11
@RestController
class LogTestApi {

private val logger = LoggerFactory.getLogger(this.javaClass)

@GetMapping("/log")
fun log():String{
logger.info("-------------log-------------")
return "---logTest---"
}
}

控制台执行结果:

1
2
3
4
c.q.s.springbootdemokt.aspect.LogAspect  : ------doBefore----------
c.q.s.s.controller.LogTestApi : -------------log-------------
c.q.s.springbootdemokt.aspect.LogAspect : ------doAfterReturning---------- 内容:---logTest---
c.q.s.springbootdemokt.aspect.LogAspect : ------doAfter----------

案例2:记录请求与响应

封装请求数据:

1
2
3
4
5
6
data class RequestLog(
var url:String,//请求的url资源
var ip:String,//请求方的IP地址
var method:String,//请求的控制器和方法
var args:Array<Any>,//请求参数
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Before("log()")
fun doBefore(joinPoint: JoinPoint){
//经过如下两步,可以拿到request请求
val attributes = RequestContextHolder.getRequestAttributes() as ServletRequestAttributes
val request = attributes.request
//封装请求数据
val requestLog = RequestLog(
url = request.requestURI,
ip = request.remoteAddr,
method = "${joinPoint.signature.declaringTypeName}.${joinPoint.signature.name}",
args = joinPoint.args
)
logger.info("------request-----{}",requestLog)
}
1
2
3
4
@AfterReturning(pointcut = "log()",returning = "result")
fun doAfterReturning(result:Any){
logger.info("------return---------内容:{}",result)
}

第十章 拦截器Intercepter

知识预览

  • 实现拦截器
  • 注册拦截器

实现拦截器

需要实现HandlerInterceptor接口,里面包含如下三个方法

  • preHandle :从servlet进入controller前拦截
  • postHandle :controller处理完,返回servlet前拦截
  • afterCompletion:完成后,一般用于释放资源等

注册拦截器

实现WebMvcConfigurer,重写addInterceptors方法,添加拦截器

案例一:实现登陆拦截

需求分析:当登陆完成后,可以访问一些页面(这些页面必须要求登陆状态),等退出登陆后,访问这些页面自动重定向到登陆页面

实现需求:

当登陆成功后,添加用户信息到session中:使用httpSession

1
2
3
4
5
6
7
8
9
@PostMapping("/login")
fun loginPost(@RequestParam username:String,@RequestParam password:String,httpSession: HttpSession):String{
val user = repository.findUserByUsernameAndPassword(username, password)
if (user != null){
httpSession.setAttribute("user",user)
return "index"
}
return "login"
}

拦截器逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LoginInterceptor : HandlerInterceptor {

override fun preHandle(request: HttpServletRequest,
response: HttpServletResponse,
handler: Any): Boolean {
//如果未登录,返回到登陆页面,否则通过
val user = request.session.getAttribute("user")
if (user == null) {
response.sendRedirect("${request.contextPath}/login")
return false
}
return true
}
}

注册拦截器:

需要实现WebMvcConfigurer,重写addInterceptors来添加拦截器

  • addInterceptor 添加拦截器
  • addPathPatterns:添加需要拦截的页面
  • excludePathPatterns:添加不需要拦截的页面,这里登陆页面不需要拦截

注意:需要使用@Configuration标识

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
class WebConfig : WebMvcConfigurer {

override fun addInterceptors(registry: InterceptorRegistry) {
super.addInterceptors(registry)
registry.addInterceptor(LoginInterceptor())
.addPathPatterns("/")
.addPathPatterns("/book/**")
.addPathPatterns("/books/**")
.excludePathPatterns("/login")
}
}

注销:清除session中的用户信息

1
2
3
4
5
6
//注销
@GetMapping("/logout")
fun loginOut(session: HttpSession):String{
session.removeAttribute("user")
return "login"
}

第十一章 博客项目初始化

本章节开始,我们将开始实战博客项目

第十二章 其他知识

6.1 JavaScript

返回上一页

1
<a class="btn btn-default" href="javascript:history.go(-1)">返回上一页</a>

6.2 BootStrap

警告框

1
2
3
4
5
<div class="alert alert-success alert-dismissible fade in" role="alert" th:unless="${#strings.isEmpty(message)}">
<a class="close" data-dismiss="alert"><span>&times;</span></a>
<strong>恭喜,</strong>
<span th:text="${message}">信息提交成功</span>
</div>

注意:遇到一个坑,以上代码中的关闭没有关闭成功,关闭行为是由jquery提供的,最后发现引入jQuery的Script标签,没有闭合造成JS无效,html5要求太严格啦

分页

1
2
3
4
5
6
<nav>
<ul class="pager">
<li class="previous"><a href="#" th:href="@{'/books?page='+${page.number-1}}" th:unless="${page.first}">上一页</a></li>
<li class="next"><a href="#" th:href="@{/books(page=${page.number}+1)}" th:unless="${page.last}">下一页</a></li>
</ul>
</nav>

标题:

1
<h3 class="page-header">注册</h3>

第十三章:Kotlin SpringBoot

10.1 常见问题

SpringBoot Validation + Kotlin校验无效的解决方法

参考博客:https://www.jianshu.com/p/239ec85b6bdd?utm_campaign=maleskine&utm_content=note&utm_medium=seo_notes&utm_source=recommendation

解决办法:使用@field @get标识符