SpringBoot
一、创建项目
在idea中,打开创建界面窗口
然后直接创建即可
1)更换源
在创建的过程中,若出现加载过慢,可进行选择更换源
会出现一个url,这个原url对应的就是官网的创建地址
https://start.spring.io
我们可以进行更改成阿里云的url
https://start.aliyun.com
2)pom.xml
在创建完毕后的SpringBoot的核心配置中,会出现以下xml内容
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
//核心配置,配置SpringBoot版本号
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
//可进行自定义修改版本
<version>2.7.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
/
//当前工程信息
<groupId>com.hyl</groupId>
<artifactId>SpringBootJUnit</artifactId>
<version>0.0.1-SNAPSHOT</version>
//jdk版本
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
//核心配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
//测试配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
//约定的依赖版本定位
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
在分模块的项目构建中,进行依赖导入的形式我们通常会定义一个专门处理依赖的模块
定义一个模块叫做parent,是专门进行依赖版本管理的模块
有两种我们常用的管理方式
-
方式一: 管理依赖的版本
-
在parent项目中,指定接下来可能很用的依赖及其版本
-
但在项目中(子父),并不会默认引入这些依赖
-
而是根据需要手动引入依赖,只不过不需要再指定版本。
-
注意:子项目中不一定非要引入父级项目管理的依赖,可以引入自定义依赖,需要指定版本号
子级引入父级管理的依赖,也可以自定义版本号(重写)。
-
**注意:父级项目的打包方式应该是 < packaging>pom< /packaging>
子项目在互相引入时,才会将依赖的jar一同引入。
否则就只会引入子项目本身。
<groupId>com.duyi.examonline</groupId> <artifactId>exam-parent</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <properties> <!--根据 version 标签定义的名称 定义相关依赖的版本--> <spring.version>5.3.14</spring.version> </properties> <!--只进行声明,并没有直接进行引入--> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> </dependencies> </dependencyManagement>
<groupId>com.duyi.examonline</groupId> <artifactId>exam-config</artifactId> <version>1.0-SNAPSHOT</version> <!--引入父级项目中,进行依赖继承--> <parent> <groupId>com.duyi.examonline</groupId> <artifactId>exam-parent</artifactId> <version>1.0-SNAPSHOT</version> </parent> <dependencies> <dependency> <!-- 只需要指定要引入父级项目中的哪个依赖即可,版本自动 --> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> </dependency> </dependencies>
-
-
方式二:管理依赖(下载)
-
父级项目直接将管理的依赖下载引入
-
子项目继承父级项目后,就会自动拥有父级引入的依赖了,就不需要自己再重复引入了
-
当然子项目可以引入其他的自定义依赖。
<groupId>com.duyi.examonline</groupId> <artifactId>exam-parent</artifactId> <version>1.0-SNAPSHOT</version> <properties> <spring.version>5.3.14</spring.version> <struts.version>1.3.8</struts.version> </properties> <!--直接引入。子项目进行继承时也会直接直接引入,且子项目中不需要进行进行依赖指定--> <dependencies> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts-core</artifactId> <version>${struts.version}</version> </dependency> </dependencies>
-
设置项目的jdk版本
-
idea创建的maven项目,默认使用的jdk都是1.5版本。
-
在开发中,一些1.7或1.8新特性就无法使用。
-
需要设置jdk版本
-
可以在项目的pom.xml配置版本号
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> ..... </properties>
-
可以在项目的pom.xml配置插件
-
可以在maven的settings.xml中配置
-
parent的作用
进行继承spring-boot-starter-parent,里面定义了若干个管理依赖的定位,继承parent模块可以避免发生出现依赖版本冲突的发生
在我们进行开发过程中,我们可以使用springboot定义好的相关依赖版本号,也可以进行自定义版本号,但容易引发依赖版本冲突
3)引导类
@SpringBootApplication
public class MySpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(MySpringbootApplication.class, args);
}
}
SpringBoot的启动类上使用@SpringBootApplication注解标识,该注解试一个组合注解,包含多个其他注解。
args数组
在启动类中的,执run方法时,所传递的args数组为一个可变型接收数组参数,可以设定临时参数进行更改服务的一些配置
@SpringBootApplication
public class MySpringbootApplication {
public static void main(String[] args) {
String[] test=new String[1];
//更改启动的服务器端口为 8089
test[0]="--server.port=8089"
//将设定的数组进行传递进去
SpringApplication.run(MySpringbootApplication.class, test);
}
}
@SpringBootApplication:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration //开启自动配置功能
//扫描当前有没有配置拦截器
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class )
Class<?>[] exclude() default {}; //排除哪些自动配置类
@AliasFor(annotation = EnableAutoConfiguration.class )
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages" )
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class,attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
@SpringBootApplication注解上标有三个注解@SpringBootConfiguration 、@EnableAutoConfiguration 、@ComponentScan。@ComponentScan是Spring中的注解,用来与配置类上,定义要扫描的组件。 其他两个注解则是SpringBoot自定义的注解。下面就来看看这两个注解的作用。
@SpringBootConfiguration :
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
}
@SpringBootConfiguration注解中没有定义任何属性信息,而该注解上有一个注解@Configuration,用于标识配置类。所以@SpringBootConfiguration注解的功能和@Configuration注解的功能相同,用于标识配置类。
@EnableAutoConfiguration :
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
@EnableAutoConfiguration注解上标注了两个注解,@AutoConfigurationPackage、@Import。@Import注解在SpringIOC一些注解的源码中比较常见,主要用来给容器导入目标bean。这里@Import注解给容器导入的组件用于自动配置:AutoConfigurationImportSelector ; 而@AutoConfigurationPackage注解试Spring自定义的注解,用于扫描启动类所在的包及其子包下的自定义类。
所以如果项目中,有类配置在当前启动类项目的上级目录,那么进行bean管理时就会出现异常
@AutoConfigurationPackage:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
@AutoConfigurationPackage注解上的@Import注解,给容器导入了Registrar组件
Registrar:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
Registrar() {
}
//拿到注解的全信息,注解所造包及其子包下的组件
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
}
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
}
}
Registrar是抽象类AutoConfigurationPackages的内部静态类,Registrar内的方法registerBeanDefinitions负责将获取到的注解所在的包及其子包下的所有组件注册进容器。这也是为什么SpringBoot的启动类要在其他类的父包或在同一个包中。
AutoConfigurationImportSelector
public class AutoConfigurationImportSelector
implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
.........
}
AutoConfigurationImportSelector 类实现类很多Aware接口,而Aware接口的功能是使用一些Spring内置的实例获取一些想要的信息,如容器信息、环境信息、容器中注册的bean信息等。而AutoConfigurationImportSelector 类的作用是将Spring中已经定义好的自动配置类注入容器中(但是的时候不发挥配置类的作用),而实现该功能的方法是selectImports方法:
selectImports:注册Spring中定义好的配置类
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
导入的配置类:
4)修改配置
在resources目录下的application.properties文件中可以进行修改相应的配置
直接以key=value的形式进行修改即可
修改端口:
# 默认端口为 8080 端口 改成 80 端口为直接访问
# http://localhost:8080/hello
# http://localhost/hello
server.port=80
关闭运行日志图标 (banner)
- spring.main,banner-mode=off
设置日志相关
- logging.level.root=debug
具体的修改要按照相应的开发框架才能进行
- https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties
- 官方文档中参考文档第一项:Application Properties
SpringBoot内置属性查询
配置文件分类
SpringBoot提供了多种属性配置方式
properties、yml、yaml
-
application.properties
server.port=80
-
application.yml
port后面的空格记得不要删除,否则配置文件不会生效
server : port: 81
-
application.yaml
server: port: 82
三种文件中,开发常用的时 yml 文件开发
文件优先级:
- properties > yml > yaml
不同配置文件相同的配置按照优先级相互覆盖,不同配置文件中的不同配置全部保留
1)文件属性提示消失问题
2)yaml格式要求
介绍
-
YAML (YAML Ain't Markup Language),一种数据序列化格式
-
优点:
- 容易阅读
- 容易与脚本语言交互
- 以数据为核心,重数据轻格式
-
YAML文件扩展名
- .yml (主流)
- .yaml
yam1语法规则
- 大小写敏感
- 属性层级关系使用多行描述,每行结尾使用冒号结束
- 使用缩进表示层级关系,同层级左侧对齐,只允许使用空格 (不允许使用Tab键)属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
-
表示注释
- 核心规则:数据前面要加空格与冒号隔开
user:
name: huxi
age: 18
字面值表示方式
boolean: TRUE #TRUE,true,True,FALSE,false,FaLse均可
float: 3.14 #6.8523015e+5 #支持科学计数法
int: 123 #0b1010 0111 0100 1010 1110 #支持二进制、八进制、十六进制
null: ~ #使用~表示nuLL
string: Helloworld #字符串可以直接书写
string2: "Hello World" #可以使用双引号包裹特殊字符
date: 2018-02-17 #日期必须使用yyyy-MM-dd格式
数组表示方式:在属性名书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔
subject:
- Java
- 前端
- 大数据
enterprise:
name: itcast
age: 16
subject:
- Java
- 前端
- 大数据
likes: [huxi,xihu] #数组书写缩略格式
users : #对象数组格式
- name: Tom
age: 4
- name: Jerry
age: 5
users : #对象数组格式二
-
name: huxi
age: 18
-
name: huxi2
age: 19
users2: [ { name:Tom , age:4 ] , { name:Jerry , age:5 } ] #对象数组缩略格式
3)读取yaml单一数据
在属性上面使用注解
@Value("${值}$")
yml数据
name: huxi
代码层
@Controller
@RequestMapping("/users")
public class TestController {
@Value("${name}$")
private String name;
@ResponseBody
public String go(){
System.out.println("springBoot welcome to you 测试中文乱码问题");
System.out.println(name)
return "springBoot welcome to you 测试中文乱码问题";
}
}
对象读取:
@Value("${对象值}$")
yml数据
user:
name: huxi
age: 18
代码层
@Controller
@RequestMapping("/users")
public class TestController {
@Value("${name}$")
private String name;
@Value("${user.age}$")
private Integer age;
@ResponseBody
public String go(){
System.out.println("springBoot welcome to you 测试中文乱码问题");
System.out.println(age)
return "springBoot welcome to you 测试中文乱码问题";
}
}
数组读取
@Value("${数组名称[索引]}$")
yml数据
subject:
- Java
- 前端
- 大数据
代码层
@Controller
@RequestMapping("/users")
public class TestController {
@Value("${name}$")
private String name;
@Value("${user.age}$")
private Integer age;
@Value("${subject[1]}$") //取到的值为 Java
private String subject;
@ResponseBody
public String go(){
System.out.println("springBoot welcome to you 测试中文乱码问题");
System.out.println(subject)
return "springBoot welcome to you 测试中文乱码问题";
}
}
数组对象值读取
@Value("${数组名称[索引].属性名称}$")
yml数据
users :
-
name: huxi
age: 18
-
name: huxi2
age: 19
代码层
@Controller
@RequestMapping("/users")
public class TestController {
@Value("${name}$")
private String name;
@Value("${user.age}$")
private Integer age;
@Value("${subject[1]}$") //取到的值为 Java
private String subject;
@Value("${users[1].name}$")
private String name2;
@ResponseBody
public String go(){
System.out.println("springBoot welcome to you 测试中文乱码问题");
System.out.println(name2)
return "springBoot welcome to you 测试中文乱码问题";
}
}
属性引用
在编写yml中,我们可能会书写多种重复性的语句。那么我们该如何进行重复语句的引用呢
如:
href: c:\User
testHref: c:\User\Java
testHref2: c:\User\py
若想引用herf的语句,只需将与代码层的书写一致即可
如:
href: c:\User
testHref: ${href}\Java
testHref2: ${href}\py
若想将文本信息让他变成转义形式的,只需将文本加上""即可
如:
test: \thello\tworld # 这个取出来的是正常的数据,即不会发生任何该改变
test: "\thello\tworld" # 这个取出来的就是转义的数据,hello world
4)读取yaml全部数据
创建一个对象属性Environment,然后使用@Autowired注解自动将对象进行注入
最后在根对象.属性进行获取
yml数据
test: springboot
subject:
- Java
- 前端
- 大数据
enterprise:
name: itcast
age: 16
subject:
- Java
- 前端
- 大数据
代码层
@Controller
@RequestMapping("/users")
public class TestController {
@Autowired
private Environment env;
@ResponseBody
public String go(){
System.out.printIn(env.getProperty("test"));
System.out.printIn(env.getProperty("enterprise.name"));
System.out.println(env.getProperty("enterprise.subject[0]"));
return "hello , spring boot!";
}
}
5)封装数据
在开发中,我们常使用到的开发习惯是将数据包装成一个对象
那么在yml中配置的对象,我们也可以将其封装成一个实体类,然后在进行使用
yml数据
mysql:
driver: com.mysql.jdbc.Driver
url: jjdvc:mysql://localhost/test
username: root
password: root
datasource:
driver: com.mysql.jdbc.Driver
url: jjdvc:mysql://localhost/test
username: root
password: root
创建一个实体类,类中的属性要与yml中配置的属性完全一致
加上一个注解@ConfigurationProperties(prefix="yml中的对象名称")
最后为实体类加上给spring管理的注解,让它成为spring的一个bean,否则spring将无法进行识别
@Component
@ConfigurationProperties(prefix="datasource")
public class DataSource{
private String driver;
private String url;
private String userName;
private String password;
}
使用
@Controller
@RequestMapping("/users")
public class TestController {
@Autowired
private DataSource dataSource;
@ResponseBody
public String go(){
System.out.println("springBoot welcome to you 测试中文乱码问题");
System.out.println(dataSource.driver)
return "springBoot welcome to you 测试中文乱码问题";
}
}
若配置@ConfigurationProperties注解时出现
进行添加以下的注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
二、整合第三技术
1)JUnit
导入测试的starter(一般创建SpirngBoot模块会自定进行导入)
pom.xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
在提供的测试模块中,创建一个类(一般都是自带的)
SpringBootJUnitApplicationTests
在里面进行注入想要测试的类即可,测试类上面要添加一个核心注解@SpringBootTest,添加这个注解后,SpringBoot就会设置当前类为JUnit加载的SpringBoot启动类
package com.hyl;
import com.hyl.dao.BookDao;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootJUnitApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
bookDao.save();
}
}
classes属性
当测试类与启动类在同一包下或在启动类包下的子包中,无需过多的配置,即可进行运行
但若是进行位置的更改,则需要进行额外的配置
在@SpringBootTest的注解中,添加属性classes=当前的测试类名称.class
或使用注解@ContextConfiguration(classes = 当前的测试类名称.class)
@SpringBootTest(classes = SpringBootJUnitApplicationTests.class)
//@ContextConfiguration(classes = SpringBootJUnitApplication.class)
class SpringBootJUnitApplicationTests {
@Autowired
private BookDao bookDao;
@Test
void contextLoads() {
bookDao.save();
}
}
因为SpringBoot测试类的具体是从Sptrng中获取的,若文件位置发生改变,则会发生异常
2)Mybatis
添加mybatis相关的依赖进去
<dependencies>
//mybatis配置
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
//mysql配置
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
//springboot核心配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
若使用模板进行构建,勾选这几个即可
完成上面的配置后,我们需要进行设置数据库的参数
application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: root
使用模板进行导入时,会自动导入mysql的数据库,在进行SpringBoot版本的使用更改时,高的版本会导入mysql8版本的,这个时候哦,我们要对mysql的配置信息添加一些新的信息(添加时区信息)
在url中添加时区信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: root
完成配置后,在dao层的接口中配置映射设置
@Mapper注解是为了让容器进行识别到,自动产生代理对象,相当于替代了原生方式使用的xml配置指定类
@Mapper不是必要的,但不使用@Mapper要写配置文件
@Mapper
public interface UserDao{
@Select("select * from user")
public User getOne();
}
每一个dao层内的类都要进行编写一个mapper注解,可以直接在启动类中进行集中的配置,就可以不用再进行编写mapper注解了
@SpringBootApplication
@MapperScan("dao层对应的包下")
public class SpringBootMybatisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
xml文件开发
在yml中指定mybatis的核心配置文件位置
mybatis:
config-location: classpath:mybatis-config.xml
核心配置文件
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typaAliases>
<!--指定实体类的位置-->
<package name="com.hyl.domain"></package>
</typaAliases>
<!-- 配置xxxMapper文件的路径配置,该文件主要存放SQL语句
resource 执行的sql映射文件路径-->
<mappers>
<mapper resource="sql文件的路径"/>
</mappers>
</configuration>
接下来的流程就按照原始的mybatis的开发流程只需即可
方式二:跳过核心配置文件
在yml进行配置
mybatis:
type-aliases-package: com.hyl.domain #指定实体类的位置
mapper-locations: classpath*:/mybatis/mapper/*.xml #指定sql执行文件的路径
示例开发
创建项目
使用 idea 中的 spring initializr 生成 maven 项目,项目命令为 mybatis-test,选择 web,mysql,mybatis 依赖,即可成功。(详细过程不赘述,如有需要学习 springboot 创建过程,可参考这篇文章。
然后依照上面的 pom 文件,补齐缺少的依赖。接着创建包 entity,service 和 mybatis 映射文件夹 mapper,创建。为了方便配置将 application.properties 改成 application.yml。由于我们时 REST 接口,故不需要 static 和 templates 目录。修改完毕后的项目结构如下:
修改启动类,增加@MapperScan("com.example.mybatistest.dao"),以自动扫描 dao 目录,避免每个 dao 都手动加@Mapper注解。代码如下:
@SpringBootApplication
@MapperScan("com.example.mybatistest.dao")
public class MybatisTestApplication {
public static void main(String[] args) {
SpringApplication.run(MybatisTestApplication.class, args);
}
}
修改 application.yml,配置项目,代码如下:
mybatis:
#对应实体类路径
type-aliases-package: com.example.mybatistest.entity
#对应mapper映射文件路径
mapper-locations: classpath:mapper/*.xml
#pagehelper物理分页配置
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
returnPageInfo: check
server:
port: 8081
spring:
datasource:
name: mysqlTest
type: com.alibaba.druid.pool.DruidDataSource
#druid连接池相关配置
druid:
#监控拦截统计的filters
filters: stat
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
#配置初始化大小,最小,最大
initial-size: 1
min-idle: 1
max-active: 20
#获取连接等待超时时间
max-wait: 6000
#间隔多久检测一次需要关闭的空闲连接
time-between-eviction-runs-millis: 60000
#一个连接在池中的最小生存时间
min-evictable-idle-time-millis: 300000
#打开PSCache,并指定每个连接上PSCache的大小。oracle设置为true,mysql设置为false。分库分表设置较多推荐设置
pool-prepared-statements: false
max-pool-prepared-statement-per-connection-size: 20
http:
encoding:
charset: utf-8
enabled: true
代码层
首先创建数据表,sql 语句如下:
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`age` tinyint(4) NOT NULL DEFAULT '0',
`password` varchar(255) NOT NULL DEFAULT '123456',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;
然后在 entity 包中创建实体类 User.java
public class User {
private int id;
private String name;
private int age;
private String password;
public User(int id, String name, int age, String password) {
this.id = id;
this.name = name;
this.age = age;
this.password = password;
}
public User(){}
//getter setter自行添加
}
在 dao 包下创建 UserDao.java
public interface UserDao {
//插入用户
int insert(User user);
//根据id查询
User selectById(String id);
//查询所有
List<User> selectAll();
}
在 mapper 文件夹下创建 UserMapper.xml,具体的 xml 编写方法查看文首的官方文档。
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.example.mybatistest.dao.UserDao">
<sql id="BASE_TABLE">
user
</sql>
<sql id="BASE_COLUMN">
id,name,age,password
</sql>
<insert id="insert" parameterType="com.example.mybatistest.entity.User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO <include refid="BASE_TABLE"/>
<trim prefix="(" suffix=")" suffixOverrides=",">
name,password,
<if test="age!=null">
age
</if>
</trim>
<trim prefix=" VALUE(" suffix=")" suffixOverrides=",">
#{name,jdbcType=VARCHAR},#{password},
<if test="age!=null">
#{age}
</if>
</trim>
</insert>
<select id="selectById" resultType="com.example.mybatistest.entity.User">
select
<include refid="BASE_COLUMN"/>
from
<include refid="BASE_TABLE"/>
where id=#{id}
</select>
<select id="selectAll" resultType="com.example.mybatistest.entity.User">
select
<include refid="BASE_COLUMN"/>
from
<include refid="BASE_TABLE"/>
</select>
</mapper>
至此使用 mybatis 的代码编写完了,之后要用时调用 dao 接口中的方法即可。
注解编写sql
上面使用的是 xml 方式编写 sql 代码,其实 mybatis 也支持在注解中编写 sql,这样可以避免编写复杂的 xml 查询文件,但同时也将 sql 语句耦合到了代码中,也不易实现复杂查询,因此多用于简单 sql 语句的编写。
要使用注解首先将 applicaton.yml 配置文件中的mapper-locations: classpath:mapper/*.xml
注释掉。然后在 UserDao.java 中加入 sql 注解,代码如下:
public interface UserDao {
//插入用户
@Insert("insert into user(name,age,password) value(#{name},#{age},#{password})")
@Options(useGeneratedKeys=true,keyColumn="id",keyProperty="id")
int insert(User user);
//根据id查询
@Select("select * from user where id=#{id}")
User selectById(String id);
//查询所有
@Select("select * from user")
List<User> selectAll();
}
3)Mybatis-Plus
1.依赖配置
由于SpringBoot创建的初始模块中,并没有提供Mybatis-Plus的定位,所以我们在进行勾选模块时,只需勾选sql的即可
然后在pom.xml中配置依赖定位
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
完成上面的配置后,我们需要进行设置数据库的参数
2.application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: root
3.代码层
完成上面的配置后,在原始的Mybatis中,我们在dao层时需要将所有的增删改查的操作都进行一遍书写
@Mapper
public interface UserDao{
@Select("select * from user")
public User getOne();
}
但使用Mybatis-Plus中,我们只需继承一个类,类中的泛型添加上对应的实体类,然后再类上添加注解@TableName("数据库表名称")即可
倘若不进行指定表的名称,则需要将实体类的名称改成与表名称一致
数据库名称
a_naem
实体类名称
A_name
AName
若存多个表的名称都以某个特定的称谓开头的,则可以在yml中进行配置
mybatis-plus:
global-config:
db-config:
table-prefix: 指定同一开头的名称
domain层
@TableName("user")
public class User{
private String name;
private String age;
...
}
dao层
@Mapper
public interface UserDao extends BaseMapper<User>{
}
@Test
package com.hyl;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootMybatisApplicationTests {
@Autowired
private UserDao userDao;
@Test
void contextLoads() {
System.out.println(userDao.selectById(1))
}
}
BaseMapper类中的所有方法
若需要进行自定义sql开发,需要进行手动自己添加方法,然后按照正常的mybatis开发流程即可
小问题:
若使用Plus中自带的添加语句,倘若主键为自增的,则可能会出现异常,因为它底层使用的主键时固定的,使用雪花算法进行添加主键,需要在yml中进行配置
mybatis-plus:
global-config:
db-config:
table-prefix: 指定同一开头的名称
id-type: auto
也可以在实体类中进行注解的配置
@Table(value="id",type=IdType.AUTO)
或
@Table(method=type.auto)
4.配置日志
mybatis-plus:
global-config:
db-config:
table-prefix: 统一表名称
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #开启日志 配置日志位置
切记,上线环境中打印日志到控制台的配置要关掉
5.分页的开启
分页操作需要设定分页对象IPage
@Test
void testGetPage(){
//new Page(从第几条开始查询,显示几条);
IPage page = new Page(1,5);
bookDao.selectPage(page,nul1);
}
IPage对象中封装了分页操作中的所有数据
- 数据
- 当前页码值
- 每页数据总量
- 最大页码值
- 数据总量
单单创建IPage对象还不行,分页操作是在MyBatisPlus的常规操作基础上增强得到,内部是动态的拼写SQL语句,因此需要增强对应的功能,使用MyBatisPlus拦截器实现
@Configuration
public class MpConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor() {
//1.定义Mp拦截器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
//2.添加具体的拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
分页条件查询
使用Querywrapper对象封装查询条件,推荐使用LambdaQueryWrapper对象,所有查询操作封装成方法调用
public class Book{
private String name;
public String getName(){
return this.name;
}
}
@Test
void testGetByCondition(){
Book book=new Book();
IPage page = new Page(1,10);
LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper<Book>();
//Book::getName 根据对象的属性名称作为字段进行模糊查询
//"Spring" 查询条件为 Spring
lqw.like(Book::getName,"Spring");
bookDao.selectPage(page,lqw);
}
- 支持动态拼写查询条件
@Test
void testGetByCondition(){
String name = "Spring":
IPage page = new Page(1,10);
LambdaQueryWrapper<Book> lqw = new LambdaQueryWrapper<Book>();
//使用工具类进行判别当前值是
lqw.like(strings.isNotEmpty(name),Book::getName,"Spring");
bookDao,selectPage(page,lqw);
}
6.快速开发业务层
在server层的接口继承由MP提供的类,里面的泛型填上对应的实体类即可
public interface IBookService extends IService<Book>{
}
继承后,MP帮我们在内部进行实现了多种方法,要是业务需求需要进行自定义方法,按照原来的开发习惯进行自定义添加即可
public interface IBookService extends IService<Book> f
//追加的操作与原始操作通过名称区分,功能类似
Boolean delete(Integer id);
Boolean insert(Book book);
Boolean modify(Book book);
Book get(Integer id);
}
然后在业务具体实现层中,进行继承类ServiceImpl<M,T>,泛型M是对应的server层接口,T是对应的实体类
继承完再实现刚刚创建的业务接口
public class BookService extends ServiceImpl<BookDao,Book> implements IbookService{
}
整体实现框架图:
7.使用xml配置文件开发
在核心配置文件中添加以下信息,进行指定实体类及xml文件的路径
mybatis-plus:
type-aliases-package: com.hyl.domain #指定对应数据库中的实体类位置
mapper-locations: classpath:mapper/*.xml #指定执行sql的xml文件的位置
4)数据源(Druid)
导入相关的技术依赖
由于Druid的定位没有被maven收录,我们需要进行手动导入
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.9</version>
</dependency>
配置yml
方式一:
在原有的基础上,添加一个type属性,属性值为数据源的位置
方式二:
直接在yml中输入druid然后再出现的结构中添加信息即可
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: root
5)smm整合
lombok(快速开发实体类)
使用方式:
导入相关依赖(无需指定版本,SpringBoot提供版本)
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
在实体类中直接输入注解@Data注解即可生成Get和Set方法以及其他的方法
唯独没有生成构造方法,若需要也可加上注解
@Data
public class User{
private String name;
private Integer age;
}
原理解析
异常处理
因为所有的异常信息都是会展示到表现层,所以在表现层中创建一个异常处理类
添加注解@ControllerAdvice来代表这是一个表现层的异常处理
或者添加注解@RestControllerAdvice来表示这是一个表现层的异常处理,且还是一个以REST风格进行开发的
/**
* @author 123
*/
@RestControllerAdvice
public class ProjectExceptionAdvice {
//添加一个方法,进行集中处理异常
//@ExceptionHandle 注解是让当前这个方法进行捕获那种异常进行处理
@ExceptionHandler(Exception.class)
public void doException(Exception e){
//自己生产开发时,记得打印错误信息,及配置日志
e.printStackTrace();
//记录开发日志
//发送邮箱给开发人员 连带对象(e)一起发送
//发送短信给用户人员
//添加异常处理方式,若当前开发中有约定的协议类,可以将方法的返回值改成协议类的类型进行返回
....
}
}
三、实用篇
1)程序打包与运行
打开maven选项,点击package进行将程序进行打包(若想要进行清空之前的打包项目,点击clean即可)
打包完毕后会在target目录下出现当前工程的jar包
打开打包完成的目录下,执行以下指令即可进行运行项目
java -jar 对应的工程jar包名称
若在测试环境测试的代码,在进行打包时,想要进行清除,不将其进行保留,可直接选择跳过测试模式
注意:
在进行打包后的运行时,在当前项目中,需要有对应的maven插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
打包插件
使用打包springboot的打包插件与不使用之间的区别
jar包描述文件(MANIFEST.MF)
解决端口占用常用指令
查询端口:
查询指定端口:
- netstat -ano |findstr "端口号"
根据进行PID查询进名称
- tasklist |findstr "进程PID号"
根据PID杀死任务
- taskkill /F /PID "进程PID号"
根据进程名称杀死任务
- taskkill -f -t -im "进行名称"
Linux打包及运行
上传jar包,后执行与win的操作一直的指令即可
若想要进行后台进行运行,需要使用以下指令
nohup java -jar 项目名称 > 指定的日志目录
设置临时属性
带属性启动SpringBoot
-
java -jar springboot.jar --server.port=80 //临时进行更改端口号,将其更改成80端口
携带多个属性启动SpringBoot,属性之间使用空格进行分隔
重点
所有的临时参数的传递,都是经过启动类时所传递的args数组进行接收
2)日志基础操作
日志的作用:
- 编程期代码的调试
- 运营期记录信息
- 记录日常运营重要信息(峰值流量,平均响应时长)
- 记录应用报错信息(错误堆栈)
- 记录运维过程数据(扩容、宕机、报警...)
创建日志对象
//创建日志对象
private static final Logger Log= LoggerFactory.getLogger(当前类);
//日志的级别输出
Log.debug("");
Log.info("");
Log.warn("");
Log.error("");
日志的级别
- TRACE:运行堆栈信息,使用率低
- DEBUG:程序员调试代码使用
- INFO记录运维过程的数据
- WARN:记录运维过程报警数据
- ERROR:记录错误堆栈信息
- FATAL:灾难信息,合并计入REEOR
设置日志输出级别
#开启debug模式,输出调试信息,常用于进程系统运行状况
debug: true
#设置日志级别,root表示根节点,即整体应用日志级别,默认级别为 info
logging:
#设置分组,进行组的日志级别管理
group:
ebank: 包,包
level:
root: debug
#对分组的日志设置级别
ebank: info
快速构建日志对象
使用lombok依赖进行快速构建日志对象
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
在所需的类上面进行添加注解
@Slf4j
class Test{
public static void main(String[] args){
//依旧使用Log进行调用方法
Log.debug("");
Log.info("");
Log.warn("");
Log.error("");
}
}
文件记录日志
在yml中进行配置
设置日志文件
logging:
file:
name: server.log
日志文件详细配置
logging:
file: server.log #日志输出的路径
logback:
rollingpolicy:
max-file-size: 4kb #设置日志最大的容量
file-name-pattern: server.%d{yyyy-MM-dd}.%i.log # 设置日志的命名格式 %d为时间
# %i为序号(相当于循环的i)
使用aop思想日志的记录
1.aop的实现
- 明确业务的流程
- 定义切入点,往往横切多个业务
- 定义增强处理(即在代理对象进行切入点实现之前、之后、最终所执行的行为)
2.引入aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.配置xml
logging:
level:
com.hyl.server: info #定义指定文件夹的日志级别
file:
path: log/mylog #定义日志输出的文件夹的地址
4.实现切面对象
// 声明当前对象为一个切面对象
@Aspect
// 交给spring进行管理
@Component
public class LogAspect{
//获取日志对象
private Logger log=LoggerFactory.getLogger(getClass());
//当server包下的 所有类中的所有方法 执行前,进行前置处理
//.* --所有类 .*(..) --所有方法
@Before("execution(* com.hyl.sercver..*.*(..))")
public void log(){
logger.info("before method run ");
}
//当server包下的 所有类中的所有方法 执行后,进行后置处理
@After("execution(* com.hyl.sercver..*.*(..))")
//使用 JoinPoint 对象获取 执行的类x信息、方法参数、方法等信息
public void logAfter(JoinPoint joinPoint){
logger.info("after method run,"+joinPoint.getTarget().getClass()+",args="
+Arrays.asList(joinPoint.getArgs())
+",method="+joinPoint.getSignature())
}
}
监控和邮件发送
3)热部署
手动热部署
添加依工具
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
然后执行快捷键CTRL+9
或点击构建项目
自动热部署
在设置,找到构建、执行部署,点击编译器,勾选自动部署项目
2022版本之前,点击ctrl+shift+alt+/弹出注册表选项框,进行以下的勾选
2022版本之后,点击设置,再点击高级设置,找到编译器选项,勾选运行时自动热部署项目
热部署范围
在yml配置文件中,进行配置
devtools:
restart:
exclude: public/**,static/js/**,static/css/test.css
#设定 public文件夹以下的所有文件不参与热部署
#设置static下的js下的所有文件不参与热部署,
#设置static下的css下的test.css文件不参与热部署
默认不触发重启的目录列表
- /META-INF/maven
- /META-INF/resources
- /resources
- /static
- /public
- /templates
关闭热部署
设置高优先级属性禁用热部署
public class SpringBootMybatisApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
4)配置高级
1.第三方bean属性绑定
原始
在以往中,我们在yml进行配置的属性,可以通过@ConfigurationProperties注解进行加载
如:实体类
public class Server{
private String idAddress;
private int port;
private long timeout;
...
}
yml文件配置
servers:
idAddress: 120.0.0.1
port: 80
timeout: -1
实体类与配置文件类的属性值一致,可使用注解 @ConfigurationProperties 指定添加属性值的上一级名称进行注入
//因为要使用注解进行读取yml配置文件里面的信息,所有要进行交给spring管理
@Component
@Data
@ConfigurationProperties(prefix="servers")
public class Server{
private String idAddress;
private int port;
private long timeout;
}
第三方
第三方bean的做法也一样
yml
server:
port: 8080 #配置端口
spring:
datasource:
first:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/first?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
second:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/second?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
第三方bean绑定
package com.hyl.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DBconfig {
//配置第一个数据源,默认级别最高
@Bean("first")
//指定数据源
@ConfigurationProperties("spring.datasource.first")
public DataSource dataSource1() {
//将数据源的配置信息进行返回
return DataSourceBuilder.create().build();
}
//配置第二个数据源
@Bean("second")
//指定数据源
@ConfigurationProperties("spring.datasource.second")
public DataSource dataSource2() {
//将数据源的配置信息进行返回
return DataSourceBuilder.create().build();
}
}
@EnableConfigurationProperties与 @ConfigurationProperties的区别
@EnableConfigurationProperties
- 开启属性绑定,并设定对应的目标是谁
- 该注解可以将@ConfigurationProperties注解对应的类加入Spring容器
@ConfigurationProperties
- 设置属性绑定
@EnableConfigurationProperties进行绑定的类中,不能使用@Component及其衍生注解的使用
若在进行添加@ConfigurationProperties注解后,idea出现报错信息需要在xml文件中添加一个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-Configuration-processor</artifactId>
</dependency
2.松散绑定
在使用@ConfigurationProperties注解时,在yml书写的变量名支持宽松绑定
实体类
//因为要使用注解进行读取yml配置文件里面的信息,所有要进行交给spring管理
@Component
@Data
@ConfigurationProperties(prefix="servers")
public class Server{
private String idAddress;
private int port;
private long timeout;
}
yml:
@Value()注解不支持松散绑定
3.常用计量单位应用
时间单位
在配置文件中,有一些配置是以时间范围为计量单位的,那么我们就可以使用jdk8提供的时间与空间的计量单位进行配置
yml:
servers:
idAddress: 120.0.0.1
port: 80
timeout: -1
serverTimeOut: 3
实体类(使用 Duration 对象为属性),然后在属性上面添加注解@DurationUnit(ChronoUnit.时间单位)
//因为要使用注解进行读取yml配置文件里面的信息,所有要进行交给spring管理
@Component
@Data
@ConfigurationProperties(prefix="servers")
public class Server{
private String idAddress;
private int port;
private long timeout;
@DurationUnit(ChronoUnit.CENTURIES)
private Duration serverTimeOut;
}
容量大小单位
使用JDK8提供的对象DataSize
然后在对应的属性上面添加注解
@DurationUnit(DataUnit.距离常量)
或者在yml配置文件中直接添加容量大小即可
servsers:
datasize: 10MB
datasize: 10 #默认大小为比特
4.bean属性就校验
添加JSR303规范坐标与Hibernate校验框架对应的坐标
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
在所需类上面进行声明吗,开启bean的校验功能
@Component
//开启注解校验
@Validated
public class Test{
//对该属性进行校验
@MAX(value=400,message="最大值不能超过400")
private int port;
}
5.进制数据转换规则
类似的使用场景,在springboot的yml文件中,配置数据库中的密码时,我们可能会使用纯数字当作密码
那么在springboot中,字面里量是可以存在二进制,八进制,十六进制的形式存在的
如:
八进制:
- 0(0-7)
十六进制:
- 0x(0-9,a-f)
那么我们在填写数据库的密码时,springboot底层解析时,可能会将二进制的数字转换成八进制或16进制
如:
dataSource:
name: root
pass: 0127 #二进制的 0127 在springboot的底层解析会将其转换层成把八进制 87
解决方案:
将数字转换成字符串
dataSource:
name: root
pass: "0127"
5)测试
1.加载测试专用属性
在启动测试环境时,可以通过propertie参数设置测试环境的专用参数,或者通过args数组传入临时参数
原始配置方法:
yml
test:
prop: testValues
测试案例
@SpringBootTest()
public class Test{
@Value("$test.prop")
private String msg;
}
原始的测试用例的配置方式,测试的数据是存在整一个项目中
测试专用属性
使用properties参数
@SpringBootTest(properties={"test.prop=testVlues"})
public class Test{
@Value("$test.prop")
private String msg;
}
使用args参数
@SpringBootTest(args={"--test.prop=testVlues2"})
public class Test{
@Value("$test.prop")
private String msg;
}
测试专用属性作用的范围小,仅存于当前的测试环境中,不会对整体的项目产生影响
测试类中启动web环境
使用参数webEnvironment进行指定web服务的启动方式
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class SpringBootMybatisApplicationTests {
@Test
void contextLoads() {
System.out.println(1);
}
}
发送虚拟请求
开启springboot提供好的虚拟调用接口
在测试类中添加上注解
//开启MVC虚拟调用
@AutoConfigureMockMvc
然后在方法中添加一个虚拟执行对象
void testMvc(@Autowired MockMvc mvc) throws Exception {
}
最后创建虚拟请求,执行虚拟请求
package com.hyl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class SpringBootMybatisApplicationTests {
@Test
//添加虚拟请求对象
void testMvc(@Autowired MockMvc mvc) throws Exception {
//创建虚拟请求
MockHttpServletRequestBuilder builder= MockMvcRequestBuilders.get("/users");
//执行对应的请求
mvc.perform(builder);
}
}
业务层测试事务回滚
在测试类当中添加注解
@Transactional
配置当前这个注解之后,所有的提交信息都会进行回滚
因为默认添加了一个注解@Rollback(),默认为true,即回滚信息,可以改为false,为不滚数据
测试类中的事务注解与业务层的事务注解有很大的相似处
测试层的事务注解:
- @Transactional
业务层的事务注解
- @Transactiona
测试用例随机值
在yml中属性的值添加如下格式即可
test:
book:
id: ${random.int} #获取整数值
name: ${random.value} #获取字符串
uuid: ${random.uuid} #获取uuid值
...
6)数据层解决方案
内置数据源
yml数据源配置
-
格式一:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm db?serverTimezone=UTC username: root password: root type: com.alibaba.druid.pool.DruidDataSource
-
格式二:
spring: datasource: druid: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/ssm db?serverTimezone=UTC username: root password: root
格式一是默认的数据源配置
格式二使用阿里巴巴的druid数据源配置
SpringBoot提供了3种内嵌的数据源对象供开发者选择
- HikaricP:默认内置数据源对象
- Tomcat提供DataSource: HikariCP不可用的情况下,且在web环境中,将使用tomcat服务器配置的数据源对象
- Commons DBCP:Hikari不可用,tomcat数据源也不可用,将使用dbcp数据源
若通用的 配置无法设置具体的数据源配置信息,仅提供基本连接相关配置,如需配置,在下一级配置中设置具体的设定
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm db?serverTimezone=UTC
username: root
password: root
type: com.alibaba.druid.pool.DruidDataSource
hikari:
maximum-pool-size: 50
7)整合Redis
1.导入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置yml中的redis连接信息
spring:
redis:
host: localhost #或ip地址
port: 6379
3.在所需类中自动装配redis
public class Test{
@Autowired
//获取redis对象
private RedisTemplate redis;
@Test
void set(){
//获取操作的数据类型
ValueOperations ops = redisTemplate.opsForValue();
//填写数据
ops.set("age",18);
}
@Test
void get(){
//获取操作的数据类型
ValueOperations ops = redisTemplate.opsForValue();
//填写数据
Object age=ops.get("age");
System.out.println(age);
}
}
Springboot读写redis客户端
在上个步骤使用的RedisTemplate对象进行连接redis客户端,RedisTemplate是需要两个泛型进行指定当前数据类型的数据的
若不进行指定key和value的数据类型,则默认为Object类型,底层会进行序列化
可以使用 StringRedisTemplate 类获作为连接redis的的客户端对象,数据默认为String类型
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@Rollback()
class SpringBootMybatisApplicationTests {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void contextLoads() {
ValueOperations<String, String> stringStringValueOperations = redisTemplate.opsForValue();
String age = stringStringValueOperations.get("age");
System.out.println(age);
}
实现技术切换(jedis)
导入相关的依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
配置客户端
spring:
redis:
host: localhost #或ip地址
port: 6379
client-type: jedis #若使用lettuce技术,可以写上lettuce,但默认就是lettuce
yml完整配置
spring:
datasource: #配置数据源
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
cache:
type: redis #更改缓存源
redis:
user-key-prefix: true #是否使用前缀
cache-null-values: false #是否缓存空值
key-prefix: hyl #是否使用前缀
time-to-live: 10s #最大活动时间
redis:
host: localhost
port: 6379
//创建连接redis客户端的对象,填写上连接 地址及端口号
Jedis jedis = new Jedis("localhost", 6379);
//添加数据
jedis.set("jedis", "hello jedis");
//获取数据
System.out.println(jedis.get("jedis"));
//关闭流
jedis.close();
lettcus与jedis区别
- jedis连接Redis服务器是直连模式,当多线程模式下使用iedis会存在线程安全问题,解决方案可以通过配置连接池使每个连接专用,这样整体性能就大受影响。
- lettcus基于Nety框架进行与Redis服务器连接,底层设计中采用StatefulRedisConnection。 StatefulRedisConnection自身是线程安全的,可以保障并发访问安全问题,所以一个连接可以被多线程复用。当然lettcus也支持多连接实例一起工作。
8)整合Mongodb
Mongodb,是一个开源,高性能,无模式的文档型的数据库,是NoSQL产品中的一种,是最象关系型数据库中的非关系型数据库
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
配置yml
spring:
data:
mongodb:
url: mongodb://localhost/hyl
加载Mongo对象
public class Test{
@Autowired
//获取mongo对象
private MongoTemplate mongo;
@Test
void set(){
Book book=new Book;
book.setName("hyl");
//填写数据
mongo.save(book);
}
@Test
void get(){
//查询数据
List<Book> list=mongo.findAll(Book.class);
System.out.println(list)
}
}
9)整合ES
导入依赖
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
10)过滤器/拦截器
过滤器和拦截器的执行链路
过滤器:
- 当信息繁多且繁杂时,可以启用过滤器进行进行筛选符合自己想要的信息,定义筛选条件的工具,我们称之为过滤器
拦截器:
- 当一个流程正在执行时,我希望干预他的进展,在执行前、执行后、执行完毕进行干预,或直接终结他的运行
相同点
- 都是aop编程思想的体现,可以在程序执行前后做一些操作
不同点:
-
过滤器依赖于servlet容器,拦截器不依赖
-
过滤器的执行由Servlet容器回调完成,而拦截器通常通过动态代理的方式来执行。
-
触发时机不一样,过滤器是在请求进入Tomcat容器后,而进入servlet前进行预处理的;拦
-
截器是在进入servlet之后,而进入controller之前处理的。
-
拦截器可以获取IOC容器中的各个bean,而过滤器就不行,拦截器归Spring管理。
过滤器使用
原生xml文件的使用:
-- web.xml
<filter>
<filter-name>struts2</filter-name>
<filter-class>com.duing.filter.CustomFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
使用方式一:
1)创建类实现Filter接口;2)注入springboot容器(代码注入 / 通过注解@WebFilter注入)
实现类:
package com.hyl.examonline.common.interceptors;
import javax.servlet.*;
import java.io.IOException;
import java.util.logging.LogRecord;
@Slf4j
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
//初始a化方法
//Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
/**
执行相应的操作 放行/拦截
*/
filterChain.doFilter(servletRequest,servletResponse);//放行
}
@Override
public void destroy() {
//Filter.super.destroy();
}
}
创建拦截器配置类,获取Filter类的bean,将器传入进去,使其生效
@Configuration
public class FilterConfig{
//创建自定义拦截器bean
@Bean
Filter testFilter(){
return new TestFilter();
}
//泛型为自定义拦截器的类名称
@Bean
public FilterRegistrationBean<TestFilter> filterRegistrationBeanl(){
//创建bean
FilterRegistrationBean<TestFilter>filterRegistrationBean=
new FilterRegistrationBean<>();
filterRegistrationBean.setFilter((TestFilter)testFilter());
//过滤规则
filterRegistrationBean.addUrlPatterns("/*");
//filterRegistrationBean.setorder();多个filter的时候order的数值越小则优先级越高
return filterRegistrationBean;
}
}
拦截器的使用
四)整合第三方技术(高级篇)
1)缓存
缓存的作用:
- 将数据库中获取到的数据添加到一个缓存区,等需要用到的时候就先去缓存区查看是否存在,若存在,则直接从缓存区进行返回数据,否则就区数据库中获取,减少服务器的负载
- 缓存是一种介于数据永久存储介质与数据应用之间的数据临时存储介质
- 使用缓存可以有效减少低数据读取过程的次数(若磁盘IO),提高系统性能
- 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间·
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
在启动类中开启缓存功能@EnableCaching
@SpringBootApplication
//开启缓存
@EnableCaching
public class SpringBootApplication {
public static void main(String[] args) {
System.setProperty("spring.devtools.restart.enabled","false");
SpringApplication.run(SpringBootApplication.class, args);
}
}
使用方法:在对应的操作上面写上一个@Cachable,里面填写上缓存空间与找寻空间的方式
//value值是缓存空间的名称,缓存空间中存储的value为返回值的数据,key是怎么获取缓存空间中的值 #为获取到形参中的id具体值
@Cacheable(value="cacheSpace",key="#id")
public Book getById(Integer id){
return bookDao.selectById(id);
}
1.变更缓存供应商:Ehcache
添加依赖:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
添加配置:
spring:
cache:
type: ehcache #更改缓存源
添加额外的xml配置
ehcache.xml
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmIns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="D: ehcache"/>
<!--默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxELementsInMemory: 最大缓存数-->
<!-- overfLowToDisk: 超过最大缓存数量是否持久化到磁盘-->
<!-- timeToidleSeconds;最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数,例如验证码-->
<!-- timeToliveSeconds; 最大存活时间-->
<!-- memorystoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
自定义缓存配置
代码层
//value值是缓存空间的名称,缓存空间中存储的value为返回值的数据,key是怎么获取缓存空间中的值 #为获取到形参中的id具体值
@Cacheable(value="cacheSpace",key="#id")
public Book getById(Integer id){
return bookDao.selectById(id);
}
xml配置
<?xml version="1.0" encoding="utf-8" ?>
<ehcache xmIns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"
updateCheck="false">
<diskStore path="D: ehcache"/>
<!--默认缓存策略 -->
<!-- external:是否永久存在,设置为true则不会被清除,此时与timeout冲突,通常设置为false-->
<!-- diskPersistent:是否启用磁盘持久化-->
<!-- maxELementsInMemory: 最大缓存数-->
<!-- overfLowToDisk: 超过最大缓存数量是否持久化到磁盘-->
<!-- timeToidleSeconds;最大不活动间隔,设置过长缓存容易溢出,设置过短无效果,可用于记录时效性数,例如验证码-->
<!-- timeToliveSeconds; 最大存活时间-->
<!-- memorystoreEvictionPolicy:缓存清除策略-->
<defaultCache
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU"/>
<!--使用name进行区分不同的缓存策略-->
<cache
name="cacheSpace"
eternal="false"
diskPersistent="false"
maxElementsInMemory="1000"
overflowToDisk="false"
timeToIdleSeconds="60"
timeToLiveSeconds="60"
memoryStoreEvictionPolicy="LRU"/>
</ehcache>
若缓存策略文件发生改变,记得要在yml中配置文件的路径
spring:
cache:
type: ehcache #更改缓存源
ehcache:
config: classpath:ehcache.xml
2.变更缓存供应商:Redis
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置yml
spring:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC
username: root
password: root
cache:
type: redis #更改缓存源
redis:
user-key-prefix: true #是否使用前缀
cache-null-values: false #是否缓存空值
key-prefix: hyl #是否使用前缀
time-to-live: 10s #最大活动时间
redis:
host: localhost
port: 6379
3.变更缓存供应商:jetcache
jetCache对SpringCache进行了封装,在原有功能基础上实现了多级缓存、缓存统计、自动刷新、异步调用、数据报表等功能
jetCache设定了本地缓存与远程缓存的多级缓存解决方案
-
本地缓存(local)
-
LinkedHashMap
-
Caffeine
-
-
远程缓存(remote)
- Redis
- Tair
添加对应依赖
<dependency>
<groupId>com.alicp.jetcache</groupId>
<artifactId>jetcache-starter-redis</artifactId>
<version>2.7.3</version>
</dependency>
配置yml
jetcache:
#local: #配置本地方案
remote: #配置远程方案
default:
type: redis
host: localhost
port: 6379
poolConfig:
maxTotal: 30 #最大初始数
在启动类进行开启启用使用注解的形式创建缓存
@EnableCreateCacheAnnotation
package com.hyl;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCreateCacheAnnotation
public class SpringBootMybatisApplication {
public static void maino(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
使用类配置
public class TestServer{
//配置缓存区 area 指定缓存配置区,不写默认为 default 区
@CreatCache(area="",name="jetCache",expire=3600,timeUnit=TimeUnit.SECONDS)
private Cache<String,String> jetCache;
public String sendCodeToSMS(String tele){
//加入缓存区
jetCache.put(tele,code);
//提取
//jetCache.get();
return tele;
}
}
本地缓存方案
配置yml
jetcache:
local: #配置本地方案
default:
type: linkedhashmap
keyConvertor: fastjson #对象作为key时,使用什么工具转成json串
limit: 100
remote: #配置远程方案
default:
type: redis
host: localhost
port: 6379
valueEncoder: java #设定存储的值,做转换的时候,转成什么格式
valueDecodr: java
poolConfig:
maxTotal: 30 #最大初始数
使用类配置
在声明缓存对象的@CreatCache注解中,添加参数cacheType来指定缓存方式(本地/远程)
public class TestServer{
//配置缓存区 area 指定缓存配置区,不写默认为 default 区
@CreatCache(area="",name="jetCache",expire=3600,timeUnit=TimeUnit.SECONDS,
cacheType=CachType.LOCAL)
private Cache<String,String> jetCache;
public String sendCodeToSMS(String tele){
//加入缓存区
jetCache.put(tele,code);
//提取
//jetCache.get();
return tele;
}
}
方法缓存
在启动类中添加注解 @EnableMethodCache(basePackages="包名称")
包名称的填写要写上覆盖到启用缓存注解的包
package com.hyl;
import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation;
import com.alicp.jetcache.anno.config.EnableMethodCache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
//jetcache启用注解的主开关
@EnableCreateCacheAnnotation
//开启方法注解缓存
@EnableMethodCache(basePackages="com.hyl")
public class SpringBootMybatisApplication {
public static void maino(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
yml配置:添加转化json的方式
jetcache:
remote: #配置远程方案
default:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson #必须配置
valueEncoder: java #设定存储的值,做转换的时候,转成什么格式 必须配置
valueDecodr: java #设定值转回来的时候,转换成java类型
poolConfig:
maxTotal: 30 #最大初始数
domain类进行序列化
@Data
class BookDao implements Serializable{
private Integer id;
}
server层使用
public class BookServerImpl implements BookServer{
@Autowired
private BookDao book;
@Override
@Cached(name="book_",key="#id",expire=3600)
public Book getById(Integer id){
return book.selectById(id);
}
@Override
// 在进行更新时,使用book对象的id作为key,使用新对象进行替换旧缓存中的值
@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book){
return bookDao.updateById>0;
}
@Override
// 在进行删除时,在book_缓存区中的对应的数据进行删除,key为对应的id
@CacheInvalidate(name="book_",key="#id")
public boolean delete(Book book){
return bookDao.updateById>0;
}
}
可在yml中配置缓存数据效率:statInterValMinutes
jetcache:
statInterValMinutes: 1 #每个一分钟在控制台打印执行的所有操作统计
remote: #配置远程方案
default:
type: redis
host: localhost
port: 6379
keyConvertor: fastjson #必须配置
valueEncoder: java #设定存储的值,做转换的时候,转成什么格式 必须配置
valueDecodr: java #设定值转回来的时候,转换成java类型
poolConfig:
maxTotal: 30 #最大初始数
4.变更缓存供应商:j2cache
- j2cache是一个缓存整合的框架,可以提供缓存的整合方案,使用各种缓存搭配使用,自身不提供缓存功能
- 使用ehcache+redis进行整合
添加依赖:
<!--核心包-->
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-core</artifactId>
<version>2.8.5-release</version>
</dependency>
<!--辅助包-->
<dependency>
<groupId>net.oschina.j2cache</groupId>
<artifactId>j2cache-spring-boot2-starter</artifactId>
<version>2.8.0-release</version>
</dependency>
<!--ehcache包-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
配置yml
j2cache:
config-location: j2cache.properties
。。。。
2)quartz
Java原始api使用
package com.hyl;
import java.util.Timer;
import java.util.TimerTask;
public class Test {
public static void main(String[] args) {
Timer timer=new Timer();
TimerTask task=new TimerTask() {
@Override
public void run() {
System.out.println(666);
}
};
/*参数一:执行的任务
* 参数二:从什么时间开始执行
* 参数三:每隔几秒开始执行
* */
timer.schedule(task,0,2000);
}
}
框架
原生的api定时任务功能简陋,后面出现了各种定时任务框架
- Quartz
- Spring Task
相关概念
- 工作(ob):用于定义具体执行的工作
- 工作明细(JobDetail):用于描述定时工作相关的信息
- 触发器(Trigger):用于描述触发工作的规则,通常使用cron表达式定义调度规则
- 调度器 (Scheduler): 描述了工作明细与触发器的对应关系
使用方式
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2.创建配置类
创建任务类:
package com.hyl.quartz;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
public class MyQuertz extends QuartzJobBean {
//指定执行的任务
@Override
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
}
}
创建配置类
package com.hyl.config;
import com.hyl.quartz.MyQuertz;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class QuartzConfig {
/**
* 创建工作明细
* 绑定具体的工作
*/
@Bean
public JobDetail goJobDetail() {
//创建任务类进行工作 storeDurably()方法,不使用这个任务,进行移除
return JobBuilder.newJob(MyQuertz.class).storeDurably().build();
}
/**
* 创建触发器
* 绑定具体的工作明细
*/
@Bean
public Trigger goTrigger() {
//设置执行时间
ScheduleBuilder ScheduleBuilder = CronScheduleBuilder.cronSchedule("执行时间");
//绑定工作明细,并将执行时间一起绑定
return TriggerBuilder.newTrigger().forJob(goJobDetail()).withSchedule(ScheduleBuilder).build();
}
}
3)整合task(简化定时任务)
简化定时任务流程
在启动类 进行开启定时任务功能
package com.hyl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
//开启定时任务
@EnableScheduling
public class SpringBootMybatisApplication {
public static void maino(String[] args) {
SpringApplication.run(SpringBootMybatisApplication.class, args);
}
}
创建一个任务类,进行执行任务
@Component
public class MyBean{
@Scheduled(cron="0/5 * * * * ?")
public void print(){
Systrm.out.println("hello bean")
}
}
也可以在yml配置文件中进行其他的配置
spring:
task:
scheduling:
# 任务调度线程池大小 默认1
pool:
size: 1
# 调度线程名称前缀 默认 scheduling
thread-name-prefix: hyl_
shutdowm:
# 线程池关闭时等待所有任务完成
await-termination: false
# 调度线程关闭前最大等待时间,确保最后一定关闭
await-termination-period: 10s
4)JavaMail(邮件)
- SMTP (Simple Mail Transfer Protocol) : 简单邮件传输协议,用于发送电子邮件的传输协议
- poP3 (Post Office Protocol- Version 3): 用于接收电子邮件的标准协议
- IMAP (Internet MailAccess Protocol) : 互联网消息协议,是POP3的替代协议
导入相关依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
配置yml
spring:
mail:
host: sstp.126.com #指定邮件发送协议和邮件方
username: 1000000@126.com #账号
password: 1111111 #邮件公司的授权码
发送简单邮件
public class SendMailServer{
//获取邮件对象
@Autowired
private JavaMailSender javaMailSender;
//发送人
private String from="1111@qq.com"
//接收人
private String to="456@126.com"
//标题
private String subject="测试邮件标题"
//正文
private String text="测试邮件正文"
public void go(){
//创建邮件消息对象
SimpleMailMessage message=new SimpleMailMessage();
// message.setFrom(from+"(测试邮箱返送人别名)")
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(text);
//message.setSenDate(设置定时发送时间);
javaMailSender.send(message);
}
}
发送多部邮件
可进行发送图片链接等网页内容
public class SendMailServer{
//获取邮件对象
@Autowired
private JavaMailSender javaMailSender;
//发送人
private String from="1111@qq.com"
//接收人
private String to="456@126.com"
//标题
private String subject="测试邮件标题"
//正文
private String text="<a href='https://www.hylweb.shop'></a>"
public void go(){
try{
//创建邮件工厂
MailMessage message=javaMailSender.creatMimeMessage();
//获取多部邮件对象 为 true 表示是否支持附件
MimeMessageHelper helper=new MimeMessageHelper(message,true);
// helper.setFrom(from+"(测试邮箱返送人别名)")
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text);
//发送文件
File f=new File("文件路径");
// 参数一为文件名称,参数二为文件对象
helper.addAttachment(f.getName,f);
//helper.setSenDate(设置定时发送时间);
javaMailSender.send(message);
}catch(Exception e){
e.printStackTrace();
}
}
}
扩展
1)常用依赖
<dependencies>
//springmvc依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.25</version>
</dependency>
//jdbc依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.3.14</version>
</dependency>
//测试依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.14</version>
</dependency>
//mybatis依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.7</version>
</dependency>
//json解析依赖
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.0</version>
</dependency>
//servlet依赖
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
//mybatis依赖
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.6</version>
</dependency>
//mysql依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.46</version>
</dependency>
//jubit4的依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
//数据库的数据源依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.15</version>
</dependency>
</dependencies>
2)配置文件的优先级
不同文件位置、类型全局配置文件加载顺序:先加载的会覆盖后加载!!
1.src/main/resoures(最高)
2.src/main/resources/config目录下(第二)
若配置里面的值为相同的,后加载会覆盖了前加载的
3.在当前工程的目录里(优先级别第三)
4.在当工程目录的config目录里(优先级别第四)
1级:classpath: application.yml
2级: classpath: config/application.yml
3级:file :application.yml
4级: file : config/application.yml作用:
- 1级与2级用于系统开发阶段设置通用属性,3级常用于项目经理进行整体项目属性调控
- 3级与4级留做系统打包后设置通用属性,1级常用于运维经理进行线上整体项目部署方案调控
3)外置文件的配置
--spring.config.location=外置文件路径
4)配置敏感字段加密
Jasypt是一个Java库,可以使开发者不需太多操作来给Java项目添加基本加密功能,而且不需要知道加密原理。
Jasypt也即Java Simplified Encryption。默认使用的是PBEWITHMD5ANDDES算法。
引入依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
加密信息:
BasicTextEncryptor textEncryptor = new BasicTextEncryptor();
textEncryptor.setPassword("G0CvDz7oJn6"); //加密所需的salt(盐)
String username = textEncryptor.encrypt("root");//要加密的数据(数据库的用户名或密码)
解密:
在application.properties中设置盐,
jasypt.encryptor.password=,然后使用方法ENC()
jasypt:
encryptor:
password=: 123456?{} #配置salt
info:
username: ENC(Mru5gwjAluKSCxkLxdGIKw==) #将程序中的加密后的信息,通过ENC()进行解密
password: ENC(A1+yAABwLTuA5ynz7zUKLQ==) #将程序中的加密后的信息,通过ENC()进行解密
5)多环境配置
在yml中使用---来进行分割环境的配置
使用spring.profiles来配置环境的名称
使用spring.profiles.active类启用环境
如:
spring:
profiles:
active: dev #写上环境的名称。进行启用环境(这里的dev属于启动开发环境)
---
#开发环境
spring:
profiles: dev
server:
prot: 8090
---
#生产环境
spring:
profiles: pro
server:
prot: 8080
---
#测试环境
spring:
profiles: test
server:
prot: 8080
同时支持外部命令 java -jar xxx.jar --spring.profiles.active=dev
多文件yml版
将不同环境的开发配置拆分成不同的文件进行配置
然后在主配置文件中指定文件的后缀即可
properties文件开发也是一样
多环境分组管理
根据功能将配置文件的信息进行拆分,形成独立的配置文件名命名规则入下:
- application-devDB.yml
- application-devMVC.yml
使用group属性进行激活特定的环境下,对哪个文件进行加载(sprinboot2.4 版本后)
spring:
profiles:
active: dev #设定启用的环境
group:
"dev": devDB,devMVC #当启用的环境为 dev 时,连带 devDB,devMVC 文件一起加载
"pro": proDB,proMVC #当启用的环境为 pro 时,连带 proDB,proMVC 文件一起加载
多环境开发控制
将设定的环境通过核心配置文件pom.xml进行动态更改
xml配置
<profiles>
<profile>
<!--设定id进行区分-->
<id>env_dev</id>
<properties>
<!--设定环境-->
<project.active>dev</project.active>
</properties>
<!--设定启动环境-->
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<!--设定id进行区分-->
<id>env_pro</id>
<properties>
<!--设定环境-->
<project.active>pro</project.active>
</properties>
</profile>
</profiles>
yml读取:
spring:
profiles:
active: @project.active@ #设定启用的环境
group:
"dev": devDB,devMVC #当启用的环境为 dev 时,连带 devDB,devMVC 文件一起加载
"pro": proDB,proMVC #当启用的环境为 pro 时,连带 proDB,proMVC 文件一起加载
多环境配置开发
创建一个DBconnector数据库接口类,,分别创建3个数据库具体环境开发的配置类
在配置类当中使用注解 @Profile注解去指定全局开发环境配置后,所执行的具体配置
如:全局开发环境配置 application.properties
spring.profiles.active=prod #指定执行生产环境中的配置
生产环境配置 application-prod.properties
server.port=8991
prod.classname= 1
prod.url= 1
prod.name= 1
prod.pass= 1.1
生产环境数据库配置类 ProdDB
package com.hyl.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
// 添加配置类注解,让spring容器进行扫描
@Configuration
//添加指定加载的环境配置
@Profile("prod")
//添加配置文件的头信息,进行头信息的匹配,从而进行给予类的属性赋值
@ConfigurationProperties("book")
public class ProdDB implements DBconnector{
private String calssname;
private String url;
private String name;
private double pass;
// 省略gei和set和toString方法
@Override
public void configure() {
System.out.println("数据库环境配置:prod");
}
}
controller拦截
package com.hyl.controller;
import com.hyl.config.DBconnector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private DBconnector dBconnector;
@GetMapping("/helloDB")
public String go() {
System.out.println(dBconnector);
dBconnector.configure();
return "hello 666 springboot";
}
}
执行结果
在配置了全局的环境配置时,当服务器进行启动后,配置的 @Profile 注解的配置类,会根据执行的环境配置进行加载对应的配置文件,这样在不同的开发环境中,就可以进行灵活切换不同的环境配置
配置文件结构总览:
配置文件:
配置类
小结:
当maven与springboot同时对多环境进行控制时,以maven为主,springboot使用@..占位符进行读取配置属性值。
基于springboot读取maven配置属性的前提下,如果再 idea下测试工程时pom.xml每次过呢更新需要手动compile方可进行生效
6)整合JPA
JPA简介:
Jpa ( Java Per s is tence API) 是 Sun 官方提出的 Java 持久化规范。它为 Java 开发人员提供了一种对
象/关联映射工具来管理 Java 应用中的关系数据。它的出现主要是为了简化现有的持久化开发工作和整合
ORM 技术,结束现在 Hibernate,TopLink,JDO 等 ORM 框架各自为营的局面。JPA 被定义为EJB
(Enterprise JavaBeans) 3.0规范的一部分。
JPA 允许 POJO(Plain Old Java Objec t s)轻松地持久化,而不需要类来实现 EJB 2 CMP规范所需
的任何接口或方法。 JPA 还允许通过注解或 XML 定义对象的关系映射,定义 Java 类如何映射到关系数
据库表。 JPA 还定义了一个运行时 Entit yManager API,用于处理对象的查询和管理事务。 同时,JPA
定义了对象级查询语言 JPQL,以允许从数据库中查询对象,实现了对数据库的解耦合,提高了程序的可
移植性,而不具体依赖某一底层数据库。
Jpa 是一套规范,不是一套产品,那么像 Hibernate他们是一套产品,如果说这些产品实现了这个 Jpa
规范,那么我们就可以叫他们为 Jpa 的实现产品。
Spring Boot Jpa 是 Spring 基于 ORM 框架、Jpa 规范的基础上封装的一套 Jpa 应用框架,可使开发
者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!
引入依赖:
-- JPA依赖
<dependenc y>
<groupId>org. springframework .boot</groupId>
<artifac tId>spring-boot- s tarter-data-jpa</artifac tId>
</dependenc y>
--mysql依赖
<dependenc y>
<groupId>mysql</groupId>
<artifac tId>mysql-connector-java</artifac tId>
<scope>runtime</scope>
</dependenc y>
配置数据库信息
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: root
配置JPA信息
#默认根据url识别
#spring.datasour ce.driver- c las s -name=com.my sql.jdbc .Driver
#自动创建|更新|验证数据库表结构
spring:
jpa:
hibernate:
ddl-auto: update
#设置数据库引擎为InnoDB
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
#打印sql语句,方便调试
show- sql: true
使用方式:
创建实体类
-
实现序列化 注解@Entit y
-
声明数据库的主键和列
-
@GeneratedValue @Column
创建数据仓库接口类
-
继承jpa仓库 拥有通用数据接口
-
extends JpaRepository<Gues t,Long>
创建控制逻辑类
- 注入仓库,调用增删改查接口
7)多数据源及事务配置
配置多数据源
SqlSession 封装了JDBC连接,是数据库连接的客户端和服务端之间的一种会话。直接用来执行sql语句。
SqlSessionFactory是单个数据库映射关系经过编译后的内存镜像。每一个MyBati s的应用程序都以一个
对象的实例为核心,作为创建SqlSession的工厂。
mybati s框架主要是围绕着SqlSes s ionFac tor y进行的:
- (1) 定义一个Configuration对象,其中包含数据源、事务、mapper文件资源以及影响数据库行为属性设置
settings。
-
(2) 通过配置对象,则可以创建一个SqlSes s ionFac tor yBui lder对象。
-
(3) 通过 SqlSes s ionFac tor yBui lder 获得SqlSes s ionFac tor y 的实例。
-
(4) SqlSes s ionFac tor y 的实例可以获得操作数据的SqlSes s ion实例,通过这个实例对数据库进行操作。
依赖包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
</dependency>
</dependencies>
yml配置文件
server:
port: 8080 #配置端口
spring:
datasource:
first:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/first?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
second:
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/second?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
username: root
password: root
读取配置文件
在config目录下就进行添加配置类,进行将配置文件的数据进行返回
package com.hyl.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DBconfig {
//配置第一个数据源,默认级别最高
@Bean("first")
//指定数据源
@ConfigurationProperties("spring.datasource.first")
public DataSource dataSource1() {
//将数据源的配置信息进行返回
return DataSourceBuilder.create().build();
}
//配置第二个数据源
@Bean("second")
//指定数据源
@ConfigurationProperties("spring.datasource.second")
public DataSource dataSource2() {
//将数据源的配置信息进行返回
return DataSourceBuilder.create().build();
}
}
加载SqlSessionFactory与SqlSessionTemplate
在配置文件中编写了几个数据源就进行配置几个数据源类
通俗地讲,SqlSessionTemplate是Mybatis—Spring的核心,是用来代替默认Mybatis实现的DefaultSqlSessionFactory,也可以说是DefaultSqlSessionFactory的优化版,主要负责管理Mybatis的SqlSession,调用Mybatis的sql方法,SqlSessionTemplate是线程安全的,通过TransactionSynchronizationManager中的ThreadLocal保存线程对应的SqlSession,可以被多个Dao共享使用。
package com.hyl.config;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import javax.xml.transform.Source;
@Configuration
//指定对应的dao与自定义的sqlSessionFactory
@MapperScan(basePackages = "com.hyl.dao",sqlSessionFactoryRef = "sqlSessionFactory")
public class DBSqlSessionFactory {
@Autowired
@Qualifier("first")
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean factoryBean =new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate() throws Exception{
SqlSessionTemplate template=new SqlSessionTemplate(sqlSessionFactory());
return template;
}
}
package com.hyl.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
/**
* 配置第二个数据源
*/
@Configuration
//指定对应的dao与自定义的sqlSessionFactory
@MapperScan(basePackages = "com.hyl.dao2",sqlSessionFactoryRef = "sqlSessionFactory2")
public class DBSqlSessionFactory2 {
@Autowired
@Qualifier("second")
private DataSource dataSource;
@Bean
public SqlSessionFactory sqlSessionFactory2() throws Exception {
SqlSessionFactoryBean factoryBean =new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
@Bean
public SqlSessionTemplate sqlSessionTemplate2() throws Exception{
SqlSessionTemplate template=new SqlSessionTemplate(sqlSessionFactory2());
return template;
}
}
整体框架类
最后在service层中按照业务需求进行创建相应的数据源类进行调动资源操作即可
package com.hyl.service.impl;
import com.hyl.dao.GuestDao;
import com.hyl.dao2.GuestDao2;
import com.hyl.service.GuestService;
import org.springframework.beans.factory.annotation.Autowired;
public class GuestServiceImpl implements GuestService {
@Autowired
private GuestDao guestDao;
@Autowired
private GuestDao2 guestDao;
@Override
public void insert() {
guestDao.insert();
}
}
事务
//事务(Transac tion)是并发控制的基本单位。所谓的事务,它是一个操作序列,这些操作要么都执行,要
//么都不执行,它是一个不可分割的工作单位。它保证了用户操作的原子性 ( Atomi c it y )、一致性
//( Cons i s tenc y )、隔离性 ( I solation ) 和持久性 ( Durabi l i l y )。
Connection con = DriverManager.getConnection(); //创建连接
try{
con.setAutoCommit(false); //开启事务
...... //
con.commit(); //try的最后提交事务
} catch() {
con.rollback(); //其中任何一个操作出错都将回滚,所有操作都不成功
} finally{
con.close(); //关闭连接
}
声明式的事务管理是基于AOP的,本质上还是数据库对事务的支持。
在spring中可以通过====l注解的方式使用。优点是:非侵入式,业务逻辑不受事务管理
代码的污染。做到方法级别的事务回滚。使用前配置事务管理器。
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
支持的参数
属性 | 类型 | 描述 |
---|---|---|
propagation | enum: Propagation | 可选的事务传播行为设置 |
isolation | enum: Isolation | 可选的事务隔离级别设置 |
readOnly | boolean | 读写或只读事务,默认读写 |
timeout | int (in seconds granularity) | 事务超时时间设置 |
使用方式
直接在service层的方法中添加上@Transactiona注解即可
8)国际化
在 resources 文件下创建一个文件夹,名称为 i18n,即国际化的意思
然后在文件夹下创建相应的配置文件 xxx.properties
配置springboot核心配置文件,在里面指定国际化文件的路径
spring:
messages:
basename: i18n.index #配置文件的路径及文件的前缀
如:
创建默认的国际化文字文件
index.properties
index.title=呼吸
创建英文的文字文件
index_en_US.properties
index.title=huxi
创建中文的文字文件
index_zh_CN.properties
index.title=呼吸
thymeleaf读取
使用标签
<th:text="#{国际化属性名称}">
<th:text="#{index.title}">
9)角色权限与Spring Security
什么是权限
权限本质就是你是谁,你处于什么地位,你能做什么
在web应用开发中,安全无疑是十分重要的,选择Spring Security来保护web应用是一个非常好的选择。Spring Security 是spring项目之中的一个安全模块,可以非常方便与spring项目无缝集成。特别是在spring boot项目中加入spring security更是十分简单。本篇我们介绍spring security,以及spring security在web应用中的使用。
添加完依赖后,我们在进行启动项目时会出现一个拦截页面,要求我们进行安全认证
假设我们现在创建好了一个springboot
的web应用
@Controller
public class AppController {
@RequestMapping("/hello")
@ResponseBody
String home() {
return "Hello ,spring security!";
}
}
页面源码如下:
<html><head><title>Login Page</title></head><body onload='document.f.username.focus();'>
<h3>Login with Username and Password</h3><form name='f' action='/login' method='POST'>
<table>
<tr><td>User:</td><td><input type='text' name='username' value=''></td></tr>
<tr><td>Password:</td><td><input type='password' name='password'/></td></tr>
<tr><td colspan='2'><input name="submit" type="submit" value="Login"/></td></tr>
<input name="_csrf" type="hidden" value="635780a5-6853-4fcd-ba14-77db85dbd8bd" />
</table>
</form></body></html>
上面的html中有个form ,其中action="/login"
,这个/login
依然是spring security
提供的。form表单提交了三个数据:
- username 用户名
- password 密码
- _csrf CSRF保护方面的内容,暂时先不展开解释
为了登录系统,我们需要知道用户名密码,spring security 默认的用户名是user,spring security启动的时候会生成默认密码(在启动日志中可以看到)。本例,我们指定一个用户名密码,在配置文件中加入如下内容:
默认的登陆用户是user
默认的登陆密码我们可以去控制台看下日志。会看到如下信息:
2021-01-19 22:29:59.325 INFO 83794 --- [ main] .s.s.UserDetailsServiceAutoConfiguration :
Using generated security password: cc7d4605-75db-49aa-aa9a-b95953b5c1e8
2021-01-19 22:29:59.424 INFO 83794 --- [ main] o.s.s.web.DefaultSecurityFilterChain :
假设我们现在创建好了一个springboot
的web应用
自定义security配置
上面我们看到默认情况下,spring为我们提供了一个「httpBasic」模式的简单登陆页面,并在控制台输出了密码(这个密码每次启动都是不一样的)。如果我们想用自己的定义的账号密码,则需要改配置。如下:
我们新建一个类SecurityConfiguration
,并加入一些代码,如下所示:
@Configuration
//开启security
@EnableWebSecurity
//继承 WebSecurityConfigurerAdapter,重写两个方法,一个为授权方法,一个为认证方法
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
//授权
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.
inMemoryAuthentication()
.withUser("spring")
.password("{noop}123456").roles("USER");
}
}
上面的配置其实和默认情况的配置几乎一样,只是这里定义了一个用户spring
,和密码123456
。(说明:密码前面的{noop}表示的是指定的PasswordEncoder)此时我们启动项目,便可以使用spring这个用户及123456密码登录了
角色-资源 访问控制
通常情况下,我们需要实现“特定资源只能由特定角色访问”的功能。假设我们的系统有如下两个角色:
- ADMIN 可以访问所有资源
- USER 只能访问特定资源
现在我们给系统增加“/product” 代表商品信息方面的资源(USER可以访问);增加"/admin"代码管理员方面的资源(USER不能访问)。代码如下:
@Controller
@RequestMapping("/product")
public class ProductTestController {
@RequestMapping("/info")
@ResponseBody
public String productInfo(){
return " some product info ";
}
}
-------------------------------------------
@Controller
@RequestMapping("/admin")
public class AdminTestController {
@RequestMapping("/home")
@ResponseBody
public String productInfo(){
return " admin home page ";
}
}
现在我们希望实现:admin可以访问所有页面,而普通用户只能方法/product页面。配置如下
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//授权角色 USER 可访问的路径为 /product/**
//授权角色 ADMIN 可访问的路径为 /admin/**
http
.authorizeRequests()
.antMatchers("/product/**").hasRole("USER")
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//认证用户为 admin 密码为 adminpass,可执行的授权角色为 "ADMIN", "USER"
//认证用户为 spring 密码为 123456,可执行的授权角色为 "USER"
auth
.inMemoryAuthentication()
.withUser("admin").password("{noop}adminpass").roles("ADMIN", "USER")
.and()
.withUser("spring").password("{noop}123456").roles("USER");
}
}
这里,我们增加了 管理员(admin,密码adminpass),以及普通用户(spring,密码123456)
同时,我们增加了链接对应的角色配置。上面的配置,我们可以知道:
-
这里,我们增加了 管理员(admin,密码adminpass),以及普通用户(spring,密码123456)
-
同时,我们增加了链接对应的角色配置。上面的配置,我们可以知道:
下面来验证一下普通用户登录,重启项目,在浏览器中输入:http://localhost:8080/admin/home。同样,我们会到达登录页面,我们输入用户名spring
,密码也为123456
结果错误页面了,拒绝访问了,信息为
There was an unexpected error (type=Forbidden, status=403).
Forbidden
我们把浏览器中的uri修改成:/product/info
,结果访问成功。可以看到some product info
。说明 spring这个USER角色只能访问 product/** ,这个结果与我们预期一致。
再来验证一下管理员用户登录,重启浏览器之后,输入http://localhost:8080/admin/home。在登录页面中输入用户名admin,密码adminpass,提交之后,可以看到admin home page
,说明访问管理员资源了。我们再将浏览器uri修改成/product/info
,刷新之后,也能看到some product info
,说明 ADMIN角色的用户可以访问所有资源,这个也和我们的预期一致。
获取当前登录用户信息
上面我们实现了“资源 - 角色”的访问控制,效果和我们预期的一致,但是并不直观,我们不妨尝试在控制器中获取“当前登录用户”的信息,直接输出,看看效果。以/product/info为例,我们修改其代码,如下:
@RequestMapping("/info")
@ResponseBody
public String productInfo(){
String currentUser = "";
Object principl = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if(principl instanceof UserDetails) {
currentUser = ((UserDetails)principl).getUsername();
}else {
currentUser = principl.toString();
}
return " some product info,currentUser is: "+currentUser;
}
这里,我们通过SecurityContextHolder
来获取了用户信息,并拼接成字符串输出。重启项目,在浏览器访问http://localhost:8080/product/info. 使用 admin的身份登录,可以看到浏览器显示some product info,currentUser is: admin
。
小结
至此,我们已经对spring security
有了一个基本的认识了。了解了如何在项目中加入spring security,以及如何控制资源的角色访问控制。spring security原不止这么简单,我们才刚刚开始。为了能够更好的在实战中使用spring security 我们需要更深入的了解。下面我们先来了解spring security的一些核心概念。
Spring Security 核心组件
spring security核心组件有:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面分别介绍。
SecurityContext
安全上下文,用户通过Spring Security 的校验之后,验证信息存储在SecurityContext中,SecurityContext的接口定义如下:
public interface SecurityContext extends Serializable {
/**
* Obtains the currently authenticated principal, or an authentication request token.
*
* @return the <code>Authentication</code> or <code>null</code> if no authentication
* information is available
*/
Authentication getAuthentication();
/**
* Changes the currently authenticated principal, or removes the authentication
* information.
*
* @param authentication the new <code>Authentication</code> token, or
* <code>null</code> if no further authentication information should be stored
*/
void setAuthentication(Authentication authentication);
}
可以看到SecurityContext
接口只定义了两个方法,实际上其主要作用就是获取Authentication
对象。
SecurityContextHolder
SecurityContextHolder看名知义,是一个holder,用来hold住SecurityContext实例的。在典型的web应用程序中,用户登录一次,然后由其会话ID标识。服务器缓存持续时间会话的主体信息。在Spring Security中,在请求之间存储SecurityContext
的责任落在SecurityContextPersistenceFilter
上,默认情况下,该上下文将上下文存储为HTTP请求之间的HttpSession
属性。它会为每个请求恢复上下文SecurityContextHolder
,并且最重要的是,在请求完成时清除SecurityContextHolder
。SecurityContextHolder是一个类,他的功能方法都是静态的(static)。
SecurityContextHolder可以设置指定JVM策略(SecurityContext的存储策略),这个策略有三种:
- MODE_THREADLOCAL:SecurityContext 存储在线程中。
- MODE_INHERITABLETHREADLOCAL:SecurityContext 存储在线程中,但子线程可以获取到父线程中的 SecurityContext。
- MODE_GLOBAL:SecurityContext 在所有线程中都相同。
SecurityContextHolder默认使用MODE_THREADLOCAL模式,即存储在当前线程中。在spring security应用中,我们通常能看到类似如下的代码:
SecurityContextHolder.getContext().setAuthentication(token);
其作用就是存储当前认证信息。
Authentication
authentication 直译过来是“认证”的意思,在Spring Security 中Authentication用来表示当前用户是谁,一般来讲你可以理解为authentication就是一组用户名密码信息。Authentication也是一个接口,其定义如下:
public interface Authentication extends Principal, Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
Object getCredentials();
Object getDetails();
Object getPrincipal();
boolean isAuthenticated();
void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
接口有4个get方法,分别获取
Authorities
, 填充的是用户角色信息。Credentials
,直译,证书。填充的是密码。Details
,用户信息。- ,
Principal
直译,形容词是“主要的,最重要的”,名词是“负责人,资本,本金”。感觉很别扭,所以,还是不翻译了,直接用原词principal来表示这个概念,其填充的是用户名。
因此可以推断其实现类有这4个属性。这几个方法作用如下:
getAuthorities
: 获取用户权限,一般情况下获取到的是用户的角色信息。getCredentials
: 获取证明用户认证的信息,通常情况下获取到的是密码等信息。getDetails
: 获取用户的额外信息,(这部分信息可以是我们的用户表中的信息)getPrincipal
: 获取用户身份信息,在未认证的情况下获取到的是用户名,在已认证的情况下获取到的是 UserDetails (UserDetails也是一个接口,里边的方法有getUsername,getPassword等)。isAuthenticated
: 获取当前 Authentication 是否已认证。setAuthenticated
: 设置当前 Authentication 是否已认证(true or false)。
UserDetails
UserDetails,看命知义,是用户信息的意思。其存储的就是用户信息,其定义如下:
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
方法含义如下:
getAuthorites
:获取用户权限,本质上是用户的角色信息。getPassword
: 获取密码。getUserName
: 获取用户名。isAccountNonExpired
: 账户是否过期。isAccountNonLocked
: 账户是否被锁定。isCredentialsNonExpired
: 密码是否过期。isEnabled
: 账户是否可用。
UserDetailsService
提到了UserDetails
就必须得提到UserDetailsService
, UserDetailsService也是一个接口,且只有一个方法loadUserByUsername
,他可以用来获取UserDetails。
通常在spring security应用中,我们会自定义一个CustomUserDetailsService来实现UserDetailsService接口,并实现其public UserDetails loadUserByUsername(final String login);
方法。我们在实现loadUserByUsername
方法的时候,就可以通过查询数据库(或者是缓存、或者是其他的存储形式)来获取用户信息,然后组装成一个UserDetails
,(通常是一个org.springframework.security.core.userdetails.User
,它继承自UserDetails) 并返回。
在实现loadUserByUsername
方法的时候,如果我们通过查库没有查到相关记录,需要抛出一个异常来告诉spring security来“善后”。这个异常是org.springframework.security.core.userdetails.UsernameNotFoundException
。
AuthenticationManager
AuthenticationManager 是一个接口,它只有一个方法,接收参数为Authentication
,其定义如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校验Authentication
,如果验证失败会抛出AuthenticationException
异常。AuthenticationException
是一个抽象类,因此代码逻辑并不能实例化一个AuthenticationException异常并抛出,实际上抛出的异常通常是其实现类,如DisabledException
,LockedException
,BadCredentialsException
等。BadCredentialsException
可能会比较常见,即密码错误的时候。
小结
这里,我们只是简单的了解了spring security中有哪些东西,先混个脸熟。这里并不需要我们一下子全记住这些名词和概念。先大概看看,有个印象。
Spring Security的一些工作原理
在第一节中,我们通过在pom文件中增加spring-boot-starter-security
依赖,便使得我们的项目收到了spring security保护,又通过增加SecurityConfiguration
实现了一些安全配置,实现了链接资源的个性化访问控制。那么这是如何实现的呢?了解其原理,可以使我们使用起来得心应手。
spring security 在web应用中是基于filter的
在spring security的官方文档中,我们可以看到这么一句话:
Spring Security’s web infrastructure is based entirely on standard servlet filters.
我们可以得知,spring security 在web应用中是基于filter的。filter我们就很熟了,在没有struts,没有spring mvc之前,我们就是通过一个个servlet,一个个filter来实现业务功能的,通常我们会有多个filter,他们按序执行,一个执行完之后,调用filterChain中的下一个doFilter。Spring Security 在 Filter 中创建 Authentication 对象,并调用 AuthenticationManager 进行校验
spring security 维护了一个filter chain,chain中的每一个filter都具有特定的责任,并根据所需的服务在配置总添加。filter的顺序很重要,因为他们之间存在依赖关系。spring security中有如下filter(按顺序的):
- ChannelProcessingFilter,因为它可能需要重定向到不同的协议
- SecurityContextPersistenceFilter,可以在web请求开头的
SecurityContextHolder
中设置SecurityContext
,并且SecurityContext
的任何更改都可以复制到HttpSession
当web请求结束时(准备好与下一个web请求一起使用) - ConcurrentSessionFilter,
- 身份验证处理-UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等。以便
SecurityContextHolder
可以修改为包含有效的Authentication
请求令牌 - SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter,记住我服务处理
- AnonymousAuthenticationFilter,匿名身份处理,更新
SecurityContextHolder
- ExceptionTranslationFilter,获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的
AuthenticationEntryPoint
- FilterSecurityInterceptor,用于保护web URI并在访问被拒绝时引发异常
这里我们列举了几乎所有的spring security filter。正是这些filter完成了spring security的各种功能。目前我们只是知道了有这些filter,并不清楚他们是怎么集成到应用中的。在继续深入了解之前,我们需要了解一下DelegatingFilterProxy
。
DelegatingFilterProxy
DelegatingFilterProxy
是一个特殊的filter,存在于spring-web模块中。DelegatingFilterProxy
通过继承GenericFilterBean
使得自己变为了一个Filter(因为GenericFilterBean implements Filter)。它是一个Filter,其命名却以proxy
结尾。非常有意思,为了了解其功能,我们看一下它的使用配置:
<filter>
<filter-name>myFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>myFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
这个配置是我们使用web.xml配置Filter时做法。但是与普通的Filter不同的是DelegatingFilterProxy
并没有实际的过滤逻辑,他会尝试寻找filter-name
节点所配置的myFilter
,并将过滤行为委托给myFilter
来处理。这种方式能够利用Spring丰富的依赖注入工具和生命周期接口,因此DelegatingFilterProxy
提供了web.xml
与应用程序上下文之间的链接。非常有意思,可以慢慢体会。
spring security入口——springSecurityFilterChain
spring security的入口filter就是springSecurityFilterChain。在没有spring boot之前,我们要使用spring security的话,通常在web.xml中添加如下配置:
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
看到没,这里配置的是DelegatingFilterProxy
。有了上面的介绍之后,我们就知道,它实际上会去找到filter-name节点中的Filter——springSecurityFilterChain,并将实际的过滤工作交给springSecurityFilterChain
处理。
在使用spring boot之后,这一xml配置被Java类配置给代替了。我们前面在代码种使用过@EnableWebSecurity
注解,通过跟踪源码可以发现@EnableWebSecurity
会加载WebSecurityConfiguration
类,而WebSecurityConfiguration
类中就有创建springSecurityFilterChain
这个Filter的代码:
@Bean(name = {"springSecurityFilterChain"})
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = (WebSecurityConfigurerAdapter)this.objectObjectPostProcessor.postProcess(new WebSecurityConfigurerAdapter() {
});
this.webSecurity.apply(adapter);
}
return (Filter)this.webSecurity.build();
}
这里,我们介绍了spring security的入口——springSecurityFilterChain,也介绍了它的两种配置形式。但是,springSecurityFilterChain是谁,怎么起作用的,我们还不清楚,下面继续看。
FilterChainProxy 和SecurityFilterChain
在spring的官方文档中,我们可以发现这么一句话:
Spring Security’s web infrastructure should only be used by delegating to an instance of
FilterChainProxy
. The security filters should not be used by themselves.spring security 的web基础设施(上面介绍的那一堆filter)只能通过委托给
FilterChainProxy
实例的方式来使用。而不能直接使用那些安全filter。
这句话似乎透漏了一个信号,上面说的入口springSecurityFilterChain
其实就是FilterChainProxy
,如果不信,调试一下 代码也能发现,确实就是FilterChainProxy
。它的全路径名称是org.springframework.security.web.FilterChainProxy
。打开其源码,第一行注释是这样:
Delegates {@code Filter} requests to a list of Spring-managed filter beans.
所以,没错了。它就是DelegatingFilterProxy
要找的人,它就是DelegatingFilterProxy
要委托过滤任务的人。下面贴出其部分代码:
public class FilterChainProxy extends GenericFilterBean {
private List<SecurityFilterChain> filterChains;//
public FilterChainProxy(SecurityFilterChain chain) {
this(Arrays.asList(chain));
}
public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains;
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
doFilterInternal(request, response, chain);
}
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
private List<Filter> getFilters(HttpServletRequest request) {
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
}
可以看到,里边有个SecurityFilterChain
的集合。这个才是众多security filter藏身之处,doFilter的时候会从SecurityFilterChain取出第一个匹配的Filter集合并返回。
小结
说到这里,可能有点模糊了。这里小结一下,梳理一下。
- spring security 的核心是基于filter
- 入口filter是springSecurityFilterChain(它会被DelegatingFilterProxy委托来执行过滤任务)
- springSecurityFilterChain实际上是
FilterChainProxy
(一个filter) FilterChainProxy
里边有一个SecurityFilterChain
集合,doFIlter的时候会从其中取。
到这里,思路清楚多了,现在还不知道SecurityFilterChain
是怎么来的。下面介绍。
再说SecurityFilterChain
前面我们介绍了springSecurityFilterChain,它是由xml配置的,或者是由@EnableWebSecurity
注解的作用下初始化的(@Import({WebSecurityConfiguration.class))。具体是在WebSecurityConfiguration类中。上面我们贴过代码,你可以返回看,这里再次贴出删减版:
@Bean( name = {"springSecurityFilterChain"})
public Filter springSecurityFilterChain() throws Exception {
// 删除部分代码
return (Filter)this.webSecurity.build();
}
最后一行,发现webSecurity.build()
产生了FilterChainProxy
。因此,推断SecurityFilterChain就是webSecurity里边弄的。贴出源码:
public final class WebSecurity extends
AbstractConfiguredSecurityBuilder<Filter, WebSecurity> implements
SecurityBuilder<Filter>, ApplicationContextAware {
@Override
protected Filter performBuild() throws Exception {
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
// 我们要找的 securityFilterChains
List<SecurityFilterChain> securityFilterChains = new ArrayList<SecurityFilterChain>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
// 创建 FilterChainProxy ,传入securityFilterChains
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet();
Filter result = filterChainProxy;
postBuildAction.run();
return result;
}
}
至此,我们清楚了,spring security 是怎么在spring web应用中工作的了。具体的细节就是执行filter里的代码了,这里不再继续深入了。我们的目的是摸清楚他是怎么工作的,大致的脉路是怎样,目前整理的内容已经达到这个目的了。
Spring Security 的一些实战
下面开始一些实战使用spring security 的实例。依然依托开篇的例子,并在此基础上调整。
通过数据库查询,存储用户和角色实现安全认证
开篇的例子中,我们使用了内存用户角色来演示登录认证。但是实际项目我们肯定是通过数据库完成的。实际项目中,我们可能会有3张表:用户表,角色表,用户角色关联表。当然,不同的系统会有不同的设计,不一定非得是这样的三张表。本例演示的意义在于:如果我们想在已有项目中增加spring security的话,就需要调整登录了。主要是自定义UserDetailsService
,此外,可能还需要处理密码的问题,因为spring并不知道我们怎么加密用户登录密码的。这时,我们可能需要自定义PasswordEncoder
,下面也会提到。
添加spring-data-jpa , 创建数据表,并添加数据
继续完善开篇的项目,现在给项目添加spring-data-jpa
,并使用MySQL数据库。因此在POM文件中加入如下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
在application.properties文件中加入数据库连接信息:
spring.datasource.url=jdbc:mysql://localhost:3306/yourDB?useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=dbuser
spring.datasource.password=******
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
这里,为了简单方便演示,我们只创建一张表,字段如下:
@Entity
public class User implements java.io.Serializable{
@Id
@Column
private Long id;
@Column
private String login;
@Column
private String password;
@Column
private String role;
// 省略get set 等
}
然后我们添加2条数据,如下:
id | login | password | role |
---|---|---|---|
1 | spring | 123456 | USER |
2 | admin | adminpass | ADMIN,USER |
密码这里都是使用了NoOpPasswordEncoder
需在SecurityConfiguration
中加入配置,后面会贴。
自定义UserDetailsService
前面我们提到过,UserDetailsService,spring security在认证过程中需要查找用户,会调用UserDetailsService的loadUserByUsername方法得到一个UserDetails,下面我们来实现他。代码如下:
@Component("userDetailsService")
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
private GrantedAuthority DEFAULT_ROLE = new SimpleGrantedAuthority("USER");
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
// 1. 查询用户
User userFromDatabase = userRepository.findOneByLogin(login);
if (userFromDatabase == null) {
//log.warn("User: {} not found", login);
throw new UsernameNotFoundException("User " + login + " was not found in db");
}
// 2. 设置角色
List<GrantedAuthority> grantedAuthorities = new ArrayList<>();
String dbRole = userFromDatabase.getRole();
if(StringUtils.isNullOrEmpty(dbRole)){
grantedAuthorities.add(DEFAULT_ROLE);
}else{
String [] roles = dbRole.split(",");
for (String r : roles){
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(r);
grantedAuthorities.add(grantedAuthority);
}
}
return new org.springframework.security.core.userdetails.User(login,
userFromDatabase.getPassword(), grantedAuthorities);
}
}
这个方法做了2件事情,查询用户以及设置角色。现在我们来修改之前的SecurityConfiguration
配置, 加入CustomUserDetailsService
bean配置,如下:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/product/**").hasAuthority("USER")
.antMatchers("/admin/**").hasAuthority("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)// 设置自定义的userDetailsService
.passwordEncoder(passwordEncoder());
}
@Autowired
private UserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
//为了演示方便,我们使用NoOpPasswordEncoder(这个就是不加密)
return NoOpPasswordEncoder.getInstance();
}
}
验证效果
上面我们自定义了userDetailsService
,此时,spring security 在其作用流程中会调用,不出意外的话,重启系统,我们使用spring登录可以看到/product/info,但是不能看/admin/home。下面我们来重启项目验证一下。
先输入spring,以及错误密码,可以看到页面报错:Bad credentials
。再输入spring ,以及正确密码123456,结果:some product info,currentUser is: spring
再将浏览器链接修改为/admin/home,结果显示:
There was an unexpected error (type=Forbidden, status=403).
Forbidden
这与我们的预期完全一致,至此,我们已经在项目中加入了spring security,并且能够通过查询数据库用户,角色信息交给spring security完成认证授权。(当然了,这里密码使用了明文,只是为了演示方便)
spring security session 无状态
还记得我们开篇所举的例子吗?我们使用管理员账号密码登录之后,就可以访问/admin/home了,此时修改浏览器地址栏为/product/info之后刷新页面,仍然可以访问,说明认证状态被保持了;如果关闭浏览器重新输入/admin/home就会提示我们重新登录,这有点session的感觉。如果此时,我们将浏览器cookie禁用掉,你会发现登录之后自动跳转只会得到403,403是拒绝访问的意思,是没有权限的意思,说明这种情况下授权状态和session是挂钩的。即这时spring security使用了session。但是不是所有的系统都需要session,我们能让spring security不适用session吗?答案是可以!
使用spring security 我们可以准确控制session何时创建以及Spring Security如何与之交互:
- always – a session will always be created if one doesn’t already exist,没有session就创建。
- ifRequired – a session will be created only if required (default),如果需要就创建(默认)。
- never – the framework will never create a session itself but it will use one if it already exists
- stateless – no session will be created or used by Spring Security 不创建不使用session
这里,我们要关注的是 stateless,通常称为无状态的。为啥要关注这个stateless无状态的情况的呢?因为目前,我们的应用基本都是前后端分离的应用。比方说,你的一套java api是给react前端、安卓端、IOS端 调用的。这个时候你还提什么session啊,这时候我们需要的是无状态,通常以一种token的方式来交互。
spring security 配置stateless 的方式如下,依然是修改我们之前定义的SecurityConfiguration
:
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
前后端分离应用中自定义token整合spring security
上面我们提到了stateless,实际中我们的前后端分离项目都是无状态的,并没有登录状态保持,服务器通过客户端调用传递的token来识别调用者是谁。
通常我们的系统流程是这样的:
- 客户端(react前端,IOS,安卓)调用“登录接口”获得一个包含token的响应(通常是个JSON,如 {"token":"abcd","expires":1234567890})
- 客户端获取数据,并携带 token参数。
- 服务端根据token发现token过期/错误,返回"请登录"状态码
- 服务器发现token正常,并解析出来是A,返回A的数据。
- ……
如果我们想在spring security项目中使用自定义的token,那么我们需要思考下面的问题:
- 怎么发token(即怎么登录?)
- 发token怎么和spring security整合。
- spring security怎么根据token得到授权认证信息。
下面从登录发token开始,这里需要使用到UsernamePasswordAuthenticationToken
,以及SecurityContextHolder
,代码如下:
@RequestMapping(value = "/authenticate",method = RequestMethod.POST)
public Token authorize(@RequestParam String username, @RequestParam String password) {
// 1 创建UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken token
= new UsernamePasswordAuthenticationToken(username, password);
// 2 认证
Authentication authentication = this.authenticationManager.authenticate(token);
// 3 保存认证信息
SecurityContextHolder.getContext().setAuthentication(authentication);
// 4 加载UserDetails
UserDetails details = this.userDetailsService.loadUserByUsername(username);
// 5 生成自定义token
return tokenProvider.createToken(details);
}
@Inject
private AuthenticationManager authenticationManager;
上面代码中1,2,3,4步骤都是和spring security交互的。只有第5步是我们自己定义的,这里tokenProvider
就是我们系统中token的生成方式(这个完全是个性化的,通常是个加密串,通常可能会包含用户信息,过期时间等)。其中的Token
也是我们自定义的返回对象,其中包含token信息类似{"token":"abcd","expires":1234567890}
.
我们的tokenProvider
通常至少具有两个方法,即:生成token,验证token。大致如下:
public class TokenProvider {
private final String secretKey;
private final int tokenValidity;
public TokenProvider(String secretKey, int tokenValidity) {
this.secretKey = secretKey;
this.tokenValidity = tokenValidity;
}
// 生成token
public Token createToken(UserDetails userDetails) {
long expires = System.currentTimeMillis() + 1000L * tokenValidity;
String token = computeSignature(userDetails, expires);
return new Token(token, expires);
}
// 验证token
public boolean validateToken(String authToken, UserDetails userDetails) {
check token
return true or false;
}
// 从token中识别用户
public String getUserNameFromToken(String authToken) {
// ……
return login;
}
public String computeSignature(UserDetails userDetails, long expires) {
// 一些特有的信息组装 ,并结合某种加密活摘要算法
return 例如 something+"|"+something2+MD5(s);
}
}
至此,我们客户端可以通过调用http://host/context/authenticate
来获得一个token了,类似这样的:{"token":"abcd","expires":1234567890}
。那么下次请求的时候,我们带上 token=abcd
这个参数(或者也可以是自定义的请求头中)如何在spring security中复原“session”呢。我们需要一个filter:
public class MyTokenFilter extends GenericFilterBean {
private final Logger log = LoggerFactory.getLogger(XAuthTokenFilter.class);
private final static String XAUTH_TOKEN_HEADER_NAME = "my-auth-token";
private UserDetailsService detailsService;
private TokenProvider tokenProvider;
public XAuthTokenFilter(UserDetailsService detailsService, TokenProvider tokenProvider) {
this.detailsService = detailsService;
this.tokenProvider = tokenProvider;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
String authToken = httpServletRequest.getHeader(XAUTH_TOKEN_HEADER_NAME);
if (StringUtils.hasText(authToken)) {
// 从自定义tokenProvider中解析用户
String username = this.tokenProvider.getUserNameFromToken(authToken);
// 这里仍然是调用我们自定义的UserDetailsService,查库,检查用户名是否存在,
// 如果是伪造的token,可能DB中就找不到username这个人了,抛出异常,认证失败
UserDetails details = this.detailsService.loadUserByUsername(username);
if (this.tokenProvider.validateToken(authToken, details)) {
log.debug(" validateToken ok...");
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(details, details.getPassword(), details.getAuthorities());
// 这里还是上面见过的,存放认证信息,如果没有走这一步,下面的doFilter就会提示登录了
SecurityContextHolder.getContext().setAuthentication(token);
}
}
// 调用后续的Filter,如果上面的代码逻辑未能复原“session”,SecurityContext中没有想过信息,后面的流程会检测出"需要登录"
filterChain.doFilter(servletRequest, servletResponse);
} catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
目前为止,我们实现了自定义的token生成类,以及通过一个filter来拦截客户端请求,解析其中的token,复原无状态下的"session",让当前请求处理线程中具有认证授权数据,后面的业务逻辑才能执行。下面,我们需要将自定义的内容整合到spring security中。
首先编写一个类,继承SecurityConfigurerAdapter
:
public class MyAuthTokenConfigurer extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
private TokenProvider tokenProvider; // 我们之前自定义的 token功能类
private UserDetailsService detailsService;// 也是我实现的UserDetailsService
public MyAuthTokenConfigurer(UserDetailsService detailsService, TokenProvider tokenProvider) {
this.detailsService = detailsService;
this.tokenProvider = tokenProvider;
}
@Override
public void configure(HttpSecurity http) throws Exception {
MyAuthTokenFilter customFilter = new MyAuthTokenFilter(detailsService, tokenProvider);
http.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter.class);
}
}
在 SecurityConfiguration
配置类中加入如下内容:
// 增加方法
private MyAuthTokenConfigurer securityConfigurerAdapter() {
return new MyAuthTokenConfigurer(userDetailsService, tokenProvider);
}
// 依赖注入
@Inject
private UserDetailsService userDetailsService;
@Inject
private TokenProvider tokenProvider;
//方法修改 , 增加securityConfigurerAdapter
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
// .... 其他配置
.and()
.apply(securityConfigurerAdapter());// 这里增加securityConfigurerAdapter
}
至此我们就完成了无状态应用中token认证结合spring security。
总结
本篇内容,我们通过一个小例子开始介绍了如何给web应用引入spring security保护;在展示了http-basic验证之后,我们使用了内存用户实验了“角色-资源”访问控制;然后我们介绍了spring security的一些核心概念;之后我们介绍了spring security 是通过filter的形式在web应用中发生作用的,并列举了filter列表,介绍了入口filter,介绍了springboot是如何载入spring security入口filter的。最后我们通过两个实战中的例子展示了spring security的使用。
spring security 功能也非常强大,但是还是挺复杂的,本篇内容如有差错还请指出。
本文作者:HuXixxx
本文链接:https://www.cnblogs.com/huxinew/p/17211667.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步