springBoot学习笔记(完结)
本文章是 👉bilibili狂神老师 springboot视频的笔记
下面是⬇️正文,目录在侧边💕
SpringBoot
简介
Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。
Spring是如何简化Java开发的
为了降低Java开发的复杂性,Spring采用了以下4种关键策略:
1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;
2、通过IOC,依赖注入(DI)和面向接口实现松耦合;
3、基于切面(AOP)和惯例进行声明式编程;
4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;
什么是SpringBoot
什么是SpringBoot呢,就是一个javaweb的开发框架,和SpringMVC类似,对比其他javaweb框架的好处,官方说是简化开发,约定大于配置, you can “just run”,能迅速的开发web应用,几行代码开发一个http接口。
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。
是的这就是Java企业级应用->J2EE->spring->springboot的过程。
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。
简单来说就是SpringBoot其实不是什么新的框架,它默认配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。
Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。
主要优点
Spring Boot的主要优点:
- 为所有Spring开发者更快的入门
- 开箱即用,提供各种默认配置来简化项目配置
- 内嵌式容器简化Web项目
- 没有冗余代码生成和XML配置的要求
hello World
1.创建基础项目说明
Spring官方提供了非常方便的工具让我们快速构建应用
Spring Initializr:https://start.spring.io/
**项目创建方式一:**使用Spring Initializr 的 Web页面创建项目
1、打开 https://start.spring.io/
2、照着图片填完即可[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IcXmnAz6-1661431067932)(D:\myDownloads\typora\img\image-20220816165026808.png)]
3、解压项目包,并用IDEA以Maven项目导入,一路下一步即可,直到项目导入完毕。
4、如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
**项目创建方式二:**使用 IDEA 直接创建项目
1、创建一个新项目
2、选择spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现
3、填写项目信息
4、选择初始化的组件(初学勾选 Web 即可)
5、填写项目路径
6、等待项目构建成功
2、项目结构分析
通过上面步骤完成了基础项目的创建。就会自动生成以下文件。
1、程序的主启动类
2、一个 application.properties 配置文件
3、一个 测试类
4、一个 pom.xml
pom.xml 分析
打开pom.xml,看看Spring Boot项目的依赖:
<!-- 父依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- web场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springboot单元测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<!-- 剔除依赖 -->
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- 打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3、编写一个http接口
1、在主程序的同级目录下,新建一个controller包,一定要在同级目录下,否则识别不到
2、在包中新建一个HelloController类
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello World";
}
}
3、编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!
简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!
4、将项目打成jar包
将项目打成jar包,点击 maven的 package
如果遇到以上错误,可以配置打包时 跳过项目运行测试用例
<!--
在工作中,很多情况下我们打包是不想执行测试用例的
可能是测试用例不完事,或是测试用例会影响数据库数据
跳过测试用例执
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!--跳过项目运行测试用例-->
<skipTests>true</skipTests>
</configuration>
</plugin>
如果打包成功,则会在target目录下生成一个 jar 包
打成了jar包后,就可以在任何地方运行了!OK
修改spring boot启图案
在浏览器上搜索:spring boot banner在线生成即可
我这里直接提提供一个图案
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕机 永无BUG //
在项目里面的,src–>resources–>创建banner.txt,把这个图案复制进去,重新运行即可
运行原理初探
我们之前写的HelloSpringBoot,到底是怎么运行的呢,Maven项目,我们一般从pom.xml文件探究起;
pom.xml
父依赖
其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
点进去,发现还有一个父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.5.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
以后我们导入依赖默认是不需要写版本;但是如果导入的包没有在依赖中管理着就需要手动配置版本了;
启动器 spring-boot-starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
springboot-boot-starter-xxx:就是spring-boot的场景启动器
spring-boot-starter-web:帮我们导入了web模块正常运行所依赖的组件;
SpringBoot将所有的功能场景都抽取出来,做成一个个的starter (启动器),只需要在项目中引入这些starter即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可 ;我们未来也可以自己自定义 starter;
主启动类
分析完了 pom.xml 来看看这个启动类
默认的主启动类
//@SpringBootApplication 来标注一个主程序类
//说明这是一个Spring Boot应用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
//以为是启动了一个方法,没想到启动了一个服务
SpringApplication.run(SpringbootApplication.class, args);
}
}
但是**一个简单的启动类并不简单!**我们来分析一下这些注解都干了什么
@SpringBootApplication
作用:标注在某个类上说明这个类是SpringBoot的主配置类 , SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;
进入这个注解:可以看到上面还有很多其他注解!
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// ......
}
@ComponentScan
这个注解在Spring中很重要 ,它对应XML配置中的元素。
作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中
@SpringBootConfiguration
作用:SpringBoot的配置类 ,标注在某个类上 , 表示这是一个SpringBoot的配置类;
我们继续进去这个注解查看
// 点进去得到下面的 @Component
@Configuration
public @interface SpringBootConfiguration {}
@Component
public @interface Configuration {}
这里的 @Configuration,说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;
里面的 @Component 这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!
我们回到 SpringBootApplication 注解中继续看
@EnableAutoConfiguration
@EnableAutoConfiguration :开启自动配置功能
以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置 ;@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;
点进注解接续查看:
@AutoConfigurationPackage :自动配置包
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
@import :Spring底层注解@import , 给容器中导入一个组件
Registrar.class 作用:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;
这个分析完了,退到上一步,继续看
@Import({AutoConfigurationImportSelector.class}) :给容器导入组件 ;
AutoConfigurationImportSelector :自动配置导入选择器,那么它会导入哪些组件的选择器呢?我们点击去这个类看源码:
1、这个类中有一个这样的方法
// 获得候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//这里的getSpringFactoriesLoaderFactoryClass()方法
//返回的就是我们最开始看的启动自动导入配置文件的注解类;EnableAutoConfiguration
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
2、这个方法又调用了 SpringFactoriesLoader 类的静态方法!我们进入SpringFactoriesLoader类loadFactoryNames() 方法
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
//这里它又调用了 loadSpringFactories 方法
return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
3、我们继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//获得classLoader , 我们返回可以看到这里得到的就是EnableAutoConfiguration标注的类本身
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//去获取一个资源 "META-INF/spring.factories"
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
//将读取到的资源遍历,封装成为一个Properties
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 factoryClassName = ((String)entry.getKey()).trim();
String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
int var10 = var9.length;
for(int var11 = 0; var11 < var10; ++var11) {
String factoryName = var9[var11];
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var13) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
}
}
}
4、发现一个多次出现的文件:spring.factories,全局搜索它
spring.factories
我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在!
WebMvcAutoConfiguration
我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration
可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!
所以,自动配置真正实现是从classpath中搜寻所有的META-INF/spring.factories配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration的JavaConfig形式的IOC容器配置类 , 然后将这些都汇总成为一个实例并加载到IOC容器中。
结论:
- SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值
- 将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;
- 整个J2EE的整体解决方案和自动配置都在springboot-autoconfigure的jar包中;
- 它会给容器中导入非常多的自动配置类 (xxxAutoConfiguration), 就是给容器中导入这个场景需要的所有组件 , 并配置好这些组件 ;
- 有了自动配置类 , 免去了我们手动编写配置注入功能组件等的工作;
SpringApplication
不简单的方法
我最初以为就是运行了一个main方法,没想到却开启了一个服务;
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
SpringApplication.run分析
分析该方法主要分两部分,一部分是SpringApplication的实例化,二是run方法的执行;
SpringApplication
这个类主要做了以下四件事情:
1、推断应用的类型是普通的项目还是Web项目
2、查找并加载所有可用初始化器 , 设置到initializers属性中
3、找出所有的应用程序监听器,设置到listeners属性中
4、推断并设置main方法的定义类,找到运行的主类
查看构造器:
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
// ......
this.webApplicationType = WebApplicationType.deduceFromClasspath();
this.setInitializers(this.getSpringFactoriesInstances();
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = this.deduceMainApplicationClass();
}
run方法流程分析
跟着源码和这幅图就可以一探究竟了!
yaml配置注入
配置文件
SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的
-
application.properties
-
- 语法结构 :key=value
-
application.yml
-
- 语法结构 :key:空格 value
**配置文件的作用 :**修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;
yaml概述
这种语言以数据作为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml
传统xml配置:
<server>
<port>8081<port>
</server>
yaml配置:
server:
prot: 8080
yaml基础语法
说明:语法要求严格!
1、空格不能省略
2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
3、属性和值的大小写都是十分敏感的。
字面量:普通的值 [ 数字,布尔值,字符串 ]
字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;
k: v
注意:
-
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;比如 :name: “kuang \n shen” 输出 :kuang 换行 shen
-
‘’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen
对象、Map(键值对)
#对象、Map格式
k:
v1:
v2:
在下一行来写对象的属性和值得关系,注意缩进;比如:
student:
name: qinjiang
age: 3
行内写法
student: {name: qinjiang,age: 3}
数组( List、set )
用 - 值表示数组中的一个元素,比如:
pets:
- cat
- dog
- pig
行内写法
pets: [cat,dog,pig]
yaml注入配置文件
yaml文件更强大的地方在于,他可以给我们的实体类直接注入匹配值!
1、在springboot项目中的resources目录下新建一个文件 application.yml
2、编写一个实体类 Dog;
package com.kuang.springboot.pojo;
@Component //注册bean到容器中
public class Dog {
private String name;
private Integer age;
//有参无参构造、get、set方法、toString()方法
}
3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:
@Component //注册bean
public class Dog {
@Value("阿黄")
private String name;
@Value("18")
private Integer age;
}
4、在SpringBoot的测试类下注入狗狗输出一下;
@SpringBootTest
class DemoApplicationTests {
@Autowired //将狗狗自动注入进来
Dog dog;
@Test
public void contextLoads() {
System.out.println(dog); //打印看下狗狗对象
}
}
结果成功输出,@Value注入成功,这是我们原来的办法对吧。
5、我们在编写一个复杂一点的实体类:Person 类
@Component //注册bean到容器中
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 Dog dog;
//有参、无参构造、get、set方法、toString()方法
}
6、我们来使用yaml配置的方式进行注入,大家写的时候注意区别和优势,我们编写一个yaml配置!
person:
name: qinjiang
age: 3
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺财
age: 1
7、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
*/
@Component //注册bean
@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 Dog dog;
//有参、无参构造、get、set方法、toString()方法
}
8、IDEA 提示,springboot配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!
<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要刷新maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
9、确认以上配置都OK之后,我们去测试类中测试一下:
@SpringBootTest
class DemoApplicationTests {
@Autowired
Person person; //将person自动注入进来
@Test
public void contextLoads() {
System.out.println(person); //打印person信息
}
}
结果:所有值全部注入成功!
注意:
将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败
加载指定的配置文件
**@PropertySource :**加载指定的配置文件;
@configurationProperties:默认从全局配置文件中获取值;
1、我们去在resources目录下新建一个person.properties文件
name=kuangshen
2、然后在我们的代码中指定加载person.properties文件
@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {
@Value("${name}")
private String name;
......
}
3、再次输出测试一下:指定配置文件绑定成功!
配置文件占位符
配置文件还可以编写占位符生成随机数
person:
name: qinjiang${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: ${person.hello:other}_旺财
age: 1
回顾properties配置
我们上面采用的yaml方法都是最简单的方式,开发中最常用的;也是springboot所推荐的!那我们来唠唠其他的实现方式,道理都是相同的;写还是那样写;配置文件除了yml还有我们之前常用的properties
【注意】properties配置文件在写中文的时候,会有乱码 , 我们需要去IDEA中设置编码格式为UTF-8;
settings–>FileEncodings 中配置;
测试步骤:
1、新建一个实体类User
@Component //注册bean
public class User {
private String name;
private int age;
private String sex;
}
2、编辑配置文件 user.properties
user1.name=kuangshen
user1.age=18
user1.sex=男
3、我们在User类上使用@Value来进行注入!
@Component //注册bean
@PropertySource(value = "classpath:user.properties")
public class User {
//直接使用@value
@Value("${user.name}") //从配置文件中取值
private String name;
@Value("#{9*2}") // #{SPEL} Spring表达式
private int age;
@Value("男") // 字面量
private String sex;
}
4、Springboot测试
@SpringBootTest
class DemoApplicationTests {
@Autowired
User user;
@Test
public void contextLoads() {
System.out.println(user);
}
}
结果正常输出:
对比小结
@Value这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图
1、@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加
2、松散绑定:这个什么意思呢? 比如我的yml中写的last-name,这个和lastName是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下
3、JSR303数据校验 , 这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性
4、复杂类型封装,yml中可以封装对象 , 使用value就不支持
结论:
配置yml和配置properties都可以获取到值 , 强烈推荐 yml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
JSR303数据校验及多环境切换
Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;
@Component //注册bean
@ConfigurationProperties(prefix = "person")
@Validated //数据校验
public class Person {
@Email(message="邮箱格式错误") //name必须是邮箱格式
private String name;
}
运行结果 :default message [不是一个合法的电子邮件地址];
使用数据校验,可以保证数据的正确性;
常见参数
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@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=) string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
多环境切换
profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;
多配置文件
我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;
例如:
application-test.properties 代表测试环境配置
application-dev.properties 代表开发环境配置
但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置文件;
我们需要通过一个配置来选择需要激活的环境:
#比如在配置文件中指定使用dev环境,我们可以通过设置不同的端口号进行测试;
#我们启动SpringBoot,就可以看到已经切换到dev下的配置了;
spring.profiles.active=dev
yaml的多文档块
和properties配置文件中一样,但是使用yml去实现不需要创建多个置文件,更加方便了 !
server:
port: 8081
#选择要激活那个环境块
spring:
profiles:
active: prod
---
server:
port: 8083
spring:
profiles: dev #配置环境的名称
---
server:
port: 8084
spring:
profiles: prod #配置环境的名称
注意:如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!
配置文件加载位置
外部加载配置文件的方式十分多,我们选择最常用的即可,在开发的资源文件中进行配置!
官方外部配置文件说明参考文档
springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
优先级由高到底,高优先级的配置会覆盖低优先级的配置;
SpringBoot会从这四个位置全部加载主配置文件;互补配置;
我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;
#配置项目的访问路径
server.servlet.context-path=/kuang
拓展,运维小技巧
指定位置加载配置文件
我们还可以通过spring.config.location来改变默认的配置文件位置
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
自动配置原理
配置文件到底能写什么?怎么写?
SpringBoot官方文档中有大量的配置,我们无法全部记住
分析自动配置原理
我们以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理;
//表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件;
@Configuration
//启动指定类的ConfigurationProperties功能;
//进入这个HttpProperties查看,将配置文件中对应的值和HttpProperties绑定起来;
//并把HttpProperties加入到ioc容器中
@EnableConfigurationProperties({HttpProperties.class})
//Spring底层@Conditional注解
//根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效;
//这里的意思就是判断当前应用是否是web应用,如果是,当前配置类生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)
//判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器;
@ConditionalOnClass({CharacterEncodingFilter.class})
//判断配置文件中是否存在某个配置:spring.http.encoding.enabled;
//如果不存在,判断也是成立的
//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的;
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
//他已经和SpringBoot的配置文件映射了
private final Encoding properties;
//只有一个有参构造器的情况下,参数的值就会从容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
//给容器中添加一个组件,这个组件的某些值需要从properties中获取
@Bean
@ConditionalOnMissingBean //判断容器没有这个组件?
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
//。。。。。。。
}
一句话总结 :根据当前不同的条件判断,决定这个配置类是否生效!
- 一但这个配置类生效;这个配置类就会给容器中添加各种组件;
- 这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
- 所有在配置文件中能配置的属性都是在xxxxProperties类中封装着;
- 配置文件能配置什么就可以参照某个功能对应的这个属性类
//从配置文件中获取指定的值和bean的属性进行绑定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}
我们去配置文件里面试试前缀,看提示!
这就是自动装配的原理!
精髓
1、SpringBoot启动会加载大量的自动配置类
2、我们看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
3、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
4、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;
**xxxxAutoConfigurartion:自动配置类;**给容器中添加组件
xxxxProperties:封装配置文件中相关属性;
了解:@Conditional
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。
我们怎么知道哪些自动配置类生效?
我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;
#开启springboot的调试类
debug=true
Positive matches:(自动配置类启用的:正匹配)
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
Unconditional classes: (没有条件的类)
【演示:查看输出的日志】
掌握吸收理解原理,即可以不变应万变!
Web开发静态资源处理
静态资源映射规则
首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如css,js等文件,这个SpringBoot怎么处理呢?
如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!
我们先来聊聊这个静态资源映射规则:
SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面;
我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;
有一个方法:addResourceHandlers 添加资源处理
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默认资源处理
logger.debug("Default resource handling disabled");
return;
}
// 缓存控制
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// webjars 配置
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 静态资源配置
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;
什么是webjars 呢?
Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。
使用SpringBoot需要使用Webjars,我们可以去搜索一下:
网站:https://www.webjars.org
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
导入完毕,查看webjars目录结构,并访问Jquery.js文件!
访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
第二种静态资源映射规则
我们去找staticPathPattern发现第二种映射规则 :/** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:
// 进入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;
比如我们访问http://localhost:8080/1.js
, 他就会去这些文件夹中寻找对应的静态资源文件;
自定义静态资源路径
我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在application.properties中配置;
spring.resources.static-locations=classpath:/coding/,classpath:/kuang/
一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了
首页处理
静态资源文件夹说完后,我们继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页!
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(), // getWelcomePage 获得欢迎页
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
return welcomePageHandlerMapping;
}
点进去继续看
private Optional<Resource> getWelcomePage() {
String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
// ::是java8 中新引入的运算符
// Class::function的时候function是属于Class的,应该是静态方法。
// this::function的funtion是属于这个对象的。
// 简而言之,就是一种语法糖而已,是一种简写
return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}
// 欢迎页就是一个location下的的 index.html 而已
private Resource getIndexHtml(String location) {
return this.resourceLoader.getResource(location + "index.html");
}
欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。
比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html
新建一个 index.html ,在我们上面的3个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!
关于网站图标说明:
与其他静态资源一样,Spring Boot在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的favicon。
1、关闭SpringBoot默认图标
#关闭默认图标
spring.mvc.favicon.enabled=false
2、自己放一个图标在静态资源目录下,我放在 public 目录下
3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!
Thymeleaf模板引擎
模板引擎
SpringBoot这个项目首先是以jar的方式,不是war,像第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。
SpringBoot推荐你可以来使用模板引擎:
模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。
我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。
引入Thymeleaf
怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:
Thymeleaf 官网:https://www.thymeleaf.org/
Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
Spring官方文档:找到我们对应的版本
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
找到对应的pom依赖:可以适当点进源码看下本来的包!
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
Maven会自动下载jar包,我们可以去看下下载的东西;
Thymeleaf分析
前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。
我们去找一下Thymeleaf的自动配置类:ThymeleafProperties
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
我们可以在其中看到默认的前缀和后缀!
我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。
使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可!
测试
1、编写一个TestController
@Controller
public class TestController {
@RequestMapping("/t1")
public String test1(){
//classpath:/templates/test.html
return "test";
}
}
2、编写一个测试页面 test.html 放在 templates 目录下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>测试页面</h1>
</body>
</html>
3、启动项目请求测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Jsv8hxoO-1661431067945)(D:\myDownloads\typora\img\image-20220817141348555.png)]
Thymeleaf 语法学习
要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;
Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!
我们做个最简单的练习 :我们需要查出一些数据,在页面中展示
1、修改测试请求,增加数据传输;
@RequestMapping("/t1")
public String test1(Model model){
//存入数据
model.addAttribute("msg","Hello,Thymeleaf");
//classpath:/templates/test.html
return "test";
}
2、我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。
我们可以去官方文档的#3中看一下命名空间拿来过来:
xmlns:th="http://www.thymeleaf.org"
3、我们去编写下前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>狂神说</title>
</head>
<body>
<h1>测试页面</h1>
<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${msg}"></div>
</body>
</html>
4、启动测试!
OK,入门搞定,我们来认真研习一下Thymeleaf的使用语法!
1、我们可以使用任意的 th:attr 来替换Html中原生属性的值!
2、我们能写哪些表达式呢?
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
1)、获取对象的属性、调用方法
2)、使用内置的基本对象:#18
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
3)、内置的一些工具对象:
#execInfo : information about the template being processed.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
==================================================================================
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
Fragment Expressions: ~{...}:片段引用表达式
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
练习测试
1、 我们编写一个Controller,放一些数据
@RequestMapping("/t2")
public String test2(Map<String,Object> map){
//存入数据
map.put("msg","<h1>Hello</h1>");
map.put("users", Arrays.asList("qinjiang","kuangshen"));
//classpath:/templates/test.html
return "test";
}
2、测试页面取出数据
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>狂神说</title>
</head>
<body>
<h1>测试页面</h1>
<div th:text="${msg}"></div>
<!--不转义-->
<div th:utext="${msg}"></div>
<!--遍历数据-->
<!--th:each每次遍历都会生成当前这个标签:官网#9-->
<h4 th:each="user :${users}" th:text="${user}"></h4>
<h4>
<!--行内写法:官网#12-->
<span th:each="user:${users}">[[${user}]]</span>
</h4>
</body>
</html>
3、启动项目测试!
我们看完语法,很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!
MVC自动配置原理
狂神说SpringBoot12:MVC自动配置原理 (qq.com)
员工管理系统
(13条消息) springboot-09-员工管理系统(CRUD)_Filwaod的博客-CSDN博客
环境搭建
新建一个SpringBoot项目
导入静态资源
- 首先在
classpath
下(resources)创建不存在的静态资源目录public
和resources
- 将
html
静态资源放置templates
目录下 - 将
asserts
目录下的css、img、js
等静态资源放置static
目录下
模拟数据库
pom.xml
中导入lombok依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
pojo
在启动类同级创建pojo包,在pojo包下新建实体类
部门表:
//部门表
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
员工表:
//员工表
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender;//0:女 1:男
private Department department;
private Date date;
}
dao
在主程序同级目录下新建dao包
然后分别编写DepartmentDao
和EmployeeDao
,并在其中模拟数据库的数据
DepartmentDao:
//注册到IOC容器中
@Repository
public class DepartmentDao {
//模拟数据库中的数据
private static Map<Integer, Department> departments = null;
static {
departments = new HashMap<>();//创建一个部门表
departments.put(1, new Department(1, "技术部"));
departments.put(2, new Department(2, "市场部"));
departments.put(3, new Department(3, "调研部"));
departments.put(4, new Department(4, "后勤部"));
departments.put(5, new Department(5, "运营部"));
}
//获得所有部门信息
public Collection<Department> departments(){
return departments.values();
}
//通过id得到部门
public Department getDepartmentByID(int id){
return departments.get(id);
}
}
12345678910111213141516171819202122232425
EmployeeDao:
//注册到IOC容器中
@Repository
public class EmployeeDao {
//模拟数据库中员工表的数据
static private Map<Integer, Employee> employees;
@Autowired//自动
private DepartmentDao departmentDao;
static {
employees = new HashMap<>();//创建一个员工表
employees.put(1, new Employee(1, "zsr", "1234@qq.com", 1, new Department(1, "技术部"), new Date()));
employees.put(2, new Employee(2, "lyr", "1345@qq.com", 1, new Department(2, "市场部"), new Date()));
employees.put(3, new Employee(3, "gcc", "5665@qq.com", 0, new Department(3, "调研部"), new Date()));
employees.put(4, new Employee(4, "zyx", "7688@qq.com", 1, new Department(4, "后勤部"), new Date()));
employees.put(5, new Employee(5, "zch", "8089@qq.com", 1, new Department(5, "运营部"), new Date()));
}
//主键自增
private static Integer initialID = 6;
//增加一个员工
public void addEmployee(Employee employee){
if (employee.getId() == null){
employee.setId(initialID);
}
employee.setDepartment(departmentDao.getDepartmentByID(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
//查询全部员工信息
public Collection<Employee> getAllEmployees(){
return employees.values();
}
//通过id查询员工
public Employee getEmployeeById(int id){
return employees.get(id);
}
//通过id删除员工
public void deleteEmployee(int id){
employees.remove(id);
}
}
首页实现
在启动类同级新建config
包用来存放自己扩展的配置类
新建一个MyMvcConfig
,扩展mvc功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
//访问"/"或"/index.html"时会变成访问index.html
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}
我们启动主程序访问测试一下,访问localhost:8080/
或者locahost:8080/index.html
出现以下页面则成功
上述测试可以看到页面有图片没有加载出来,且没有css和js的样式,这就是因为我们html页面中静态资源引入的语法出了问题,在SpringBoot中,推荐使用Thymeleaf
作为模板引擎,我们将其中的语法改为Thymeleaf,所有页面的静态资源都需要使用其接管
注意所有html都需要引入Thymeleaf命名空间
xmlns:th="http://www.thymeleaf.org"
然后修改所有页面静态资源的引入,使用@{…} 链接表达式
例如index.html
中:
注意:第一个/代表项目的classpath
,也就是这里的resources
目录
其他页面亦是如此,再次测试访问,正确显示页面
页面国际化
统一properties编码
编写i18n国际化资源文件
在resources目录下新建一个i18n
包(和static目录同级),其中放置国际化相关的配置
其中新建三个配置文件,用来配置语言:
login.properties
:无语言配置时候生效login_en_US.properties
:英文生效login_zh_CN.properties
:中文生效
命名方式是下划线的组合:文件名_语言_国家.properties
;
以此方式命名,IDEA会帮我们识别这是个国际化配置包,自动绑定在一起转换成如下的模式:
绑定在一起后,我们想要添加更过语言配置,只需要在大的资源包右键添加到该绑定配置文件即可
此时只需要输入区域名即可创建成功,比如输入en_US
,就会自动识别
然后打开英文或者中文语言的配置文件,点击Resource Bundle
进入可视化编辑页面
进入到可视化编辑页面后,点击加号,添加属性,首先新建一个login.tip
代表首页中的提示
然后对该提示分别做三种情况的语言配置,在三个对应的输入框输入即可
接下来再配置所有要转换语言的变量
然后打开三个配置文件的查看其中的文本内容,可以看到已经做好了全部的配置
login.properties
login.tip=请登录
login.password=密码
login.remember=记住我
login.btn=登录
login.username=用户名
login_en_US.properties
login.tip=Please sign in
login.password=password
login.remember=remember me
login.btn=login
login.username=username
login_zh_CN.properties
login.tip=请登录
login.password=密码
login.remember=记住我
login.btn=登录
login.username=用户名
配置国际化资源文件名称
在Spring中,国际化主要是通过ResourceBundleMessageSource
这个类来实现的
Spring Boot通过MessageSourceAutoConfiguration
为我们自动配置好了管理国际化资源文件的组件
可以发现其中都有MessageSource
。
我们在IDEA中查看以下MessageSourceAutoConfiguration
类
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
//......
}
主要了解messageSource()
这个方法。
可以看到,它的参数为MessageSourceProperties
对象,我们看看这个类
public class MessageSourceProperties {
/**
* Comma-separated list of basenames (essentially a fully-qualified classpath
* location), each following the ResourceBundle convention with relaxed support for
* slash based locations. If it doesn't contain a package qualifier (such as
* "org.mypackage"), it will be resolved from the classpath root.
*/
private String basename = "messages";
/**
* Message bundles encoding.
*/
private Charset encoding = StandardCharsets.UTF_8;
类中首先声明了一个属性basename
,默认值为messages
;
我们翻译其注释:
- 逗号分隔的基名列表(本质上是完全限定的类路径位置)
- 每个都遵循ResourceBundle约定,并轻松支持于斜杠的位置
- 如果不包含包限定符(例如"org.mypackage"),它将从类路径根目录中解析
123
意思是:
- 如果你不在springboot配置文件中指定以.分隔开的国际化资源文件名称的话
- 它默认会去类路径下找
messages.properties
作为国际化资源文件
这里我们自定义了国际化资源文件,因此我们需要在SpringBoot配置文件application.properties
中加入以下配置指定我们配置文件的名称
spring.messages.basename=i18n.login
其中i18n
是存放资源的文件夹名,login
是资源文件的基本名称。
首页获取显示国际化值
利用#{...}
消息表达式,去首页index.html
获取国际化的值
重启项目,访问首页,可以发现已经自动识别为中文
配置国际化组件实现中英文切换
1. 添加中英文切换标签链接
上述实现了登录首页显示为中文,我们在index.html
页面中可以看到两个标签
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
也就对应着视图中的
那么我们怎么通过这两个标签实现中英文切换呢?
2. 自定义地区解析器组件
怎么实现我们自定义的地区解析器呢?我们首先来分析一波源码
在Spring中有关于国际化的两个类:
Locale
:代表地区,每一个Locale对象都代表了一个特定的地理、政治和文化地区LocaleResolver
:地区解析器
首先搜索WebMvcAutoConfiguration
,可以在其中找到关于一个方法localeResolver()
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
//如果用户配置了,则使用用户配置好的
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
//用户没有配置,则使用默认的
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
该方法就是获取LocaleResolver
地区对象解析器:
- 如果用户配置了则使用用户配置的地区解析器
- 如果用户没有配置,则使用默认的地区解析器
我们可以看到默认地区解析器的是AcceptHeaderLocaleResolver
对象,我们点入该类查看源码
可以发现它继承了LocaleResolver
接口,实现了地区解析
因此我们想自定义一个地区解析器,只需要让这个类继承LocaleResolver
接口,重写其方法,并注入容器中即可
我们在config
包下新建MyLocaleResolver
,作为自己的国际化地区解析器
我们在index.html
中,编写了对应的请求跳转
- 如果点击中文按钮,则跳转到
/index.html(l='zh_CN')
页面 - 如果点击English按钮,则跳转到
/index.html(l='en_US')
页面
<!--这里传入参数不需要使用?使用key=value-->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
因此我们自定义的地区解析器MyLocaleResolver
中,需要处理这两个带参数的链接请求
public class MyLocaleResolver implements LocaleResolver {
//解析请求
@Override
public Locale resolveLocale(HttpServletRequest request) {
//获取请求中的国际化参数
String language = request.getParameter("l");
//默认的地区
Locale locale = Locale.getDefault();
//如果请求的链接参数不为空,携带了国际化参数
if (!StringUtils.isEmpty(language)) {
String[] split = language.split("_");//zh_CN(语言_地区)
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
为了让我们的区域化信息能够生效,还需要将其注入到容器中
//自定义的国际化组件生效
@Bean
public LocaleResolver localeResolver() {
return new MyLocaleResolver();
}
我们重启项目,来访问一下,发现点击按钮可以实现成功切换!
点击中文按钮,跳转到http://localhost:8080/index.html?l=zh_CN
,显示为中文
点击English按钮,跳转到http://localhost:8080/index.html?l=en_US
,显示为英文
但是还有一个问题:在英文界面输入错误的用户名密码,弹出错误信息时,界面又会变成中文的。
登录功能的实现
登录,也就是当我们点击登录
按钮的时候,会进入一个页面,这里进入dashboard
页面
因此我们首先在index.html
中的表单编写一个提交地址/user/login
,并给名称和密码输入框添加name
属性为了后面的传参
新建controller
包,在其中新建类loginController
,处理登录请求
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model) {
//如果用户名和密码正确
if ("admin".equals(username) && "123456".equals(password))
return "dashboard";//跳转到dashboard页面
//如果用户名或者密码不正确
else {
model.addAttribute("msg", "用户名或者密码错误");//显示错误信息
return "index";//跳转到首页
}
}
}
然后我们在index.html
首页中加一个标签用来显示controller
返回的错误信息
<p style="color: red" th:text="${msg}"></p>
1
我们再测试一下,启动主程序,访问localhost:8080
如果我们输入正确的用户名和密码,则重新跳转到dashboard
页面,浏览器url为http://localhost:8080/user/login?username=admin&password=123456
随便输入错误的用户名12
,输入错误的密码12
浏览器url为http://localhost:8080/user/login?username=12&password=123456
,页面上附有错误提示信息
到此我们的登录功能实现完毕,但是有一个很大的问题,浏览器的url暴露了用户的用户名和密码,这在实际开发中可是重大的漏洞,泄露了用户信息,因此我们需要改变首页(dashboard)的映射
我们在自定义的配置类MyMvcConfig
中添加一句话
registry.addViewController("/main.html").setViewName("dashboard");
也就是访问/main.html
页面就跳转到dashboard
页面
然后我们稍稍修改一下LoginController
,当登录成功时重定向到main.html
页面,也就跳转到了dashboard
页面
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model) {
//如果用户名和密码正确
if ("admin".equals(username) && "123456".equals(password))
return "redirect:/main.html";//重定向到main.html页面,也就是跳转到dashboard页面
//如果用户名或者密码不正确
else {
model.addAttribute("msg", "用户名或者密码错误");//显示错误信息
return "index";//跳转到首页
}
}
}
我们再次重启测试,输入正确的用户名和密码登陆成功后,浏览器不再携带泄露信息
但是这又出现了新的问题,不登录,直接访问localhost/main.html
也会跳转到dashboard
的页面,这就引入了接下来的拦截器
登录拦截器
为了解决上述遗留的问题,我们需要自定义一个拦截器;在config
目录下,新建一个登录拦截器类LoginHandlerInterceptor
用户登录成功后,后台会得到用户信息;如果没有登录,则不会有任何的用户信息;
我们就可以利用这一点通过拦截器进行拦截:
- 当用户登录时将用户信息存入
session
中,访问页面时首先判断session
中有没有用户的信息 - 如果没有,拦截器进行拦截;
- 如果有,拦截器放行
因此我们首先在LoginController
中当用户登录成功后,存入用户信息到session
中
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session) {
//如果用户名和密码正确
if ("admin".equals(username) && "123456".equals(password)) {
session.setAttribute("LoginUser", username);
return "redirect:/main.html";//重定向到main.html页面,也就是跳转到dashboard页面
}
//如果用户名或者密码不正确
else {
model.addAttribute("msg", "用户名或者密码错误");//显示错误信息
return "index";//跳转到首页
}
}
}
然后再在实现自定义的登录拦截器,继承HandlerInterceptor
接口
- 其中获取存入的
session
进行判断,如果不为空,则放行; - 如果为空,则返回错误消息,并且返回到首页,不放行。
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//用户登录成功后,应该有自己的session
Object session = request.getSession().getAttribute("LoginUser");
if (session == null) {
request.setAttribute("msg", "权限不够,请先登录");
request.getRequestDispatcher("/index.html").forward(request, response);
return false;
} else {
return true;
}
}
}
然后配置到bean中注册,在MyMvcConfig
配置类中,重写关于拦截器的方法,添加我们自定义的拦截器,注意屏蔽静态资源及主页以及相关请求的拦截
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html", "/", "/user/login", "/css/**", "/js/**", "/img/**");
}
然后重启主程序进行测试,直接访问http://localhost:8080/main.html
展示员工信息——查
1. 实现Customers视图跳转
目标:点击dashboard.html
页面中的Customers
展示跳转到list.html
页面显示所有员工信息
因此,我们首先给dashboard.html
页面中Customers
部分标签添加href
属性,实现点击该标签请求/emps
路径跳转到list.html
展示所有的员工信息
同样修改list.html
对应该的代码为上述代码
我们在templates
目录下新建一个包emp
,用来放所有关于员工信息的页面,我们将list.html
页面移入该包中
然后编写请求对应的controller
,处理/emps
请求,在controller包下,新建一个EmployeeController
类
@Controller
public class EmployeeController {
@Autowired
private EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAllEmployees();
model.addAttribute(employees);
return "emp/list";//返回到list页面
}
}
然后我们重启主程序进行测试,登录到dashboard
页面,再点击Customers
,成功跳转到/emps
但是有些问题:
- 我们点击了
Customers后
,它应该处于高亮状态,但是这里点击后还是普通的样子,高亮还是在Dashboard
上 list.html
和dashboard.html
页面的侧边栏和顶部栏是相同的,可以抽取出来·
2. 提取页面公共部分
在templates
目录下新建一个commons
包,其中新建commons.html
用来放置公共页面代码
利用th:fragment
标签抽取公共部分(顶部导航栏和侧边栏)
commons.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<!--顶部导航栏,利用th:fragment提取出来,命名为topbar-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
<a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Company
name</a>
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">Sign out</a>
</li>
</ul>
</nav>
<!--侧边栏,利用th:fragment提取出来,命名为sidebar-->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="siderbar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-file">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
<polyline points="13 2 13 9 20 9"></polyline>
</svg>
Orders
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-shopping-cart">
<circle cx="9" cy="21" r="1"></circle>
<circle cx="20" cy="21" r="1"></circle>
<path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
</svg>
Products
</a>
</li>
<li class="nav-item">
<a class="nav-link" th:href="@{/emps}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-users">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
</svg>
Customers
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-bar-chart-2">
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
Reports
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-layers">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
Integrations
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Saved reports</span>
<a class="d-flex align-items-center text-muted"
href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
class="feather feather-plus-circle">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="16"></line>
<line x1="8" y1="12" x2="16" y2="12"></line>
</svg>
</a>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Current month
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Last quarter
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Social engagement
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
Year-end sale
</a>
</li>
</ul>
</div>
</nav>
</html>
然后删除dashboard.html
和list.html
中顶部导航栏和侧边栏的代码。再在dashboard.html
和list.html
删除的部分插入提取出来的公共部分topbar
和sidebar
<!--顶部导航栏-->
<div th:replace="~{commons/commons::topbar}" }></div>
12
<!--侧边栏-->
<div th:replace="~{commons/commons::siderbar}"></div>
测试,成功看到侧边栏和顶部栏,代表我们插入成功
3. 点击高亮处理
我们可以传递参数判断点击了哪个标签实现相应的高亮
首先在dashboard.html
的侧边栏标签传递参数active
为dashboard.html
同样在list.html
的侧边栏标签传递参数active
为list.html
然后我们在公共页面commons.html
相应标签部分利用thymeleaf
接收参数active
,利用三元运算符判断决定是否高亮
再次重启主程序测试,登录成功后,首先Dashboard
高亮
再点击Customers
,Customers
高亮,成功
也就是登录成功后会传递Dashboard,所以Dashboard高亮,点击Customers,传递list,所以Customers高亮。
4. 显示员工信息
修改list.html
页面,显示我们自己的数据值
修改完成后,重启主程序,登录完成后查看所有员工信息,成功显示
接下来修改一下性别的显示和date的显示,并添加编辑和删除两个标签,为后续做准备
<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>date</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.getId()}"></td>
<td th:text="${emp.getLastName()}"></td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.getDepartment().getDepartmentName()}"></td>
<td th:text="${#dates.format(emp.getDate(),'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<a class="btn btn-sm btn-primary">编辑</a>
<a class="btn btn-sm btn-danger">删除</a>
</td>
</tr>
</tbody>
再次重启主程序测试,成功
增加员工实现——增
1. list页面增加添加员工按钮
首先在list.html
页面增添一个增加员工按钮,点击该按钮时发起一个请求/add
<h2><a class="btn btn-sm btn-success" th:href="@{/add}">添加员工</a></h2>
然后编写对应的controller
,处理点击添加员工的请求
这里通过get
方式提交请求,在EmployeeController
中添加一个方法add
用来处理list
页面点击提交按钮的操作,返回到add.html
添加员工页面,我们即将创建
@GetMapping("/add")
public String add(Model model) {
//查出所有的部门信息,添加到departments中,用于前端接收
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments", departments);
return "emp/add";//返回到添加员工页面
}
2. 创建添加员工页面add.html
在templates/emp
下新建一个add.html
我们复制list.html
中的内容,将其中的table
用表格代替
<form>
<div class="form-group">
<label>LastName</label>
<input type="text" name="lastName" class="form-control" placeholder="lastname:zsr">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-control" placeholder="email:xxxxx@qq.com">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--注意这里的name是department.id,因为传入的参数为id-->
<select class="form-control" name="department.id">
<option th:each="department:${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<!--springboot默认的日期格式为yy/MM/dd-->
<input type="text" name="date" class="form-control" placeholder="birth:yyyy/MM/dd">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
我们重启主程序看看
点击添加员工,成功跳转到add.html
页面
下拉框中的内容不应该是1、2、3、4、5;应该是所有的部门名,我们遍历得到
<!--其中th:value用来表示部门的id,我们实际传入的值为id-->
<option th:each="department:${departments}" th:text="${department.getDepartmentName()}" th:value="${department.getId()}"></option>
3. add页面添加员工请求
在add.html
页面,当我们填写完信息,点击添加按钮,应该完成添加返回到list
页面,展示新的员工信息;因此在add.html
点击添加按钮的一瞬间,我们同样发起一个请求/add
,与上述提交按钮发出的请求路径一样,但这里发出的是post请求,Restful风格
然后编写对应的controller
,同样在EmployeeController
中添加一个方法addEmp
用来处理点击添加按钮的操作
@PostMapping("/add")
public String addEmp(Employee employee) {
employeeDao.addEmployee(employee);//添加一个员工
return "redirect:/emps";//重定向到/emps,刷新列表,返回到list页面
}
我们重启主程序,进行测试,进入添加页面,填写相关信息,注意日期格式默认为yyyy-MM-dd
application.properties`修改了默认的日期格式,默认的日期格式为`yyyy/MM/dd
#日期格式化
spring.mvc.date-format=yyyy-MM-dd
然后点击添加按钮,成功实现添加员工
修改员工信息——改
1. list页面编辑按钮增添请求
当我们点击编辑标签时,应该跳转到编辑页面edit.html
(我们即将创建)进行编辑
因此首先将list.html
页面的编辑标签添加href
属性,实现点击请求/edit/id
号到编辑页面
<a class="btn btn-sm btn-primary" th:href="@{/edit/}+${emp.getId()}}">编辑</a>
然后编写对应的controller
,在EmployeeController
中添加一个方法edit
用来处理list
页面点击编辑按钮的操作,返回到edit.html
编辑员工页面,我们即将创建
//restful风格接收参数
@RequestMapping("/edit/{id}")
public String edit(@PathVariable("id") int id, Model model) {
//查询指定id的员工,添加到empByID中,用于前端接收
Employee employeeByID = employeeDao.getEmployeeByID(id);
model.addAttribute("empByID", employeeByID);
//查出所有的部门信息,添加到departments中,用于前端接收
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments", departments);
return "/emp/edit";//返回到编辑员工页面
}
2. 创建编辑员工页面edit
在templates/emp
下新建一个edit.html
复制add.html
中的代码,稍作修改
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/edit}" method="post">
<div class="form-group">
<label>LastName</label>
<input th:value="${empByID.getLastName()}" type="text" name="lastName" class="form-control"
placeholder="lastname:zsr">
</div>
<div class="form-group">
<label>Email</label>
<input th:value="${empByID.getEmail()}" type="email" name="email" class="form-control"
placeholder="email:xxxxx@qq.com">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input th:checked="${empByID.getGender()==1}" class="form-check-input" type="radio"
name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input th:checked="${empByID.getGender()==0}" class="form-check-input" type="radio"
name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!--注意这里的name是department.id,因为传入的参数为id-->
<select class="form-control" name="department.id">
<option th:selected="${department.getId()==empByID.department.getId()}"
th:each="department:${departments}" th:text="${department.getDepartmentName()}"
th:value="${department.getId()}">
</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<!--springboot默认的日期格式为yy/MM/dd-->
<input th:value="${empByID.getDate()}" type="text" name="date" class="form-control"
placeholder="birth:yy/MM/dd">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
启动主程序测试,点击编辑1号用户
成功跳转到edit.html
,且所选用户信息正确
但是日期的格式不太正确,我们规定一下显示的日期格式
<!--springboot默认的日期格式为yy/MM/dd-->
<input th:value="${#dates.format(empByID.getDate(),'yyyy-MM-dd HH:mm:ss')}" type="text" name="date" class="form-control"
placeholder="birth:yy-MM-dd">
3. edit页面编辑完成提交请求
在edit.html
点击修改按钮的一瞬间,我们需要返回到list
页面,更新员工信息,因此我们需要添加href
属性,实现点击按钮时发起一个请求/edit
然后编写对应的controller,处理点击修改按钮的请求
同样在EmployeeController
中添加一个方法EditEmp
用来处理edit
页面点击添加的操作
@PostMapping("/edit")
public String EditEmp(Employee employee) {
employeeDao.addEmployee(employee);//修改一个员工
return "redirect:/emps";//修改完成重定向到/emps,刷新列表
}
然后指定修改人的id
<input type="hidden" name="id" th:value="${empByID.getId()}">
重启测试,同样修改1号用户名称为dddd
然后点击修改
成功修改并返回到list.html
删除员工信息——删
当我们点击删除
按钮时,应该发起一个请求,删除指定的用户,然后重新返回到list
页面显示员工数据
<a class="btn btn-sm btn-success" th:href="@{/delete/}+${emp.getId()}}">删除</a>
1
然后编写对应的controller
,处理点击删除按钮的请求,删除指定员工,重定向到/emps
请求,更新员工信息
@GetMapping("/delete/{id}")
public String delete(@PathVariable("id") Integer id) {
employeeDao.deleteEmployeeByID(id);
return "redirect:/emps";
}
重启测试,点击删除按钮即可删除指定员工
404页面定制
只需要在templates
目录下新建一个error
包,然后将404.html
放入其中,报错SpringBoot
就会自动找到这个页面
我们可以启动程序测试,随便访问一个不存在的页面,出现的404页面即是我们自己的404.html
注销操作
在我们提取出来的公共commons
页面,顶部导航栏处中的标签添加href
属性,实现点击发起请求/user/logout
然后编写对应的controller
,处理点击注销标签的请求,在LoginController
中编写对应的方法,清除session
,并重定向到首页
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/index.html";
}
重启测试,登录成功后,点击log out
即可退出到登录页
整合JDBC
Sping Data 官网:https://spring.io/projects/spring-data
数据库相关的启动器 :可以参考官方文档:
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
创建测试项目测试数据源
-
我去新建一个项目测试:springboot-03-data; 创建项目时,勾选
JDBC API
和MySQL Driver
spring data
-
项目创建完成之后,我们去pom.xml中可以发现,自动引入了如下启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
在resources中创建application.yml
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 #这个是MySQL 5版本的,要是八版本的话应该是:driver-class-name: com.mysql.cj.jdbc.Driver driver-class-name: com.mysql.jdbc.Driver
-
测试
@SpringBootTest class Springboot04DataApplicationTests { @Autowired //如果dataSource爆红线的话,可以这样@Autowired(required=false) DataSource dataSource; @Test void contextLoads() throws SQLException { //默认数据源 System.out.println(dataSource.getClass()); //获得连接 Connection connection = dataSource.getConnection(); System.out.println(connection); connection.close(); } }
结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource , 我们并没有手动配置
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
JDBCTemplate
1、有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
2、即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
3、数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
4、Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用
5、JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类
JdbcTemplate主要提供以下几类方法:
-
execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
-
update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
-
query方法及queryForXXX方法:用于执行查询相关语句;
-
call方法:用于执行存储过程、函数相关语句。
测试:创建一个controller的包,然后创建一个UserController类
package com.edu.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
import java.util.Map;
/**
* @author java小墩墩
* @version 1.0
*/
@RestController
@RequestMapping("jdbc")
public class UserController {
@Autowired(required = false)
JdbcTemplate jdbcTemplate;
@RequestMapping("/select")
public List<Map<String,Object>> select(){
String sql="select * from user";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
return list;
}
@RequestMapping("/insert")
public String insert(){
String sql="insert into user values(9,'是我是我','951')";
jdbcTemplate.update(sql);
return "insertOk";
}
@RequestMapping("/update/{id}")
public String update(@PathVariable("id") int id){
String sql = "update user set name =? ,pwd = ? where id = " +id;
Object[] objects = new Object[2];
objects[0] = "www";
objects[1] = "999999999";
jdbcTemplate.update(sql,objects);
return "updateOk";
}
@RequestMapping("/delete/{id}")
public String delete(@PathVariable("id") int id){
String sql = "delete from user where id = ?";
jdbcTemplate.update(sql,id);
return "deleteOk";
}
}
整合Druid
Druid简介
Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
配置数据源
1.导入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
2.在配置文件中切换数据源
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
#这个是MySQL 5版本的,要是八版本的话应该是:driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 切换数据源
3.可以测试是否切换成功,然后可以设置一些属性;
注意:空格不要错了,不然会报错
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
#这个是MySQL 5版本的,要是八版本的话应该是:driver-class-name: com.mysql.cj.jdbc.Driver
driver-class-name: com.mysql.jdbc.Driver
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
4.需要自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;我们需要 自己添加 DruidDataSource 组件到容器中,并绑定属性;
@Configuration
public class DruidConfig {
/*
将自定义的 Druid数据源添加到容器中,不再让 Spring Boot 自动创建
绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource从而让它们生效
@ConfigurationProperties(prefix = "spring.datasource"):作用就是将 全局配置文件中
前缀为 spring.datasource的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
*/
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource() {
return new DruidDataSource();
}
}
5.测试
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
//DI注入数据源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
//看一下默认数据源
System.out.println(dataSource.getClass());
//获得连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
//关闭连接
connection.close();
}
}
配置Druid监控
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
所以第一步需要设置 Druid 的后台管理页面,比如登录账号、密码等;
配置后台管理:
这个是一个方法,在DruidConfig类中
//配置Druid监控管理后台的servlet
//内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");
// 这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
// 的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
Map<String, String> initParams = new HashMap<>();
initParams.put("loginUsername","admin");//设置用户名,一定要用"loginUsername",是固定的,下面也一样
initParams.put("loginPassword","123456");//设置密码
//后台允许谁可以访问
//initParams.put("allow", "localhost"):表示只有本机可以访问
//initParams.put("allow", ""):为空或者为null时,表示允许所有访问
initParams.put("allow","");
//deny:Druid 后台拒绝谁访问
//initParams.put("kuangshen", "192.168.1.20");表示禁止此ip访问
//设置初始化参数
bean.setInitParameters(initParams);
return bean;
}
配置完毕后,我们可以选择访问 :http://localhost:8080/druid/
,输入用户名密码就能进入
配置 Druid web 监控 filter 过滤器
这个是一个方法,在DruidConfig类中
//配置 Druid 监控 之 web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStaFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
Map<String, String> initParams = new HashMap<>();
initParams.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParams);
//"/*" 表示过滤所有请求
bean.setUrlPatterns(Arrays.asList("/*"));
return bean;
}
整合MyBatis
-
导入 MyBatis 所需要的依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.1</version> </dependency>
-
配置数据库连接信息
application.properties
spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
-
创建实体类
package com.edu.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author java小墩墩 * @version 1.0 */ //记得导入依赖Lombok @Data @NoArgsConstructor @AllArgsConstructor public class User { private int id; private String name; private String pwd; }
-
创建mapper包,再创建UserMapper接口
@Mapper : 表示本类是一个 MyBatis 的 Mapper
package com.edu.mapper; import com.edu.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.springframework.stereotype.Repository; import java.util.List; /** * @author java小墩墩 * @version 1.0 */ @Mapper @Repository public interface UserMapper { //查询所有用户 public List<User> getUserList(); //插入用户 public int insertUser(User user); //修改用户 public int updateUser(User user); //删除用户 public int deleteUser(@Param("uid") int id); }
也可在启动类加注解,扫描获取所有的mapper接口
@MapperScan(“com.kuang.mapper”
-
在资源目录(resources)下创建文件夹mybatis,再创建mapper文件夹,在文件夹中创建UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.edu.mapper.UserMapper"> <!--查询所有用户--> <select id="getUserList" resultType="User"> select * from user; </select> <!--插入用户--> <insert id="insertUser" parameterType="User"> insert into user values(#{id},#{name},#{pwd}); </insert> <!--更新用户--> <update id="updateUser" parameterType="User"> update user set name = #{name},pwd=#{pwd} where id=#{id}; </update> <!--删除用户--> <delete id="deleteUser" parameterType="_int"> delete from user where id = #{uid}; </delete> </mapper>
-
在application.properties中添加:
#整合mybatis mybatis.type-aliases-package=com.edu.pojo mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
-
创建controller包,再创建UserController类
package com.edu.controller; import com.edu.mapper.UserMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; /** * @author java小墩墩 * @version 1.0 */ @RestController public class UserController { @Autowired private UserMapper userMapper; @RequestMapping("/queryList") public List queryList(){ return userMapper.getUserList(); } }
-
测试即可
springSecurity(安全)
SpringSecurity简介
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
实战测试
-
新建一个初始的springboot项目
web模块
-
加入thymeleaf依赖:
<dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> </dependency> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-java8time</artifactId> </dependency>
-
导入静态资源,放置如下:
-
编写控制器
package com.edu.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; /** * @author java小墩墩 * @version 1.0 */ @Controller public class RouterController { @RequestMapping({"/","/index"}) public String index(){ return "index"; } @RequestMapping("/toLogin") public String toLogin(){ return "views/login"; } @RequestMapping("/level1/{id}") public String level1(@PathVariable("id") int id){ return "views/level1/"+id; } @RequestMapping("/level2/{id}") public String level2(@PathVariable("id") int id){ return "views/level2/"+id; } @RequestMapping("/level3/{id}") public String level3(@PathVariable("id") int id){ return "views/level3/"+id; } }
-
然后运行测试一下,看看是否能进入首页:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1c3zGiwI-1661431067947)(D:\myDownloads\typora\img\image-20220822183633542.png)]
-
引入
Spring Security 模块
和thymeleaf和springsecurity整合模块
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
-
创建
config
包,新建SecurityConfig
类,这里是完整的注意要添加注解:@EnableWebSecurity //开启WebSecurity模式
package com.edu.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; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; /** * @author java小墩墩 * @version 1.0 */ @EnableWebSecurity //开启WebSecurity模式 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { //授权规则 http.authorizeRequests() .antMatchers("/").permitAll()//首页所有人都可以访问 .antMatchers("/level1/**").hasRole("vip1")// level1下的只有权限为vip1的可以访问 .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); //开启登录功能 http.formLogin() //登录时跳到spring security 默认的登录页 .loginPage("/toLogin") //登录时跳到自己定义的登录页 .usernameParameter("username") //检验用户名 .passwordParameter("password") //校验密码 .loginProcessingUrl("/login"); //登陆表单提交请求 //开启注销功能 http.logout() .logoutSuccessUrl("/");//注销成功跳到首页 // 开启记住我功能 http.rememberMe().rememberMeParameter("remember"); http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求 } //认证 @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) //内存中验证 .withUser("kuang").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3") .and() .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1") .and() .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3"); } }
-
index.html页面
注意:
-
在html中 加入
xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
-
修改登录,注销 那段
-
权限修改
sec:authorize="hasRole('vip1')" :有vip1权限的显示这段代码 sec:authorize="hasRole('vip2')" :有vip2权限的显示这段代码 sec:authorize="hasRole('vip3')" :有vip3权限的显示这段代码
全部内容如下:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>首页</title> <!--semantic-ui--> <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet"> <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet"> </head> <body> <!--主容器--> <div class="ui container"> <div class="ui segment" id="index-header-nav" th:fragment="nav-menu"> <div class="ui secondary menu"> <a class="item" th:href="@{/index}">首页</a> <!--登录注销--> <div class="right menu"> <!--未登录--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i> 登录 </a> </div> <div> <a class="item"> <i class="address card icon"></i> 用户名: <span sec:authentication="name"></span> <!-- 用户角色,不知道什么原因,不能用,用了会报错500--> <!-- 用户角色:<span sec:authentication="principal.authorities"></span>--> </a> </div> <!-- 已经登陆--> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="address card icon"></i>注销 </a> </div> </div> </div> </div> <div class="ui segment" style="text-align: center"> <h3>Spring Security Study by 秦疆</h3> </div> <div> <br> <div class="ui three column stackable grid"> <div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 1</h5> <hr> <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div> <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div> <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 2</h5> <hr> <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div> <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div> <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 3</h5> <hr> <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div> <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div> <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div> </div> </div> </div> </div> </div> </div> </div> <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script> <script th:src="@{/qinjiang/js/semantic.min.js}"></script> </body> </html>
login.html中,在密码下面增加:
<div class="field"> <input type="checkbox" name="remember">记住我 </div>
-
shiro
什么是Shiro
● Apache Shiro
是一个Java 的安全(权限)框架。
● Shiro
可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
● Shiro
可以完成,认证,授权,加密,会话管理,Web集成,缓存等.
● 下载地址:http://shiro.apache.org/
有哪些功能
● Authentication
: 身份认证、登录,验证用户是不是拥有相应的身份;
● Authorization
:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
● Session Manager
: 会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
● Cryptography
: 加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
● Web Support
Web支持,可以非常容易的集成到Web环境;
● Caching
缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
● Concurrency
: Shiro支持多线程应用的并发验证,即,如在-个线程中开启另-一个线程,能把权限自动的传播过去
● Testing
:提供测试支持;
● RunAs
:允许一个用户假装为另-一个用户(如果他们允许)的身份进行访问;
● Remember Me
:记住我,这个是非常常见的功能,即一次登录后, 下次再来的话不用登录了
Shiro架构(外部)
● subject
: 应用代码直接交互的对象是Subject, 也就是说Shiro的对外API核心就是Subject, Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager; Subject其实是一个门面, SecurityManageer 才是实际的执行者
● SecurityManager
: 安全管理器,即所有与安全有关的操作都会与SercurityManager交互, 并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
● Realm
: Shiro从Realm获取安全数据 (如用户,角色,权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法;也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看DataSource;
Shiro架构(内部)
● Subject
: 任何可以与应用交互的用户;
● Security Manager
:相当于SpringMVC中的DispatcherSerlet; 是Shiro的心脏, 所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject, 且负责进行认证,授权,会话,及缓存的管理。
● Authenticator
:负责Subject认证, 是-一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
● Authorizer
:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中
的那些功能;
● Realm
: 可以有一个或者多个的realm, 可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以- -般在应用中都需要实现自己的realm
● SessionManager
:管理Session生 命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
● CacheManager
: 缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
● Cryptography
:密码模块,Shiro 提高了一些常见的加密组件用于密码加密, 解密等
Shiro快速开始
创建一个普通maven
项目springboot-06-shiro
,然后删除src目录,这样的话就可以在这个项目里新建很多model.
在springboot-06-shiro里新建model shiro-01-helloshiro
父依赖pom.xml
<dependencies>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.21</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
<scope>runtime</scope>
</dependency>
</dependencies>
log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache=WARN
# Spring
log4j.logger.org.springframework=WARN
# Default Shiro logging
log4j.logger.org.apache.shiro=INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
shiro.ini
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz
# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
Quickstart.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.text.IniRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
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) {
DefaultSecurityManager defaultSecurityManager=new DefaultSecurityManager();
IniRealm iniRealm=new IniRealm("classpath:shiro.ini");
defaultSecurityManager.setRealm(iniRealm);
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 获得当前用户对象 Subject
Subject currentUser = SecurityUtils.getSubject();
// 通过当前用户拿到session
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 + "]");
}
// 判断当前用户是否被认证
if (!currentUser.isAuthenticated()) {
//Token:令牌
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.");
}
//粗粒度
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.");
}
//细粒度
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!");
}
//注销
currentUser.logout();
//结束
System.exit(0);
}
}
执行
总结下来,就是下面这几个方法:springSecurity中都有!
springboot整合shiro
项目结构图
下面文件的创建都按照下图
数据库
程序代码
新建一个springboot项目,勾选web,thymeleaf模块
pom.xml
<dependencies>
<!--shiro-thymeleaf整合-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
<!-- lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
</dependency>
<!--引入mybatis,这是mybatis官方提供的适配springboot的,而不是springboot自己的-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!-- 连接数据库的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!--shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<!-- thymeleaf-->
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
application.yml
spring:
datasource:
username: root
password: 123456
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.jdbc.Driver
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.kuang.pojo
mapper-locations: classpath:mapper/*.xml
UserRealm.java
package com.edu.config;
import com.edu.pojo.User;
import com.edu.service.UserService;
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.AuthorizingRealm;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @author java小墩墩
* @version 1.0
*/
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了授权");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo ();
//拿到当前登录的对象
Subject subject = SecurityUtils.getSubject();
//拿到user对象
User currentUser = (User) subject.getPrincipal();
//添加权限(数据库中拿的)
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了认证");
UsernamePasswordToken userToken=(UsernamePasswordToken)token;
// //用户名、密码 模拟从数据库中获取
// String name = "root";
// String password = "1111";
//
// //用户名认证
// if (!userToken.getUsername().equals(name)){
// return null;//抛出异常 UnknownAccountException
// }
//
// //密码认证,shiro做
// return new SimpleAuthenticationInfo("",password,"");
//连接真实数据库
User user = userService.queryUserByName(userToken.getUsername());
if (user==null){//没有这个人
return null;//抛出异常 UnknownAccountException
}
Subject subject = SecurityUtils.getSubject();
Session session = subject.getSession();
session.setAttribute("loginUser",user);
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}
}
ShiroConfig.java
package com.edu.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
import com.fasterxml.jackson.databind.deser.impl.NullsAsEmptyProvider;
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;
/**
* @author java小墩墩
* @version 1.0
*/
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean 第三步
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
Map<String, String> filterMap = new LinkedHashMap<>();
// 添加Shiro内置过滤器
/**
* Shiro内置过滤器,可以实现权限相关的拦截器
* 常用的过滤器:
* anon:无需认证(登陆)可以访问
* authc:必须认证才可以访问
* user:如果使用rememberMe的功能,可以直接访问
* perms:该资源必须得到资源权限才可以访问
* role:该资源必须得到角色权限才可以访问
*/
//拦截,必须有什么权限才能访问
// filterMap.put("/user/*","perms[root]");
filterMap.put("/user/add","perms[user:add]");
filterMap.put("/user/update","perms[user:update]");
//拦截,必须认证才能访问
filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//设置登录请求
bean.setLoginUrl("/toLogin");
//若访问时用户未被授权,则跳转至未授权页面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
//DefaultWebSecurityManager 第二步
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联UserRealm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
//整合ShiroDialect:用来整合shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
MyController.java
package com.edu.controller;
import com.sun.org.apache.xpath.internal.operations.Mod;
import javafx.print.PaperSource;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
/**
* @author java小墩墩
* @version 1.0
*/
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,shiro");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@RequestMapping("/login")
public String login(String username,String password,Model model){
//获取当前用户
Subject subject = SecurityUtils.getSubject();
//封装用户登录
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
try {
subject.login(token);//执行登录的方法,如果没有异常就说明ok了
return "index";//登录成功跳到首页
} catch (UnknownAccountException e) {
model.addAttribute("msg","用户名不存在");
return "login";
}catch (IncorrectCredentialsException e){
model.addAttribute("msg","密码错误");
return "login";
}
}
@RequestMapping("/noauth")
@ResponseBody
public String noAuth(){
return "未授权,无法访问";
}
@RequestMapping("/logout")
public String logout(){
//获取当前用户
Subject subject = SecurityUtils.getSubject();
// session 会销毁,在SessionListener监听session销毁,清理权限缓存
subject.logout();
return "redirect:index";
}
}
User.java
package com.edu.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author java小墩墩
* @version 1.0
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
UserMapper.java
package com.edu.mapper;
import com.edu.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
/**
* @author java小墩墩
* @version 1.0
*/
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.edu.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="com.edu.pojo.User">
select * from user where name = #{name}
</select>
</mapper>
UserService.java
package com.edu.service;
import com.edu.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.springframework.stereotype.Service;
/**
* @author java小墩墩
* @version 1.0
*/
public interface UserService {
public User queryUserByName(String name);
}
UserServiceImpl.java
package com.edu.service;
import com.edu.mapper.UserMapper;
import com.edu.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @author java小墩墩
* @version 1.0
*/
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="https://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<div th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</div>
<div th:if="${session.loginUser!=null}">
<a th:href="@{/logout}">注销</a>
</div>
<div shiro:hasPermission="user:add">
<a th:href="@{/user/add}">add</a>
</div>
<div shiro:hasPermission="user:update">
<a th:href="@{/user/update}">update</a>
</div>
</body>
</html>
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.w3.org/1999/xhtml?">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p th:text="${msg}" style="color:red;"></p>
<form th:action="@{/login}">
<p>用户名: <input type="text" name="username"></p>
<p>密码:<input type="password" name="password"></p>
<p><input type="submit" name="提交"></p>
</form>
</body>
</html>
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>
总结
Shiro 三大要素
subject` -> `ShiroFilterFactoryBean`
`securityManager` -> `DefaultWebSecurityManager`
`realm`
实际操作中对象创建的顺序 : `realm -> securityManager -> subject
流程梳理:
1.用户进入首页点击跳转,Shiro内置过滤器进行拦截,看过滤器的设置,未认证跳转到登录页面,未授权跳转到未授权界面;
2.认证
- 用户进入登录页面,输入用户名密码准备进行认证,点击登录按钮后,会请求/login,
- 首先调用Subject.login(token) 进行登录,其会自动委托给SecurityManager,
- SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,Authenticator 会把相应的token 传入Realm,从Realm 获取身份验证信息,如果没有返回/抛出异常表示身份验证失败了,如果有就返回AuthenticationInfo验证信息,此信息中包含了身份(pricipals)及凭证,也就是账号密码。
3.授权
- 对subject进行授权,调用方法isPermitted(“permission串”),递交给SecurityManager
- SecurityManager将权限检测操作委托给Authorizer授权管理器对象
- Authorizer执行Realm(自定义的Realm)从数据库查询权限数据并封装
- Authorizer对用户授权信息进行判定(判断用户访问资源时需要什么权限,假如用户所具有的权限包含这个资源访问时所需要的权限,那么用户就可以访问这个资源了)。
Swagger
概念
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。
特点
- 号称世界上最流行的API框架
- Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
- 直接运行,在线测试API
- 支持多种语言 (如:Java,PHP等)
- 官网:https://swagger.io/
springboot集成swagger
1.新建springboot项目,添加web依赖
2.导入swagger所需依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
3.要使用Swagger,我们需要编写一个配置类-SwaggerConfig来配置 Swagger
@Configuration //配置类
@EnableSwagger2// 开启Swagger2的自动配置
public class SwaggerConfig {
}
4.访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;
配置swagger
1、Swagger实例Bean是Docket
,所以通过配置Docket实例来配置Swaggger。
在SwaggerConfig类中
@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2);
}
2.可以通过apiInfo()
属性配置文档信息
//配置文档信息
private ApiInfo apiInfo() {
Contact contact = new Contact("联系人名字", "http://xxx.xxx.com/联系人访问链接", "联系人邮箱");
return new ApiInfo(
"Swagger学习", // 标题
"学习演示如何配置Swagger", // 描述
"v1.0", // 版本
"http://terms.service.url/组织链接", // 组织链接
contact, // 联系人信息
"Apach 2.0 许可", // 许可
"许可链接", // 许可连接
new ArrayList<>()// 扩展
);
}
3、Docket 实例关联上 apiInfo()
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
4、重启项目,访问测试 http://localhost:8080/swagger-ui.html
看下效果;
配置扫描接口
1、构建Docket时通过select()
方法配置怎么扫描接口。
在SwaggerConfig类中
@Bean
public Docket docket() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.build();
}
2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类
3、除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
//RequestHandlerSelectors 配置要扫描接口的方式
//basePackage 指定要扫描的包
//any() 扫描全部
//none() 不扫描
//withMethodAnnotation 扫描方法上的注解,参数是注解的反射对象(GetMapping.class)
//withClassAnnotation 扫描类上的注解(RestController.class)
4、除此之外,我们还可以配置接口扫描过滤:
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.paths(PathSelectors.ant("/kuang/**")) //请求中有kuang的
.build();
}
配置Swagger开关
1、通过enable()
方法配置是否启用swagger,如果是false,swagger将不能在浏览器中访问了
@Bean
public Docket docket(Environment environment){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false)
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.paths(PathSelectors.ant("/hello"))
.build();
}
2、如何动态配置当项目处于test、dev环境时显示swagger,处于prod时不显示
在resource中创建
application-dev.properties
server.port=8081
application-pro.properties
server.port=8082
application.properties
spring.profiles.active=dev
在SwaggerConfig类中
@Bean
public Docket docket(Environment environment) {
// 设置要显示swagger的环境
Profiles of = Profiles.of("dev", "test");
// 判断当前是否处于该环境
// 通过 enable() 接收此参数判断是否要显示
boolean flag = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag) //配置是否启用Swagger,如果是false,在浏览器将无法访问
.select()// 通过.select()方法,去配置扫描接口,RequestHandlerSelectors配置如何扫描接口
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
// 配置如何通过path过滤,即这里只扫描请求以/kuang开头的接口
.paths(PathSelectors.ant("/kuang/**"))
.build();
}
springboot切换使用环境,如果是Profiles.of("pro", "test")
这里有的环境,则正常显示,如果是这里没有的则无法使用。
配置API分组
1、如果没有配置分组,默认是default。通过groupName()
方法即可配置分组
@Bean
public Docket docket(Environment environment) {
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.groupName("hello") // 配置分组
// 省略配置....
}
2.如何配置多个分组
?配置多个分组只需要配置多个docket即可
@Bean
public Docket docket(Environment environment){
Profiles of = Profiles.of("dev", "test");
boolean flag = environment.acceptsProfiles(of);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("狂神")
.enable(flag)
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.controller"))
.paths(PathSelectors.ant("/hello"))
.build();
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
}
配置注释信息
实体类上注解
@ApiModel("用户实体类")
public class User {
@ApiModelProperty("用户名")
public String name;
@ApiModelProperty("密码")
public String password;
}
方法返回值中有存在实体类,就会被扫描
@GetMapping("/user")
public User hello2(){
return new User();
}
方法上注解
@ApiOperation("测试方法")
@GetMapping("/test")
public String hello3(@ApiParam("用户名") String name){
return name;
}
常用注解
@Api(tags = "xxx模块说明")
:作用在模块类上
@ApiOperation("xxx接口说明")
:作用在接口方法上
@ApiModel("xxxPOJO说明")
:作用在模型类上:如VO、BO
@ApiModelProperty(value = "xxx属性说明",hidden = true)
:作用在方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam("xxx参数说明")
:作用在参数、方法和字段上,类似@ApiModelProperty
相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
其他皮肤
1、默认
访问 http://localhost:8080/swagger-ui.html
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
2、bootstrap-ui
访问 http://localhost:8080/doc.html
<!-- 引入swagger-bootstrap-ui包 /doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.1</version>
</dependency>
3、Layui-ui
访问 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
4、mg-ui
访问 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
<groupId>com.zyplayer</groupId>
<artifactId>swagger-mg-ui</artifactId>
<version>1.0.6</version>
</dependency>
异步、定时、邮件任务
异步任务
1.创建一个AsyncService类
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中......");
}
}
2.创建一个AsyncController类
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello(){
asyncService.hello();
return "sucess";
}
}
3.测试,发现界面没有立即返回结果,而是等后台执行完才返回。
4.给hello方法添加@Async注解;
@Service
public class AsyncService {
@Async //告诉spring这是一个异步方法
public void hello(){
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中......");
}
}
SpringBoot就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解@EnableAsync ,开启异步注解功能;
@EnableAsync //开启异步注解功能
@SpringBootApplication
public class Springboot08TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot08TaskApplication.class, args);
}
}
重启测试,网页瞬间响应,后台代码继续执行!
邮件任务
邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持
- 邮件发送需要引入spring-boot-start-mail
- SpringBoot 自动配置MailSenderAutoConfiguration
- 定义MailProperties内容,配置在application.yml中
- 自动装配JavaMailSender
- 测试邮件发送
1.引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
查看一下MailProperties
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host;
private Integer port;
private String username;
private String password;
private String protocol = "smtp";
private Charset defaultEncoding;
private Map<String, String> properties;
private String jndiName;
}
2.配置文件
spring.mail.username=24736743@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
# qq需要配置ssl
spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在QQ邮箱中的设置->账户->开启pop3和smtp服务
3.Spring单元测试
@SpringBootTest
class Springboot08TaskApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
//方式一:简单的邮件
SimpleMailMessage message = new SimpleMailMessage();
message.setSubject("通知-明天放假");
message.setText("今晚7:30开会");
message.setTo("24736743@qq.com");
message.setFrom("24736743@qq.com");
mailSender.send(message);
}
@Test
void contextLoads2() throws MessagingException {
//方式二:复杂的邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setSubject("通知-明天上课");
helper.setText("<b style='color:red'>今天 7:30来开会</b>",true);
helper.addAttachment("1.jpg",new File("C:\\Users\\Lenovo\\Desktop\\1.jpg"));
//helper.addAttachment("2.jpg",new File(""));
helper.setTo("24736743@qq.com");
helper.setFrom("24736743@qq.com");
mailSender.send(mimeMessage);
}
}
查看邮箱,邮件接收成功!
定时任务
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式,提供了两个接口。
- TaskExecutor接口
- TaskScheduler接口
两个注解:
- @EnableScheduling
- @Scheduled
cron表达式:
1.创建一个ScheduledService类
@Service
public class ScheduledService {
//cron表达式
@Scheduled(cron = "0 9 20 * * 0-7") //每天20:09执行
public void hello(){
System.out.println("hello Scheduled....");
}
}
2.需要在主程序上增加@EnableScheduling 开启定时任务功能
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class Springboot08TaskApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot08TaskApplication.class, args);
}
}
测试
3、我们来详细了解下cron表达式;
http://www.bejson.com/othertools/cron/
4.常用的表达式
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED 表示每个星期三中午12点
(7)0 0 12 * * ? 每天中午12点触发
(8)0 15 10 ? * * 每天上午10:15触发
(9)0 15 10 * * ? 每天上午10:15触发
(10)0 15 10 * * ? 每天上午10:15触发
(11)0 15 10 * * ? 2005 2005年的每天上午10:15触发
(12)0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
(18)0 15 10 15 * ? 每月15日上午10:15触发
(19)0 15 10 L * ? 每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
Dubbo和Zookeeper集成
分布式理论
什么是分布式系统?
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
分布式服务架构
当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。
什么是RPC
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理
步骤解析:
RPC两个核心模块:通讯,序列化。
测试环境搭建
Zookeeper
ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
下载Zookeeper:https://mirrors.tuna.tsinghua.edu.cn/apache/zookeeper/
选择apache-zookeeper-x.x.x-bin.tar.gz
下载,带x.x.x-bin
的是已编译的,另一个运行会报错。
运行/bin/zkServer.cmd
,初次运行会报错,没有zoo.cfg
配置文件;
可能遇到问题:闪退 !
解决方案:编辑zkServer.cmd
文件末尾添加pause
。这样运行出错就不会退出,会提示错误信息,方便找到原因。
创建zoo.cfg
配置文件
将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。
注意几个重要位置:
- dataDir=./ 临时数据存储的目录(可写相对路径)
- clientPort=2181 zookeeper的端口号
修改完成后再次启动zookeeper
使用zkCli.cmd
测试
ls /
:列出zookeeper根下保存的所有节点create –e /kuangshen 123
:创建一个kuangshen节点,值为123get /kuangshen
:获取/kuangshen节点的值
Dubbo
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo官网 http://dubbo.apache.org/zh-cn/index.html
服务提供者(Provider)
:暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer)
:调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry)
:注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
监控中心(Monitor)
:服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
window下安装dubbo-admin
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
下载地址:https://github.com/apache/dubbo-admin/tree/master
下载这个
解压进入目录
修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
dubbo.registry.address=zookeeper://127.0.0.1:2181
12345678910
在项目目录下打包dubbo-admin
即:xxxx\xxx\dubbo-admin-master-0.2.0
目录下,在前面加 cmd 再加一个空格,然后输入下面这个
mvn clean package -Dmaven.test.skip=true
1
执行 dubbo-admin\target 下的dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
1
注意:执行jar包前要先执行zookeeper的服务端
执行完毕,我们去访问一下 http://localhost:7001/
, 这时候我们需要输入登录账户和密码,我们都是默认的root-root;
登录成功后,查看界面
SpringBoot 整合Dubbo + zookeeper
先创建一个空项目,然后创建两个module服务提供者
和一个服务消费者
分别为:
provider-server
consumer-server
1.导包,两个模块的包都是一样的
<!--dubbo-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.3</version>
</dependency>
<!--zookeeper-->
<!-- 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>
2.服务提供者:provider-server
编写接口
package com.kuang.provider.service;
public interface TicketService {
public String getTicket();
}
12345
编写实现类
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;
@Service //将服务发布出去
@Component //放在容器中
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!
在springboot配置文件中配置dubbo相关属性!
server.port=8001
#当前应用名字
dubbo.application.name=provider-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
#扫描指定包下服务
dubbo.scan.base-packages=com.edu.service
3.服务消费者:consumer-server
消费者的服务类
注意:要将``provider-server中的TicketService接口,复制一份到
consumer-server`的service包下
package com.edu.service;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
/**
* @author java小墩墩
* @version 1.0
*/
@Service
public class UserService {
@Reference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名
TicketService ticketService;
public void bugTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到"+ticket);
}
}
配置参数
server.port=8002
#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同
测试类编写
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
userService.bugTicket();
}
}
测试
1.启动zookeeper服务端
2.运行dubbo的jar包
3.运行springboot项目(开启服务者)
4.消费者消费测试,结果:
监控中心 :
这就是SpingBoot + dubbo + zookeeper
实现分布式开发的应用,其实就是一个服务拆分的思想。
问题:
启动springboot项目后,zookeeper服务端直接强迫关闭连接。
解决方法:(可以试试,不一定解决)
可以在网上所搜,window端口占用解决方法,然后跟着步骤走,看看你的端口号有没有被占用,如果有先关闭,在重新开启就可以解决;
完!
程序员,不要停下学习的脚步!