Spring Boot
1. 介绍及第一个Spring Boot
Spring Boot是一个快速开发框架,可以迅速搭建出一套基于Spring框架体系的应用,是Spring Cloud的基础。
开启了各种自动装配,不需要编写各种配置文件,只需要引入相关依赖就行
约定大于配置
- 特点
- 不需要web.xml
- 不需要springmvc.xml
- 不需要tomcat,spring boot内嵌了tomcat
- 不需要配置JSON解析,支持rest架构
- 个性化配置非常简单
1.1 微服务
微服务是一个中架构风格,它要求我们在开发一个应用的时候,这个应用必须构建成一个系列小小服务的组合;可以通过http的方法进行互通。要说为微服务架构,先得说说我们以前的单体引用架构。
架构:MVC三层架构, MVVM ,微服务架构
业务:service:userService ===>模块!
SpringMVC,controller ===>提供接口!
单体应用架构
所谓单体引用(all in one)是指,我们将一个应用的中的所有应用的中的所有应用服务封装在一个应用中,无论是erp。crm或是其他系统,都吧数据库访问,web访问,等等功能方到一个war包内
- 好处是:易于开发测试,,部署起来十分方便,当需要拓展的时候,只需要将war复制多份,然后放在多个服务器上,在做负载均衡就可以
- 缺点是:单体应用架构的缺点是,哪怕我要修改非常小的地方,我都需要停掉整个服务,重新打包,部署这个应用war包,特别是对于一个大型应用,我们不可能吧所有的内容都放在一个应用里,我们如何维护,如何分工都是问题
微服务架构
all in one 的架构方式没我们把所有的功能单元放在一个应用里面。整个应用部署在服务器上,如果负载能力不行,我们将整个应用水平赋值,进行扩展,之后负载均衡
所谓微服务架构,就是打破之前的all in one的架构方式,把每个功能元素独立出来,把独立出来的功能元素动态组合,需要的功能元素才组合在一起,需要时间多一些,可以整合多个功能的元素。所以微服务架构是对功能元素进行复制,
好处:
- 节省,调用资源
- 每个功能元素的服务都是一个可替换的可独立升级的软件代码,
详细阐述了什么是微服务:https://www.martinfowler.com/articles/microservices.html
中文版:https://www.cnblogs.com/liuning8023/p/4493156.html
http: rpc
用户下单: controller! 1000ms
消息队列:
仓库冻结:资金冻结,验证,购买成功,仓库数量减少,仓库解冻,资金解冻 10 s
如何构建微服务
一个大型系统的微服务架构,就像一个复杂交织,神经网络,每一个神经元就像是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统,查缓存,连数据库,浏览页面,结账,支付等服务都是一个一个独立的功能服务,都被微化了,他们作为一个个微服务共同构建了一个庞大的系统,如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:
- 构建一个个功能独立的微服务应用单元,可以使用SpringBoot,可以帮助我们快速的构建一个应用;
- 大型分布式网络服务的调用,这部分由Springcloud来完成,实现分布式;
- 在分布式中间,进行流式数据计算,批处理,我们有spring cloud data flow。
- spring 为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案
高内聚,低耦合,
1.2使用
官方提供了一个快速生成网站!idea集成了这个网站!
- 可以在官网直接下载,导入idea开发(官网在哪里)
- 直接使用idea创建一个SpringBoot项目(常用)
- 创建maven工程,自己引入依赖
1.自己创建maven
-
创建maven工程,导入依赖
<!-- 继承父包--> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> </parent> <dependencies> <!-- web启动jar包--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.5.2</version> </dependency> </dependencies>
-
创建student实体类
@Data @AllArgsConstructor public class Student { private long id; private String name; private int age; }
-
创建Repository
注解@RequestBody接收的参数是来自requestBody中,即请求体。一般用于处理非 Content-Type: application/x-www-form-urlencoded编码格式的数据,比如:application/json、application/xml等类型的数据。
就application/json类型的数据而言,使用注解@RequestBody可以将body里面所有的json数据传到后端,后端再进行解析。
GET请求中,因为没有HttpEntity,所以@RequestBody并不适用。
POST请求中,通过HttpEntity传递的参数,必须要在请求头中声明数据的类型Content-Type,SpringMVC通过使用
在GET请求中,不能使用@RequestBody
- 在POST请求,可以使用@RequestBody和@RequestParam,但是如果使用@RequestBody,对于参数转化 的配置必须统一。
- 可以使用多个@RequestParam获取数据,@RequestBody不可以
public interface StudentRepository { Collection<Student> findAll(); Student findById(long id); void saveOrUpdate(Student student); void deleteById(long id); }
@Repository public class StudentRepositoryImpl implements StudentRepository { private static Map<Long, Student> studentMap; static { studentMap = new HashMap<>(); studentMap.put(1L, new Student(1L, "西游记", 45)); studentMap.put(2L, new Student(2L, "西厢记", 444)); studentMap.put(3L, new Student(3L, "西使记", 465)); } @Override public Collection<Student> findAll() { return studentMap.values(); } @Override public Student findById(long id) { return studentMap.get(id); } @Override public void saveOrUpdate(Student student) { studentMap.put(student.getId(), student); } @Override public void deleteById(long id) { studentMap.remove(id); } }
-
创建controller
@RestController public class StudentController { @Autowired private StudentRepository studentRepository; @GetMapping("/findAll") public Collection<Student> findAll() { return studentRepository.findAll(); } @GetMapping("/findById/{id}") public Student findById(@PathVariable("id") long id) { return studentRepository.findById(id); } @PostMapping("/save") //RequestBody 将参数变为请求体,可以用于接收json数据 public void save(@RequestBody Student student) { studentRepository.saveOrUpdate(student); } @PutMapping("/update") public void update(@RequestBody Student student) { studentRepository.saveOrUpdate(student); } @DeleteMapping("/delete") public void delete(long id) { studentRepository.deleteById(id); } }
-
创建启动类:启动类的级别要比其他类高才行,一般创建在和mvc包同级目录
// 整个spring boot 工厂的入口,启动类 @SpringBootApplication public class Application { public static void main(String[] args) { // 运行整个spring应用 SpringApplication.run(Application.class,args); } }
-
springboot配置文件:application.yml名字必须这个
# 配置或更改端口 server: port: 110
2.官网配置
https://start.spring.io/
在官网进行配置,然后下载jar包导入到IDEA,选择Maven
3.使用IDEA集成的Spring Boot创建
-
新建项目,选择Spring Initializr
然后根据要求和配置进行设置,然后下一步
然后就是选择要使用的依赖,也可以不选,进去里面自己写
-
其他就是很正常的了
2. 原理初探
2.1 自动装配:
pom.xml:
- Spring-boot-dependencies:核心依赖在父项目
- 我们在写或者引入一些Springboot依赖的时候,不需要指定版本
启动器
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
-
启动器:就是Springboot的启动场景
-
比如:spring-boot-starter-web,他就会帮我们自动当如相关依赖
-
springboot会将所有的功能场景,都变成一个个的启动器
-
我们要使用什么功能,就只需要找到对应的启动器就可以了
2.2 主程序:
@SpringBootApplication
public class Spring01HelloworldApplication {
public static void main(String[] args) {
SpringApplication.run(Spring01HelloworldApplication.class, args);
}
}
-
注解:
-
@SpringBootConfiguration springboot的 配置 @Configuration spring配置类 @Component spring组件 @EnableAutoConfiguration 自动装配 @AutoConfigurationPackage:spring自动配置包 @Import({AutoConfigurationImportSelector.class}) 自动装配包 注册 @Import({Registrar.class}) 配置导入选择 //获取所有的配置 List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
-
加载项目自动装配文件
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}
自动配置的核心文件
META-INF/spring.factories
springboot所有的自动配置都是在启动的时候扫描并加载:META-INF/spring.factories
所有的自动配置类都在这里面,但是不一定生效,判断条件是否成立,只要导入了对应的start,就有对应的启动器,有了启动器我们自动装配就会生效,然后就配置成功了!
2.3 分析自动装配的原理:
-
启动的时候加载住配置类,开启了自动装配功能,
@EnableAutoConfiguration
-
@EnableAutoConfiguration
的作用:-
利用AutoConfigurationImportSelector给容器导入组件,导入什么组件呢?
-
查看selectImports()方法的内容,他返回了一个autoConfigurationEntry,
getAutoConfigurationEntry
获取组件实体, -
通过
getCandidateConfigurations()
方法来加载候选配置, -
loadFactoryNames()
获取配置名字,从META-INF/spring.factories
这个配置文件中,加载全部配置存放到Properties中;返回封装好的结果,其中的加载参数就是EnableAutoConfiguration; -
protected Class<?> getSpringFactoriesLoaderFactoryClass() { return EnableAutoConfiguration.class; }
-
过程描述就是,把
META-INF/spring.factories
把里面所有的EnableAutoConfiguration值加入到容器中
-
2.4 @ConditionalOn
SpringBoot有大量的配置,有的生效有的不生效,他通过这个注解来判断是否符合调减,符合条件就生效这个配置,我们可以通过自动装配注解来实现对应的自动装配。
2.5 如何配置的
在我们的配置文件中,能配置的东西都存在一个规律
他们一定会有个文件叫xxxProperties ,
- xxxProperties:绑定配置文件,@ConfigurationProperties(prefix = “server”, ignoreUnknownFields = true)
- xxxAutoConfiguration:自动装配默认值
- 了解这两个知识,我们就可以自定义配置了
2.6 如何看看我们的自动配置类是否生效
在配置文件中添加:debug:true,在我们启动类之后会看到那些生效那些没生效,会以类似日志的方式输出
Positive matches:
已经自动装配并且生效的
negative matches:
没有生效的
总结:
- SpringBoot启动会加载大量的配置类,用来自动装配
- 如果我们要使用功能,要去查看功能是狗在SpringBoot默认写好的自动装配类中
- 自动装配类中配置了很多组件,只要我们用的组件存在就不需要手动配置了
- 容器中自动配置类添加属性的时候,会从properties类中获取某些属性,我们只需要在配置文件中指定这些属性的值即可
- xxxProperties:绑定配置文件,封装配置文件中的相关属性
- xxxAutoConfiguration:自动装配类,给容器添加组件
2.7 主程序的运行:
执行的步骤:
- 推断应用类型是普通的java项目还是web项目
- 查找并加载所有可用的初始化器,设置到initializers实行中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并且设置main方法定义类,找到运行的主类
3. yml语法
这个语法对空格要求及其严格
# 注释
# 赋值
name: Tzeao
# 对象
student:
name: Tzeao
age: 15
# 行内写法
student: {name: Tzeao,age: 15}
# 数组
pats:
- pig
- cat
- dog
# 行内
pats: [cat,pig]
2.1.赋值
yaml可以直接给实体类赋值,也可以使用spEL表达式:${}
package com.tzeao.pojo;
import org.springframework.stereotype.Component;
@Component
public class Pig {
private String name;
private int age;
public Pig(String name, int age) {
this.name = name;
this.age = age;
}
public Pig() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Pig{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package com.tzeao.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Component
//这个注解就是就是用来绑定 yml配置的 prefix前缀同配置文件的第一个绑定
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Pig pig;
public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Pig pig) {
this.name = name;
this.age = age;
this.happy = happy;
this.birth = birth;
this.maps = maps;
this.lists = lists;
this.pig = pig;
}
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getHappy() {
return happy;
}
public void setHappy(Boolean happy) {
this.happy = happy;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Map<String, Object> getMaps() {
return maps;
}
public void setMaps(Map<String, Object> maps) {
this.maps = maps;
}
public List<Object> getLists() {
return lists;
}
public void setLists(List<Object> lists) {
this.lists = lists;
}
public Pig getPig() {
return pig;
}
public void setPig(Pig pig) {
this.pig = pig;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", happy=" + happy +
", birth=" + birth +
", maps=" + maps +
", lists=" + lists +
", pig=" + pig +
'}';
}
}
person:
#还可以用${}进行赋值
#name: Tzeao
#使用uuid函数
name: ${random.uuid}
age: ${random.int}
happy: true
birth: 2021/07/19
maps: {K: 2,Q: 3}
lists:
- code
- 吃饭
- 睡觉
- 打豆豆
pig:
#如果person.age不存在,那么就选择aaa
name: ${person.age:aaa}_aa
age: 55
将配置文件中每个属性的值,映射这个组件中;
告诉SpringBoot将奔雷所有的属性和配置文件中相关的配置进行绑定
参数perfix = “person” 将person组件(类)和yml中person的值对应绑定
指定配置文件:properties文件
@PropertySource(value = "classpath:application.properties")
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
2.2. 松散语法
就是yaml写的是last-name,对应的是实体类属性就是lastName, ==-==后面跟着的字母默认要大写
2.3. JSR303校验
可以在字段是增加一层过滤器验证,可以保证数据的合法性
需要导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
如果是由yml进行赋值操作的话,那么就要在实体类上加上**@Validated**用于开启数据验证,如果是进行web的看
下面==数据校验==笔记
2.4. 多环境切换及配置文件位置
1、配置文件位置
file代表在项目名称下面,与.idea/src为同一级目录
classpath代表在资源目录里面
file:./config/
file:./
classpath:/config/
classpath:/
优先级:
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置,相同覆盖;
2、多环境切换
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;
例如:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
同一优先级下有多个配置文件,如何快速切换
就是在主的配置文件下使用
- properties
# springboot 的多环境配置: 可以选择激活那一个配置文件
spring.profiles.active: dev # 配置的名称
- yaml
server:
port: 8080
# springboot 的多环境配置: 可以选择激活那一个配置文件
spring:
profiles:
active: dev
# --- 代表分割模块
---
server:
port: 8081
# 取名字
spring:
profiles: dev
---
server:
port: 8082
# 取名字
spring:
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!
4. 静态资源导入原理
静态资源映射规则
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
第一种
addResourceHandlers 添加资源处理
读一下源代码:比如所有的 /webjars/ , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
} else {
this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
registration.addResourceLocations(new Resource[]{resource});
}
});
}
}
- 什么是webjars?
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
第二种静态资源映射规则
去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:
以下四个存放路径都可以被识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
优先级:2>3>4
如果自定义了静态资源路径,那么上面的就全部失效
spring:
mvc:
static-path-pattern:
5. 定制启动首页
第一种:
在public静态资源文件夹里面写index.html,这个可以直接访问
第二种:
写在Template里面,这样就需要controller跳转才能访问
第三种:
直接在资源文件里面写index.html
还有自定义title的图标,这个2.0以上的版本没了
可以由前端来解决
6. SringMVC配置和扩展
官网:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
6.1 配置
如果需要自定义一些功能,就写一个组件,然后交给spring boot就行,它会自动装配,在配置文件里面写配置就可以替代默认的配置,也可以自己继承一些类来实现
创建一个config文件,里面专门写这些扩展
6.2 扩展
我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyMvcConfig;
//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 浏览器发送/test , 就会跳转到test页面;
registry.addViewController("/test").setViewName("test");
}
}
1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个类WebMvcAutoConfigurationAdapter
2、这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)
3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration
这个父类中有这样一段代码:
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 从容器中获取所有的webmvcConfigurer
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
}
}
}
4、我们可以在这个类中去寻找一个我们刚才设置的viewController当做参考,发现它调用了一个
protected void addViewControllers(ViewControllerRegistry registry) {
this.configurers.addViewControllers(registry);
}
5、我们点进去看一下
public void addViewControllers(ViewControllerRegistry registry) {
Iterator var2 = this.delegates.iterator();
while(var2.hasNext()) {
// 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addViewControllers(registry);
}
}
就是实现你要扩展的接口,然后把功能写进去就行
也可以在配置文件里面写自定义的格式,如日期的,配置完成后,springboot的格式化器就会就行转换
6.3 全面接管SpringMVC
全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
==@EnableWebMvc。==可以让所有的spring boot的默认配置全部失效
7. 整合JSP
-
创建Maven项目并添加web支持
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.5.2</version> </parent> <dependencies> <dependency> <!-- web组件--> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 整合JSP--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency> <!-- 处理JSP--> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> </dependency> <!-- jstl--> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>
-
创建配置文件:application.yml
# 配置端口号,默认8080 server: port: 0000 # 设置视图解析器 spring: mvc: view: prefix: / suffix: .jsp
-
创建Handler
@Controller @RequestMapping("/hello") public class HelloHandler { @RequestMapping("/index") public String index(){ System.out.println("index.."); return "index"; } }
-
index.jsp
<html> <body> <h2>Hello World!</h2> </body> </html>
-
其他修改与ssm无异,如mvc结构之类
8. 整合Thymeleaf模板引擎
Thymeleaf官网:https://www.thymeleaf.org/
springboot 可以结合Thymeleaf模板来整合HTML,使用原生的HTML作为视图。
Thymeleaf模板是面向Web和独立环境的java模板引擎,能够处理HTML,XML,JavaScript.CSS等等资源
使用:
1、pom.xml添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
# 添加nekohtml的目的:为了非严格遵守w3c编程规范
# 比如 <input /> 必须要有结束符 否则编译报错
<dependency>
<groupId>net.sourceforge.nekohtml</groupId>
<artifactId>nekohtml</artifactId>
<version>1.9.22</version>
</dependency>
2、application.yml
# 配置或更改端口
server:
port: 8080
# 配置thymeleaf的视图解析器
spring:
thymeleaf:
#在资源文件里面创建一个专门存放HTML代码的文件夹,引入路径
prefix: classpath:/temlates/
suffix: .html
# 模型
mode: HTML5
#编码
encoding: UTF-8
3、Handler
@Controller
@RequestMapping("/index")
public class IndexHandler {
@RequestMapping("/index")
public String index() {
System.out.println("index..");
return "index";
}
}
4、在资源文件里面创建的temlates文件里面创建html文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
其他操作不变,还是可以使用Model,除了HTML取数据有所改变
需要在html里面引入命名空间
<!DOCTYPE html>
<!--需要引入-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
内容
</html>
<!--循环,类似java的forEach-->
<tr th:each="student:${list}">
<!--th:text就是要写入的文本信息-->
<td th:text="${student.id}"></td>
<td th:text="${student.name}"></td>
<td th:text="${student.age}"></td>
</tr>
如果希望客户端可以直接访问HTML资源,那么就将这些资源放置在static文件里面,否则必须通过Handler的后台映射才可以访问静态资源
2.1 常用语法
th:名称
标准表达式:
${...}
变量表达式,Variable Expressions
@{...}
链接表达式,Link URL Expressions
#{...}
消息表达式,Message Expressions
~{...}
代码块表达式,Fragment Expressions
*{...}
选择变量表达式,Selection Variable Expressions
-
赋值、拼接
<!--结合el表达式--> <!--取值--> <p th:text="${name}"></p> <!--拼接--> <p th:text="'你真的是'+${name}"></p> <p th:text="|你真的是${name}|"></p>
-
条件判断:if/unless
th:if-表示添加成立时显示内容,th:unless-表示条件不成立时显示内容
<!--判断--> <p th:if="${name == true}" th:text="if条件成立"></p> <p th:unless="${name != true}" th:text="unless条件成立"></p>
-
循环
<table> <tr> <th>学生ID</th> <th>学生姓名</th> <th>学生年龄</th> </tr> <tr th:each="student:${list}"> <td th:text="${student.id}"></td> <td th:text="${student.name}"></td> <td th:text="${student.age}"></td> </tr> </table>
<table> <tr> <th>学生ID</th> <th>学生姓名</th> <th>学生年龄</th> </tr> <!-- 和Java的增强for循环一样 stat用于取位置信息--> <tr th:each="student,stat:${list}"> <!-- 0开始--> <td th:text="${stat.index}"></td> <!-- 1开始--> <td th:text="${stat.count}"></td> <td th:text="${student.id}"></td> <td th:text="${student.name}"></td> <td th:text="${student.age}"></td> </tr> </table>
stat 是状态变量,属性:
- index 集合中元素的index
- count 集合中元素的count
- size 集合的大小
- current 当前迭代变量
- even/odd 当前迭代是否为偶数/奇数
- first 当前迭代的元素是否是第一个
- last 当前迭代的元素是否是最后一个
th:style 动态设置样式
th:style="'background-color:'+@{${stat.odd}?'#006400'}"
-
URL
Thymeleaf 对于URL的处理是通过@{…}进行处理的,结合th:href,th:src
<a th:href="@{https://www.baidu.com}">跳转</a>
<!-- 获取参数并携带参数-->
<a th:href="@{....../{name}(name=${test})}">跳转</a>
注意,使用th的都要由后台进行映射跳转,不然是解析不了的
<!--后台传地址-->
<img th:src="${src}" />
<!--直接写地址-->
<img th:src="@{https://tse1-mm.cn.bing.net/th?id=OIP-C.mIbpDwFNRitWDc7Jwye9twHaEK&w=168&h=100&c=8&rs=1&qlt=90&o=6&dpr=1.34&pid=3.1&rm=2}" >
-
三目运算
<!--三目运算 gt大于 lt小于 --> <p th:text="${age gt 20? '好':'不好'}"></p>
主要是这个:${age gt 20? ‘好’:‘不好’}
gt 大于
ge 大于等于
eq 等于
lt 小于
le 小于等于
ne 不等于
-
switch
<!--switch 要使用嵌套关系--> <div th:switch="${age}"> <p th:case="10">女</p> <p th:case="20">男</p> <p th:case="*">都不是</p> </div>
2. 2 基本对象
- #ctx:上下文对象
- #vars:上下文变量
- #locale:区域对象
- #request:HttpServletRequest对象
- #response:HttpServletResponse对象
- #seesion:HttpSession对象
- #servletContext:ServletContext对象
<p th:text="${#request.getAttribute('test')}"></p>
<p th:text="${#session.getAttribute('a')}"></p>
<p th:text="${#locale.country}"></p>
@RequestMapping("/index6")
public String index6(HttpServletRequest request) {
request.setAttribute("test","ssssss");
request.getSession().setAttribute("a",855);
return "index";
}
2.3 内嵌对象
可以通过#直接访问
- dates:获取日期
- calendars:java.util.Calendar
- numbers:格式数字
- string:java.util.String
- objects:java.util.Object
- bools:对布尔求值的方法
- array:操作数组
- list:操作集合
- sets:操作集合
- maps:操作集合
<!--格式化时间-->
<p th:text="${#dates.format(date,'yyyy-MM-dd HH:mm:sss')}"></p>
<!--创建当前时间,精确到天-->
<p th:text="${#dates.createToday()}"></p>
<!--创建当前时间,精确到秒-->
<p th:text="${#dates.createNow()}"></p>
<!--判断是否为空-->
<p th:text="${#strings.isEmpty(name)}"></p>
<!--判断List是否为空-->
<p th:text="${#lists.isEmpty(users)}"></p>
<!--输出字符串⻓度-->
<p th:text="${#strings.length(name)}"></p>
<!--拼接字符串-->
<p th:text="${#strings.concat(name,name,name)}"></p>
<!--创建⾃定义字符串-->
<p th:text="${#strings.randomAlphanumeric(count)}"></p>
@GetMapping("/util")
public String util(Model model){
model.addAttribute("name","zhangsan");
model.addAttribute("users",new ArrayList<>());
model.addAttribute("count",22)
;
model.addAttribute("date",new Date());
return "index";
}
9.数据校验
需要引入validator依赖的,不过Spring Boot自带
@Valid 数据校验注解
空检查 @Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格. @NotEmpty 检查约束元素是否为NULL或者是EMPTY. Booelan检查 @AssertTrue 验证 Boolean 对象是否为 true @AssertFalse 验证 Boolean 对象是否为 false 长度检查 @Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) Validates that the annotated string is between min and max included. 日期检查 @Past 验证 Date 和 Calendar 对象是否在当前时间之前 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 @Pattern 验证 String 对象是否符合正则表达式的规则 数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null @Min 验证 Number 和 String 对象是否大等于指定的值 @Max 验证 Number 和 String 对象是否小等于指定的值 @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度 @Digits 验证 Number 和 String 的构成是否合法 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。 @Range(min=, max=) 检查数字是否介于min和max之间. @Range(min=10000,max=50000,message="range.bean.wage") private BigDecimal wage; @Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证) @CreditCardNumber 信用卡验证 @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 @ScriptAssert(lang= ,script=, alias=) @URL(protocol=,host=, port=,regexp=, flags=)
@Data
public class User {
@NotNull(message = "id不能为空")
private Long id;
@NotEmpty(message = "姓名不能为空")
@Length(min = 2,message = "姓名不能小于两位")
private String name;
@Min(value = 16,message = "年龄必须大于16")
private int age;
}
@RequestMapping("/validator")
// BindingResult错误信息存到这个里面 @Valid 校验
public void validatorUser(@Valid User user, BindingResult bindingResult) {
System.out.println(user);
// 是否错误
if (bindingResult.hasErrors()) {
// ObjectError专门获取错误信息的
List<ObjectError> list = bindingResult.getAllErrors();
for (ObjectError error : list){
System.out.println(error.getCode()+error.getDefaultMessage()+"/");
}
}
}
10. 整合JDBC
10.1 第一种使用JdbcTemplate模板
这种xxxTtemplate的是Spring Boot写好的模板,拿来就可以使用
这个语句帮忙自动设置了事务管理
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
application.yml
# 配置或更改端口
server:
port: 9090
# 配置thymeleaf的视图解析器
spring:
thymeleaf:
#在资源文件里面创建一个专门存放HTML代码的文件夹,引入路径
prefix: classpath:/temlates/
suffix: .html
# 模型
mode: HTML5
#编码
encoding: UTF-8
#数据源
datasource:
url: jdbc:mysql://loaclhost:3306/test?useSSL=true&useUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
#驱动
driver-class-name: com.mysql.cj.jdbc.Driver
实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
@NotNull(message = "id不能为空")
private int id;
@NotEmpty(message = "姓名不能为空")
@Length(min = 2,message = "姓名不能小于两位")
private String name;
@Min(value = 30,message = "年龄必须大于16")
private double score;
}
repository
public interface UserRepository {
List<User> findAll();
User findById(int id);
void save(User user);
void update(User user);
void deleteById(int id);
}
@Repository
public class UserRepositoryImpl implements UserRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public List<User> findAll() {
// BeanPropertyRowMapper接收数据库查询到的数据,括号里面填模板
return jdbcTemplate.query("select * from test.table_name", new BeanPropertyRowMapper<>(User.class));
}
@Override
public User findById(int id) {
// 占位符的赋值,在这里是要使用数组,来进行赋值的,所以要new一个数组
return jdbcTemplate.queryForObject("select * from test.table_name where id = ?", new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
}
@Override
public void save(User user) {
//这个的传参 因为是动态参数 所以可以直接给
jdbcTemplate.update("insert into table_name(id, name, score) VALUES (?,?,?)", user.getId(), user.getName(), user.getScore());
}
@Override
public void update(User user) {
jdbcTemplate.update("update table_name set name = ?,score = ? where id = ?", user.getName(), user.getScore(), user.getId());
}
@Override
public void deleteById(int id) {
jdbcTemplate.update("delete from table_name where id = ?", id);
}
}
Handler
@RestController
@RequestMapping("/user")
public class UserHandler {
@Autowired
private UserRepository userRepository;
@RequestMapping("/findAll")
public List<User> findAll(){
return userRepository.findAll();
}
@RequestMapping("/findById/{id}")
public User findById(@PathVariable("id") int id){
return userRepository.findById(id);
}
@RequestMapping("/update")
public void update(@RequestBody User user){
userRepository.update(user);
}
@RequestMapping("/save")
public void save(@RequestBody User user){
userRepository.save(user);
}
@RequestMapping("/delete")
public void delete(int id){
userRepository.deleteById(id);
}
}
有可能遇到的错误:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘form test.table_name’ at line 1] with root cause
sql语句好好检查,这个是语句错了,特别是当表名报错时
com.mysql.cj.jdbc.exceptions.CommunicationsException
一般是数据源配置有问题 着重检查url
第二种使用原始的JDBC步骤
application.yam配置没变
// 把数据源注册过来 class com.zaxxer.hikari.HikariDataSource
@Autowired
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
// 查看默认的数据源
System.out.println(dataSource.getClass());
// 获取数据库连接
Connection connection = dataSource.getConnection();
// 传入sql语句
PreparedStatement preparedStatement = connection.prepareStatement();
//查询之类
preparedStatement.executeQuery();
//要关闭所有流
connection.close();
}
10.3 自定义数据源
上面使用的都是Spring Boot 自带的数据源,号称最快的数据源
Druid
1. 使用
-
导入依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>
-
切换为自定义的数据源,application.yml
spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mbtest?useSSL=true&useUnicode=true&characterEncoding=utf-8 #切换数据源 type: com.alibaba.druid.pool.DruidDataSource
-
上面两个步骤就完成了自定义的数据源了
-
设置Druid的自带配置
#Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
要使Druid的配置生效,就要进行属性绑定,
-
创建配置文件夹
-
创建Druid配置绑定类
package com.tzeao.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.Filter; import javax.sql.DataSource; import java.util.HashMap; @Configuration public class DruidConfig { // 将配置文件中的Druid配置,绑定到DruidDataSource,并且返回 @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource druidDataSource() { return new DruidDataSource(); } @Bean //后台监控 ;相当于web.xml //因为springboot内置了servlet容器,所以没有web.xml,替代方法就是ServletRegistrationBean注册到bean public ServletRegistrationBean statViewServlet() { // 代码是固定的 ServletRegistrationBean<StatViewServlet> bena = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*"); // 配置登录的账号密码 这个名称也是固定的:initParameters HashMap<String, String> initParameters = new HashMap<>(); // 增加配置 key是固定的 initParameters.put("loginUsername", "admin"); initParameters.put("loginPassword", "123456"); // 允许谁访问 initParameters.put("allow", "");// 为空就是都可以访问,localhost 就是本机访问,也可以写具体的用户访问 // 禁止谁访问 initParameters.put("tzeao", "1222333");//key可以随便写,后面感谢禁止访问的IP地址 bena.setInitParameters(initParameters);//设置初始化参数 return bena; } // filter @Bean public FilterRegistrationBean webServletFilter() { FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(); bean.setFilter(new WebStatFilter()); // 过滤的请求 HashMap<String, String> initParameters = new HashMap<>(); // 不拦截 initParameters.put("exclusions", "*.js,*.css,/druid/*"); bean.setInitParameters(initParameters); return bean; } }
-
-
全部配置好就能使用了,进入Druid后台,路径"/druid"
2. 使用官方starter方式
-
导入依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.6</version> </dependency>
-
分析自动配置
-
扩展配置项 spring.datasource.druid
-
DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
-
DruidStatViewServletConfiguration.class, 监控页的配置:spring.datasource.druid.stat-view-servlet;默认开启
-
DruidWebStatFilterConfiguration.class, web监控配置;spring.datasource.druid.web-stat-filter;默认开启
-
DruidFilterConfiguration.class}) 所有Druid自己filter的配置
-
-
配置文件
spring: datasource: url: jdbc:mysql://localhost:3306/db_account username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false
11. 整合Mybatis
这个是半自动化的,事务也已经帮忙实现了
依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mybatis 和spring boot 整合的组件-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
</dependencies>
创建数据库表
CREATE table student(
id INT PRIMARY KEY auto_increment,
name VARCHAR(11),
score DOUBLE,
birthday date
)
创建实体类
@Data
public class Student {
private int id;
private String name;
private double score;
private Date birthday;
}
创建repository
public interface StudentRepository {
List<Student> findAll();
Student findById(int id);
void save(Student student);
void updateById(Student student);
void deleteById(int id);
}
创建mapper.xml:放在资源目录下的mapper文件里面
<?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">
<!-- namespace 对应一个mapper接口-->
<mapper namespace="com.tzeao.repository.StudentRepository">
<select id="findAll" resultType="Student">
select *
from mbtest.student;
</select>
<select id="findById" resultType="Student" parameterType="_int">
select *
from mbtest.student
where id = #{id};
</select>
<update id="updateById" parameterType="student">
update mbtest.student
set name = #{name},
score=#{score},
birthday=#{birthday}
where id = #{id}
</update>
<insert id="save" parameterType="Student">
insert into mbtest.student(name, score, birthday)
values (#{name}, #{score}, #{birthday});
</insert>
<delete id="deleteById" parameterType="_int">
delete
from mbtest.student
where id = #{id};
</delete>
</mapper>
创建StudentHandler 注入 StudentRepository
@RestController
public class StudentHandler {
@Autowired
public StudentRepository studentRepository;
@RequestMapping("/findAll")
public List<Student> findAll() {
return studentRepository.findAll();
}
@RequestMapping("/findById/{id}")
public Student findById(@PathVariable("id") int id) {
return studentRepository.findById(id);
}
@RequestMapping("/save")
public void save(@RequestBodyStudent student) {
studentRepository.save(student);
}
@RequestMapping("/update")
public void update(@RequestBody Student student) {
studentRepository.updateById(student);
}
@RequestMapping("/delete/{id}")
public void delete(@PathVariable("id") int id) {
studentRepository.deleteById(id);
}
}
创建yml配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/mbtest?useSSL=true&userUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis:
#mapper文件路径
mapper-locations: classpath:/mapper/*xml
# 别名
type-aliases-package: com.tzeao.entity
Consider defining a bean of type ‘com.tzeao.repository.StudentRepository’ in your configuration.
这个错误是因为springboot没有办法扫描到这个接口,需要在启动类加注解@MapperScan
启动类
@SpringBootApplication
//扫描mybatis的接口类StudentRepository,也可以在mapper/dao里面的接口直接添加@Mapper注解
@MapperScan("com.tzeao.repository")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
12. 整合Spring Date JPA
JPA是一套规范,本身不能使用
Hibernate 框架就是JPA的一个实现
Spring Date JPA 不是对JPA规范的具体实现,本身就是一个抽象层
1、依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
2、创建实体类
@Data
//因为ORM是全自动的,所以不需要使用sql语句和xml文件,只需要使用注解直接和数据表进行映射就行
@Entity//映射表
public class Student {
// 然后每一个字段也都要一一映射起来
@Id // 注释主键的
@GeneratedValue(strategy = GenerationType.IDENTITY) //,提供了主键的生成策略,为一个实体生成一个唯一标识的主键
private int id;
@Column //与表形成映射关系
private String name;
@Column
private double score;
@Column
private Date birthday;
}
@Table 声明此对象映射到数据库的数据表,通过它可以为实体指定表(talbe),目录(Catalog)和schema的名字。该注释不是必须的,如果没有则系统使用默认值(实体的短类名)。
@Id 声明此属性为主键。
@Column 声明该属性与数据库字段的映射关系。
@Entity表明这是一个实体类,要与数据库做orm映射,默认表的名字就是类名,表中的字段就是类中的属性。
@GeneratedValue提供了主键的生成策略,存在的意义主要就是为一个实体生成一个唯一标识的主键(JPA要求每一个实体Entity,必须有且只有一个主键)
@GeneratedValue注解有两个属性,分别是strategy和generator,其中generator属性的值是一个字符串,默认为"",其声明了主键生成器的名称(对应于同名的主键生成器@SequenceGenerator和@TableGenerator)。
有四种主键生成策略:
- GenerationType.TABLE, 使用一个特定的数据库表格来保存主键。
- GenerationType.SEQUENCE, 根据底层数据库的序列来生成主键,条件是数据库支持序列。 这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)。
- GenerationType.IDENTITY,主键由数据库自动生成(主要是支持自动增长的数据库,如mysql)
- GenerationType.AUTO,主键由程序控制,也是GenerationType的默认值。
3、创建Repository接口
// 不需要直接写增删改查的方法和sql语句的xml文件了,直接继承JpaRepository接口,这个接口已经帮忙写好了,因为是泛型所以需要传实体类和主键的类型就行
public interface StudentRepository extends JpaRepository<Student, Integer> {
}
4、创建Handler,注入StudentRepository
@RestController
public class StudentHandler {
@Autowired
private StudentRepository studentRepository;
@RequestMapping("/findAll")
private List<Student> findAll() {
return studentRepository.findAll();
}
@RequestMapping("/findById/{id}")
private Student findById(@PathVariable("id") int id) {
return studentRepository.findById(id).get();
}
@RequestMapping("/save")
private void save(@RequestBody Student student) {
studentRepository.save(student);
}
@RequestMapping("/update")
private void update(@RequestBody Student student) {
studentRepository.save(student);
}
@RequestMapping("/delete/{id}")
private void findAll(@PathVariable("id") int id) {
studentRepository.deleteById(id);
}
}
也可以在StudentRepository里面自己定义方法,但是命名要符合规范,不能乱写
application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/mbtest?useSSL=true&userUnicode=true&characterEncoding=UTF-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置jpa
jpa:
#输出sql
show-sql: true
#进行格式化
properties:
hibernate:
format_sql: true
启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
13. 整合MongoDB
是非关系型数据库,但是非常像关系型数据库,是一个面向文档的数据库
MongoDB是一款为web应用程序和互联网基础设施设计的数据库管理系统。
它的格式是BSON,类似JSON的格式
1、依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
2、创建实体类
@Data
//映射 非关系型数据库的表
@Document(collation = "name")
public class Student {
@Id
private String id;
@Field(value = "student_age") //映射普通字段
private Integer age;
@Field(value = "student_name")
private String name;
}
3、创建Repository接口
@Repository
public interface StudentRepository extends MongoRepository<Student,String> {
}
4、创建Handler
@RestController
public class StudentHandler {
@Autowired
private StudentRepository studentRepository;
@RequestMapping("/findAll")
public List<Student> findAll() {
return studentRepository.findAll();
}
@RequestMapping("/findById/{id}")
public Student findById(@PathVariable("id") String id) {
return studentRepository.findById(id).get();
}
@RequestMapping("/save")
public void save(Student student) {
studentRepository.insert(student);
}
@RequestMapping("/update")
public void update(Student student) {
studentRepository.insert(student);
}
@RequestMapping("/delete/{id}")
public void delete(@PathVariable("id") String id) {
studentRepository.deleteById(id);
}
}
5、application.yml
spring:
data:
mongodb:
# 本机IP
host: 127.0.0.1
#端口
port: 27017
# 数据库
database: mongo
# 默认没有账号密码
#spring.data.mongodb.username=
#spring.data.mongodb.password=
6、启动类
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
14. 整合Redis
看情况
redis环境搭建
1、阿里云按量付费redis。经典网络
2、申请redis的公网连接地址
3、修改白名单 允许0.0.0.0/0 访问
这个是非关系型数据库
以键值对的形式
基于key-value形式的数据字典,结构⾮常简单,没有数据表的概念,直接⽤键值对的形式完成数据的管理,Redis⽀持5种数据类型:字符串列表集合有序集合哈希
配置好时的测试:
// 这就是之前 RedisAutoConfiguration 源码中的 Bean
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
/** redisTemplate 操作不同的数据类型,API 和 Redis 中的是一样的
* opsForValue 类似于 Redis 中的 String
* opsForList 类似于 Redis 中的 List
* opsForSet 类似于 Redis 中的 Set
* opsForHash 类似于 Redis 中的 Hash
* opsForZSet 类似于 Redis 中的 ZSet
* opsForGeo 类似于 Redis 中的 Geospatial
* opsForHyperLogLog 类似于 Redis 中的 HyperLogLog
*/
// 除了基本的操作,常用的命令都可以直接通过redisTemplate操作,比如事务……
// 和数据库相关的操作都需要通过连接操作
//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//connection.flushDb();
redisTemplate.opsForValue().set("key", "呵呵");
System.out.println(redisTemplate.opsForValue().get("key"));
}
8.1 入门
redis对象都是要序列化的,实体类继承序列化接口就行,连接池建议使用自带的
依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
2、创建实体类,实现序列化接⼝,否则⽆法存⼊Redis数据库。
package com.southwind.entity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class Student implements Serializable {
private Integer id;
private String name;
private Double score;
private Date birthday;
}
3、创建控制器。
@RestController
public class StudentHandler {
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/set")
public void set(@RequestBody Student student){
redisTemplate.opsForValue().set("student",student);
}
@GetMapping("/get/{key}")
public Student get(@PathVariable("key") String key){
return (Student) redisTemplate.opsForValue().get(key);}
@DeleteMapping("/delete/{key}")
public boolean delete(@PathVariable("key") String key){
redisTemplate.delete(key);
return redisTemplate.hasKey(key);
}
}
4、创建配置⽂件application.yml
spring:
redis:
# 远程IP地址 ,默认是本地
host: localhost
# 端口号 这个是默认的6379
port: 6379
#连接池
# lettuce:
# pool:
# max-wait:
5、创建启动类
package com.tzeao;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringbootRedisApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootRedisApplication.class, args);
}
}
2. 自定义RedisTemplate
@Configuration
public class RedisConfig {
/**
* 编写自定义的 redisTemplate
* 这是一个比较固定的模板
*/
@Bean
@SuppressWarnings("all")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
// 为了开发方便,直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// Json 配置序列化
// 使用 jackson 解析任意的对象
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 使用 objectMapper 进行转义
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// Hash 的 key 采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 采用 jackson 的序列化方式
template.setValueSerializer(jackson2JsonRedisSerializer);
// Hash 的 value 采用 jackson 的序列化方式
template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 把所有的配置 set 进 template
template.afterPropertiesSet();
return template;
}
}
这个类配置完成,就可以再其他类就行调用了,因为有一个系统的类和这个一样,可能会调用到那一个取,所以可以使用注解来指定调用@Qualifier(“redisTemplate”)
3. Redis工具类
@Component
public final class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// =============================common============================
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
// ============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
* @param key 键
* @param delta 要增加几(大于0)
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
* @param key 键
* @param delta 要减少几(小于0)
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
// ================================Map=================================
/**
* HashGet
* @param key 键 不能为null
* @param item 项 不能为null
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
* @param key 键
* @param map 对应多个键值
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
// ============================set=============================
/**
* 根据key获取Set中的所有值
* @param key 键
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0)
expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
// ===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
* @param key 键
* @param value 值
* @param time 时间(秒)
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0)
expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
}
15. 整合Spring Security
安全框架:Shiro、Spring Security
在网站设计之初就应该考虑
用于认证、授权
1、依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、创建Handler
@Controller
public class HelloHandler {
@GetMapping("/index")
public String index() {
return "index ";
}
}
3、创建HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello</h1>
</body>
</html>
4、创建application.yml
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
5、创建启动类Application
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
启动会有登录页面:
用户名:user
密码:在控制台随机生成的
自定义用户名-密码
spring:
thymeleaf:
prefix: classpath:/templates/
suffix: .html
# 配置登陆页面,自定义
security:
user:
# 用户名
name: Tzeao
#密码
password: 123456
8.1 权限管理
用户认证和授权:
package com.tzeao.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Controller
@EnableWebSecurity
public class MySecurity extends WebSecurityConfigurerAdapter {
// 根据官方的来,选择http
// 方法都是链式结构
// 请求授权
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,功能页只有对应权限的人才能访问
// authorizeRequests 认证请求
// antMatchers 访问地址
// permitAll 所有人都可以访问
// hasRole 指定角色访问 hasAllRole 所有角色可以访问
http.authorizeRequests().antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 没有权限就跳转到登录页面
http.formLogin();
}
// 认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// inMemoryAuthentication:内存认证·
// jdbcAuthentication 连接数据库认证
}
}
把权限给角色,然后角色把权限给用户
定义两个HTML资源:index.html、admin.html,同时定义两个⻆⾊ADMIN和USER,ADMIN拥有访问index.html和admin.html的权限,USER只有访问index.html的权限。
6、创建SecurityConfig类。
//通过继承这个类来拥有管理的权限
@Controller
//用于启用Web安全的注解
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加账户和角色
// 设置密码编码,添加账户名称,添加密码,添加角色,通过and()继续添加角色
auth.inMemoryAuthentication().passwordEncoder(new MyPasswordEncoder()).withUser("Tzeao").password(new MyPasswordEncoder().encode("123")).roles("USER").and().withUser("admin").password(new MyPasswordEncoder().encode("123")).roles("ADMIN", "USER");
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//设置角色和权限的关系
// 访问的路径 启动角色权限判断 如果一个路径有多个角色可以访问那么就使用access()进行添加角色
http.authorizeRequests().antMatchers("/admin").hasRole("ADMIN")
.antMatchers("/index").access("hasRole('ADMIN') or hasRole('USER')")
// 表示上面的请求都要进行安全验证
.anyRequest().authenticated().and()
// 自定义登陆页面
.formLogin().loginPage("/login")
// 表示登录相关的请求不需要验证
.permitAll()
// 登录退出也不需要验证
.and().logout().permitAll()
// 防止网站攻击
.and().csrf().disable();
}
}
编码类
//实现PasswordEncoder
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
// 判断传过来的密码是否匹配
return s.equals(charSequence.toString());
}
}
7、修改Handler
@Controller
public class HelloHandler {
@GetMapping("/index")
public String index() {
return "index";
}
@GetMapping("/admin")
public String admin() {
return "admin";
}
@GetMapping("/login")
public String login() {
return "login";
}
}
HTML
login
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form th:action="@{/login}" method="post">
⽤户名:
<input type="text" name="username"/><br/>
密码:
<input type="text" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
</body>
</html>
index
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Hello World</h1>
<form action="/logout" method="post">
<input type="submit" value="退出"/>
</form>
</body>
</html>
admin
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>后台管理系统</h1>
<form action="/logout" method="post">
<input type="submit" value="退出"/>
</form>
</body>
</html>
8.2 注销和权限控制
-
注销,然后在前端写跳转到"/logout"
// 注销 http.logout();
-
根据每一个角色的权限来显示页面
- 需要用到Thymeleaf和security的整合包
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
-
HTML导入命名空间
xmlns:sec="http://www.thymeleaf.org/extras/spring-security"
-
<!--如果未登录 显示登陆页面--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div> <!--如果已经登陆 显示用户名+注销--> <div sec:authorize="isAuthenticated()"> <a class="item"> 用户名:<span sec:authentication="name"></span> <!-- 权限:<span sec:authentication="principal.authorities"></span>--> </a> <a class="item" th:href="@{/logout}"> <i class="address card icon"></i> 注销 </a>
-
sec:authorize="isAuthenticated()"
用于判断该用户是否被认证,也就是是否登录
sec:authorize="hasRole('vip1')"
只允许对应角色访问
16. Shiro
Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。
Shiro 官网地址:http://shiro.apache.org/
Shiro三个核心组件:
-
Subject:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。Subject代表了当前用户的安全操作,SecurityManager则管理所有用户的安全操作。
-
SecurityManager:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。
-
Realm: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
Shiro内置了可以连接大量安全数据源(又名目录)的Realm,如LDAP、关系数据库(JDBC)、类似INI的文本配置资源以及属性文件等。如果缺省的Realm不能满足需求,你还可以插入代表自定义数据源的自己的Realm实现(继承 AuthorizingRealm 类)。
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
16.1 简单入门
创建一个简单的Maven类
导入相关依赖:官方依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<resources>
<resource>
<directory>${basedir}/src/main/webapp</directory>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
<include>**/*.properties</include>
<include>**/*.yml</include>
</includes>
</resource>
</resources>
Quickstart类:
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main(String[] args) {
// The easiest way to create a Shiro SecurityManager with configured
// realms, users, roles and permissions is to use the simple INI config.
// We'll do that by using a factory that can ingest a .ini file and
// return a SecurityManager instance:
// Use the shiro.ini file at the root of the classpath
// (file: and url: prefixes load from files and urls respectively):
DefaultSecurityManager securityManager = new DefaultSecurityManager();
IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
securityManager.setRealm(iniRealm);
// for this simple example quickstart, make the SecurityManager
// accessible as a JVM singleton. Most applications wouldn't do this
// and instead rely on their container configuration or web.xml for
// webapps. That is outside the scope of this simple quickstart, so
// we'll just do the bare minimum so you can continue to get a feel
// for things.
SecurityUtils.setSecurityManager(securityManager);
// Now that a simple Shiro environment is set up, let's see what you can do:
// get the currently executing user:
Subject currentUser = SecurityUtils.getSubject();
// Do some stuff with a Session (no need for a web or EJB container!!!)
Session session = currentUser.getSession();
session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");
if (value.equals("aValue")) {
log.info("Retrieved the correct value! [" + value + "]");
}
// let's login the current user so we can check against roles and permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//say who they are:
//print their identifying principal (in this case, a username):
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
//test a role:
if (currentUser.hasRole("schwartz")) {
log.info("May the Schwartz be with you!");
} else {
log.info("Hello, mere mortal.");
}
//test a typed permission (not instance-level)
if (currentUser.isPermitted("lightsaber:wield")) {
log.info("You may use a lightsaber ring. Use it wisely.");
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only.");
}
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//all done - log out!
currentUser.logout();
System.exit(0);
}
}
log4j:
#将等级为DEBUG的日志信息输出到console和file这两个目的地,console和file的定义在下面的代码
log4j.rootLogger=DEBUG,console,file
#控制台输出的相关设置
log4j.appender.console = org.apache.log4j.ConsoleAppender
log4j.appender.console.Target = System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout = org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=【%c】-%m%n
#文件输出的相关设置
log4j.appender.file = org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/kuang.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=【%p】【%d{yy-MM-dd}】【%c】%m%n
#日志输出级别
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
shiro.ini:
[main]
listener = org.apache.shiro.config.event.LoggingBeanEventListener
shiro.loginUrl = /login.jsp
shiro.postOnlyLogout = true
sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager
sessionManager.sessionIdCookie.sameSite = NONE
securityManager.sessionManager = $sessionManager
securityManager.sessionManager.sessionIdUrlRewritingEnabled = false
# We need to set the cipherKey, if you want the rememberMe cookie to work after restarting or on multiple nodes.
# YOU MUST SET THIS TO A UNIQUE STRING
securityManager.rememberMeManager.cipherKey = kPH+bIxk5D2deZiIxcaaaA==
[users]
# format: username = password, role1, role2, ..., roleN
root = secret,admin
guest = guest,guest
presidentskroob = 12345,president
darkhelmet = ludicrousspeed,darklord,schwartz
lonestarr = vespa,goodguy,schwartz
[roles]
# format: roleName = permission1, permission2, ..., permissionN
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
[urls]
# The /login.jsp is not restricted to authenticated users (otherwise no one could log in!), but
# the 'authc' filter must still be specified for it so it can process that url's
# login submissions. It is 'smart' enough to allow those requests through as specified by the
# shiro.loginUrl above.
/login.jsp = authc
/logout = logout
/account/** = authc
/remoting/** = authc, roles[b2bClient], perms["remote:invoke:lan,wan"]
16.2 SpringBoot 整合Shiro
-
创建一个springboot 项目
-
导入依赖
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency>
-
编写配置类,在config文件里面
@Configuration public class ShiroConfig { // 3.ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) { ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); // 绑定Manager,设置安全管理器 bean.setSecurityManager(defaultWebSecurityManager); return bean; } // 2.DefaultWebSecurityManager @Bean // @Qualifier绑定下面创建的realm方法 public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); // 关联realm manager.setRealm(userRealm); return manager; } // 1.创建realm对象,需要自定义 @Bean public UserRealm userRealm() { return new UserRealm(); } }
-
自定义Realm
public class UserRealm extends AuthorizingRealm { // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了认证"); return null; } }
-
创建测试使用的html页面,简简单单就行
-
编写Controller
package com.tzeao.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model){ model.addAttribute("msg","Hello World"); return "index"; } @RequestMapping("/add") public String add(Model model){ model.addAttribute("msg","Hello World"); return "user/add"; } @RequestMapping("/update") public String update(Model model){ model.addAttribute("msg","Hello World"); return "user/update"; } }
-
在index.html.编写跳转
<!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>首页</h1> <span th:text="${msg}"></span> <a th:href="@{/add}">add</a>|<a th:href="@{/update}">up</a> </body> </html>
16.2 登录拦截
// 添加shiro内置的过滤器
/*
anon: 无需认证就可以访问
authc: 必须认证才能访问
user: 必须用有 记住我 功能才能使用
perms: 拥有某个资源的权限才能访问
role: 拥有某个角色的权限才能访问
*/
Map<String, String> linkedMap = new LinkedHashMap<>();
// 编写 ,路径为controller里面的路径
linkedMap.put("/add", "anon");
linkedMap.put("/update", "authc");
bean.setFilterChainDefinitionMap(linkedMap);
// 没有权限时会跳转 登录页面
bean.setLoginUrl("/tologin");
这个是在ShiroConfig类里面的getShiroFilterFactoryBean方法里面配置
16.3 用户认证
认证是在自定义realm里面写
controller:
@RequestMapping("/login")
public String login(Model model,String username,String password){
// 获取用户信息
Subject subject = SecurityUtils.getSubject();
// 封装用户的登录数据,加密为令牌
UsernamePasswordToken token = new UsernamePasswordToken(username,password);
// 执行登录的方法
try {
subject.login(token);
return "index";
} catch (UnknownAccountException e) {//用户不存在
model.addAttribute("msg","用户不存在");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}
}
userRealm:
package com.tzeao.config;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
public class UserRealm extends AuthorizingRealm {
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权");
return null;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证");
// 用户数据在数据库取,这里伪造一下
String name = "root";
String password = "123";
// 这个是通过全局获取到controller里面用户填写的token
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 判断
if (!token.getUsername().equals(name)){
return null;//抛出异常 UnknownAccountException
}
// 密码认证
return new SimpleAuthenticationInfo("",password,"");
}
}
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<span th:text="${msg}"></span>
<form th:action="@{/login}" method="post">
用户名<input type="text" name="username"><br>
密码<input type="password" name="password"><br>
<input type="submit" value="提交">
</form>
</body>
</html>
16.4 整合MyBatis
-
导入依赖,Mybatis,mysql,数据源可以使用默认的,这里使用Druid
-
实体类
@Data public class Student { private int id; private String name; private String pwd; }
-
yaml配置文件
spring: datasource: username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis?useSSL=true&useUnicode=true&characterEncoding=utf-8 #切换数据源 type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 mybatis: type-aliases-package: com.tzeao.pojo mapper-locations: classpath:mapper/*.xml
-
mapper
@Repository public interface UserMapper { Student query(String name); }
<?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"> <!-- namespace 对应一个mapper接口--> <mapper namespace="com.tzeao.mapper.UserMapper"> <select id="query" resultType="Student" parameterType="String"> select * from mybatis.user where name = #{name}; </select> </mapper>
-
service
public interface UserService { Student query(String name); }
@Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public Student query(String name) { return userMapper.query(name); } }
-
userRealm
package com.tzeao.config; import com.tzeao.pojo.Student; import com.tzeao.service.UserService; import com.tzeao.service.UserServiceImpl; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthenticatingRealm; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; public class UserRealm extends AuthorizingRealm { @Autowired UserServiceImpl userService; // 授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权"); return null; } // 认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行了认证"); // 用户数据在数据库取,这里伪造一下 // 这个是通过全局获取到controller里面用户填写的token UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; // 通过数据库查询, Student user = userService.query(token.getUsername()); if (user==null){ return null; } // 密码认证 可以进行加密操作 return new SimpleAuthenticationInfo("",user.getPwd(),""); } }
16.5 请求授权
ShiroConfig
package com.tzeao.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
// 3.ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("getDefaultWebSecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
// 绑定Manager,设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
// 添加shiro内置的过滤器
/*
anon: 无需认证就可以访问
authc: 必须认证才能访问
user: 必须用有 记住我 功能才能使用
perms: 拥有某个资源的权限才能访问
role: 拥有某个角色的权限才能访问
*/
Map<String, String> linkedMap = new LinkedHashMap<>();
// 编写 ,路径为controller里面的路径
linkedMap.put("/add", "anon");
// linkedMap.put("/update", "authc");
// 授权
linkedMap.put("/update","perms[user:update]");
// linkedMap.put("/toBrowse","perms[user:browse]");
bean.setFilterChainDefinitionMap(linkedMap);
// 没有权限时会跳转 登录页面
bean.setLoginUrl("/tologin");
// 没有授权是的页面
// bean.setUnauthorizedUrl("/noauth");
return bean;
}
// 2.DefaultWebSecurityManager
@Bean
// @Qualifier绑定下面创建的realm方法
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
// 关联realm
manager.setRealm(userRealm);
return manager;
}
// 1.创建realm对象,需要自定义
@Bean
public UserRealm userRealm() {
return new UserRealm();
}
}
UserRealm:
package com.tzeao.config;
import com.tzeao.pojo.Student;
import com.tzeao.service.UserService;
import com.tzeao.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthenticatingRealm;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
public class UserRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userService;
// 授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//设置所有用户都拥有的权限
// info.addStringPermission("user:browse");
Subject subject = SecurityUtils.getSubject();
// 在数据库得到每一个用户拥有的权限,要得到这个用户信息,密码认证那里第一个参数要传值
Student currentUser = (Student) subject.getPrincipal();
//设置当前用户权限,获取数据库用户的权限,然后赋值
info.addStringPermission(currentUser.getPerm());
return info;
}
// 认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了认证");
// 用户数据在数据库取,这里伪造一下
// 这个是通过全局获取到controller里面用户填写的token
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 通过数据库查询,
Student user = userService.query(token.getUsername());
if (user==null){
return null;
}
// 密码认证 可以进行加密操作
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
Subject currentUser = SecurityUtils.getSubject();
#登出
currentUser.logout();
16.5 整合Thymeleaf模板
导入依赖:
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
配置ShiroConfig:
// 整合ShiroDialect
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
在HTML页面
-
命名空间
xmlns:shiro=http://www.pollix.at/thymeleaf/shiro
-
<!--是否有这个权限--> <div shiro:hasPermission="user:update"> <a th:href="@{/add}">add</a>| </div>
17. Swagger
17.1 简介
**前后端分离:**VUE+SpringBoot 基本上都用这一套
**后端时代:**前端只用管理静态页面,html===》后端,使用模版引擎 jsp=》后端主力
前后端分离时代:
- 后端:后端控制层,服务层,数据访问层【后端团队】
- 前端:前端控制层,视图层,【前端团队】
- 伪造后端数据,json,已经存在数据,不需要后端,前端工程依旧可以跑起来
- 前后端如何交互 ====》API
- 前后端相对独立,松耦合
- 前后端甚至可以部署在不同的服务器上
产生一个问题:
- 前后端联调,前端和后端人员无法做到及时协商,解决问题,导致问题爆发
- 需要一个东西可以解决这个问题
解决问题:
- 首先指定计划,实时更新API,较低集成风险
- 早些年:指定word计划文档
- 前后端分离:
- 前端测试后端接口:postman
- 后端提供接口,需要使用更新最新的消息及改动!
官网:https://swagger.io/
Swagger:
- 号称世界上最流行的api框架
- Restful Api文档在线自动生成工具==>api文档和api定义同步更新
- 直接运行,可以在线测试api接口;
- 执行多种语言(c#,java,php)
在项目中使用Swagger需要Springfox
- swagger2
- ui
17.2 SpringBoot 集成 Swagger
-
新建SprringBoot项目====web
-
导入依赖
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>
-
编写一个Hello工程
-
配置Swagger–> config
@Configuration public class SwaggerConfig { }
-
启动—访问http://localhost:8080/swagger-ui/index.html
17.3 配置Swagger
配置Swagger的Docke的Bean实例
1.基本信息配置:
package com.tzeao.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
@Configuration
public class SwaggerConfig {
// 配置Swagger的Docke的Bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
// 配置Swagger信息 == apiInfo
public ApiInfo apiInfo() {
// 作者信息
Contact contact = new Contact("Tzeao", "https://www.baidu.com", "773238750@qq.com");
return new ApiInfo("Tzeao的Swagger日记", "在一个清朗夏夜,望着繁密群星,有一种可望不可及的失望。我们如此可怜吗?不,决不!我们要征服宇宙。", "1.0", "https://www.baidu.com", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList());
}
}
2. 扫描接口及开关
在Docket().select()…build()
package com.tzeao.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
@Configuration
public class SwaggerConfig {
// 配置Swagger的Docke的Bean实例
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
// basePackage 指定要扫描的包
// any 扫描全部
// none 不扫描
// withClassAnnotation 扫描类上的注解 参数是注解的反射对象 RestController.class 类上的注解
// withMethodAnnotation 扫描方法上的注解 参数是注解的反射对象 RequestMapping.class方法上的注解
.apis(RequestHandlerSelectors.basePackage("com.tzeao.controller"))
// 过滤路径
.paths(PathSelectors.ant("/")).build();
}
// 配置Swagger信息 == apiInfo
public ApiInfo apiInfo() {
// 作者信息
Contact contact = new Contact("Tzeao", "https://www.baidu.com", "773238750@qq.com");
return new ApiInfo("Tzeao的Swagger日记", "在一个清朗夏夜,望着繁密群星,有一种可望不可及的失望。我们如此可怜吗?不,决不!我们要征服宇宙。", "1.0", "https://www.baidu.com", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList());
}
}
是否启动Swagger
enable(false)//关闭
判断项目是否是生存环境,来指定是否开启Swagger
package com.tzeao.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import org.springframework.web.bind.annotation.RestController;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import java.util.ArrayList;
@Configuration
public class SwaggerConfig {
// 配置Swagger的Docke的Bean实例
@Bean
public Docket docket(Environment environment) {
// 设置要显示Swagger的生存环境
Profiles of = Profiles.of("dev");
// 判断当前是否处在要显示Swagger的环境中
boolean b = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.enable(b)
.select()
.apis(RequestHandlerSelectors.basePackage("com.tzeao.controller"))
.build();
}
// 配置Swagger信息 == apiInfo
public ApiInfo apiInfo() {
// 作者信息
Contact contact = new Contact("Tzeao", "https://www.baidu.com", "773238750@qq.com");
return new ApiInfo("Tzeao的Swagger日记", "在一个清朗夏夜,望着繁密群星,有一种可望不可及的失望。我们如此可怜吗?不,决不!我们要征服宇宙。", "1.0", "https://www.baidu.com", contact, "Apache 2.0", "http://www.apache.org/licenses/LICENSE-2.0", new ArrayList());
}
}
3. API分组和接口注释
分组
.groupName("Tzeao")
有多个Docket方法就可以有多个分组
实体类信息配置
只要接口中返回的是实体类,那么实体类就会被扫描到
@PostMapping("/user")
public User user(){
return new User();
}
注释
实体类
//@Api(注释)
@ApiModel("用户信息")//给实体类注释
public class User {
@ApiModelProperty("用户ID") //给实体类的字段注释
public String id;
@ApiModelProperty("用户姓名") //给实体类的字段注释
public String name;
// 私有的变量 注解写在get方法上
private String age;
@ApiModelProperty("用户ID") //给实体类的字段注释
public String getAge() {
return age;
}
}
接口
@ApiOperation("控制类")//给接口注释
@PostMapping("/user")
// @ApiParam("sss") 给参数注释
public User user(@ApiParam("sss") int a){
return new User();
}
自带测试页面
类似postman
PS:处于安全考虑,我们在发布的时候需要关闭Swagger
18. 任务
18.1 异步任务
正常是执行完所有方法才返回数据,加上注解后就是先返回,再执行方法,再返回,就是分开来,就是ajax
@Service
public class AsyncService {
// 告诉SpringBoot这是一个异步请求
@Async
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理");
}
}
//开启异步
@EnableAsync
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/a")
public String hello() {
asyncService.hello();
return "ok";
}
}
18.2 邮件任务
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
打开邮箱设置,开启POP3协议,获取授权码
-
配置yaml
spring: mail: username: 773238750@qq.com #密码就是授权码 password: whbvbgeajerdbfgg #smtp.。。。.com host: smtp.qq.com #qq邮箱要开启加密验证 properties: mail: smtp: ssl: enable: true
-
编写
@SpringBootTest public class Test { @Autowired JavaMailSenderImpl mailSender; @org.junit.jupiter.api.Test void contextLoads() { // 简单 SimpleMailMessage mailMessage = new SimpleMailMessage(); // 主题 mailMessage.setSubject("Tzeao你好"); // 内容 mailMessage.setText("你在一个清朗夏夜,望着繁密群星,有一种可望不可及的失望。我们如此可怜吗?不,决不!我们要征服宇宙。"); // 收件人 mailMessage.setTo("773238750@qq.com"); // 发送人 自己接受 mailMessage.setFrom("773238750@qq.com"); mailSender.send(mailMessage); } }
复杂点的邮件
@org.junit.jupiter.api.Test
void contextLoads2() throws MessagingException {
// 复杂邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 组装
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
// 主题
helper.setSubject("Tzeao你好");
// 内容 ,可以编写html,然后”,“加true就会解析
helper.setText("你在一个清朗夏夜,望着繁密群星,有一种可望不可及的失望。我们如此可怜吗?不,决不!我们要征服宇宙。");
// 附件
helper.addAttachment("微信图片_20210628231525.jpg", new File("E:\\君莫笑\\Pictures\\图片\\Pictures\\hgd\\微信图片_20210628231525.jpg"));
// 收件人
helper.setTo("773238750@qq.com");
// 发送人 自己接受
helper.setFrom("773238750@qq.com");
int i = 1;
while (i <= 3){
mailSender.send(mimeMessage);
i++;
}
}
18.3 定时任务
TsdkScheduler 任务调度者
TaskExecutor 任务执行者
@EnableScheduling //开启定时功能
@Scheduled//什么时候执行
Corn表达式 --->这个再百度找在线生成器,要用就百度
@Service
public class ScheduledService {
// cron表达式
// 秒 分 时 日 月 周几
@Scheduled(cron = "0 25 3 * * ?")
public void hello(){
System.out.println("hello");
}
}
//开启定时功能
@EnableScheduling
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
19. 分布式Doubbo+Zookeeper
1. 什么是分布式系统?
“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
单一应用架构
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护
垂直应用架构
当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
缺点:公用模块无法重复利用,开发性的浪费
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。
流动计算架构
当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
RPC两个核心模块:通讯,序列化。
2. Dubbo
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网快速开始 | Apache Dubbo
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
调用关系说明
l 服务容器负责启动,加载,运行服务提供者。
l 服务提供者在启动时,向注册中心注册自己提供的服务。
l 服务消费者在启动时,向注册中心订阅自己所需的服务。
l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
3. Dubbo环境搭建
-
因为要使用到注册中心所以要下载
-
下载Apache ZooKeeper,解压
-
运行/bin/zkServer.cmd,初次运行会报错,没有zoo.cfg配置文件;
可能遇到问题:闪退 !
解决:
修改zoo.cfg配置文件
将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。
注意几个重要位置:
dataDir=./ 临时数据存储的目录(可写相对路径)
clientPort=2181 zookeeper的端口号
修改完成后再次启动zookeeper
-
使用cmd管理员模式进入bin目录打开
-
使用zkCli.cmd测试
ls /:列出zookeeper根下保存的所有节点
-
-
-
下载Dubbo
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。apache/dubbo-admin at master (github.com)
解压进入目录
修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址
在项目目录下打包dubbo-admin
mvn clean package -Dmaven.test.skip=true
-
-
没爆红就成功
-
执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
【注意:zookeeper的服务一定要打开!】
执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户root和密码root ;
-
框架搭建
-
启动zookeeper !
-
IDEA创建一个空项目;
-
创建一个模块,实现服务提供者:provider-server , 选择web依赖即可
-
项目创建完毕,我们写一个服务,比如卖票的服务;
-
编写接口
public interface TicketService { public String getTicket(); }
-
实现类
public class TicketServiceImpl implements TicketService{ @Override public String getTicket() { return "Tzeao"; } }
-
创建一个模块,实现服务消费者:consumer-server , 选择web依赖即可
-
项目创建完毕,我们写一个服务,比如用户的服务;
编写service
public class UserService { //我们需要去拿去注册中心的服务 }
-
-
服务提供者
导入依赖
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</version> </dependency> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> <!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
在springboot配置文件中配置dubbo相关属性!
server.port=8002 #当前应用名字 dubbo.application.name=provider-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181 #扫描指定包下服务 dubbo.scan.base-packages=com.tzeao.service
service下面的实现类要加注解
@DubboService //将服务发布出去 @Component // 加上这个注解就能被zookeeper扫描到 public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "Tzeao"; } }
-
-
服务消费者
-
导入依赖,和之前的依赖一样;
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter --> <dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.3</version> </dependency> <!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient --> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> <!-- 引入zookeeper --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.14</version> <!--排除这个slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
-
配置参数
#当前应用名字 dubbo.application.name=consumer-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
-
本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
-
编写服务类
@Service // 这里是调用springboot public class UserService { //我们需要去拿去注册中心的服务 @Reference //引用 在这里定义一个接口和提供服务一样路径的 TicketService ticketService; public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket); } }
-
测试,记住zookeeper一定要开启
package com.tzeao; import com.tzeao.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ConsumerApplicationTests { @Autowired UserService userService; @Test void contextLoads() { userService.bugTicket(); } }
-
高版本会有错误,原因不得而知
20. 文件上传
-
表单
<form method="post" enctype="multipart/form-data" th:action="@{/upload}"> <input type="file" name="one"> <!-- 多文件上传--> <input type="file" name="plural" multiple> <input type="submit"> </form>
-
控制器
@RequestMapping("/up") public String up(){ return "up"; } @PostMapping("/upload") //使用MultipartFile封装 传过来的文件 @RequestPart("one")绑定表单input的name,多文件使用数组MultipartFile[] public String load(@RequestPart("one") MultipartFile one,@RequestPart("plural") MultipartFile[] plural) throws IOException { // 文件不为空 if (!one.isEmpty()){ // 保存 String name = one.getOriginalFilename(); one.transferTo(new File("D:/"+name)); } // 多文件 if (plural.length>0){ for (MultipartFile file : plural) { if (!file.isEmpty()){ String filename = file.getOriginalFilename(); file.transferTo(new File("D:/a/"+filename)); } } } return "up"; }
-
配置yaml文件
spring: servlet: multipart: # 单个文件的最大--大小 max-file-size: 100MB # 整个请求的最大大小 max-request-size: 200MB
21. 整合Mybatis-Plus
MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
1. 入门
-
导入依赖
这个依赖里面就有mybatis,jdbc
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.3.1</version> </dependency>
自动配置
-
MybatisPlusAutoConfiguration 配置类,MybatisPlusProperties 配置项绑定。mybatis-plus:xxx 就是对mybatis-plus的定制
-
SqlSessionFactory 自动配置好。底层是容器中默认的数据源
-
**mapperLocations 自动配置好的。有默认值。*classpath*:/mapper/*/*.xml;任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件,放在 mapper下
-
容器中也自动配置好了 SqlSessionTemplate
-
@Mapper 标注的接口也会被自动扫描;建议直接 @MapperScan(“com.atguigu.admin.mapper”) 批量扫描就行
-
-
创建一个测试数据库
CREATE TABLE user ( id BIGINT(20) NOT NULL COMMENT '主键ID', name VARCHAR(30) NULL DEFAULT NULL COMMENT '姓名', age INT(11) NULL DEFAULT NULL COMMENT '年龄', email VARCHAR(50) NULL DEFAULT NULL COMMENT '邮箱', PRIMARY KEY (id) );
INSERT INTO user (id, name, age, email) VALUES (1, 'Jone', 18, 'test1@baomidou.com'), (2, 'Jack', 20, 'test2@baomidou.com'), (3, 'Tom', 28, 'test3@baomidou.com'), (4, 'Sandy', 21, 'test4@baomidou.com'), (5, 'Billie', 24, 'test5@baomidou.com');
-
编写实体类
@Data // 这里的字段名 数据库都应该存在 ,可以使用临时的 public class User { // 代表在数据库中不存在 @TableField(exist = false) private String aa; private Long id; private String name; private Integer age; private String email; }
-
编写Mapper接口
public interface UserMapper extends BaseMapper<User> { // 继承BeanMapper就可以不要mapper文件 }
-
使用
@SpringBootApplication @MapperScan("com.tzeao.mapper") public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
@Autowired UserMapper userMapper; @org.junit.jupiter.api.Test void test(){ User user = userMapper.selectById(1L); System.out.println(user); } }
-
yaml
spring: datasource: url: jdbc:mysql://localhost:3306/test username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver druid: aop-patterns: com.atguigu.admin.* #监控SpringBean filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙) stat-view-servlet: # 配置监控页功能 enabled: true login-username: admin login-password: admin resetEnable: false web-stat-filter: # 监控web enabled: true urlPattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: stat: # 对上面filters里面的stat的详细配置 slow-sql-millis: 1000 logSlowSql: true enabled: true wall: enabled: true config: drop-table-allow: false
Service CRUD 接口
- 通用 Service CRUD 封装IService (opens new window)接口,进一步封装 CRUD 采用
get 查询单行
remove 删除
list 查询集合
page 分页
前缀命名方式区分Mapper
层避免混淆,- 泛型
T
为任意实体对象- 建议如果存在自定义通用 Service 方法的可能,请创建自己的
IBaseService
继承Mybatis-Plus
提供的基类- 对象
Wrapper
为 条件构造器
类型 | 参数名 | 描述 |
---|---|---|
T | entity | 实体对象 |
Collection | entityList | 实体对象集合 |
int | batchSize | 插入批次数量 |
-
创建业务接口
public interface UserService extends IService<User> { }
-
实现业务接口
@Service // 第一个参数为要操作的mapper接口,第二个为实体类 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{ }
-
使用
@Autowired UserService userService; @org.junit.jupiter.api.Test void test1(){ List<User> list = userService.list(); System.err.println(list); }
-
分页使用
// 获取页数 泛型传实体类 第一个参数为当前的页数(通过前端来传),第二个为每页的数据 Page<User> page = new Page<>(1,2); // 分页查询 Page<User> page1 = userService.page(page); // 获取全部页数 long pages = page1.getPages(); // 获取当前页数 long current = page1.getCurrent(); // 获取每页的数据,并且将每页封装成集合 List<User> records = page1.getRecords();
-
上面总页数没有显示,需要分页插件
@Configuration @MapperScan("com.tzeao.mapper") public class PageConfig { // 最新版 @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 拦截器 PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); // 开启 count 的 join 优化,只针对部分 left join paginationInterceptor.setOverflow(false); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }
22. Junit
1.常用注解
-
**@Test 😗*表示方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将会由Jupiter提供额外测试
-
**@ParameterizedTest 😗*表示方法是参数化测试,下方会有详细介绍
-
**@RepeatedTest 😗*表示方法可重复执行,下方会有详细介绍
-
**@DisplayName 😗*为测试类或者测试方法设置展示名称
-
**@BeforeEach 😗*表示在每个单元测试之前执行
-
**@AfterEach 😗*表示在每个单元测试之后执行
-
**@BeforeAll 😗*表示在所有单元测试之前执行
-
**@AfterAll 😗*表示在所有单元测试之后执行
-
**@Tag 😗*表示单元测试类别,类似于JUnit4中的@Categories
-
**@Disabled 😗*表示测试类或测试方法不执行,类似于JUnit4中的@Ignore
-
**@Timeout 😗*表示测试方法运行如果超过了指定时间将会返回错误
-
**@ExtendWith 😗*为测试类或测试方法提供扩展类引用
2. 断言机制
断言(assertions)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是 org.junit.jupiter.api.Assertions 的静态方法。JUnit 5 内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有的测试运行结束以后,会有一个详细的测试报告;
1、简单断言
用来对单个值进行简单的验证。如:
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值是否为 true |
assertFalse | 判断给定的布尔值是否为 false |
assertNull | 判断给定的对象引用是否为 null |
assertNotNull | 判断给定的对象引用是否不为 null |
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
2、数组断言
通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
3、组合断言
assertAll 方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
4、异常断言
断言方式Assertions.assertThrows() ,配合函数式编程就可以进行使用。
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}
5、超时断言
Junit5还提供了Assertions.assertTimeout() 为测试方法设置了超时时间
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
6、快速失败
通过 fail 方法直接使得测试失败
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
还有 —
前置条件:AssumptionsTest
嵌套测试
参数化测试:@ParameterizedTest
@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
@NullSource: 表示为参数化测试提供一个null的入参
@EnumSource: 表示为参数化测试提供一个枚举入参
@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)
指标监控
SpringBoot Actuator