目录
一.Spring入门
1.Why Spring?
Spring的核心
2.Maven
Maven命令
Maven核心概念
依赖管理dependencies
3.初识Spring
二.Spring依赖注入
1.Java注解(Annotation)
2.Spring Bean
总结
3.Spring Resource
4.Spring Bean 生命周期
三.Spring MVC
1.Spring Boot
2.Spring Controller
3.Get Request(一)
4.Get Request(二)
我们注意,创建SpringBoot WEB工程,就去网站https://start.spring.io/可以直接创建,然后解压,通过我们的编译器打开文件夹,就可以了
四.Spring Thymeleaf入门
1.Thymeleaf入门
2.Thymeleaf变量
模板变量
3.Thymeleaf循环语句
4.Thymeleaf表达式
字符串处理
数据转化
5.Thymeleaf条件语句
五.Spring Template 进阶
1.Thymeleaf表单
2.Spring Validation(一)
Validation注解
3.Spring Validation(二)
Control改造
user/add.html改造
4.Thymeleaf布局(Layout)
六.Spring Boot入门
1.Spring Boot ComponentScan
2.Spring Boot Logger运用
3.Spring Boot Properties
配置文件格式:配置项名称=配置项值
配置的意义
自定义配置项
七.Spring Session
1.Cookie
读Cookie
写Cookie
2.Spring Session API
Session
3.Spring Session配置
前置知识点:配置
Session配置(添加依赖)
4.Spring Request拦截器
Spring是java语言中必须要掌握的框架,它已经有超过10年的稳定期了,基本上所有采用Java语言的公司都会采用Spring Framework 这个框架基本上是全能的,涵盖了Java的各个领域
依赖注入(DI)是Spring最核心的技术点
使用Spring大大降低了开发难度和助力团队开发,它更加强调了面向对象。
Maven是一个项目管理和构建自动化工具
Maven提供了一个命令行工具可以把工程打包成Java支持的格式(比如jar),并且支持部署到中央仓库,这样使用者只需要通过工具就可以很快捷的运用其他人写的代码,只需要添加依赖即可
从这个架构里可以看到借助中央仓库,我们可以把Java代码任意共享给别人,这对于团队协同开发来说是至关重要的,可以说Java工程化发展到现在,Maven起到了决定性作用
Maven使用惯例优于配置的原则,在没有定制之前,所有的项目都有如下的结构
${basedir}代表的是Java工程的根路径,在我们这里就是工程的根目录。
一个Maven项目在默认情况下会产生JAR文件,另外,编译后的classes会放在${basedir}/target/class下面,JAR文件会放在${basedir}/target下面
Maven工具的安装就不再赘述
想要使用Maven工具,是要在命令行里输入指令的方式来执行的
简单介绍三条命令
1.mvn clean compile 编译命令,Mavenue自动扫描src/main/java下的代码并且完成编译,执行完成后,会在根目录下生成target/classes目录(存放所有的class)
2.mvn clean package 编译并打包命令,先执行compile命令,在执行jar打包命令,这个结果会把所有的java文件和资源打包成一个jar,jar是java的一个压缩格式,方便我们灵活运用多个代码
3.mvn clean install 执行安装命令,是compile package install的集合,安装到本地的Maven仓库目录里,这个目录是${user_home}/.m2
仓库,依赖管理,POH,插件,生命周期这五个概念都会运行在Maven的配置文件中,即强约定的XML格式文件,它的文件名一定是pom.xml
1.POM(Project Object Model)
一个Java项目所有的配置都放置在POM文件中大概有如下行为:
定义项目的类型,名字
管理依赖关系
定制插件
约定大于一切,请记住这句话
Maven坐标
示例:
com.youkeda.course app jar 1.0-SNAPSHOT 这四个标签组成了Maven坐标
Maven属性配置
示例:
1.8 ${java.version} UTF-8 ${java.version}
学了Maven坐标后,有了Maven坐标我们可以通过Maven依赖管理来运行别人写的代码了
dependency就是用于指定当前工程依赖于其他代码库的,Maven会自动管理jar依赖
一旦我们在pom.xml里声明了dependency信息,会先去本地用户目录下.m2文件夹内查找对应的文件,如果没有找到那么就会触发从中央仓库下载的行为,下载完就会存放在本地的.m2文件夹内
添加依赖,在
中添加依赖,例如在Jave网络编程中我们用到的fastjson这个库
com.alibaba fastjson 1.2.62 注意
标签只能有一个,而其中的依赖可以有很多个
间接依赖
间接依赖是mvn成功的要素,简单来说就是一个A工程依赖了一个A库,当前B工程依赖了这个A工程,那么B工程也自动依赖了A库
插件体系
插件体系让Maven工具变得高度可定制
示例
org.apache.maven.plugins maven-compiler-plugin 3.8.1 这里声明了一个maven-compiler-plugin插件用于执行maven compile的,你会发现maven的插件其实也是存放在中央仓库的坐标,也就是一切皆是jar
搞定了前置技术,现在来配置创建一个Spring工程,目前来说我们基本使用的是Spring5
Spring5的Maven坐标如下
org.springframework spring-context 5.2.1.RELEASE
Spring强调面向接口编程,所以大部分情况下Spring代码都会有接口和实现类
看下面代码
@Service public class MessageServiceImpl implements MessageService{ public String getMessage() { return "Hello World!"; } }
@service就是一个注解
Annotation(注解)
Annotation是可以在编译运行阶段读取的,Spring就通过运行阶段动态的获取Annotation,从而完成了很多自定义的行为
loC容器是Spring框架最核心的组件,没有loC就没有Spring框架,在Spring框架中,通过依赖注入实现loC
在Spring的世界中,所有的Java对象都会通过loC容器转变为Bean(Spring对象的一种称呼,以后我们都用Bean来表示Java对象),构成应用程序主干和由Spring loC容器管理的对象称为beans,beans和他们之间的依赖关系反应在容器使用的配置元数据中。基本上所有的Bean都是由接口+实现类来完成的,用户想要获取Bean的实例直接从loC容器获取就可以了,不需要关系实现类
1.Spring目前主流配置元数据的方式是基于Annotation的,所以我们这里也是以Annotation为例
2.Annotation类型的loC容器对应的类是
org.springframework.context.annotation.AnnotationConfigApplicationContext
我们如果想要启动loC容器,需要运行
ApplicationContext context = new AnnotationConfigApplicationContext("fm.douban");
这段代码的含义就是启动loC容器,并且自动加载fm.douban下的Bean(只要引用了Spring注解的类都可以被加载)
3.AnnotationConfigApplicationContext这个类的构造函数有两种:
AnnotationConfigApplicationContext(String basePackages)//根据包名实例化 AnnotationConfigApplicationContext(Class clazz)//根据自定义包扫描行为实例化
4.Spring官方声明为Spring Bean 的注解有以下几种:
org.springframework.stereotype.Service org.springframework.stereotype.Component org.springframework.stereotype.Controller org.springframework.stereotype.Repository
只要在类上引用这类注解,就可以被loC容器加载
@Component注解是通用的Bean注解,其余三个注解都是扩展自Component
@Service代表的是Service Bean
@Controller作用于Web Bean
@Repository作用于持久化相关的Bean
实际上这四个注解都可以被loC容器加载,一般情况下,我们使用@Service,如果是Web服务那就使用@Controller
Spring依赖注入,简单来说就是一种获取其他实例的规范
假设我们现在有一个类SubjectService,那么在任何使用SubjectService的地方,都需要实例化对象
SubjectService subjectService = new SujectServiceImpl();
问题来了:万一SubjectServiceImpl依赖的**Service太多,那么就需要大量编码,new出很多服务实例,而且代码也容易出错
加入注解的作用,就是让Spring系统自动管理各种实例
所谓管理,就是用@Service注解把SubjectServiceImpl和SongServiceImpl等等所有的服务实现,都标记成SpringBean;然后,在任何使用服务的地方,用@Autowired注解标记,告诉Spring这里需要注入实现类的实例
项目启动的过程中,Spring会自动实例化服务实现类,然后自动注入到变量中,不需要开发者大量的写new代码了
@Service和@Autowired是相辅相成的,如果实现类没有加@Service,就意味着没有标记成SpringBean,那么即使加了@Autowired也无法注入实例
而
private SongService songService;
没有加@Autowired,Spring亦无法注入实例
每个Annotation(注解)都有各自特定的功能,Spring检查到代码中有注解,就自动完成特定功能,减少代码量,降低系统复杂度
把实现类上加@Service,然后在其他类中需要实例的时候,写下如下代码就可以
@Autowired
private SongService songService;
文件处理方案Spring Resource
文件在电脑的某个位置或则文件在工程目录下,都是可以IO来读写,但是文件如果在工程的src/main/resource目录下,我们在Maven中知道着是Maven工程存放文件的地方,由于Maven在执行package的时候,会把resources目录下的文件一起打包进jar包里(我们之前提到过jar是Java的压缩文件),显然这种情况用File对象是读取不到的,因为文件已经在jar里了。
怎么解决呢?
classpath
在Java内部中,我们一般把文件路径称为classpath,所以读取内部文件就是从classpath内读取,classpath指定的文件不能解析成File对象,但是可以解析成InputStream,我们借助Java IO就可以读取出来了
classpath类似虚拟目录,它的根目录是从/开始代表的是src/main/java或者src/main/resources目录
如何使用classpath读取文件(设resources目录下有一个data.json文件)
//添加依赖
commons-io commons-io 2.6 public class Test { public static void main(String[] args) { // 读取 classpath 的内容 InputStream in = Test.class.getClassLoader().getResourceAsStream("data.json"); // 使用 commons-io 库读取文本 try { String content = IOUtils.toString(in, "utf-8"); System.out.println(content); } catch (IOException e) { // IOUtils.toString 有可能会抛出异常,需要我们捕获一下 e.printStackTrace(); } } }
InputStream in = Test.class.getClassLoader().getResourceAsStream("data.json");
这段代码的含义是从Java运行的类加载器(ClassLoader)实例中查找文件,Test.class指的是当前的Test.java编译后的Java class文件。
如果是Maven、Spring工程,文件在resources中这种情况使用classpath方法读取文件,也就是上文代码演示的方法
在Spring当中定义一个
org.springframework.core.io.Resource
类来封装文件,这个类的优势在于可以支持普通File也可以支持classpath文件,并且在Spring中通过
org.springframework.core.io.ResourceLoader
服务提供任意文件的读写,可以在任意的Spring Bean中引入ResourceLoader
@Autowired private ResourceLoader loader;
演示一下在Spring当中如果读取文件
public interface FileService { String getContent(String name); }
import fm.douban.service.FileService; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.stereotype.Service; import java.io.IOException; import java.io.InputStream; @Service public class FileServiceImpl implements FileService { @Autowired private ResourceLoader loader; @Override public String getContent(String name) { try { InputStream in = loader.getResource(name).getInputStream(); return IOUtils.toString(in,"utf-8"); } catch (IOException e) { return null; } } }
FileService fileService = context.getBean(FileService.class); String content = fileService.getContent("classpath:data/urls.txt"); System.out.println(content);
此外,最让人眼前一亮的是,Resource还可以加载远程文件,比如说
String content2 = fileService.getContent("https://www.zhihu.com/question/34786516/answer/822686390"); System.out.println(content2);
总结:在Spring Resource当中,把本地文件、classpath文件、远程文件都封装成Resource对象来统一加载
我们在大部分情况下掌握init方法即可,这个init方法名称可以是任意名称,因为我们是通过注解来声明init的,我们以SubjectServiceImpl为例
mport javax.annotation.PostConstruct;
@Service
public class SubjectServiceImpl implements SubjectService {
@PostConstruct
public void init(){
System.out.println("启动啦");
}
}
只需要在方法上加@PostConstruct注解,就代表该方法在Spring Bean 启动后会自动执行
@PostConstruct的完整包路径是
javax.annotation.PostConstruct
有了init方法后,之前static代码块的内容就可以移动到init方法里了
总结 Spring生命周期可以让我们更轻松的初始化一些行为以及维护数据,现在只需掌握初始化方法即可
Sping Boot的核心还是Spring
①有了Spring Boot就不需要额外部署Tomcat这类服务器了,Spring Boot默认集成了Tomcat
②SpringBoot自定义了打包格式,通过这个直接把一个JavaWeb工程转化成普通的Java工程,启动一个main方法就可以把Spring工程启动起来,极大的降低了开发难度
③SpringBoot默认集成了你能想到的第三方框架和服务,比如数据库连接、NoSQL,安全等等,开发者也不需要关心复杂的Maven依赖了,开箱即用
④SpringBoot 提供了标准的属性配置文件,支持应用参数动态配置,让代码运行更加灵活
SpringBoot强调开箱即用
SpringBoot默认集成了Web,所以可以通过网页访问
SpringMVC是JavaWeb的一种实现框架
网页加载过程,在SpringBoot方案里,一个网页请求到了服务器后,首先我们进入的是JavaWeb服务器,然后进入SpringBoot应用,最后匹配到一个SpringController(这其实也是一个Spring Bean),然后路由到具体某一个Bean方法,执行完返回结果输出到客户端
从这个流程中我们可以得知:我们只需要掌握SpringController就可以自己提供Web服务了
SpringController技术有三个核心点:
Bean的配置:Controller注解运用
网络资源的加载:加载网页
网址路由配置:RequestMapping注解运用
1.Controller注解
Spring Controller本身也是一个SpringBean,只是它多提供了Web能力,我们只需要在类上提供一个@Controller注解就可以了
2.加载网页
在Spring Boot应用中,一般把网页存放在src/main/resources/static,在controller中,会自动加载static下的html内容,所以通过SpringBoot建设网站也是非常简单的
import org.springframework.stereotype.Controller; @Controller public class HelloControl { public String say(){ return "hello.html"; } }
return "hello.html" 返回的是html文件路径,当执行这段代码的时候,SpringBoot实际加载的是src/main/resources/static/hello.html文件
resources文件,我们之前说过这是classpath文件,Spring很强大,自动帮我们做了加载,所以我们只需要写hello.html即可(注意文件路径不要写static)
如果我们的html文件是存放在src/main/resources/static/html/hello.html中
那么return语句应该是
return "html/hello.html";
3.RequestMapping注解
对于Web服务器来说,必须实现的一个能力就是解析URL,并且提供资源给调用者,这个过程称为路由
SpringMVC完美的支持了路由的能力,并且简化了路由配置,只需要在提供Web访问的方法上添加一个@RequestMapping注解就可以完成配置了
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloControl {
@RequestMapping("/hello")
public String say(){
return "html/hello.html";
}
}
Http网络中,最常用的两个协议是get和post
平常我们浏览网站,看视频,看图片都是用的get协议,如何用SpringMVC来支持Http服务器get协议,通过get协议我们可以渲染网页
在SpringMVC中,定义一个URL参数也非常重要,只需要我们在方法上面添加对应的参数和参数注解就可以了,话不多说,上代码
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @Controller public class SongListControl { @RequestMapping("/songlist") public String index( @RequestParam("id") String id){ return "html/songList.html"; } }
RequestParam注解的包路径是
org.springframework.web.bind.annotation.RequestParam
由于SpringMVC的注解都是在
org.springframework.web.bind.annotation
包内,所以我们import的时候,直接用
import org.springframework.web.bind.annotation.*;
就可以省去很多的导入
多个参数的写法
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; @Controller public class SongListControl { @RequestMapping("/songlist") public String index(@RequestParam("id") String id, @RequestParam("pageNum") int pageNum){ return "html/songList.html"; } }
如果我们在Request方法里面声明了多个参数,那么URL访问的时候就必须传递该参数
@GetMapping
我们知道@RequestMapping注解用于解析URL请求路径,这个注解默认是支持所有的Http Method的。放开所有的HttpMethod这样是不安全的,一般我们还会明确指定method,比如get请求
用@GetMapping替换@RequestMapping,他们的包路径是一样的,之前的代码可以改成
import org.springframework.web.bind.annotation.*; @GetMapping("/songlist") public String index(@RequestParam("id") String id,@RequestParam("pageNum") int pageNum){ return "html/songList.html"; }
可以通过来访问
http://xxxx/songlist?id=xxx&pageNum=1
非必要传递参数
@RequestParam(name="pageNum",required = false) int pageNum
当前的参数名称是pageNum required=false表示不是必须的
输出JSON数据
目前来说通用的Web数据格式就是JSON,在Spring当中配置JSON数据非常简单
@GetMapping("/api/foos")
@ResponseBody
public String getFoos(@RequestParam("id") String id) {
return "ID: " + id;
}
添加@ResponseBody注解
我们调用URL
https://xxxx/api/foos?id=100
public class User{ private String id; private Stirng name; // 省略了 getter、setter } public class UserControl{ //缓存 User 数据 private static Map
users = new HashMap(); /** * 初始化数据 */ @PostConstruct public void init(){ User user = new User(); user.setId("100"); user.setName("张三"); users.put(user.getId(),user); } @GetMapping("/api/user") @ResponseBody public User getUser(@RequestParam("id") String id) { return users.get(id); } } 最后页面返回的是JSON字符串,这也是Spring MVC比较强大和方便的地方,一般我们把这种输出JSON数据的方法称为API
Thymeleaf是一种模板框架,与Html天然相融合.数据+模板+引擎渲染出真实的页面
那么如何初始化Thymeleaf
添加依赖
org.springframework.boot
spring-boot-starter-thymeleaf
数据传递
SpringMVC把页面数据层封装的非常完善,只需要在方法参数中引入一个Model对象,就可以通过Model对象传递数据到页面中了。
首先我们正确导入这个Model类
import org.springframework.ui.Model;
@Controller
public class SongListControl {
@Autowired
private SongListService songListService;
@RequestMapping("/songlist")
public String index(@RequestParam("id")String id,Model model){
SongList songList = songListService.get(id);
//传递歌单对象到模板当中
//第一个 songList 是模板中使用的变量名
// 第二个 songList 是当前的对象实例
model.addAttribute("songList",songList);
return "songList";
}
}
SpringMVC中对于模板文件有固定的存放位置,放置在工程src/main/resources/templates
所以上面的return "songList";其实是会去查找src/main/resources/templates/songList.html文件,系统会自动匹配到后缀的,所以不需要写成return "songList.html";
文件内容如下
豆瓣歌单
Thymeleaf模板文件在src/main/resources/templates目录下
下面这这段代码的意思是,让软件能够识别thymeleaf语法
xmlns:th="http://www.thymeleaf.org"
th:text这个属性就是Thymeleaf自定义的Html标签属性,th是Thymeleaf的缩写,所以如果你看到了th:开头的标签属性,那么就代表的是Thymeleaf语法
th:text语法的作用就是会动态替换掉html标签的内部内容,这句话啥意思呢?
Hello
这段代码的执行结果就是用msg变量值替换了span标签内的Hello字符串,比如说msg变量值是你好,那么代码的渲染结果是
你好
${msg}这个语法就是表示获取模板中的变量msg
一般情况下,模板的变量都是会存放在模板的上下文中,所以我们如果想要调用变量,就需要先设置到模板上下文中去
model.addAttribute("songList",songList);
就可以完成上下文变量的设置
再举个例子:
import org.springframework.ui.Model; @Controller public class DemoControl { @RequestMapping("/demo") public String index(Model model){ String str = "你好"; model.addAttribute("msg",str); return "demo"; } }
第一个参数设置的就是上下文变量名(可随便定义),第二个参数设置的就是变量值
Thymeleaf的for循环也是使用标签属性来完成的,
th:each
就代表循环语句,示例如下:
-
,显示行数数字,从1开始,记住用法即可
Thymeleaf表达式对于动态数据的处理很方便,应用于字符串处理和数据转化
字符串拼接示例:
Thymeleaf默认继承了大量工具类,可以方便的进行数据转化,一般我们使用最多的是dates
如果想处理LocalDate和LocalDateTime类,可以添加如下依赖
org.thymeleaf.extras thymeleaf-extras-java8time 3.0.4.RELEASE 这个库会自动添加一个新的工具类temporals
工具类使用的是#{工具类}
我们一般使用dates/temporals用于处理日期类型到字符串的转化
动态页面必然会遇到if/else的情况,Thymeleaf也支持if/else能力,同样也是以th:开头的属性,if表达式的值是ture的情况下就会执行渲染
男
男
女
model.addAttribute("users",users);
我们来制作一个简易的图书管理系统
package com.bookstore.control;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
@Controller
public class BookControl {
// 当页面访问 http://localhost:8080/book/add.html 时
// 渲染 addBook.html 模板
@GetMapping("/book/add.html")
public String addBookHtml(Model model){
return "addBook";
}
}
package com.bookstore.control;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import com.bookstore.model.*;
@Controller
public class BookControl {
//缓存所有书籍数据
private static List books = new ArrayList<>();
@GetMapping("/book/add.html")
public String addBookHtml(Model model){
return "addBook";
}
@PostMapping("/book/save")
public String saveBook(Book book){
books.add(book);
return "saveBookSuccess";
}
}
PostMapping和@GetMapping不同点在于PostMapping只接收http method为post请求的数据
新增templates/saveBookSuccess.html文件
添加书籍
添加书籍成功
一般情况下,我们把html表单的method设置为post。这样可以保证数据传输安全,SpringMVC就需要接收post请求
如下
添加书籍
添加书籍
在实际的工作过程中对于数据的保存是离不开数据验证的,比如name必须输入等校验规则,Spring对于数据验证的支持也非常好,我们可以借助Spring Validation来处理表单数据的验证
JSR380定义了一些注解用于数据校验,这些注解可以直接设置在Bean属性上
@AssertTrue 是否为true
@Size 约定字符串长度
@Min 字符串的最小长度
@Max 字符串的最大长度
@Email 是否是邮箱格式
@NotEmpty 不允许为null或者为空,可以用于判断字符串、集合(常用)
package com.bookstore.model;
import javax.validation.constraints.*;
public class User {
@NotEmpty(message = "名称不能为 null")
private String name;
@Min(value = 18, message = "你的年龄必须大于等于18岁")
@Max(value = 150, message = "你的年龄必须小于等于150岁")
private int age;
@NotEmpty(message = "邮箱必须输入")
@Email(message = "邮箱不正确")
private String email;
// standard setters and getters
}
校验是可以累加的,如上面的@Min和@Max,系统会按照顺序校验,任何一条触发就会抛处校验错误到上下文中
创建一个表单页 user/addUser.html模板文件,用于管理员添加用户
添加用户
添加用户
import javax.validation.Valid;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.BindingResult;
import com.bookstore.model.*;
@Controller
public class UserControl {
@GetMapping("/user/add.html")
public String addUser() {
return "user/addUser";
}
@PostMapping("/user/save")
public String saveUser(@Valid User user, BindingResult errors) {
if (errors.hasErrors()) {
// 如果校验不通过,返回用户编辑页面
return "user/addUser";
}
// 校验通过,返回成功页面
return "user/addUserSuccess";
}
}
@Valid完整包路径是
javax.validation.Valid;
BindingResult类路径是
org.springframework.validation.BindingResult
添加用户
添加用户成功
我们经常会遇到这种情况
Thymeleaf同样也支持这种功能,我们只需要把错误结果返回到模板中就ok了
把数据传输到页面,如果先要显示具体的字段信息,就得结合模型来传输
我们需要改造一下UserControl.addUser方法,添加模型参数
@GetMapping("/user/add.html")
public String addUser(Model model) {
User user = new User();
model.addAttribute("user",user);
return "user/addUser";
}
在user/add.html模板当中,我们得去处理错误的状态,增加错误的样式
th:object
为了让表单验证的状态生效,你还需要在form标签里面添加一个th:object="${user}"属性
th:object用于替换对象,使用了这个就不需要每次都编写user.xxx,可以直接操作xxx了
关于错误信息,如果想要显示错误的状态,我们就得定义一个错误的css class
例如
.error { color: red; }
如果某个html标签拥有这个error,那么就会出现红色的字体,就说明错误了
可以使用th:classappend这个语法,支持我们动态的管理样式
如果想要显示出来错误信息,可以使用th:errors=*{age}"属性,这个会自动取出错误信息来
注意 * ,由于我们在form中使用了th:object="${user}",所以我们可通过*{age}来获得具体的值
大多数网站都有导航,公共的东西,在一个网站里访问页面往往会显示相同的布局
按照这个布局,我们可以把导航和底部做成布局组件,每个页面直接套用就可以了
th:include + th:replace 方案来完成布局的开发
layout.html
布局 页面正文内容在container这个节点上,我们使用了一个th:include="::content"语法
::content指的是选择器,这个选择器指的就是加载当前页面的th:fragment的值
当页面渲染的时候,布局会合并content这个fragment内容一起渲染,下面我们配置一下fragment
user/list.html
th:replace="layout"这里指定了布局的名称,一旦声明后,页面会被替换成layout的内容
这个“layout”指的就是templates/layout.html
th:fragment="content"
fragment是片段的意思,当页面渲染的时候,可以通过选择器指定使用这个片段
我们在之前学到过loC的工作模式,通过启动容器,Spring框架通过解析属性的注解,自动的把所需要的Bean实例注入到属性中
加了@SpringBootApplication注解的类是启动类,是整个系统的启动入口
fm.douban.app.Application类是启动类,而SpringBoot框架就会默认扫描fm.douban.app包
(会默认扫描启动类所在的包及其子包进行解析)
但是fm.douban.service、fm.douban.service.impl不是启动类的子包,所以不会自动扫描,也不会实例化Bean,自然会报错
解决办法
1.为启动类的注解@SpringBootApplication加一个参数,告知系统需要额外扫描的包
@SpringBootApplication(scanBasePackages={"fm.douban.app", "fm.douban.service"})
public class AppApplication {
public static void main(String[] args) {
SpringApplication.run(AppApplication.class, args);
}
2.使用@ComponentScan,用于指定多个需要额外自动扫描的包(不太常用)
在Spring这种比较复杂的系统中,System.out.println()打印的内容会输出到什么地方,是不确定的,所以在企业级的项目中,都是用日志系统来记录信息的
步骤:
使用日志系统有两步:
1.配置
在SpringBoot系统的标准配置文件application.properties中增加日志级别配置:
logging.level.root=info
表示所有日志(root)都为info级别
我们也可以为不同的包定义不同的级别
logging.level.fm.douban.app=info
就表示fm.douban.app包及其子包中的所有类都输出info级别的日志
优先级 级别 含义和作用 方法名 最高 error 错误信息日志 error() 高 warn 暂时不出错但高风险的警告信息日志 warn() 中 info 一般的提示语,普通数据等不紧要的信息日志 info() 低 debug 进开发阶段需要关注的调试信息日志 debug() 级别的作用:
日志在哪一个级别,就输出这个级别以及比这个级别高的级别的日志,不输出更低级别的日志,在开发阶段配置为debug,在项目发布时调为info或者更高级别,即可做到不改代码而控制只输出关心的日志
编码
import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.RestController; import javax.annotation.PostConstruct; @RestController public class SongListControl { //实例化日志对象 private static final Logger LOG = LoggerFactory.getLogger(SongListControl.class); @PostConstruct public void init(){ LOG.info("SongListControl 启动啦"); } }
之前我着重谈到,想要创建SpringBoot工程,需要去到我们的官网https://start.spring.io/
去勾选我们的项目需求,创建完毕即可,SpringBoot框架中已经免除了大量的手动配置
但是对于一些特定的情况,我们需要手动在application.properties配置文件中自定义,这是一个固定的位置,框架会自动加载 并且解析这个配置文件
ogging.level.root=info
logging.level.fm.douban.app=info
我们约定:
配置项名称能够准确的表达作用含义,用 . 来分割单词
相同的前缀的配置项写在一起
不同前缀的配置项之间空一行
配置的主要作用,是把可变的内容从代码中剖剥离出来,做到不修改代码的情况下,方便的修改这些可变的或者常变的内容,这个过程避免硬编码,做到解耦
我们可以在application.properties配置文件中加入自定义的配置项
song.name=good boy
框架会自动加载并且自动解析这个文件
那么代码中怎么使用自定义配置项呢(注意与配置文件中保持一致)
import org.springframework.beans.factory.annotation.Value; public class SongListControl { @Value("${song.name}") private String songName; }
有配置项,并且使用了注解,并且没有出错,系统才能自动完成赋值
从图中看,服务端既要返回Cookie给客户端,也要读取客户端提交的Cookie,我们先来学习服务端Spring工程如何使用Cookie的,有读、写两个操作
为control类的方法添加一个HttpServletRequest参数。通过request.getCookies()取得cookie数组,然后循环遍历该数组即可
使用注解读Cookie
如果知道cookie的名字,就可以通过注解的方式索取,不需要再遍历cookie数组了
为control类的方法增加一个@CookieValue("xxxx") String xxx参数即可,注意使用时要填入正确的cookie名字
系统会自动解析并传入同名的cookie
import org.springframework.web.bind.annotation.CookieValue; @RequestMapping("/songlist") public Map index(@CookieValue("JSESSIONID") String jSessionId) { Map returnData = new HashMap(); returnData.put("result", "this is song list"); returnData.put("author", songAuthor); returnData.put("JSESSIONID", jSessionId); return returnData; }
为control类的方法上添加一个HttpServletResponse参数,调用response.addCookie()方法,添加Cookie实例对象即可
import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletResponse; @RequestMapping("/songlist") public Map index(HttpServletResponse response) { Map returnData = new HashMap(); returnData.put("result", "this is song list"); returnData.put("name", songName); Cookie cookie = new Cookie("sessionId","CookieTestInfo"); // 设置的是 cookie 的域名,就是会在哪个域名下生成 cookie 值 cookie.setDomain("baidu.com"); // 是 cookie 的路径,一般就是写到 / ,不会写其他路径的 cookie.setPath("/"); // 设置cookie 的最大存活时间,-1 代表随浏览器的有效期,也就是浏览器关闭掉,这个 cookie 就失效了。 cookie.setMaxAge(-1); // 设置是否只能服务器修改,浏览器端不能修改,安全有保障 cookie.setHttpOnly(false); response.addCookie(cookie); returnData.put("message", "add cookie successful"); return returnData; }
注意,Cookie类的构造函数,第一个参数是cookie名称,第二个参数是cookie值。
上文,我们学习了Cookie放在客户端,可以存储用户登录信息,主要用于辨别用户身份
但是如果真的把用户ID,登录状态等重要信息放入cookie,会带来安全隐患,因为网络上很不安全,cookie可能会拦截,甚至伪造
采用Session 会话机制可以解决这个问题,用户ID,登录状态等重要信息不存放在客户端,而是存放在服务端,从而避免安全隐患
使用会话机制时,Cookie作为Session id的载体与客户端通信,上一节的代码,Cookie中的JSESSIONID就是这个作用
名字为JSESSIONID的cookie,是专门用来记录用户session的,JSESSIONID是标准的通用的名字
Session读操作
与cookie相似,从HttpServletRequest对象中获得HttpSession对象,使用的语句是request.getSession(),返回的结果是对象,在attribute属性中用key value的形式存储多个数据
假设存储登录信息的数据key是userLoginInfo,那么语句就是
session.getAttribute("userLoginInfo")
登录信息类
登录信息实例的对象因为要在网络上传输,就必须实现序列化接口Serializable
下面根据两个需求属性字段来演示一下
import java.io.Serializable; public class UserLoginInfo implements Serializable { private String userId; private String userName; }
下面是操作代码
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @RequestMapping("/songlist") public Map index(HttpServletRequest request, HttpServletResponse response) { Map returnData = new HashMap(); returnData.put("result", "this is song list"); // 取得 HttpSession 对象 HttpSession session = request.getSession(); // 读取登录信息 UserLoginInfo userLoginInfo = (UserLoginInfo)session.getAttribute("userLoginInfo"); if (userLoginInfo == null) { // 未登录 returnData.put("loginInfo", "not login"); } else { // 已登录 returnData.put("loginInfo", "already login"); } return returnData; }
Session写操作
假设登录成功,怎么记录登录信息到Session呢?
既然从HttpSession对象中读取到登录信息用的是getAttribute()方法,那么写入登录信息就用setAttribute()方法
下面代码演示使用Session完成登录的过程,(略去了校验用户名和密码步骤)
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; @RequestMapping("/loginmock") public Map loginMock(HttpServletRequest request, HttpServletResponse response) { Map returnData = new HashMap(); // 假设对比用户名和密码成功 // 仅演示的登录信息对象 UserLoginInfo userLoginInfo = new UserLoginInfo(); userLoginInfo.setUserId("12334445576788"); userLoginInfo.setUserName("ZhangSan"); // 取得 HttpSession 对象 HttpSession session = request.getSession(); // 写入登录信息 session.setAttribute("userLoginInfo", userLoginInfo); returnData.put("message", "login successful"); return returnData; }
这个打开页面会看到 message:login successful,表示写入数据已经成功了,我们可以在方法外再读取这些数据
上节我们谈到了Session的操作,在操作中,没有涉及到cookie,系统会自动把默认的JSESSIONID放在默认的cookie中
但是Cookie作为session id的载体,也可以修改属性
SpringBoot也提供了编程式的配置方式,主要用于配置Bean
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringHttpSessionConfig {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
在类上添加@Configuration注解,就表示这是一个配置类,系统会自动扫描处理,在方法上添加@Bean注解,表示把此方法返回的对象实例注册成Bean
org.springframework.session
spring-session-core
在类上额外添加一个注解@EnableSpringHttpSession开启Session然后注册两个bean
CookieSerializer:读取cookie中的sessionId信息
MapSessionRepository:session信息在服务器上的存储仓库
import org.springframework.session.MapSessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
@EnableSpringHttpSession
public class SpringHttpSessionConfig {
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
// 用正则表达式配置匹配的域名,可以兼容 localhost、127.0.0.1 等各种场景
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
serializer.setCookiePath("/");
serializer.setUseHttpOnlyCookie(false);
// 最大生命周期的单位是秒
serializer.setCookieMaxAge(24 * 60 * 60);
return serializer;
}
// 当前存在内存中
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
背景:在实际项目中,会有大量的页面功能是需要判断用户是否登录的,例如电商的网站,订单,购物车,等等都需要登录,那么让每一个页面都判断是否登录,未登录跳转到登录页面,就太繁琐了,不利于维护
所以需要一种统一处理相同逻辑的机制,Spring提供了HandlerInterceptor(拦截器)满足这种场景的需求
1.创建拦截器
拦截器必须实现HandlerInterceptor接口,可以在三个点进行拦截
①Controller方法执行之前,这是最常用的拦截点,例如是否登录的验证就要在preHandle()方法中处理
②Controller方法执行之后。例如记录日志,统计方法执行时间等,就要在postHandle()方法中处理
③整个请求完成后,不常用的拦截点,例如整个请求的执行时间的时候用,在afterCompletion方法中处理
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; public class InterceptorDemo implements HandlerInterceptor { // Controller方法执行之前 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 只有返回true才会继续向下执行,返回false取消当前请求 return true; } //Controller方法执行之后 @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { } // 整个请求完成后(包括Thymeleaf渲染完毕) @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
preHandle()方法参数中有HttpServletRequest和HttpServletResponse,可以像contro中一样使用Session
2.实现WebMvcConfigurer
创建一个类实现WebMvcConfiguer,并且实现addInterceptors()方法,这个步骤用于管理拦截器
这个实现类要加上@Configuration注解,上文提到,说明这是个配置类,让框架自动扫描并且处理
管理拦截器,设置拦截范围比较重要,常用addPathPatterns("/**")表示拦截所有的URL
也可调用excludePathPatterns()方法排除某些URL
import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebAppConfigurerDemo implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 多个拦截器组成一个拦截器链 // 仅演示,设置所有 url 都拦截 registry.addInterceptor(new UserInterceptor()).addPathPatterns("/**"); } }
通常拦截器会放在interceptor包里,而用于管理拦截器的配置类,会放在另一个包config里