第一章 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.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查询:这里使用的是类名,同时必须给类名取个别名
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}">
使用的时候则用星号 *{}
注意:th需要导入头文件:xmlns:th=”http://www.w3.org/1999/xhtml"
5.3静态资源
本小节中,你将学习到
引入bootstrap资源
thymeleaf引入静态资源
BootStrap
一:引入资源
下载bootstrap的zip包,用于生产环境的 Bootstrap,解压,拷贝css,js,fonts文件夹到项目的resources目录下
二:thymeleaf引入静态资源
header中引入:
注意 :
href使用th:href,th:href引入资源只有在项目运行的时候才能有效
引入语法规则:@{/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>×</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标识符