springboot

发布时间 2024-01-07 19:26:08作者: PursueExcellence

介绍

Spring回顾

Spring(容器)是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson 。

Spring是为了解决企业级应用开发的复杂性而创建的,简化开发。

Spring是如何简化Java开发的

为了降低Java开发的复杂性,Spring采用了以下4种关键策略:

​ 1、基于POJO的轻量级和最小侵入性编程,所有东西都是bean;

​ 2、通过IOC,依赖注入(DI)和面向接口实现松耦合;

​ 3、基于切面(AOP)和惯例进行声明式编程;

​ 4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;

什么是SpringBoot

介绍

学过javaweb的同学就知道,开发一个web应用,从最初开始接触Servlet结合Tomcat, 跑出一个Hello Wolrld程序,是要经历特别多的步骤;后来就用了框架Struts,再后来是SpringMVC,到了现在的SpringBoot,过一两年又会有其他web框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;

言归正传,什么是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 领域最热门的技术。

优点

1、为所有Spring开发者更快的入门

2、开箱即用,提供各种默认配置来简化项目配置

3、内嵌式容器简化Web项目

4、没有冗余代码生成和XML配置的要求

第一个SpringBoot程序

准备工作

我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。

通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

我的环境准备:

  • java version1.8
  • Maven-3.6.1
  • SpringBoot 2.x 最新版

开发工具:IDEA

创建项目

官网创建

image-20230906161353524

image-20230906161427480

image-20230906161515354

image-20230906161627680

下载项目压缩包并到指定的文件夹解压

image-20230906161749615

用idea打开

image-20230906173746539

image-20230906173816501

image-20230906173838849

image-20230906161827059

如果导包出现问题,可能是springboot版本的原因,尝试切换版本

idea创建

idea集成了springboot网站,所以可以从idea直接创建,一般开发也是用idea直接创建

image-20230906161137398

image-20230906161231568

创建完成

image-20230906161308758

启动项目

image-20230906181240671

启动成功

image-20230906181257711

测试访问

image-20230906181502295

因为这时候项目什么都没有,所以是错误页面

pom.xml分析

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!--有一个父项目-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.15</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.jjh</groupId>
    <artifactId>helloword2</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>helloword2</name>
    <description>helloword2</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!--
            spring-boot-starter:所有的springboot依赖都是使用这个开头的
        -->
        <!-- 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- 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>

</project>

基础开发

目录结构

image-20230907154758650

基础接口编写

HelloController

@Controller
@RequestMapping("/hello")
public class HelloController {

    @GetMapping("hello")
    // @ResponseBody 用于返回字符串,而不是寻找视图。
    // 用在类上的注解@RestController能实现相同的效果
    @ResponseBody
    public String hello(){
        return "hello";
    }

}

修改服务端口

springboot内嵌tomcat,所以服务的默认端口是8080,可通过配置文件修改

application.properties

# 服务端口
server.port=8081

修改banner标志

banner.txt

             _
            | |
 _ __   ___ | |__  _   _  __ _
| '_ \ / _ \| '_ \| | | |/ _` |
| | | | (_) | |_) | |_| | (_| |
|_| |_|\___/|_.__/ \__,_|\__, |
                          __/ |
                         |___/

原理初探

pom.xml

父依赖

其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件!

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.15</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

点进去,发现还有一个父依赖

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.7.15</version>
</parent>

这里才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;

注意:

  • 以后我们导入依赖默认是不需要写版本;
  • 但是如果导入的包没有在依赖中管理着就需要手动配置版本了;

启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

启动器:说白了就是springboot的启动场景

比如spring-boot-starter-web启动器,帮我们自动导入了web环境所有的依赖

springboot会将所有的功能场景,都变成一个个启动器

我们想要什么功能,就只需要找到对应的启动器(starter)就可以了

注解

启动类

// @SpringBootApplication:标注这个类是一个springboot的应用
@SpringBootApplication
public class Springboot01HellowordApplication {

    public static void main(String[] args) {
        // 将springboot启动
        SpringApplication.run(Springboot01HellowordApplication.class, args);
    }

}

但是一个简单的启动类并不简单!我们来分析一下这些注解都干了什么

注解

@SpringBootApplication

标注在某个类上说明这个类是SpringBoot的主配置类 ,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用;

进入这个注解:可以看到上面还有很多其他注解!

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {

@ComponentScan

这个注解在Spring中很重要 ,它对应XML配置中的元素。

作用:自动扫描并加载符合条件的组件或者bean , 将这个bean定义加载到IOC容器中

@SpringBootConfiguration

SpringBoot的配置类 ,标注在某个类上 , 表示这个类是一个SpringBoot的配置类;

我们继续进去这个注解查看

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}
@Configuration

说明这是一个配置类 ,配置类就是对应Spring的xml 配置文件;

@Component

这就说明,启动类本身也是Spring中的一个组件而已,负责启动应用!

@EnableAutoConfiguration

开启自动配置功能,以前我们需要自己配置的东西,而现在SpringBoot可以自动帮我们配置

@EnableAutoConfiguration告诉SpringBoot开启自动配置功能,这样自动配置才能生效;

@AutoConfigurationPackage

自动配置包

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

@import

Spring底层注解@import , 给容器中导入一个组件

Registrar.class:将主启动类的所在包及包下面所有子包里面的所有组件扫描到Spring容器 ;

@Import({})

@Import({AutoConfigurationImportSelector.class})

AutoConfigurationImportSelector:自动配置导入选择器

那么它会导入哪些组件的选择器呢?我们点击去这个类看源码

1.getCandidateConfigurations 获取所有的配置

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
    ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
    Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. 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<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoader == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }

    String factoryTypeName = factoryType.getName();
    return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

3、我们继续点击查看 loadSpringFactories 方法

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = (Map)cache.get(classLoader);
    if (result != null) {
        return result;
    } else {
        HashMap result = new HashMap();

        try {
            Enumeration urls = classLoader.getResources("META-INF/spring.factories");

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                UrlResource resource = new UrlResource(url);
                // 所有的资源都加载到配置类中
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                Iterator var6 = properties.entrySet().iterator();

                while(var6.hasNext()) {
                    Entry<?, ?> entry = (Entry)var6.next();
                    String factoryTypeName = ((String)entry.getKey()).trim();
                    String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                    String[] var10 = factoryImplementationNames;
                    int var11 = factoryImplementationNames.length;

                    for(int var12 = 0; var12 < var11; ++var12) {
                        String factoryImplementationName = var10[var12];
                        ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                            return new ArrayList();
                        })).add(factoryImplementationName.trim());
                    }
                }
            }

            result.replaceAll((factoryType, implementations) -> {
                return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
            });
            cache.put(classLoader, result);
            return result;
        } catch (IOException var14) {
            throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
        }
    }
}

4、发现一个多次出现的文件:spring.factories,全局搜索它

image-20230907181305042

我们根据源头打开spring.factories , 看到了很多自动配置的文件;这就是自动配置根源所在

我们在上面的自动配置类随便找一个打开看看,比如 :WebMvcAutoConfiguration

图片

可以看到这些一个个的都是JavaConfig配置类,而且都注入了一些Bean,可以找一些自己认识的类,看着熟悉一下!

启动

SpringApplication

@SpringBootApplication
public class Springboot01HellowordApplication {

    public static void main(String[] args) {
        // 将springboot启动
        SpringApplication.run(Springboot01HellowordApplication.class, args);
    }

}

最初以为就是运行了一个main方法,没想到却开启了一个服务

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方法流程分析

图片

springboot打成jar包,是以进程的方式在电脑中运行

结论

springboot所有自动配置都是在启动的时候扫描并加载:spring.factories文件。所有的自动配置都在这里面,但是不一定生效,要判断条件是否成立,只要导入了对应的start,就有了对应的启动器。有了启动器,我们的自动装配就会生效。然后就能配置成功

运行过程:

1、SpringBoot在启动的时候从类路径下的META-INF/spring.factories中获取指定的值

2、将这些值作为自动配置类导入容器 , 自动配置类就生效 , 帮我们进行自动配置工作;

3、以前我们需要自动配置的东西,现在springboot帮我们做了

4、整个J2EE的整体解决方案和自动配置都在spring-boot-autoconfigure的jar包中

5、他会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器

6、容器中也会存在非常多的xxxAutoConfiguration的文件(),就是这些类给容器中导入了这个场景需要的所有组件并自动配置

7、有了自动配置类,免去了我们手动配置的过程

SpringBoot配置

配置文件

SpringBoot使用一个全局的配置文件 , 配置文件名称是固定的

  • application.properties
    • 语法结构:key=value
  • application.yaml
    • 语法结构:key:空格 value

配置文件的作用: 修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;

比如我们可以在配置文件中修改Tomcat 默认启动的端口号!测试一下!

application.properties

server.port=8081

application.yaml

server:
  port: 8081

yaml

概述

YAML是 "YAML Ain't a Markup Language" (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)

这种语言以数据作为中心,而不是以标记语言为重点!

以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下yaml和xml

传统xml配置:

<server>
    <port>8081<port>
</server>

yaml配置:

server:
  prot: 8080

基础语法

说明:语法要求严格!

  • 空格不能省略
  • 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
  • 属性和值的大小写都是十分敏感的

字面量

字面量:普通的值 [ 数字,布尔值,字符串 ]

字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;

name: jinjiahuan
age: 23

注意:

  • '''' 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思; 比如 :name: "kuang \n shen" 输出 :kuang 换行 shen
  • '' 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出 比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

对象、Map(键值对)

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.yaml

2、编写一个实体类 Dog;

//注册bean到容器中
@Component  
public class Dog {    
    private String name;    
    private Integer age; 
    
    //有参无参构造、get、set方法、toString()方法 
    
}

3、思考,我们原来是如何给bean注入属性值的!@Value,给狗狗类测试一下:

@Value("阿黄")
private String name;
@Value("18")
private Integer age;

4、在SpringBoot的测试类下注入狗狗输出一下;

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //将狗狗自动注入进来
    @Autowired
    Dog dog;

    @Test
    void contextLoads() {
        System.out.println(dog);
    }

}

结果成功输出,@Value注入成功,这是我们原来的办法对吧。

image-20230912182202172

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()方法  
}

7、我们来使用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

8、我们刚才已经把person这个对象的所有值都写好了,我们现在来注入到我们的类中!

image-20230912183247336

image-20230912183341075

pom.xml添加依赖

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

person

//注册bean到容器中
@Component
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的person下面的所有属性一一对应
*/
@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()方法

9、确认以上配置都OK之后,我们去测试类中测试一下:

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //将person自动注入进来
    @Autowired
    Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }

}

image-20230912183831540

yaml配置注入到实体类完全OK!

课堂测试:

  • 将配置文件的key 值 和 属性的值设置为不一样,则结果输出为null,注入失败
  • 在配置一个person2,然后将 @ConfigurationProperties(prefix = "person2") 指向我们的person2;

其他情况注入

Map注入

上述Person类maps类型是Map<String,Object>,现在我们修改这个类型为Map<String,Dog>这种情况该如何注入呢。

image-20231112133759130

方式一

maps: {k1: {name: haha,age: 3},k2: {name: heihei,age: 3}}

方式二

maps:
  k1.name: haha
  k1.age: 3
  k2.name: heihei
  k2.age: 3

会进行自动封装

list、set集合注入

方式一
lists: 
 - code
 - girl
 - music
方式二
lists: code,girl,music #会进行自动封装

properties配置文件注入

配置文件编码设置

properties配置文件会出现乱码问题,需要修改idea配置

image-20230913155827973

加载指定的配置文件

1、resources目录下新建文件jinjiahuan.properties文件

name=jinjiahuan

2、person类使用@PropertySource加载指定的配置文件

//注册bean到容器中
@Component
// yaml配置文件注入
// @ConfigurationProperties(prefix = "person")
// 加载指定的配置文件
@PropertySource(value="classpath:jinjiahuan.properties")
public class Person {
    // SPEL表达式获取配置文件的指定key值
    @Value("${name}")
    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()方法

    public Person() {
    }

3、测试

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //将person自动注入进来
    @Autowired
    Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }

}

image-20230913160814265

总结:

  • @PropertySource :加载指定的配置文件;
  • @configurationProperties:默认从全局配置文件中获取值;

配置文件中使用表达式

在yaml配置文件中,可以SPEL(可以理解为是Spring的表达式)表达式

application.yaml

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:
    # ${person.hello:other} 类似于三木运算符,如果person.hello的值存在就使用,不存在就用other,本配置文件
    # name的值为 other_旺财
    name: ${person.hello:other}_旺财 
    age: 1

对比小结

@Value这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图

图片

功能

@ConfigurationProperties只需要写一次即可 , @Value则需要每个字段都添加

松散绑定

比如 yaml中写的 last-name,实体类的属性是驼峰命名的 lastName,也可以成功注入值。last-name中的 - 表示后面跟着的第一个字母默认是大写的。

JSR303数据校验

这个就是我们可以在字段是增加一层过滤器验证 , 可以保证数据的合法性

Springboot中可以用@validated来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。我们这里来写个注解让我们的name只能支持Email格式;

使用步骤

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Person

@Component //注册bean到容器中
@ConfigurationProperties(prefix = "person") // yaml配置文件注入
@Validated // 开启JSR303数据校验
public class Person {
    // 保证注入的name值必须是邮箱格式
    @Email(message="这不是一个邮箱")
    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()方法

    public Person() {
    }

application.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

Springboot02ConfigApplicationTests测试

@SpringBootTest
class Springboot02ConfigApplicationTests {

    //将person自动注入进来
    @Autowired
    Person person;

    @Test
    void contextLoads() {
        System.out.println(person);
    }

}

image-20230913164943324

数据校验成功,属性name值不是邮箱格式,注入失败

都有那些数据校验格式:

@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 对象是否符合正则表达式的规则

.......等等
除此以外,我们还可以自定义一些数据校验规则

image-20230913165309558

复杂类型封装

yml中可以封装对象 , 使用value就不支持

结论

1、配置yml和配置properties都可以获取到值 , 强烈推荐 yml;

2、如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;

3、如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!

多环境切换

profile是Spring对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;

配置文件位置

SpringBoot项目中可以在多个地方存放配置文件,分别是

  • file/config 项目目录下的config目录
  • file/ 项目目录
  • classpath/config classpath目录下的 config目录
  • classpath/ classpath目录下

image-20230913173153837

优先级(由高到低):file/config→file/→classpath/config→classpath/

高优先级的配置会覆盖低优先级的配置;

SpringBoot会从这四个位置全部加载主配置文件;互补配置;

多环境配置文件

我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;

但是Springboot并不会直接启动这些配置文件,它默认使用application.properties主配置

如果要使用其他的配置文件,需要在application.properties默认配置文件中激活

properties配置文件

application.properties

# 默认环境
# 激活想要使用的配置文件,如果没有指定,就是用默认的环境配置
spring.profiles.active=dev

application-dev.properties

# 开发环境
server.port=8081

application-test.properties

# 测试环境
server.port=8082

测试:

image-20230913174811397

使用的就是指定的开发环境的配置

yaml配置文件

yaml配置文件配置多环境比较简单,可以在一个文件中使用 --- 进行分割

和properties配置文件中一样,但是使用yml去实现不需要创建多个配置文件,更加方便了 !

application.yaml

server:
  port: 8080
#选择要激活那个环境块
spring:
  profiles:
    active: dev

---
server:
  port: 8081
spring:
  profiles: dev #配置环境的名称


---

server:
  port: 8082
spring:
  profiles: test  #配置环境的名称

测试

image-20230913175143956

指定的是dev环境配置

注意: 如果yml和properties同时都配置了端口,并且没有激活其他环境 , 默认会使用properties配置文件的!

自动配置原理

分析

我们以 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: (没有条件的类)

SpringBoot Web开发

创建新项目

1、创建新的项目名称 springboot-03-web

2、删除掉多余的文件(gitignore,HELP.md,mvnw,mvnw.cmd),获得干净的项目

3、编写HelloController接口,进行测试,保证项目运行没有问题

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello(){
        return "hello,world";
    }

}

image-20230914152533646

项目运行没有问题

目录结构:

image-20230914152653571

静态资源

第一种静态资源映射规则

源码解读

SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置类里面,我们先找到这个配置类

image-20230914154030547

image-20230914154133638

找到这个自动配置类,在里面找到WebMvcAutoConfigurationAdapter类

image-20230914154325125

WebMvcAutoConfigurationAdapter类中有很多配置方法,有一个方法 addResourceHandlers 添加资源处理

public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        logger.debug("Default resource handling disabled");
    } else {
     this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
     this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
            registration.addResourceLocations(this.resourceProperties.getStaticLocations());
            if (this.servletContext != null) {
              ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
              registration.addResourceLocations(new Resource[]{resource});
            }

        });
    }
}

通过源码解读,发现只要是/webjars/** 的路径访问格式, 都要去 classpath:/META-INF/resources/webjars/ 找对应资源;

webjars

Webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可,现在可以通过maven导入需要的静态资源

网站:https://www.webjars.org

要使用jQuery,我们只要在pom文件中引入jQuery对应版本的pom依赖即可!

pom.xml

<!--静态资源jquery依赖-->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

发现依赖已经导入进来

image-20230914160228745

现在我们访问静态资源

image-20230914160739285

访问静态资源成功

为什么能够找到jquery.js这个静态资源呢?

观察访问路径http://localhost:8080/webjars/jquery/3.4.1/jquery.js

  • http://localhost:8080/webjars 通过解读源码,我们知道访问路径符合/webjars/**格式
  • 所以会到classpath:/META-INF/resources/webjars/目录下找对应的资源
  • 通过jquery/3.4.1/jquery.js就会最终锁定jquery.js文件

第二种静态资源映射规则

源码解读

那我们项目中要是使用自己的静态资源该怎么导入呢?

还是看WebMvcAutoConfigurationAdapter类中addResourceHandlers方法,我们看下一行代码;

else {
  this.addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
  this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
     registration.addResourceLocations(this.resourceProperties.getStaticLocations());
     if (this.servletContext != null) {
     ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
     registration.addResourceLocations(new Resource[]{resource});
}

image-20230914162750436

我们去找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/" 
};

image-20230914163013109

ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容

所以得出结论,以下四个目录存放的静态资源可以被我们识别

"classpath:/META-INF/resources/" // webjars所在的目录
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;

image-20230914163441297

在public目录下创建1.js文件

image-20230914163746538

hello,success

测试访问

image-20230914163812787

成功访问静态资源文件

自定义静态资源映射规则

application.properties

spring.mvc.static-path-pattern=/hello/,classpath:/kuang/

在访问1.js

image-20230914165616411

发现源码当中的默认配置已经失效

总结

1、在springboot中,我们可以使用以下方式处理静态资源

  • 资源存放目录:WEB-INF/resources/webjars → 访问路径:localhost:8080/webjars/
  • 资源存放目录:public、static、resources、/** (根目录下)→访问路径:localhost:8080

2、优先级

  • 优先级由高到低:resources → static → public
  • 习惯上 public 存放一些公共资源

3、在templates目录下的所有页面,只能通过Controller来跳转

首页和图标处理

源码解读

静态资源文件夹说完后,我们继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页

WebFluxAutoConfiguration类中

private Resource getWelcomePage() {
    String[] var1 = this.resourceProperties.getStaticLocations();
    int var2 = var1.length;

    for(int var3 = 0; var3 < var2; ++var3) {
        String location = var1[var3];
        Resource indexHtml = this.getIndexHtml(location);
        if (indexHtml != null) {
            return indexHtml;
        }
    }

    ServletContext servletContext = this.getServletContext();
    if (servletContext != null) {
        return this.getIndexHtml((Resource)(new ServletContextResource(servletContext, "/")));
    } else {
        return null;
    }
}

private Resource getIndexHtml(String location) {
    return this.getIndexHtml(this.resourceLoader.getResource(location));
}

欢迎页,静态资源文件夹下的所有 index.html 页面;被 /** 映射。

比如我访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html

在静态资源目录(resources、static、public)中任意一个目录下新建一个 index.html ,然后访问测试 http://localhost:8080/ 看结果!

image-20230914173735973

启动服务测试

image-20230914173829391

放在其他静态资源目录下也是一样的结果

跳转首页实现

如果想要通过controller跳转到首页,需要将index.html放到templates目录下

image-20230914174302266

在templates(模板引擎)目录下的所有页面,只能通过Controller来跳转

编写IndexController

image-20230914175033870

// 在templates(模板引擎)目录下的所有页面,只能通过Controller来跳转
// 这个需要模板引擎的支持
@Controller
public class IndexController {

    @GetMapping("/index")
    public String index(){
        return "index";
    }

}

因为还没有添加模板引擎的依赖,所以访问首页失败,后续讲解模板引擎如何添加

image-20230914174946103

网站图标说明

网站图标与其他静态资源一样,Spring Boot在配置的静态资源位置中查找 favicon.ico文件。如果存在这样的文件,它将自动用作应用程序的favicon。

1、关闭SpringBoot默认图标

application.properties

#关闭默认图标
spring.mvc.favicon.enabled=false

2、自己放一个图标(favicon.ico)放在静态资源目录下,我放在 public 目录下

image-20230915162214118

3、清除浏览器缓存!刷新网页,发现图标已经变成自己的了!

注意: 可能新版的springboot不支持图标

Thymeleaf模板引擎

模板引擎

前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。

jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。

那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?

SpringBoot推荐你可以来使用模板引擎:

模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:

image-20230915162926652

模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。

我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。

引入Thymeleaf

怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。给大家三个网址:

找到对应的pom依赖:可以适当点进源码看下本来的包!

<!--thymeleaf-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Maven会自动下载jar包,我们可以去看下下载的东西;

image-20230915163614512

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";

我们可以在其中看到默认的前缀和后缀!

我们只需要把我们的html页面放在类路径下的templates下,thymeleaf就可以帮我们自动渲染了。

使用thymeleaf什么都不需要配置,只需要将他放在指定的文件夹下即可!

测试

1、编写一个ThymeleafController

@Controller
public class ThymeleafController {

    @RequestMapping("/testOne")
    public String testOne(Model model){
        model.addAttribute("users", Arrays.asList("Json","Andy","Lisa"));
        return "testOne";
    }
}

2、编写一个测试页面testOne.html放在templates目录下

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
<h3 th:each="user:${users}" th:text="${user}"></h3>
</body>
</html>

3、启动项目请求测试

image-20230915164754546

Thymeleaf 语法学习

基础认识

要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;

Thymeleaf 官网:https://www.thymeleaf.org/ , 简单看一下官网!我们去下载Thymeleaf的官方文档!

我们做个最简单的练习 :我们需要查出一些数据,在页面中展示

1、修改测试请求,增加数据传输;

@Controller
public class ThymeleafController {

    @RequestMapping("/testOne")
    public String testOne(Model model){
        model.addAttribute("msg","<h1>Hello,Thymeleaf</h1>");
        return "testOne";
    }
}

2、我们要使用thymeleaf,需要在html文件中导入命名空间的约束,方便提示。

<html lang="en" xmlns:th="http://www.thymeleaf.org">

3、我们去编写下前端页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf</title>
</head>
<body>
<h1>测试页面</h1>

<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
<div th:text="${msg}"></div> <!--th:text 转义-->
<div th:utext="${msg}"></div> <!--th:utext 不转义-->
</body>
</html>

4、启动测试!

image-20230915171720126

语法学习

替换Html中原生属性值

我们可以使用任意的 th:attr 来替换Html中原生属性的值!

图片

表达式

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>Thymeleaf</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自动配置与自定义扩展

springboot已经对mvc进行了一些配置,如果我们想拓展的话,查看官网文档。

地址 :https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

image-20230915183623285

文档解读

// SpringMvc自动配置
Spring MVC Auto-configuration
// Spring Boot为Spring MVC提供了自动配置,它可以很好地与大多数应用程序一起工作。
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.
// 自动配置在Spring默认设置的基础上添加了以下功能:
The auto-configuration adds the following features on top of Spring’s defaults:
// 包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars 
// 自动注册了Converter:
// 转换器,这就是我们网页提交数据到后台自动封装成为对象的东西,比如把"1"字符串自动转换为int类型
// Formatter:【格式化器,比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象】
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters
// SpringMVC用来转换Http请求和响应的的,比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;
Support for HttpMessageConverters (covered later in this document).
// 定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首页定制
Static index.html support.
// 图标定制
Custom Favicon support (covered later in this document).
// 初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

/*
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的@configuration类,类型为WebMvcConfigurer,但不添加@EnableWebMvc注解。
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration 
(interceptors, formatters, view controllers, and other features), you can add your own 
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc. 

/*
如希望提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter ExceptionHandlerExceptionResolver的自定义实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。
*/
If you wish to provide custom instances of RequestMappingHandlerMapping,RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

// 如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

官方文档中看到,如果我们想保留mvc的一些默认,并且希望能拓展我们自己的配置,可以添加自己的@configuration类,类型为WebMvcConfigurer,但不添加@EnableWebMvc注解。

自定义视图解析器

在com.jjh下新建包config作为自己的配置包,config包下新建一个MyMvcConfig配置类,实现自定义视图解析器

MyMvcConfig

// 如果,你想自定义一些定制化的功能,只要写这个组件,然后将他交给springboot,springboot就会帮我们自动配置
// 扩展springmvc dispachservlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    // ViewResolver 实现了驶入解析器接口的类,我们就可以把它看成视图解析器
    @Bean
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    // 自定义一个自己的视图解析器MyViewResolver
    public static class MyViewResolver implements ViewResolver{
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }

}

格式化转换器

找到格式化转换器

WebMvcAutoConfiguration

@Bean
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化规则 
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService((new DateTimeFormatters()).dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
this.addFormatters(conversionService);
return conversionService;
}

进入this.mvcProperties.getFormat()方法

public WebMvcProperties.Format getFormat() {
    return this.format;
}

点击this.format查看

image-20230916162148668

可以看到format默认格式化规则在配置文件中获取,所以我们可以在配置文件中定制我们的规则

application.properties

# 自定义配置日期格式化
spring.mvc.format=yyyy-MM-dd

修改SpringBoot的默认配置

这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。

SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;

SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;

如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

扩展使用SpringMVC 官方文档如下:

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

示例演示

我们要做的就是编写一个@Configuration注解类,并且类型要为WebMvcConfigurer,还不能标注@EnableWebMvc注解;我们去自己写一个;我们新建一个包叫config,写一个类MyViewControllerConfig,例如之前我们自定义的视图解析器

MyViewControllerConfig

@Configuration
public class MyViewControllerConfig implements WebMvcConfigurer {
    
    // 视图跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器访问路径/kuang , 就会跳转到test页面;
        registry.addViewController("/kuang").setViewName("testOne");
    }
}

目录结构

image-20230916173530165

image-20230916163800124

示例原理分析

1、WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有一个WebMvcAutoConfigurationAdapter

2、这个类上有一个注解,在做其他自动配置时会导入:@Import(EnableWebMvcConfiguration.class)

3、我们点进EnableWebMvcConfiguration这个类看一下,它继承了一个父类:DelegatingWebMvcConfiguration

这个父类中有这样一段代码:

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
  // 从容器中获取所有的webmvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }
    }
}

4、我们可以在这个类中去寻找一个我们刚才设置的viewController当做参考,发现它调用了一个

protected void addViewControllers(ViewControllerRegistry registry) {
    this.configurers.addViewControllers(registry);
}

5、我们点进去看一下

public void addViewControllers(ViewControllerRegistry registry) {
    Iterator var2 = this.delegates.iterator();

    while(var2.hasNext()) {
        // 将所有的WebMvcConfigurer相关配置来一起调用!包括我们自己配置的和Spring给我们配置的
        WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
        delegate.addViewControllers(registry);
    }

}

所以得出结论:所有的WebMvcConfiguration都会被作用,不止Spring自己的配置类,我们自己的配置类当然也会被调用;

为什么不能加@EnableWebMvc注解

上面官方文档上提到,自己拓展的功能类不能加@EnableWebMvc注解,现在我们分析为什么不能加这个注解

现在我们加上这个注解看一下

@Configuration // 如果我们要拓展使用springmvc,官方建议我们这样去做
@EnableWebMvc // 这玩意就是导入了一个类:DelegatingWebMvcConfiguration:从容器中获取所有的webmvcconfig配置
public class MyViewControllerConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/kuang").setViewName("testOne");
    }
}

@EnableWebMvc 注解会导入DelegatingWebMvcConfiguration类,这个类会获取所有的mvc配置,查看他的父类

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

发现他的父类是WebMvcConfigurationSupport。

我们再来看下WebMvcAutoConfiguration类

image-20230916174549015

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})代表容器中没有注册WebMvcConfigurationSupport类,自动配置类才会生效,相反如果注入这个类,自动配置类就会失效,导致所有的配置都不会生效

而如果我们在自己拓展配置类中加上@EnableWebMvc注解,就会注入WebMvcConfigurationSupport,导致自动配置类WebMvcConfigurationSupport失效,所以官方提示自己拓展的配置类不能加这个注解。

员工管理系统

准备工作

创建springboot项目

项目名称 springboot-04-employee-governance 员工管理系统

前端页面

  • 将html页面放入templates目录
  • 将css,js,img放入到static目录

image-20230919164337810

实体类

dao层,模拟数据,增加增删改查功能

引入lombok依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

DepartmentDao

@Repository
public class DepartmentDao {

    //模拟数据库数据

    private static Map<Integer, Department> departments = null;

    static {
        //创建一个部门表
        departments = new HashMap<Integer, Department>();

        departments.put(101,new Department(101,"教学部"));
        departments.put(102,new Department(102,"市场部"));
        departments.put(103,new Department(103,"教研部"));
        departments.put(104,new Department(104,"运营部"));
        departments.put(105,new Department(105,"后勤部"));
    }

    /**
     * 获取所有部门
     * @return
     */
    public Collection<Department> getAllDepartment(){
        return departments.values();
    }

    /**
     * 根据id得到部门
     * @param id
     * @return
     */
    public Department getDepartmentById(Integer id){
        return departments.get(id);
    }
}

EmployeeDao

// 存储层注解,将类交给spring管理
@Repository
public class EmployeeDao {
    //模拟数据库数据

    private static Map<Integer, Employee> employees = null;
    /**
     * 员工所属部门
     */
    @Autowired
    private DepartmentDao departmentDao;

    static {
        //创建一个员工表
        employees = new HashMap<Integer, Employee>();

        employees.put(1001,new Employee(1001,"AA","A123456@qq.com",1,new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"BB","B123456@qq.com",0,new Department(102,"市场部")));
        employees.put(1003,new Employee(1003,"CC","C123456@qq.com",1,new Department(103,"教研部")));
        employees.put(1004,new Employee(1004,"DD","D123456@qq.com",0,new Department(104,"运营部")));
        employees.put(1005,new Employee(1005,"EE","E123456@qq.com",1,new Department(105,"后勤部")));
    }

    //主键自增
    private static Integer ininId = 1006;

    /**
     * 增加一个员工
     */
    public void save(Employee employee) {
        if (employee.getId() == null) {
            employee.setId(ininId++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));

        employees.put(employee.getId(),employee);
    }

    /**
     * 查询全部员工信息
     * @return
     */
    public Collection<Employee> getAll() {
        return employees.values();
    }

    /**
     * 通过id查询员工
     * @param id
     * @return
     */
    public Employee getEmployeeById(Integer id) {
        return employees.get(id);
    }

    /**
     * 删除员工通过id
     * @param id
     */
    public void delete(Integer id) {
        employees.remove(id);
    }
}

目录结构

image-20230919164828676

首页实现

引入模板引擎

templates下的页面需要通过controller访问,数据渲染需要用到模板引擎

<!--引入模板引擎-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

方式一

不推荐使用controller的方式

IndexController

@Controller
public class IndexController {
    @RequestMapping({"/","/index.htm","/index.html"})
    public String index(){
        return "index";
    }
}

方式二

推荐使用拓展mvc配置的方式

MyMvcConfig

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.htm").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }
}

目录结构

image-20230919171305045

加载静态资源

导入thymeleaf包

<html lang="en" xmlns:th="http://www.thymeleaf.org">

thymeleaf接管

将所有页面的静态资源使用thymeleaf接管

image-20230919171456927

<!-- css的导入 -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/css/signin.css}" rel="stylesheet">

<!-- 图片的导入 -->
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">

<!-- js导入 -->
<script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
<script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>

<script type="text/javascript" th:src="@{/js/feather.min.js}"></script>

<script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>

@{/css/bootstrap.min.css} @符号实现万能匹配,自动匹配存放静态资源目录下的资源

测试

image-20230919171751784

修改访问项目的路径

application.properties

# 修改项目访问路径
server.servlet.context-path=/jjh

启动测试

image-20230919171931857

发现原访问路径失效

image-20230919172005208

浏览器开发者模式查看

image-20230919172057198

@符号万能匹配自动加上了配置文件指定的路径

页面国际化

准备工作

在IDEA中统一设置properties的编码问题!UTF-8

image-20230919172503106

编写配置文件

创建i18n目录

在resources资源文件下新建一个i18n目录,存放国际化配置文件

创建配置文件

i18n目录下创建login.properties和login_zh_CN.properties;IDEA会自动识别国际化操作;文件夹变了!统一在资源包下

image-20230919172915160

如果想要在添加配置文件,进行如下操作

image-20230919173034484

image-20230919173239618

image-20230919173255547

添加配置

安装插件

这个插件可以方便对配置文件的可视化配置

image-20230919173656359

操作

进行可视化配置

image-20230919173818272

image-20230919174552453

image-20230919174641715

配置生效

修改配置文件

我们已经进行了页面国际化的配置,如何让配置生效?

万变不离其宗,根据官方文档得知,页面国际化的自动配置类是MessageSourceAutoConfiguration,我们进入到这个类中一探究竟

@AutoConfiguration
@ConditionalOnMissingBean(
    name = {"messageSource"},
    search = SearchStrategy.CURRENT
)
@AutoConfigureOrder(-2147483648)
@Conditional({MessageSourceAutoConfiguration.ResourceBundleCondition.class})
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];

    public MessageSourceAutoConfiguration() {
    }

    @Bean
    @ConfigurationProperties(
        prefix = "spring.messages"
    )
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }
    
    // 获取 properties 传递过来的值进行判断
    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
        //  设置国际化文件的基础名(去掉语言国家代码)
messageSource.setBasenames(	StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
        //  ...
return messageSource;
}

得知spring.messages是配置页面国际化的前缀名,配置文件对应的配置类是MessageSourceProperties

再来看配置类

public class MessageSourceProperties {
    private String basename = "messages";
    private Charset encoding;
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration cacheDuration;
    private boolean fallbackToSystemLocale;
    private boolean alwaysUseMessageFormat;
    private boolean useCodeAsDefaultMessage;

basename就是指向我们手动配置的地址,默认是messages

所以我们在配置文件中指向我们手动配置的地址

application.properties

# 我们的配置文件放置的真实位置
spring.messages.basename=i18n.login

index.html首页先改一处,查看效果

<!-- 图片的导入 -->
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<!-- th:text="#{login.tip}" 使用自己的配置,模板引擎国际化消息使用 # -->
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>

测试

之前是

image-20230919172005208

修改后

image-20230920164118010

测试成功,将其他地方按照同样的方式修改即可

<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>

<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">

<input type="checkbox" value="remember-me"> [[#{login.remember}]]

<button class="btn btn-lg btn-primary btn-block" type="submit"  th:text="#{login.btn}">Sign in</button>

查看效果

image-20230920181352169

配置国际化解析

源码分析

根据按钮自动切换中文英文!

在WebMvcAutoConfiguration中有localeResolver方法

image-20230921150015362

@Bean
@ConditionalOnMissingBean(
    name = {"localeResolver"}
)
public LocaleResolver localeResolver() {
// 如果用户配置了LocaleResolver,那么就用户配置的地区解析器
if (
 this.webProperties.getLocaleResolver() == 	  	            			      org.springframework.boot.autoconfigure.web.WebProperties.LocaleResolver.FIXED
) {
 return new FixedLocaleResolver(this.webProperties.getLocale());
} else {
 // 如果用户没有进行配置,就是用默认的地区解析器 
 AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
 localeResolver.setDefaultLocale(this.webProperties.getLocale());
 return localeResolver;
}
}

我们先看下默认的地区解析器

AcceptHeaderLocaleResolver

public Locale resolveLocale(HttpServletRequest request) {
    Locale defaultLocale = this.getDefaultLocale();
    // 默认的就是根据请求头带来的区域信息获取Locale进行国际化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
        return defaultLocale;
    } else {
        Locale requestLocale = request.getLocale();
        List<Locale> supportedLocales = this.getSupportedLocales();
        if (!supportedLocales.isEmpty() && !supportedLocales.contains(requestLocale)) {
            Locale supportedLocale = this.findSupportedLocale(request, supportedLocales);
            if (supportedLocale != null) {
                return supportedLocale;
            } else {
                return defaultLocale != null ? defaultLocale : requestLocale;
            }
        } else {
            return requestLocale;
        }
    }
}

自定义区域解析器

前端html

前端index.html在切换语言按钮上加上链接

<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

image-20230921150754349

public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 获取请求参数
        String lang = request.getParameter("l");
        // 如果请求中没有区域值,就用默认的
        Locale locale = Locale.getDefault();
        if(StringUtils.hasText(lang)){
            // 分割请求参数
            String[] split = lang.split("_");
            // 语言、地区
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}
注册自定义解析器

注册自定义解析器 MyLocaleResolver,在MyMvcConfig配置类上加上 @Bean即可交给spring管理

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.htm").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }

    // 注册bean
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

image-20230921151310481

在这里插入图片描述

总结

页面国际化:

  • 配置i18n文件
  • 在配置文件中进行配置,使我们配置的i18n生效
  • 如果我们需要在项目中进行按钮的自动切换,我们需要自定义一个组件LocalResolver
  • 将我们自己写的组件配置到Spring容器中 @Bean
  • #{}

登录功能实现

功能实现

表单提交地址

index.html

<form class="form-signin" th:action="@{/user/login}">

image-20230921152653781

LoginController

@Controller
public class LoginController {
    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){
        if(StringUtils.hasText(username) && "123456".equals(password)){
            // 登录成功
            return "dashboard";
        }

        model.addAttribute("msg", "用户名或密码错误");
        return "index";
    }
}

错误信息提示

index.html

<!-- 如果用户名或者密码错误,给出提示 -->
<p style="color:red;" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>

验证

image-20230921154135627

映射跳转

通过main.html跳转

LoginController

@Controller
public class LoginController {
    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model){
        if(StringUtils.hasText(username) && "123456".equals(password)){
            // 登录成功 重定向到 main.html,main.html只是一个迷惑网页,其实是一个路径
            return "redirect:/main.html";
        }

        // 告诉用户,登录失败了
        model.addAttribute("msg", "用户名或密码错误");
        return "index";
    }
}

image-20230921154821907

MyMvcConfig

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.htm").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("index");
    }

    // 注册bean
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }
}

image-20230921154901522

测试

image-20230921155136276

登录成功之后,访问路径是http://localhost:8080/jjh/main.html,但是实际看到的是dashboard.html页面

地址栏泄露密码

问题

image-20230921155433550

解决方案

提交方式改为post

index.html

<form class="form-signin" th:action="@{/user/login}" method="post">

登录拦截器

如果访问路径是http://localhost:8080/jjh/main.html,不进行登录,也能进入到系统主页,不符合要求

image-20230921162009233

所以我们要添加拦截器

编写拦截器

优化LoginController

// 登录成功,将用户名写进session
session.setAttribute("loginUser",username);
@Controller
public class LoginController {
    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model,
                        HttpSession session){
        if(StringUtils.hasText(username) && "123456".equals(password)){
            // 登录成功,将用户名写进session
            session.setAttribute("loginUser",username);
            // 登录成功 重定向到 main.html,main.html只是一个迷惑网页,其实是一个路径
            return "redirect:/main.html";
        }

        // 告诉用户,登录失败了
        model.addAttribute("msg", "用户名或密码错误");
        return "index";
    }
}

LoginHandlerInterceptor

public class LoginHandlerInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object             handler) throws Exception {
        // 获取用户的session,如果没有session,表示用户没有进行登录,进行拦截
        // 如果有session,表示用户已经登录,进行放行
        HttpSession session = request.getSession();
        String loginUser = (String) session.getAttribute("loginUser");
        if(!StringUtils.hasText(loginUser)){
            request.setAttribute("msg","没有权限,请先登录");
            // 请求转发到登录页
            request.getRequestDispatcher("/index.html").forward(request,response);
            // 拦截
            return false;
        } else {
            // 放行
            return true;
        }
    }
    
}

配置拦截器

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    // 添加视图控制器
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.htm").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
        registry.addViewController("/main.html").setViewName("dashboard");
    }

    // 注册bean
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerInterceptor())
        // 设置拦截器要拦截的路径 /**表示所有路径        
        .addPathPatterns("/**")
        // 不用拦截的路径        
        .excludePathPatterns("/","/index.html","/index.htm", // 这是访问首页的路径,不用拦截
                "/user/login", // 登录的路径,不用拦截
                "/img/**","/css/**","/js/**"); // 静态资源路径,不用拦截
    }
    
}

image-20230921164206826

测试

直接使用http://localhost:8080/jjh/main.html访问,成功被拦截

image-20230921164242402

系统主页显示用户

[[ ${session.loginUser} ]]

dashboard.html

image-20230921165903995

测试

image-20230921165925736

员工列表展示

目录结构

image-20230922161959826

EmployeeController

@Controller
public class EmployeeController {
    @Autowired
    EmployeeDao employeeDao;

    @RequestMapping("/emps")
    public String list(Model model){
        Collection<Employee> employees = employeeDao.getAll();
        model.addAttribute("emps",employees);
        return "emp/list";
    }
}

修改侧边栏链接

dashboard.html、list.html

<li class="nav-item">
    <a th:class="nav-lin" th:href="@{/emps}">
        <svg ...>
            ...
        </svg>
        员工管理
    </a>
</li>

提取公共页面

提取

顶部导航栏侧边栏 每个页面都需要用到,是公共部分,所以把 顶部导航栏侧边栏 提取出来

image-20230922153822074

templates目录下面创建commons目录,在commons目录下面创建commons.html放公共代码,以供其它页面调用

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

    <!--顶部导航栏 th:fragment="topbar" 将顶部导航栏抽取出来,叫做topbar-->
    <nav class="..." th:fragment="topbar">
        ...
    </nav>

    <!--侧边栏 th:fragment="sidebar" 将侧边栏抽取出来,叫做sidebar-->
    <nav class="..." th:fragment="sidebar">
        ...
    </nav>

</html>

其他页面调用

在其他页面调用刚刚提取出来的公共页面

th:insert="~{commons/commons::topbar}"
th:replace="~{commons/commons::sidebar}"
insert 和 replace 效果一样,一个插入,一个是替换;insert 会在外面多加一层div

dashboard.html、list.html

<!--顶部导航栏-->
<div th:insert="~{commons/commons::topbar}"></div>

<div class="container-fluid">
    <div class="row">
        <!--侧边栏-->
        <div th:replace="~{commons/commons::sidebar}"></div>
		...
    </div>
</div>

侧边活动页高亮

引用页面传参

dashboard.html

<!--侧边栏 (active='main.html') 调用commons.html页面的sidebar(侧边栏),并将active作为参数传递给commons -->
<!--dashboard.html页面被加载时,就已经将参数传递给了commons.html页面-->
<div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>

list.html

<!--侧边栏-->
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>

接收参数

引用页面传参给公共页面,公共页面commons.html接收参数并进行判断,决定指定的列表项高亮显示

<li class="nav-item">
    <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
        ...
        首页
    </a>
</li>
<li class="nav-item">
    <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
        ...
        员工管理
    </a>
</li>
// 三元运算符
// 如果接收到的 active 等于 main.html,那么 class 的值就为 nav-link active,否则为 nav-link
th:class="${active=='main.html'?'nav-link active':'nav-link'}"

员工信息循环展示

遍历数据

后端传递的是一个集合,前端进行遍历

list.html

<table class="table table-striped table-sm">
    <!--标题栏-->
   <thead>
      <tr>
         <th>id</th>
         <th>lastName</th>
         <th>email</th>
         <th>gender</th>
         <th>department</th>
         <th>birth</th>
         <th>操作</th>
      </tr>
   </thead>
   <!--数据展示--> 
   <tbody>
      <!--th:each="emp:${emps}" 将每一次遍历出来的数据,给emp这个变量-->
      <tr th:each="emp:${emps}">
         <td th:text="${emp.getId()}"></td>
         <td th:text="${emp.getLastName()}"></td>
         <td th:text="${emp.getEmail()}"></td>
         <!--后端传递的真实数据是0,1 0:女 1:男-->  
         <td th:text="${emp.getGender()}==0 ? '女':'男'"></td>
         <td th:text="${emp.getDepartment().getDepartmentName}"></td>
         <!--日期格式化-->  
         <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
         <td>
            <!--添加操作按钮--> 
            <button class="btn btn-sm btn-primary">编辑</button>
            <button class="btn btn-sm btn-danger">删除</button>
         </td>
      </tr>
   </tbody>
</table>

添加操作按钮

 <td>
    <!--添加操作按钮--> 
    <button class="btn btn-sm btn-primary">编辑</button>
    <button class="btn btn-sm btn-danger">删除</button>
 </td>

值判断

<!--后端传递的真实数据是0,1 0:女 1:男-->
<td th:text="${emp.getGender()}==0 ? '女':'男'"></td>

日期格式化

<!--日期格式化-->  
<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>

测试

image-20230922162036872

添加员工

添加按钮

位于 list.html 页面的员工列表上面

<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加员工</a></h2>

image-20230922172144565

跳转到添加页Controller

从员工列表页跳转到添加页,controller要实现:

  • 将需要的数据传递到添加页
  • 视图跳转到添加页

因为需要获取添加页需要的数据,所以这里使用@GetMapping

@GetMapping("/emp")
public String toAddPage(Model model){
    // 部门信息传给添加页面
    Collection<Department> departments = departmentDao.getAllDepartment();
    model.addAttribute("departments",departments);
    // 视图跳转到添加页
    return "emp/add";
}

添加页

复制list.html页面,重命名为add.html作为添加页。导航栏和侧边栏都是使用的公共页面,所以不用改变,原list.html页面

的数据展示区部分替换成添加数据的表单即可

image-20230922173118803

add.html的

image-20230922172955700

替换为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/emp}" method="post">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" name="lastName" class="form-control" placeholder="姓名">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" class="form-control" placeholder="xxx@xxx.xxx">
        </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>
            <!-- 提交的是id,所以name为department.id,提交后可以自动绑定Employee中Department属性对象id -->
            <select class="form-control" name="department.id">
                <!-- 遍历部门信息:显示名称,值为id -->
                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}"    				       th:value="${dept.getId()}">1</option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" name="birth" class="form-control"  placeholder="2020-07-25">
        </div>
        <button type="submit" class="btn btn-primary">添加</button>
    </form>
</main>

注意:下拉框提交的时候应提交一个属性,因为其在controller接收的是一个Employee,否则不能自动绑定,会报错

添加员工Controller

可以发现,添加员工的访问路径和跳转添加页的访问路径都是 /emp ,但是风格不同。利用的就是restful

风格

@PostMapping("/emp")
public String add(Employee employee){
    // 调用底层业务方法保存员工信息
    employeeDao.save(employee);
    // 跳转到员工列表页
    return "redirect:/emps";
}

自定义日期格式

针对birth这个日期字段来说,前端的数据类型是String,后端的数据类型是Date,所以在接收这个字段时,后端可能因为类型转换异常报错,所以需要后端需要指定日期格式。这样前端按照指定的格式进行传递参数,后端能够实现正确的数据类型转换

源码解析

查看默认的日期格式

WebMvcProperties类中

@Deprecated
@DeprecatedConfigurationProperty(
    replacement = "spring.mvc.format.date"
)
public String getDateFormat() {
    return this.format.getDate();
}

如果下载源码,可以看到默认的日期格式是 yyyy/MM/dd

配置指定的日期格式

因为WebMvcProperties可以通过配置文件进行配置,所以在配置文件中手动指定日期格式

application.properties

# 时间日期格式化
spring.mvc.date-format=yyyy-MM-dd

测试

image-20230922174249899

image-20230922174310322

修改员工

编辑按钮

编辑按钮修改使用a标签

<!--th:href="@{/update/}+${emp.getId()}" => /update/1003 Restful风格-->
<!--后端使用注解@PathVariable接收参数-->
<a class="btn btn-sm btn-primary" th:href="@{/update/}+${emp.getId()}">编辑</a>

跳转到修改页Controller

EmployeeController

// 跳转到员工修改页面
@GetMapping("update/{id}")
public String toUpdate(
        @PathVariable("id") Integer id, Model model){
    // 员工信息
    Employee employee = employeeDao.getEmployeeById(id);
    model.addAttribute("emp", employee);

    // 部门信息
    Collection<Department> departments = departmentDao.getAllDepartment();
    model.addAttribute("departments",departments);

    return "emp/update";
}

修改页

复制add.html页面

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">

标签中的内容修改为

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <!--隐藏域(员工id)-->
        <input type="hidden" name="id" th:value="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input type="text" name="lastName" class="form-control" placeholder="姓名" th:value="${emp.getLastName()}">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input type="email" name="email" class="form-control" placeholder="xxx@xxx.xxx" th:value="${emp.getEmail()}">
        </div>
        <div class="form-group">
            <label>Gender</label><br>
            <div class="form-check form-check-inline">
                <!--当员工性别值为1时 checked 有效-->
                <input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp.getGender()==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" th:checked="${emp.getGender()==1}">
                <label class="form-check-label">女</label>
            </div>
        </div>
        <div class="form-group">
            <label>Department</label>
            <select class="form-control" name="department.id">
                <!--${dept.getId()==emp.getDepartment().getId()} 显示被修改员工的部门-->
                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"  th:selected="${dept.getId()==emp.getDepartment().getId()}">1</option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input type="text" name="birth" class="form-control"  placeholder="2020-07-25"  th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd')}">
        </div>
        <button type="submit" class="btn btn-primary">修改</button>
    </form>
</main>

修改员工Controller

@PostMapping("updateEmp")
public String update(Employee employee){
    employeeDao.save(employee);
    return "redirect:/emps";
}

删除员工

删除按钮

<a class="btn btn-sm btn-danger" th:href="@{/del/}+${emp.getId()}">删除</a>

删除员工Controller

@GetMapping("/del/{id}")
public String del(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/emps";
}

404页

404.html 页面放入到 templates 目录下面的 error 目录中

其它页面类似;如:500等

image-20230922183318235

测试

image-20230922183255565

注销

把登录时添加的session值设为null

注销按钮

<a class="nav-link" th:href="@{/user/logout}">推出登录/注销</a>

注销Controller

LoginOutController

@Controller
public class LoginOutController {

    @RequestMapping("/user/logout")
    public String logout(HttpSession session){
        session.setAttribute("loginUser",null);
        return "index";
    }

}

聊聊如何写好一个网站

前端

模板:别人写好的,我们拿来改成自己需要的

框架:组件,自己手动组合拼接,Bootstrap,Layui,semantic...

SpringBoot整合JDBC

SpringData简介

对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。

Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。

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-data-jdbc ; 引入相应的模块!基础模块

img

项目建好之后,发现自动帮我们导入了如下的启动器

<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>

编写配置文件

application.yml

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.jdbc.Driver

测试类测试

SpringbootDataJdbcApplicationTests

@SpringBootTest
class SpringbootDataJdbcApplicationTests {

    @Autowired
    private DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        //看一下默认数据源
        System.out.println(dataSource.getClass());
        //获得连接
        Connection connection = dataSource.getConnection();
        System.out.println(connection);

        // xxx Template:SpringBoot已经配置好的模板bean,拿来即用 比如CRUD模板

        //关闭连接
        connection.close();
    }

}

结果:我们可以看到他默认给我们配置的数据源为 : class com.zaxxer.hikari.HikariDataSource , 我们并没有手动配置

我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceAutoConfiguration文件:

@AutoConfiguration(
    before = {SqlInitializationAutoConfiguration.class}
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
    type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class})
public class DataSourceAutoConfiguration {

根据springboot的自动配置原理,会有一个读取配置文件的DataSourceProperties类

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

所以能够获取我们配置文件的内容

在自动配置类中可以看到

@Import(
    {Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class}
)
protected static class PooledDataSourceConfiguration {
    protected PooledDataSourceConfiguration() {
    }
}

这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.2.5 默认使用HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源

HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;

可以使用 spring.datasource.type 指定自定义的数据源类型,值为要使用的连接池实现的完全限定名。

关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是我们需要先了解一个对象 JdbcTemplate

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主要提供以下几类方法:

  1. execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
  2. update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
  3. query方法及queryForXXX方法:用于执行查询相关语句
  4. call方法:用于执行存储过程、函数相关语句。

测试

编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试;

JdbcController

package com.jjh.controller;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/jdbc")
public class JdbcController {


    /**
     * Spring Boot 默认提供了数据源,默认提供了 org.springframework.jdbc.core.JdbcTemplate
     * JdbcTemplate 中会自己注入数据源,用于简化 JDBC操作
     * 还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
     */
    @Autowired
    JdbcTemplate jdbcTemplate;

    //查询employee表中所有数据
    //List 中的1个 Map 对应数据库的 1行数据
    //Map 中的 key 对应数据库的字段名,value 对应数据库的字段值
    @GetMapping("/list")
    public List<Map<String, Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    //新增一个用户
    @GetMapping("/add")
    public String addUser(){
        //插入语句,注意时间问题
        String sql = "insert into user(id, name,pwd) values (6,'小明','123456')";
        jdbcTemplate.update(sql);
        //查询
        return "addOk";
    }

    //修改用户信息
    @GetMapping("/update/{id}")
    public String updateUser(@PathVariable("id") int id){
        //插入语句
        String sql = "update user set name=? where id="+id;
        //数据
        Object[] objects = new Object[1];
        objects[0] = "秦疆";
        jdbcTemplate.update(sql,objects);
        //修改
        return "updateOk";
    }

    //删除用户
    @GetMapping("/delete/{id}")
    public String delUser(@PathVariable("id") int id){
        //插入语句
        String sql = "delete from user where id=?";
        jdbcTemplate.update(sql,id);
        //删除
        return "deleteOk";
    }

}

image-20230930223404970

到此,CURD的基本操作,使用 JDBC 就搞定了。https://docs.spring.io/spring-boot/docs/2.0.4.RELEASE/reference/htmlsingle/#using-boot-starter)

原理探究

org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration 数据源配置类作用 :根据逻辑判断之后,添加数据源;

SpringBoot默认支持以下数据源:

  • com.zaxxer.hikari.HikariDataSource (Spring Boot 2.0 以上,默认使用此数据源)
  • org.apache.tomcat.jdbc.pool.DataSource
  • org.apache.commons.dbcp2.BasicDataSource

可以使用 spring.datasource.type 指定自定义的数据源类型,值为 要使用的连接池实现的完全限定名。默认情况下,它是从类路径自动检测的。

@Configuration
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
    name = {"spring.datasource.type"}
)
static class Generic {
    Generic() {
    }
    @Bean
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
   }
}

整合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 数据源,如何实现数据库监控。

Github地址:https://github.com/alibaba/druid/

com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:

配置 缺省值 说明
name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this). 另外配置此属性至少在1.0.5版本中是不起作用的,强行设置name会出错 详情-点此处
url 连接数据库的url,不同数据库不一样。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 连接数据库的用户名
password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/使用ConfigFilter
driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName
initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive 8 最大连接池数量
maxIdle 8 已经不再使用,配置了也没效果
minIdle 最小连接池数量
maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
validationQueryTimeout 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法
testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis 1分钟(1.0.14) 有两个含义: 1) Destroy线程会检测连接的间隔时间,如果连接空闲时间大于等于minEvictableIdleTimeMillis则关闭物理连接 2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun 不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis 30分钟(1.0.14) 连接保持空闲而不被驱逐的最长时间
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 日志用的filter:log4j 防御sql注入的filter:wall
proxyFilters 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

配置数据源

依赖

<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>

切换数据源

之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源。

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource # 自定义数据源

测试是否切换成功

image-20231001095433019

其他参数配置

既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码

image-20231001095714215

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.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

application.yml

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.jdbc.Driver
    # 自定义数据源
    type: com.alibaba.druid.pool.DruidDataSource
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    filters: stat,wall,log4j

导入Log4j 的依赖

<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

现在需要程序员自己为 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();
    }
}

application.yml

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    driver-class-name: com.mysql.jdbc.Driver
    # 自定义数据源
    type: com.alibaba.druid.pool.DruidDataSource
    filters: stat,wall,log4j
    # 设置最大连接数
    maxActive: 25

去测试类中测试一下;看是否成功!

@SpringBootTest
class SpringbootDataJdbcApplicationTests {

    @Autowired
    private DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {

        //查看数据源
        System.out.println(dataSource.getClass());
        DruidDataSource druidDataSource = (DruidDataSource) dataSource;
        System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
    }

}

image-20231001103632799

可以看见,数据源配置生效

配置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"); //后台管理界面的登录账号
    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/login.html

img

img

配置 Druid web 监控 filter 过滤器

DruidConfig

//配置 Druid 监控 之  web 监控的 filter
//WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计
@Bean
public FilterRegistrationBean webStatFilter() {
    FilterRegistrationBean bean = new FilterRegistrationBean();
    bean.setFilter(new WebStatFilter());

    //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计
    Map<String, String> initParams = new HashMap<>();
    initParams.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*");
    bean.setInitParameters(initParams);

    //"/*" 表示过滤所有请求
    bean.setUrlPatterns(Arrays.asList("/*"));
    return bean;
}

平时在工作中,按需求进行配置即可,主要用作监控

整合mybatis

新建项目

1、新建项目名springboot-05-mybatis

2、选择相关组件:web、jdbc、mysql.driver

导入依赖

<!-- 引入 myBatis,这是 MyBatis官方提供的适配 Spring Boot 的,而不是Spring Boot自己的-->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.1.0</version>
</dependency>

配置数据库连接信息

application.properties

spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

测试类测试是否成功连接数据库

@Autowired
DataSource dataSource;

@Test
public void contextLoads() throws SQLException {

    System.out.println("数据源>>>>>>" + dataSource.getClass());
    Connection connection = dataSource.getConnection();
    System.out.println("连接>>>>>>>>>" + connection);
    System.out.println("连接地址>>>>>" + connection.getMetaData().getURL());
    connection.close();

}

查看输出结果,数据库配置OK!

创建实体类

创建pojo包,包下创建类User

package com.jjh.pojo;

public class User {

    private int id;
    private String name;
    private String pwd;

    public User() {
    }

    public User(int id, String name, String pwd) {
        this.id = id;
        this.name = name;
        this.pwd = pwd;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", pwd='" + pwd + '\'' +
                '}';
    }

}

配置Mapper接口类

创建mapper包,包下建接口UserMapper

package com.jjh.mapper;

import com.jjh.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

import java.util.List;

//@Mapper : 表示本类是一个 MyBatis 的 Mapper,等价于以前 Spring 整合 MyBatis 时的 Mapper 接口
@Mapper
@Repository
public interface UserMapper {

    //选择全部用户
    List<User> selectUser();
    //根据id选择用户
    User selectUserById(int id);
    //添加一个用户
    int addUser(User user);
    //修改一个用户
    int updateUser(User user);
    //根据id删除用户
    int deleteUser(int id);

}

Mapper映射文件

resources下创建目录mybatis,myubatis下创建目录mapper,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.jjh.mapper.UserMapper">

    <select id="selectUser" resultType="User">
        select * from user
    </select>

    <select id="selectUserById" resultType="User">
        select * from user where id = #{id}
    </select>

    <insert id="addUser" parameterType="User">
        insert into user (id,name,pwd) 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 = #{id}
    </delete>
</mapper>

SpringBoot 整合

以前 MyBatis 未与 spring 整合时,配置数据源、事务、连接数据库的账号、密码等都是在 myBatis 核心配置文件中进行的myBatis 与 spring 整合后,配置数据源、事务、连接数据库的账号、密码等就交由 spring 管理。因此,在这里我们即使不使用mybatis配置文件也完全ok!

既然已经提供了 myBatis 的映射配置文件,自然要告诉 spring boot 这些文件的位置

application.properties

# 整合mybatis
#指定myBatis的核心配置文件与Mapper映射文件
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 注意:类型别名设置,对应实体类的路径
mybatis.type-aliases-package=com.jjh.pojo

已经说过 spring boot 官方并没有提供 myBaits 的启动器,是 myBatis 官方提供的开发包来适配的 spring boot,从 pom.xml 文件中的依赖包名也能看出来,并非是以 spring-boot 开头的;

同理上面全局配置文件中的这两行配置也是以 mybatis 开头 而非 spring 开头也充分说明这些都是 myBatis 官方提供的

可以从 org.mybatis.spring.boot.autoconfigure.MybatisProperties 中查看所有配置项

@ConfigurationProperties(
    prefix = "mybatis"
)
public class MybatisProperties {
    public static final String MYBATIS_PREFIX = "mybatis";
    private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
    private String configLocation;
    private String[] mapperLocations;
    private String typeAliasesPackage;
    private Class<?> typeAliasesSuperType;
    private String typeHandlersPackage;
    private boolean checkConfigLocation = false;
    private ExecutorType executorType;
    private Class<? extends LanguageDriver> defaultScriptingLanguageDriver;
    private Properties configurationProperties;
    @NestedConfigurationProperty
    private Configuration configuration;

maven配置资源过滤问题

<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
</resources>

编写controller

建controller包,包下创建UserController

package com.jjh.controller;

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    //选择全部用户
    @GetMapping("/selectUser")
    public String selectUser(){
        List<User> users = userMapper.selectUser();
        for (User user : users) {
            System.out.println(user);
        }

        return "ok";
    }
    //根据id选择用户
    @GetMapping("/selectUserById")
    public String selectUserById(){
        User user = userMapper.selectUserById(1);
        System.out.println(user);
        return "ok";
    }
    //添加一个用户
    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(5,"阿毛","456789"));
        return "ok";
    }
    //修改一个用户
    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(5,"阿毛","421319"));
        return "ok";
    }
    //根据id删除用户
    @GetMapping("/deleteUser")
    public String deleteUser(){
        userMapper.deleteUser(5);
        return "ok";
    }

}

启动项目访问进行测试

image-20231001161500737

image-20231001161518847

目录结构

image-20231001161636103

步骤总结

步骤:

Mybatis整合包

mybatis-spring-boot-starter

1.导入包

2.配置文件

3.mybatis配置

4.编写sql

5.service层调用dao层

6.controller调用service层、

注:配置数据库连接信息(不变)

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    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

集成SpringSecurity

SpringSecurity简介

Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式

Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。

“认证”(Authentication)

身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。

身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。

“授权” (Authorization)

授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。

这个概念是通用的,而不是只在Spring Security 中存在。

环境搭建

创建初始工程

1、创建一个名为springboot-07-security工程

2、引入web依赖

image-20231001163350505

3、引入thymeleaf依赖

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

4、导入静态资源

image-20231001210603830

5、编写controller并且测试

新建controller包,包下创建RouterController

package com.jjh.controller;

@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;
    }

}

测试并运行

image-20231001210757209

可以看到环境搭建成功

导入SpringSecurity依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

定义请求授权规则

新建包config,包下创建SecurityConfig类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 链式编程
        // 首页所有人都能访问,功能页只有对应有权限的人才能访问
        // 请求授权的规则
        http.authorizeRequests()
                // 首页所有人都能访问
                .antMatchers("/").permitAll()
                // vip1的用户只能访问/level1/**下面的资源
                .antMatchers("/level1/**").hasAnyRole("vip1")
                .antMatchers("/level2/**").hasAnyRole("vip2")
                .antMatchers("/level3/**").hasAnyRole("vip3");

    }
}

测试发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!

image-20231001212517575

在configure()方法中加入以下配置,开启自动配置的登录功能(如果没有权限就会跳转到登录页面)

// 没有权限就会跳转到登录页面,需要开启登录的页面
// 就会跳转到http://localhost:8080/login路径,页面是SpringSecurity集成的页面
http.formLogin();

image-20231001213029426

定义认证规则

重写configure(AuthenticationManagerBuilder auth)方法,也可以去jdbc中去取

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    //在内存中定义,也可以在jdbc中去拿....
    auth.inMemoryAuthentication()
            .withUser("kuangshen").password("123456").roles("vip2","vip3")
            .and()
            .withUser("root").password("123456").roles("vip1","vip2","vip3")
            .and()
            .withUser("guest").password("123456").roles("vip1");
}

测试,我们可以使用这些账号登录进行测试!发现会报错!

image-20231001220553404

原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   //在内存中定义,也可以在jdbc中去拿....
   //Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
   //要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
   //spring security 官方推荐的是使用bcrypt加密方式。
   
   auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
          .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
          .and()
          .withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
          .and()
          .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2");
}

测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!搞定

权限控制和注销

1、开启自动配置的注销功能

@Override
protected void configure(HttpSecurity http) throws Exception {
    // 链式编程
    // 首页所有人都能访问,功能页只有对应有权限的人才能访问
    // 请求授权的规则
	// ...

    // 没有权限就会跳转到登录页面,需要开启登录的页面
    // 就会跳转到http://localhost:8080/login路径,页面是SpringSecurity集成的页面
    // ...

    // 注销,开启了注销功能,注销之后默认跳转到登录页
    http.logout();

    // 注销之后设置跳转到首页
    // http.logout().logoutSuccessUrl("/index");
}

2、我们在前端,增加一个注销的按钮,index.html 导航栏中

<a class="item" th:href="@{/logout}">
	<i class="sign-out icon"></i> 注销
</a>

在这里插入图片描述

3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?

// 注销之后设置跳转到首页
http.logout().logoutSuccessUrl("/index");

5、测试,注销完毕后,

6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如kuangshen这个用户,它只有 vip2,vip3功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?

我们需要结合thymeleaf中的一些功能

导入thymeleaf和security结合的Maven依赖:

<!-- 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>

7、修改我们的 前端页面

导入命名空间

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

image-20231002204141258

修改导航栏,增加认证判断

<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 sec:authorize="isAuthenticated()" >
                <!--如果已经登录,显示用户名,注销-->
                <a class="item">
                    用户名:<span sec:authentication="principal.username"></span>
                    角色:<span sec:authentication="principal.authorities"></span>
                </a>
                <a class="item" th:href="@{/logout}">
                    <i class="sign-out icon"></i> 注销
                </a>
            </div>
        </div>
    </div>
</div>

8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;

9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();

SecurityConfig.java

//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.csrf().disable();

image-20231002204439511

到目前为止已近实现了权限的访问资源不同,接下来还想实现对不同用户的页面显示内容不同:

关键代码:sec:authorize=“hasRole()” //判断当前的权限是否有这个权限直接加在想要设置权限的标签内即可

index.html

<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('vip=3')">
            <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>

记住我及首页定制

记住我

现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单

1、开启记住我功能

//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//...
   //记住我
   http.rememberMe();
}

2、测试

登录时选择记住我功能

image-20231002205515378

关闭浏览器后直接访问首页

image-20231002205613103

发现用户已经登录

查看浏览器cookie

image-20231002205759912

3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie

4、结论:登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了。 如果点击注销,则会删除这个cookie,具体的原理我们在JavaWeb阶段都讲过了,这里就不在多说了!

定制登录页

现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢

1、在刚才的登录页配置后面指定 loginpage

http.formLogin().loginPage("/toLogin");

2、然后前端也需要指向我们自己定义的 login请求

<a class="item" th:href="@{/toLogin}">
   <i class="address card icon"></i> 登录
</a>

3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:

<form th:action="@{/toLogin}" method="post">

4、测试

image-20231002211731963

登录之后应该显示首页,但是现在报错

image-20231002211813672

原因:因为SpringSecurity默认的登录页请求是/login,现在我们换成了toLogin,所以在登录时,访问的是我们自定义的请求,目前是/usr/login,但是我们的controller没有处理这个请求

image-20231002212653144

解决方式一

在login.html页面,提交登录时请求路径设置为toLogin

<form th:action="@{/toLogin}" method="post">
    <div class="field">
        <label>Username</label>
        <div class="ui left icon input">
            <input type="text" placeholder="Username" name="username">
            <i class="user icon"></i>
        </div>
    </div>
    <div class="field">
        <label>Password</label>
        <div class="ui left icon input">
            <input type="password" name="password">
            <i class="lock icon"></i>
        </div>
    </div>
    <input type="submit" class="ui blue submit button"/>
</form>

测试

image-20231002213027257

登录成功,进入到首页

image-20231002213339074

解决方式二

如果使用定制的登录页提交时,就想使用默认的/login提交路径进行提交,可以进行如下设置

自己定制的登录页面login.html提交路径使用/login

<form th:action="@{/login}" method="post">

授权规则配置SecurityConfig.configure()方法

// 没有权限就会跳转到登录页面,需要开启登录的页面
// 就会跳转到http://localhost:8080/login路径,页面是SpringSecurity集成的页面
// http.formLogin();
// 定制自己的登录页
// http.formLogin().loginPage("/toLogin")
// loginProcessingUrl表示在使用自己定制的登录页进行登录时,真正的提交路径是/login
http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");

测试

image-20231002213027257

登录成功

image-20231002214156973

自定义登录参数

现在有一个问题,在进行登录提交时,SpringSecurity默认接收到的参数名时username和password,如果前端传递的参数名不是这两个,会发生什么事情呢,我们来测试一下

现在的登录页面的提交参数是username和password

<div class="field">
    <label>Username</label>
    <div class="ui left icon input">
        <input type="text" placeholder="Username" name="username">
        <i class="user icon"></i>
    </div>
</div>
<div class="field">
    <label>Password</label>
    <div class="ui left icon input">
        <input type="password" name="password">
        <i class="lock icon"></i>
    </div>
</div>

现在我们换成name和pwd

<div class="field">
    <label>Username</label>
    <div class="ui left icon input">
        <input type="text" placeholder="Username" name="name">
        <i class="user icon"></i>
    </div>
</div>
<div class="field">
    <label>Password</label>
    <div class="ui left icon input">
        <input type="password" name="pwd">
        <i class="lock icon"></i>
    </div>
</div>

测试

image-20231002214927237

提交后发现报错,原因就是因为我们改变了默认提交参数的参数名

image-20231002214955337

怎样解决,自定义参数名即可解决

SecurityConfig.config()方法

// 没有权限就会跳转到登录页面,需要开启登录的页面
// 就会跳转到http://localhost:8080/login路径,页面是SpringSecurity集成的页面
// http.formLogin();
// http.formLogin().loginPage("/toLogin").loginProcessingUrl("/login");
// 自定义登录提交的参数名
http.formLogin().loginPage("/toLogin")
        .usernameParameter("name")
        .passwordParameter("pwd")
        .loginProcessingUrl("/login");

测试,成功

可以在自定义登录页上加上remember me功能

login.html

<div class="field">
    <input type="checkbox" name="remember"> 记住我
</div>
<input type="submit" class="ui blue submit button"/>

后台接收remember参数

SecurityConfig.config()方法

// 开启记住我功能,默认保存两周
http.rememberMe().rememberMeParameter("remember");

亲测,成功

Shiro

概述

简介

Apache Shiro是一个强大且易用的Java安全框架

可以完成身份验证、授权、密码和会话管理

Shiro 不仅可以用在 JavaSE 环境中,也可以用在 JavaEE 环境中

官网: http://shiro.apache.org/

下载shiro

进入官网http://shiro.apache.org/

image-20231003204750955

点击Download

image-20231003204838887

学习使用的版本是1.4.2

image-20231003204931444

下载zip包

image-20231003205000009

下载完成解压打开查看

image-20231003205109902

下载完成

功能

Shiro 安全框架入门知识_应用程序

Authentication:身份认证/登录,验证用户是不是拥有相应的身份;

Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;

Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;

Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;

Web Support:Web支持,可以非常容易的集成到Web环境;

Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;

Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;

Testing:提供测试支持;

Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;

Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

从外部看

在这里插入图片描述

应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:

Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;

SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;

Realm:域,Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。

也就是说对于我们而言,最简单的一个Shiro应用:

  1. 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
  2. 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。

从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入

外部架构

在这里插入图片描述

Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;

SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。

Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;

Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;

Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;

SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);

SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;

CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能

Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的

认证流程

在这里插入图片描述

用户 提交 身份信息、凭证信息 封装成 令牌 交由 安全管理器 认证

快速入门

新建项目

1、创建名为springboot-08-shiro的maven项目

2、删除src文件夹,这样可以将springboot-08-shiro作为父工程,在其下能够创建诺干个moudle

3、在父工程中创建名为01-hello-shiro的maven moudle项目

image-20231003210205378

搭建shiro环境

1、下载shiro 1.4.2 版本

2、打开下载的shrio

image-20231003210033557

3、将pom.xml中的依赖包复制到01-hello-shiro子项目的pom文件中

shiro的pom.xml文件

image-20231003210314431

发现,依赖包没有指定版本号,我们可以去maven仓库查看依赖包的版本号,选择最多使用的那个下载

01-hello-shiro的pom依赖

<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>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.21</version>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
</dependencies>

4、复制shiro中的log4j.properties和shiro.ini文件到项目的相应位置上

image-20231003210718165

image-20231003210745991

复制shiro.ini文件时,如果没有高亮显示,idea下载ini插件即可

img

5、shiro的Quickstart.java代码复制到项目的指定位置

image-20231003210957255

image-20231003211017907

启动,日志成功打印,完成初始环境搭建

image-20231003211141563

源码分析

先简单分析一下源码,混个脸熟

Quickstart类

1.通过 SecurityUtils 获取当前执行的用户 Subject

// 获取当前的用户对象
Subject currentUser = SecurityUtils.getSubject();

2.通过 当前用户拿到 Session,shiro的session

// 通过当前用户拿到shiro的session,注意不是http的session
Session session = currentUser.getSession();

3.用 Session 存值取值

session.setAttribute("someKey", "aValue");
String value = (String) session.getAttribute("someKey");

4.判断用户是否被认证

// 判断当前的用户是否被认证
currentUser.isAuthenticated()

5.执行登录操作

currentUser.login(token); // 执行登录操作

6.打印其标识主体

currentUser.getPrincipal()

7.注销

// 注销功能
currentUser.logout();

全部代码

public class Quickstart {

    // 使用日志框架输出
    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

        // The easiest way to create a Shiro SecurityManager with configured
        // realms, users, roles and permissions is to use the simple INI config.
        // We'll do that by using a factory that can ingest a .ini file and
        // return a SecurityManager instance:

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();

        // for this simple example quickstart, make the SecurityManager
        // accessible as a JVM singleton.  Most applications wouldn't do this
        // and instead rely on their container configuration or web.xml for
        // webapps.  That is outside the scope of this simple quickstart, so
        // we'll just do the bare minimum so you can continue to get a feel
        // for things.
        SecurityUtils.setSecurityManager(securityManager);

        // Now that a simple Shiro environment is set up, let's see what you can do:

        // get the currently executing user:
        // 获取当前的用户对象
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        // 通过当前用户拿到shiro的session,注意不是http的session
        Session session = currentUser.getSession();
        session.setAttribute("someKey", "aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Subject=>session[" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        // 判断当前的用户是否被认证
        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.");
        }

        //test a typed permission (not instance-level)
        // 简单的权限,粗粒度
        if (currentUser.isPermitted("lightsaber:wield")) {
            log.info("You may use a lightsaber ring.  Use it wisely.");
        } else {
            log.info("Sorry, lightsaber rings are for schwartz masters only.");
        }

        //a (very powerful) Instance Level permission:
        // 细粒度,代表更高级的权限
        if (currentUser.isPermitted("winnebago:drive:eagle5")) {
            log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'.  " +
                    "Here are the keys - have fun!");
        } else {
            log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
        }

        //all done - log out!
        // 注销功能
        currentUser.logout();

        System.exit(0);
    }
}

Springboot集成Shiro

新建项目

1、在刚刚的父工程下新建一个名为shiro-springboot的springboot项目moudle

image-20231005215947896

image-20231005220031843

为了和视频讲解的一致,springboot使用2.2.1.RELEASE版本

image-20231005220230921

2、导入thymeleaf依赖

<dependency>
    <groupId>org.thymeleaf</groupId>
    <artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>

创建controlelr

MyController

@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro");
        return "index";
    }

}

新建index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
</body>
</html>

启动测试,亲测成功,初始项目完成

集成shiro

目录结构

image-20231005225819900

导入依赖包

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.4.1</version>
</dependency>

编写配置文件

Shiro 三大要素:

  • subject -> ShiroFilterFactoryBean ----当前用户
  • securityManager -> DefaultWebSecurityManager ----管理所有用户
  • Realm

实际操作中对象创建的顺序 : realm -> securityManager -> subject ----连接数据

编写realm类UserRealm

public class UserRealm extends AuthorizingRealm {
    
    //两个方法的功能主要实现了授权和认证,我们添加两个输出一会看看效果

    //授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权方法");
        return null;
    }

    //认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证方法");

        return null;
    }
}

在Config文件夹下编写ShiroConfig.java文件,将三大Bean注入到spring中

①根据前面提供的顺序,先编写realm类:

//1:创建realm对象,需要自定义
@Bean(name = "userRealm")
public UserRealm userRealm(){
    return new UserRealm();
}

②创建用户管理,这里需要用到前面创建的realm,因此在realm之后进行编写

//2:创建管理用户需要用到
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm);

    return securityManager;

}

③创建subject,需要传入一个securityManager,因此最后进行编写(是不是很像套娃)

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean subject = new ShiroFilterFactoryBean();
    //设置安全管理器
    subject.setSecurityManager(securityManager);

    return subject;
}

补充spring的知识点:

defaultWebSecurityManager方法的参数需要用到userRealm对象,所以使用@Qualifier("userRealm")从spring容器中引入需要的对象。

@Qualifier("xxx")引入xxx对象的方式有两种,xxx既可以是方法名,也可以是自定义的名字

  • xxx如果是在定义Bean时,给bean自定义了一个对象名,那么引入的时候就要使用自定义名称,比如上面的defaultWebSecurityManager方法
  • xxx如果没有指定名字,在引用时就用@Bean注解作用在那个方法上的方法名,比如上面的shiroFilterFactoryBean方法

搭建简单环境

目录结构

image-20231005231857275

1、新建一个首页 index.html,首页上有三个链接,一个登录,一个增加用户,一个删除用户

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<a href="@{/user/add}">add</a>
<a href="@{/user/update}">update</a>
</body>
</html>

2、新建一个增加用户页面 add.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>

3、新建一个修改用户页面 update.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>

4、编写对应的 Controller MyController

package shirospringboot.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class MyController {

    @RequestMapping({"/","/index"})
    public String toIndex(Model model){
        model.addAttribute("msg","hello,shiro");
        return "index";
    }

    @RequestMapping("/user/add")
    public String add(){
        return "user/add";
    }

    @RequestMapping("/user/update")
    public String update(){
        return "user/update";
    }

}

亲测,成功,环境搭建完毕

实现登录拦截

目录结构

image-20231007213427627

login.html 新建登录页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>login</h1>
<form>
    <p>用户名:<input type="text" name="username"></p>
    <p>密  码:<input type="password" name="password"></p>
    <p><input type="submit">提交</p>
</form>
</body>
</html>

MyController 新增一个跳转到登录页面的接口

@RequestMapping("/toLogin")
public String login(){
    return "login";
}

ShiroConfig.shiroFilterFactoryBean

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(securityManager);

    //添加shiro的内置过滤器
    /**
         anon:无需认证就可以访问
         authc:必须认证才可访问
         user:必须拥有   记住我才能用
         perms:拥有对某个资源的权限才能访问
         role:拥有某个角色权限才能访问
     **/
    Map<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
    // 添加页面,所有人都能够访问
    filterChainDefinitionMap.put("/user/add","anon");
    // 编辑页面,经过认证才能进行访问
    filterChainDefinitionMap.put("/user/update","authc");
    // 可以使用通配符
    // 表示只要是/user格式的访问路径,都要经过权限验证
    // filterChainDefinitionMap.put("/user/*","authc");

    // 默认情况下,如果没有登录权限,会报404错误
    // 进行下面的配置,没有登录权限,会跳转到指定的页面
    bean.setLoginUrl("/toLogin");
    
    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return bean;
}

实现用户认证

实现用户认证需要去realm类的认证方法中去配置

这里我们先把用户名和密码写死,实际中是要去数据库中去取的

UserRealm.doGetAuthenticationInfo方法

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证方法");

    // 用户名,密码,真实的项目中在数据库中获取
    String name = "root";
    String password = "123456";

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    // 用户名认证
    if(!userToken.getUsername().equals(name)){
        // 如果登录时填写的用户名和数据库中保存的用户名不一致,返回null
        // 返回null,代表会抛出UnknownAccountException用户名不存在异常
        return null;
    }

    // 密码认证,shiro自己去做,我们不用关心
    return new SimpleAuthenticationInfo("",password,"");
}

MyController新增登录登录提交接口

@RequestMapping("/login")
public String login(String username,String password,Model model){
    // 获取当前用户
    Subject subject = SecurityUtils.getSubject();

    // 封装用户的登录信息,将username和password进行加密获得Token
    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username, password);

        try {
            // 执行登录方法,如果没有异常证明登录成功
            // 该方法会对用户的登录信息进行核对校验
            subject.login(usernamePasswordToken);
            return "index";
        } catch (UnknownAccountException e) { // 用户名不存在异常
            model.addAttribute("msg","用户名不存在异常");
            e.printStackTrace();
            return "login";
        } catch (IncorrectCredentialsException e) { // 密码不存在异常
            model.addAttribute("msg","密码不存在异常");
            e.printStackTrace();
            return "login";
        }

    return "index";

}

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>login</h1>
<form th:action="@{/login}" method="post">
    <p th:text="${msg}" style="color: red"></p>
    <p>用户名:<input type="text" name="username"></p>
    <p>密  码:<input type="password" name="password"></p>
    <p><input type="submit"></p>
</form>
</body>
</html>

亲测,成功,实现了登录认证

Shiro整合Mybatis

导入依赖

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.1</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.22</version>
</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>

编写配置文件

application.yml

spring:
  datasource:
    username: root
    password: 123456
    #?serverTimezone=UTC解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.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

application.properties

// 配置简洁类名
mybatis.type-aliases-package=shirospringboot.pojo
// 指定Mapper接口映射文件
mybatis.mapper-location=classpath:mapper/*.xml

创建pojo包,添加lombok依赖

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>

完善项目结构

创建pojo,controller,service,mapper包,参考前面的springboot整合mybatis代码哦

代码编写

基础代码

User

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private int id;
    private String name;
    private String pwd;
    
}

UserMapper

@Repository
@Mapper
public interface UserMapper {

    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="shirospringboot.mapper.UserMapper">

    <select id="queryUserByName" parameterType="String" resultType="User">
        select * from user where name=#{name}
    </select>

</mapper>

UserService

public interface UserService {

    User queryUserByName(String name);

}

UserServiceImpl

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    public User queryUserByName(String name){
        return userMapper.queryUserByName(name);
    }

}

测试一下

ShiroSpringbootApplicationTests

@SpringBootTest
class ShiroSpringbootApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {

        System.out.println(userService.queryUserByName("狂神"));

    }

}

成功

image-20231007230149599

实现用户认证

用户认证需要获取用户信息,现在我们从数据库中获取

UserRealm.doGetAuthenticationInfo

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证方法");

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    // 连接真实数据库
    User user = userService.queryUserByName(userToken.getUsername());

    // 用户名认证
    if(user==null){
        // 如果登录时填写的用户名和数据库中保存的用户名不一致,返回null
        // 返回null,代表会抛出UnknownAccountException用户名不存在异常
        return null;
    }

    // 密码认证,shiro自己去做,我们不用关心
    return new SimpleAuthenticationInfo("",user.getPwd(),"");
}

测试 成功

image-20231007230942282

image-20231007230920374

image-20231007231026583

授权功能的实现

授权实现

首先,添加授权代码

ShiroConfig.shiroFilterFactoryBean

// 授权,正常情况下,没有授权会跳转到未授权页面
// 表示只有user用户,并且有add权限才能访问/user/add
filterChainDefinitionMap.put("/user/add","perms[user:add]");
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(securityManager);

    //添加shiro的内置过滤器
    /**
         anon:无需认证就可以访问
         authc:必须认证才可访问
         user:必须拥有   记住我才能用
         perms:拥有对某个资源的权限才能访问
         role:拥有某个角色权限才能访问
     **/
    Map<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
    // 添加页面,所有人都能够访问
    // filterChainDefinitionMap.put("/user/add","anon");
    // 编辑页面,经过认证才能进行访问
    filterChainDefinitionMap.put("/user/update","authc");
    // 可以使用通配符
    // 表示只要是/user格式的访问路径,都要经过权限验证
    // filterChainDefinitionMap.put("/user/*","authc");

    // 授权,正常情况下,没有权限会跳转到未授权页面
    // 表示有 user:add 权限才能访问/user/add
    filterChainDefinitionMap.put("/user/add","perms[user:add]");

    // 默认情况下,如果没有认证成功,会报404错误
    // 进行下面的配置,没有认证成功,会跳转到指定的页面
    bean.setLoginUrl("/toLogin");
    
    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return bean;
}

测试使用的是 狂神 123456 进行登录访问,所以虽然经过了认证,但是user/add页面没有权限访问

image-20231008204745928

实现未授权页面

因为没有权限会报401授权异常,下面我们写一个未授权的接口和页面

MyController

@RequestMapping("/unauthorized")
@ResponseBody
public String unauthorized(){
  return "未经授权,无法访问此页面";
}

在ShiroConfig.shiroFilterFactoryBean中设置未授权页面

// 未授权页面
bean.setUnauthorizedUrl("/unauthorized");
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(securityManager);

    //添加shiro的内置过滤器
    /**
         anon:无需认证就可以访问
         authc:必须认证才可访问
         user:必须拥有   记住我才能用
         perms:拥有对某个资源的权限才能访问
         role:拥有某个角色权限才能访问
     **/
    Map<String, String> filterChainDefinitionMap=new LinkedHashMap<>();
    // 添加页面,所有人都能够访问
    // filterChainDefinitionMap.put("/user/add","anon");
    // 编辑页面,经过认证才能进行访问
    filterChainDefinitionMap.put("/user/update","authc");
    // 可以使用通配符
    // 表示只要是/user格式的访问路径,都要经过权限验证
    // filterChainDefinitionMap.put("/user/*","authc");

    // 授权,正常情况下,没有授权会跳转到未授权页面
    // 表示有 user:add 权限才能访问/user/add
    filterChainDefinitionMap.put("/user/add","perms[user:add]");

    // 默认情况下,如果没有认证成功,会报404错误
    // 进行下面的配置,没有认证成功,会跳转到指定的页面
    bean.setLoginUrl("/toLogin");

    // 未授权页面
    bean.setUnauthorizedUrl("/unauthorized");
    
    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return bean;
}

测试,发现如果没有权限会跳转到我们指定的页面

image-20231008210827501

为用户授权

硬编码(不推荐,帮助理解)

因为具有user:add的权限才能访问/user/add请求,下面为所有的用户进行授权

UserRealm.doGetAuthorizationInfo

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了授权方法");

    // 为用户授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    info.addStringPermission("user:add");

    return info;
}

这样就为登录用户进行了授权,测试,发现用户成功访问add页面

image-20231008214104510

但是这样显然不合理,因为所有的登录用户都授予了这个权限,在真实的业务中,角色从数据库中拿,不是用上述的硬编码

实际的授权方式

修改数据库,增加一个权限字段perms,类型varchar,长度100,维护用户数据

image-20231008214635920

现在用户维护了权限字段,在认证时可以获取到用户信息,现在需要把认证时获取到的用户信息传递到授权中

UserRealm.doGetAuthenticationInfo

//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证方法");

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    // 连接真实数据库
    User user = userService.queryUserByName(userToken.getUsername());

    // 用户名认证
    if(user==null){
        // 如果登录时填写的用户名和数据库中保存的用户名不一致,返回null
        // 返回null,代表会抛出UnknownAccountException用户名不存在异常
        return null;
    }

    // 密码加密 方式 MD5 MD5盐值
    // 密码认证,shiro自己去做,我们不用关心
    // user,将user作为参数,这样就能通过(User) subject.getPrincipal()获取到user对象
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}

UserRealm.doGetAuthorizationInfo

//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    System.out.println("执行了授权方法");

    // 为用户授权
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();

    // 为登录用户手动授权
    // info.addStringPermission("user:add");

    // 拿到当前登录的对象
    Subject subject = SecurityUtils.getSubject();
    User currentUser = (User) subject.getPrincipal();
    // 为当前登录用户动态授权
    info.addStringPermission(currentUser.getPerms());

    return info;
}

ShiroConfig.shiroFilterFactoryBean

@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("defaultWebSecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
    //设置安全管理器
    bean.setSecurityManager(securityManager);
    Map<String, String> filterChainDefinitionMap=new LinkedHashMap<>();

    // 为/user/add增加授权控制
    filterChainDefinitionMap.put("/user/add","perms[user:add]");
    // 为/user/update增加授权控制
    filterChainDefinitionMap.put("/user/update","perms[user:update]");

    // 默认情况下,如果没有认证成功,会报404错误
    // 进行下面的配置,没有认证成功,会跳转到指定的页面
    bean.setLoginUrl("/toLogin");

    // 未授权页面
    bean.setUnauthorizedUrl("/unauthorized");
    
    bean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return bean;
}

亲测,成功

Shiro和thymeleaf整合

整合

首先要导入shrio和thymeleaf结合的依赖

<!-- shrio和thymeleaf结合的依赖 -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

ok接下来就要去前端页面编写标签了,之前在security中用到的是sec,这里用的shiro,当然也要导入命名空间

下面是spring-security的命名空间

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"

shiro的:

xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"

ShiroConfig增加getShiroDialect方法

// 整合ShiroDialect:用来整合shiro:thyemleaf
@Bean
public ShiroDialect getShiroDialect(){
    return  new ShiroDialect();
}

index.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<a th:href="@{/toLogin}">登录</a>

<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>

亲测,成功

登录按钮控制

现在的情况是即使用户登录成功,页面仍然有登录按钮,不合理

如何解决?

用户登录成功之后,往shiro的session中方用户数据,然后前端进行判断控制登录按钮的显隐

UserRealm.doGetAuthenticationInfo

Subject subject = SecurityUtils.getSubject();
// 这个是shiro的session
Session session = subject.getSession();
session.setAttribute("loginUser",user);
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    System.out.println("执行了认证方法");

    UsernamePasswordToken userToken = (UsernamePasswordToken) authenticationToken;

    // 连接真实数据库
    User user = userService.queryUserByName(userToken.getUsername());

    // 用户名认证
    if(user==null){
        // 如果登录时填写的用户名和数据库中保存的用户名不一致,返回null
        // 返回null,代表会抛出UnknownAccountException用户名不存在异常
        return null;
    }

    Subject subject = SecurityUtils.getSubject();
    // 这个是shiro的session
    Session session = subject.getSession();
    session.setAttribute("loginUser",user);

    // 密码加密 方式 MD5 MD5盐值
    // 密码认证,shiro自己去做,我们不用关心
    // user,将user作为参数,这样就能通过(User) subject.getPrincipal()获取到user对象
    return new SimpleAuthenticationInfo(user,user.getPwd(),"");
}

index.html

<div th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登录</a>
</div>
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<div th:if="${session.loginUser==null}">
    <a th:href="@{/toLogin}">登录</a>
</div>
    
<p th:text="${msg}"></p>
    
<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>

Swagger

学习目标:

  • 了解Swagger的概念及作用
  • 掌握在项目中集成Swagger自动生成API文档

Swagger简介

前后端分离

流行框架 Vue + SpringBoot

  • 前端 -> 前端控制层、视图层 【前端团队】
    • 伪造后端数据,json。已经存在了,不需要后端,前端工程依旧能够跑起来
  • 后端 -> 后端控制层、服务层、数据访问层 【后端团队】
  • 前后端通过API进行交互
  • 前后端相对独立,且松耦合

产生的问题

  • 前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发

解决方案

  • 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
  • 早些年制定word计划文档
  • 前后端分离:前端测试后端接口:postman 后端提供接口,需要实时更新最新的消息及改动!

Swagger

  • 号称世界上最流行的API框架
  • Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
  • 直接运行,在线测试API接口(其实就是controller requsetmapping)
  • 支持多种语言 (如:Java,PHP等)
  • 官网:https://swagger.io/

Springboot集成Swagger

SpringBoot集成Swagger => springfox,两个jar包

  • Springfox-swagger2
  • swagger-springmvc

使用Swagger

要求:jdk 1.8 + 否则swagger2无法运行

步骤

新建项目

新建一个SpringBoot-web项目,名为 swagger-demo

controller

编写HelloController并测试,确保运行成功

@RestController
public class HelloController {
    @RequestMapping({"/hello","/"})
    public String hello() {
        return "hello";
    }
}

亲测成功

maven依赖

<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>

swagger配置类

新建config包,包下建SwaggerConfig类

//配置类
@Configuration
// 开启Swagger2的自动配置
@EnableSwagger2
public class SwaggerConfig {
}

测试swagger

访问测试 :http://localhost:8080/swagger-ui.html ,可以看到swagger的界面;

image-20231215183656769

配置Swagger

1、Swagger实例Bean是Docket,所以通过Docket实例来配置Swagger,通过Docket对象接管了原来默认的配置

SwaggerConfig

@Bean //配置docket以配置Swagger具体参数
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2);
}

2、可以通过apiInfo()属性配置文档信息

SwaggerConfig

//配置文档信息
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()

SwaggerConfig

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}

亲测,成功

image-20231215184420014

配置扫描接口

image-20231010212725146

1、构建Docket时通过select()方法配置怎么扫描接口

SwaggerConfig.docket

@Bean
public Docket docket() {
   return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      // 通过.select()方法,去配置扫描接口
      .select()
      // RequestHandlerSelectors配置如何扫描接口
      // basePackage(final String basePackage) 根据包路径扫描接口
      .apis(RequestHandlerSelectors.basePackage("com.example.swaggerdemo.controller"))
      .build();
}

2、重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类

image-20231010210735150

3、除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:

//配置docket以配置Swagger具体参数
@Bean
public Docket docket() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            // 通过.select()方法,去配置扫描接口
            .select()
            // RequestHandlerSelectors用来配置扫描接口方式
            // basePackage()指定扫描的包
            // any()  扫描所有,项目中的所有接口都会被扫描到
            // none()  不扫描接口
            // 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
                // withMethodAnnotation(final Class<? extends Annotation> annotation)
            // 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
                // withClassAnnotation(final Class<? extends Annotation> annotation)
            // basePackage(final String basePackage) 根据包路径扫描接口  
         .apis(RequestHandlerSelectors.basePackage("com.example.swaggerdemo.controller"))
            .build();
}

4、除此之外,我们还可以配置接口扫描过滤:

// 配置如何通过path过滤,这里只扫描请求以/kuang开头的接口
.paths(PathSelectors.ant("/kuang/**"))
@Bean
public Docket docket() {
    return new Docket(DocumentationType.SWAGGER_2)
        .apiInfo(apiInfo())
        // 通过.select()方法,去配置扫描接口
        .select()
        // basePackage(final String basePackage) 根据包路径扫描接口
        .apis(RequestHandlerSelectors.basePackage("com.example.swaggerdemo.controller"))
        // 配置如何通过path过滤,这里只扫描请求以/kuang开头的接口
        .paths(PathSelectors.ant("/kuang/**"))
        .build();
}

配置Swagger开关

配置开关

//配置是否启用Swagger,如果是false,在浏览器将无法访问,默认是true
.enable(false)

SwaggerConfig.docket

@Bean
public Docket docket() {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            //配置是否启用Swagger,如果是false,在浏览器将无法访问
            .enable(false)
            // 通过.select()方法,去配置扫描接口
            .select()
            // basePackage(final String basePackage) 根据包路径扫描接口
            .apis(RequestHandlerSelectors.basePackage("com.example.swaggerdemo.controller"))
            // 配置如何通过path过滤,这里只扫描请求以/kuang开头的接口
            // .paths(PathSelectors.ant("/kuang/**"))
            .build();
}

测试,浏览器无法访问swagger

指定环境配置开关

可以指定相应的环境是否开启swagger功能

为了测试,我们在resources下新建dev和pord不同环境的配置文件

application.properties

spring.profiles.active=dev

application-dev.properties

server.port=8081

application-prod.properties

server.port=8081

现在我们想让swagger在dev开发环境显示,在prod生产环境不显示

SwaggerConfig.docket

// 设置要显示swagger的环境,在dev和test环境可以显示
Profiles of = Profiles.of("dev", "test");
// 判断当前是否处于该环境
// 通过 enable() 接收此参数判断是否要显示
boolean flag = environment.acceptsProfiles(of);    
//配置是否启用Swagger,如果是false,在浏览器将无法访问
.enable(flag)
    
//配置docket以配置Swagger具体参数
@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())
            //配置是否启用Swagger,如果是false,在浏览器将无法访问
            .enable(flag)
            // 通过.select()方法,去配置扫描接口
            .select()
            // basePackage(final String basePackage) 根据包路径扫描接口
            .apis(RequestHandlerSelectors
            .basePackage("com.example.swaggerdemo.controller"))
            // 配置如何通过path过滤,这里只扫描请求以/kuang开头的接口
            // .paths(PathSelectors.ant("/kuang/**"))
            .build();
}

亲测成功

image-20231010215621208

image-20231010215639890

配置API分组

1、如果没有配置分组,默认是default。通过groupName()方法即可配置分组:

SwaggerConfig-docket

// 配置分组
.groupName("jjh1")
//配置docket以配置Swagger具体参数
@Bean
public Docket docket(Environment environment) {
    return new Docket(DocumentationType.SWAGGER_2)
            .apiInfo(apiInfo())
            // 配置分组
            .groupName("jjh1")
            
            ...
            
}

亲测,成功

image-20231010221909769

2、如何配置多个分组?配置多个分组只需要配置多个docket即可:

SwaggerConfig

//配置docket以配置Swagger具体参数
@Bean
public Docket docket1(Environment environment) {...}

@Bean
public Docket docket2(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("jjh2");
}
@Bean
public Docket docket3(){
    return new Docket(DocumentationType.SWAGGER_2).groupName("jjh3");
}

亲测,成功

image-20231010222155469

实体配置

1、pojo包新建一个实体类User

// ApiModel给类加文档注释
@ApiModel("用户实体")
public class User {
    // ApiModelProperty 给字段加文档注释
    @ApiModelProperty("用户名")
    public String username;
    @ApiModelProperty("密码")
    public String password;
}

2、只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:

HelloController

@RequestMapping("/getUser")
public User getUser(){
    return new User();
}

亲测,成功

image-20231010222837590

注:并不是因为@ApiModel这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel和@ApiModelProperty这两个注解只是为实体添加注释的。

@ApiModel为类添加注释

@ApiModelProperty为类属性添加注释

常用注解

Swagger的所有注解定义在io.swagger.annotations包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

在这里插入图片描述

我们也可以给请求的接口配置一些注释

@ApiOperation("这是一个接口")
@PostMapping("/guo")
@ResponseBody
public String guo(@ApiParam("这个名字会被返回")String username){
   return username;
}

这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!

相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。

Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。

最后我学会的:

①给swagger中添加注释

②修改页面的标题等内容

③简单的操作一些请求测试

任务

新建springboot项目,版本为2.2.1.RELEASE,名为springboot-09-test,引入Spring Web依赖

image-20231011210728279

异步任务

所谓异步,在某些功能实现时可能要花费一定的时间,但是为了不影响客户端的体验,选择异步执行

案例

1、创建一个service包,包下创建类AsyncService

@Service
public class AsyncService {

    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在传输...");
    }
}

2、创建controller包,包下建类AsyncController

@RestController
public class AsyncController {

    @Autowired
    AsyncService asyncService;

    @RequestMapping("/async")
    public String async(){
        asyncService.hello();
        return "ok";
    }
}

3、测试发现,在执行/async请求时,网站会延时三秒再显示ok,后台数据也会三秒后显示数据正在传输

image-20231011211720122

现在想要做到前端快速响应我们的页面,后台去慢慢的传输数据,就要用到springboot自带的功能

①想办法告诉spring我们的异步方法是异步的,所以要在方法上添加注解

    @Async
    public void hello(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("数据正在传输...");
    }

②去springboot主程序中开启异步注解功能

@EnableAsync
@SpringBootApplication
public class SwaggerDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SwaggerDemoApplication.class, args);
    }

}

重启即可,测试发现页面会及时相应,数据在后台慢慢传输

邮件任务

邮件发送,在我们的日常开发中,也非常的多,Springboot也帮我们做了支持

  • 邮件发送需要引入spring-boot-start-mail
  • SpringBoot 自动配置MailSenderAutoConfiguration
  • 定义MailProperties内容,配置在application.yml中
  • 自动装配JavaMailSender
  • 测试邮件发送

案例

1、首先你需要去邮箱网站开启POP3/SMTP,拿到邮箱的授权码

image-20231011212935333

2、引入pom依赖

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-mail</artifactId>
</dependency>

看它spring-boot-starter-mail的依赖,可以看到 jakarta.mail,这是邮件发送必须的包,被spring-boot-starter-mail集成了

<dependency>
   <groupId>com.sun.mail</groupId>
   <artifactId>jakarta.mail</artifactId>
   <version>1.6.4</version>
   <scope>compile</scope>
</dependency>

3、查看自动配置类:MailSenderAutoConfiguration

在这里插入图片描述

ok我们点进去,看到里面存在bean,JavaMailSenderImpl

在这里插入图片描述

然后我们去看下配置文件

@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;
}

4、配置文件:application.properties

spring.mail.username=你的邮箱
spring.mail.password=你的邮箱授权码
spring.mail.host=smtp.163.com
# qq需要配置ssl,其他邮箱不需要
spring.mail.properties.mail.smtp.ssl.enable=true

5、Spring单元测试

Springboot09TestApplicationTests

简单邮件发送

@Test
public void contextLoads() {
    //邮件设置1:一个简单的邮件
    SimpleMailMessage message = new SimpleMailMessage();
    message.setSubject("这是一个测试邮件发送标题");
    message.setText("这是一个测试邮件发送内容");

    message.setTo("1426311648@qq.com");
    message.setFrom("1426311648@qq.com");
    mailSender.send(message);
}

亲测,成功

image-20231011223351315

复杂邮件发送

@Test
public void contextLoads2() throws MessagingException {
//邮件设置2:一个复杂的邮件
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("I:\\learningMaterials\\springboot\\资源\\1.png"));	helper.setTo("1426311648@qq.com");
helper.setFrom("1426311648@qq.com");
mailSender.send(mimeMessage);
}

亲测,成功

image-20231011223430480

我们只需要使用Thymeleaf进行前后端结合即可开发自己网站邮件收发功能了!

定时任务

项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring为我们提供了异步执行任务调度的方式

提供了两个接口:
 - TaskScheduler 任务调度者
 - TaskExecutor  任务执行者

两个注解:
 - @EnableScheduling 开启定时功能的注解
 - @Scheduled 指定是什么时候执行

案例

1、主程序上增加@EnableScheduling 开启定时任务功能

@EnableAsync //开启异步注解功能
@EnableScheduling //开启基于注解的定时任务
@SpringBootApplication
public class Springboot09TestApplication {

    public static void main(String[] args) {
        SpringApplication.run(Springboot09TestApplication.class, args);
    }

}

2、创建一个ScheduledService,我们里面存在一个hello方法,他需要定时执行,怎么处理呢?

@Service
public class ScheduledService {

    // 秒   分   时    日   月   周几
    // 30 15 10 * * ? 每天的10点15分30秒执行一次
    // 30 0/5 10,18 * * ? 每天的10点和8点,每隔五分钟执行一次
    // 30 15 10 ? * 1-6 每个月周一到周六10点15分30秒执行一次
    //注意cron表达式的用法;
    @Scheduled(cron = "0 * * * * 0-7")
    public void hello(){
        System.out.println("hello.....");
    }
}

亲测,成功

image-20231011224718004

cron表达式

网址

网址:http://www.bejson.com/othertools/cron/

image-20231011230505657

常用表达式

(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触发

集成redis

概述

SpringData

SpringBoot 操作数据都是使用 ——SpringData

以下是 Spring 官网中描述的 SpringData 可以整合的数据源

在这里插入图片描述

可以发现 Spring Data Redis

lettuce

在 SpringBoot 2.X 之后,原来的 Jedis 被替换为了 lettuce

在这里插入图片描述

Jedis 和 lettuce 区别

Jedis :采用的是直连的服务,如果有多个线程操作的话是不安全的,就需要使用 Jedis Pool 连接池取解决。问题就会比较多。

lettuce :底层采用 Netty ,实例可以在多个线程中共享,不存在线程不安全的情况。可以减少线程数据了,性能更高。

部分源码

自动配置

  1. 找到 spring.factories

​ spring-boot-autoconfigure-2.3.4.RELEASE.jar → META-INF → spring.factories

  1. 在 spring.factories 中搜索 redis

    在这里插入图片描述

    可以得出配置 Redis,只需要配置 RedisAutoConfiguration 即可

  2. 点进 RedisAutoConfiguration

    在这里插入图片描述

  3. 点进 RedisProperties

    在这里插入图片描述

  4. 回到 RedisAutoConfiguration,观察它做了什么

    在这里插入图片描述

Jedis.pool 不生效

  1. 在 RedisAutoConfiguration 类中的 RedisTemplate 方法需要传递一个 RedisConnectionFactory 参数。点进这个参数

  2. 这是一个接口,查看实现类

    在这里插入图片描述

  3. 查看 Jedis 的实现类,下载源码

    在这里插入图片描述

    会发现 ,这个类中很多没有实现的地方。所以 Jedis Pool 不可以

  4. 查看 Lettuce 的实现类

    在这里插入图片描述

    没有问题

  5. 这也说明 SpringBoot 更推荐使用 Lettuce

使用

  1. 新建一个 SpringBoot 项目,勾选上以下

    在这里插入图片描述

  2. 相关依赖

    <dependency>
    	<groupId>org.springframework.boot</groupId>
    	<artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
  3. 配置连接,application.properties

    # SpringBoot,所有的配置类,都有一个自动配置类 RedisAutoConfiguration
    # 自动配置类都会绑定一个properties配置文件 RedisProperties
    # 配置 Redis
    # 默认使用的就是本地的redis,这里如果是本地的不用进行配置
    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    
  4. 测试

    Springboot10RedisApplicationTests

    // 这就是之前 RedisAutoConfiguration 源码中的 Bean
    @Autowired
    private RedisTemplate redisTemplate;
    
    @Test
    void contextLoads() {
    	/** redisTemplate 操作不同的数据类型,API 和 Redis 中的是一样的
    	 * opsForValue 类似于 Redis 中的 String
    	 * opsForList 类似于 Redis 中的 List
    	 * opsForSet 类似于 Redis 中的 Set
    	 * opsForHash 类似于 Redis 中的 Hash
    	 * opsForZSet 类似于 Redis 中的 ZSet
    	 * opsForGeo 类似于 Redis 中的 Geospatial
    	 * opsForHyperLogLog 类似于 Redis 中的 HyperLogLog
    	 */
    
    	// 除了基本的操作,常用的命令都可以直接通过redisTemplate操作,比如事务……
    
    	// 和数据库相关的操作都需要通过连接操作
    	//RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
    	//connection.flushDb();
    
    	redisTemplate.opsForValue().set("key", "呵呵");
    	System.out.println(redisTemplate.opsForValue().get("key"));
    }
    

序列化

为什么要序列化

  1. 新建一个实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Component
    // 实体类序列化在后面加上 implements Serializable 
    public class User {
        private String name;
        private String age;
    }
    
  2. 编写测试类,先不序列化

    @Test
    public void test() throws JsonProcessingException {
    	User user = new User("圆","20");
    	// 使用 JSON 序列化
    	//String s = new ObjectMapper().writeValueAsString(user);
    	// 这里直接传入一个对象
    	redisTemplate.opsForValue().set("user", user);
    	System.out.println(redisTemplate.opsForValue().get("user"));
    }
    
  3. 执行结果

    在这里插入图片描述

    如果序列化就不会报错

  4. 所以一般实体类都要序列化

为什么要自定义序列化

执行 3. 使用 中的那个测试类,向数据库中插入了一个中文字符串,虽然在 Java 端可以看到返回了中文,但是在 Redis 中查看是一串乱码。

在这里插入图片描述

解决这个问题就需要修改默认的序列化规则。

源码

  1. 在 RedisAutoConfiguration 类的 redisTemplate 方法中用到了一个 RedisTemplate 的对象

    在这里插入图片描述

  2. 点进 RedisTemplate

    在这里插入图片描述

  3. 查看这些序列化对象在哪赋值的

    在这里插入图片描述

    但我们需要的是 JSON 序列化,所以就需要自定义一个配置类,编写自己的template

自定义配置类

RedisConfig

@Configuration
public class RedisConfig {
    /**
     *  编写自定义的 redisTemplate
     *  这是一个比较固定的模板
     */
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        // 为了开发方便,直接使用<String, Object>
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);

        // Json 配置序列化
        // 使用 jackson 解析任意的对象
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        // 使用 objectMapper 进行转义
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key 采用 String 的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // Hash 的 key 采用 String 的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value 采用 jackson 的序列化方式
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // Hash 的 value 采用 jackson 的序列化方式
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        // 把所有的配置 set 进 template
        template.afterPropertiesSet();

        return template;
    }
}

清空一下数据库

再次执行之前那个插入 User 对象的测试类

发现执行成功,没有报错,并且在 Redis 中也没有转义字符了

在这里插入图片描述

但是去获取这个对象或者中文字符串的时候还是会显示转义字符,怎么解决?

只需要在启动 Redis 客户端的时候加上 –raw 即可

redis-cli --raw -p 6379

在这里插入图片描述

工具类

狂神-Redis-工具类

Dubbo和Zookeeper集成

分布式理论

在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。

分布式系统(distributed system)是建立在网络之上的软件系统。

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。(所以分布式不应该在一开始设计系统时就考虑到)因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。

Dubbo

简介

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。

在Dubbo的官网文档有这样一张图

在这里插入图片描述

单一应用架构(All inOne)

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

在这里插入图片描述

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用

缺点:

1、性能扩展比较难

2、协同开发问题

3、不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

在这里插入图片描述

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。

缺点:公用模块无法重复利用,开发性的浪费

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。

在这里插入图片描述

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。

在这里插入图片描述

RPC

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;

推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b

说白了就是不同于调用本地的而是调用远程资源和方法

在这里插入图片描述

步骤分析:

在这里插入图片描述

RPC两个核心模块:通讯,序列化。

Dubbo的概念和介绍

Dubbo是什么

Dubbo是一个分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。简单的说,dubbo就是个服务框架,如果没有分布式的需求,其实是不需要用的,只有在分布式的时候,才有dubbo这样的分布式服务框架的需求,并且本质上是个服务调用的东东,说白了就是个远程服务调用的分布式框架

其核心部分包含: 1》远程通讯: 提供对多种基于长连接的NIO框架抽象封装,包括多种线程模型,序列化,以及“请求-响应”模式的信息交换方式。 2》集群容错: 提供基于接口方法的透明远程过程调用,包括多协议支持,以及软负载均衡,失败容错,地址路由,动态配置等集群支持。 3》自动发现: 基于注册中心目录服务,使服务消费方能动态的查找服务提供方,使地址透明,使服务提供方可以平滑增加或减少机器。

Dubbo能做什么

1.透明化的远程方法调用,就像调用本地方法一样调用远程方法,只需简单配置,没有任何API侵入。 2.软负载均衡及容错机制,可在内网替代F5等硬件负载均衡器,降低成本,减少单点。 3.服务自动注册与发现,不再需要写死服务提供方地址,注册中心基于接口名查询服务提供者的IP地址,并且能够平滑添加或删除服务提供者。

搭建测试环境

Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

dubbo官网 http://dubbo.apache.org/zh-cn/index.html

工作原理

在这里插入图片描述

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

l 服务容器负责启动,加载,运行服务提供者。

l 服务提供者在启动时,向注册中心注册自己提供的服务。

l 服务消费者在启动时,向注册中心订阅自己所需的服务。

l 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

l 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

l 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

Zookeeper介绍

官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

上面的解释有点抽象,简单来说zookeeper=文件系统+监听通知机制。

文件系统

Zookeeper维护一个类似文件系统的数据结构

在这里插入图片描述

每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

有四种类型的znode:

  • PERSISTENT-持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

监听通知机制

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。

就这么简单,下面我们看看Zookeeper能做点什么呢?

Zookeeper能做什么 zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。

假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。

在这里插入图片描述

如上,你大致应该了解zookeeper是个什么东西,大概能做些什么了,我们马上来学习下zookeeper的安装及使用

windows下安装zookeeper

1、下载zookeeper :我们下载3.4.14 , 最新版!解压zookeeper

​ 安装包地址:链接:https://pan.baidu.com/s/1_iRikIPD7J115f8r8uTLKg 提取码:xd91\

​ 下载地址:https://archive.apache.org/dist/zookeeper/

2、以管理员身份运行/bin/zkServer.cmd ,初次运行会报错,没有zoo.cfg配置文件;

可能遇到问题:闪退 !

解决方案:编辑zkServer.cmd文件末尾添加pause 。这样运行出错就不会退出,会提示错误信息,方便找到原因。

在这里插入图片描述

3、修改zoo.cfg配置文件

将conf文件夹下面的zoo_sample.cfg复制一份改名为zoo.cfg即可。

注意几个重要位置:

  • dataDir=./ 临时数据存储的目录(可写相对路径)

  • clientPort=2181 zookeeper的端口号

  • 修改完成后再次启动zookeeper,启动成功

    image-20231016213431223

4、使用zkCli.cmd测试

image-20231016213527892

ls /:列出zookeeper根下保存的所有节点

[zk: 127.0.0.1:2181(CONNECTED) 4] ls /
[zookeeper]

create -e /jjh 123:创建一个jjh 节点,值为123

image-20231016214126175

取新创建的jjh节点的值 get /jjh

image-20231016214342305

window下安装dubbo-admin

dubbo-admin 0.1版本(原版本)

dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。

但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。

我们这里来安装一下:

1、下载dubbo-admin

地址 :https://github.com/apache/dubbo-admin/tree/master 视频使用的是0.1版本

image-20231216103005960

2、解压进入目录

修改 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
# zookeeper默认地址,如果没有改变zookeeper配置,这里不需要动
dubbo.registry.address=zookeeper://127.0.0.1:2181

3、在项目目录下使用mvn打包

// -Dmaven.test.skip=true 跳过检查步骤,节省时间
mvn clean package -Dmaven.test.skip=true

第一次打包的过程有点慢,需要耐心等待!直到成功!

4、执行

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

【注意:zookeeper的服务一定要打开!】

执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,我们都是默认的root-root;

登录成功后,查看界面

dubbo-admin 0.6版本(下载版本)

视频讲解使用的是0.1版本,但是在下载的时候没有找到,所以下载了0.6版本。步骤和0.1版本基本一致,下面进行配图说明

下载之后解压,查看文件夹

image-20231016223938806

使用的是 dubbo-admin-server,点进去查看配置文件

image-20231016224058950

基本上和0.1版本大差不差

image-20231016224135766

然后在项目目录下进行打包,打包完成后运行jar包 dubbo-admin-server-0.6.0.jar

image-20231016224312958

image-20231016224342159

jar包运行完成之后,访问http://localhost:38080,使用root root登录

image-20231016224549878

SpringBoot + Dubbo + zookeeper

框架搭建

启动zookeeper

image-20231017210754124

image-20231017210815607

IDEA创建一个空项目

image-20231017211332611

服务提供者

目录结构

image-20231021223454773

创建provider-server模块

创建一个模块,实现服务提供者:provider-server , 选择web依赖即可

image-20231017212106483

image-20231017212137351

springboot使用2.2.1.RELEASE版本

image-20231021211332732

编写服务

项目创建完毕,我们写一个服务,比如卖票的服务;

编写接口

public interface TicketService {
    public String getTicket();
}

编写实现类

public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
    }
}

导入依赖

将服务提供者注册到注册中心,我们需要整合Dubbo和zookeeper,所以需要导包

我们从dubbo官网进入github,看下方的帮助文档,找到dubbo-springboot,找到依赖包

<!-- Dubbo Spring Boot Starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.3</version>
</dependency>

zookeeper的包我们去maven仓库下载,zkclient;

<!-- https://mvnrepository.com/artifact/com.github.sgroschupf/zkclient -->
<dependency>
    <groupId>com.github.sgroschupf</groupId>
    <artifactId>zkclient</artifactId>
    <version>0.1</version>
</dependency>

【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖;

<!-- zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖 -->
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>2.12.0</version>
</dependency>
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.14</version>
    <!--排除这个slf4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>

配置文件配置dubbo

# 应用服务端口
server.port=8081
# dubbo配置
# 应用服务的名字
dubbo.application.name=provider-server
# 注册中心(zookeeper)的地址
dubbo.registy.address=zookeeper://127.0.0.1:2181
# 那些服务要被注册
dubbo.scan.base-package=com.jjh.service

配置注解

在service的实现类中配置服务注解,发布服务!注意导包问题,因为这里的Service注解需要导入的是dubbo中的注解,而不是spring的注解,所以要把其注入就要用Componet,不过最新版本的好像已经解决这个问题:一个新的注解@DubboService

@Service //可以被扫描到,在项目一启动就自动注册到注册中心(zookeeper)
@Component //放在容器中,交给spring管理,使用了dubbo后,尽量就不要使用spring的serbvice注解了
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《狂神说Java》";
    }
}

逻辑理解 :应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!

服务测试

启动zookeeper

image-20231021214459049

image-20231021214524133

启动dubbo-admin的jar包,检测服务是否被成功注册

image-20231021214709928

image-20231021214745439

访问dubbo管理地址 http://localhost:38080

image-20231021214820944

启动provider-server(服务提供者)

image-20231021215103041

查看dubbo的监控页面,服务成功被注册

image-20231216115658181

服务消费者

目录结构

image-20231021223429993

1、和服务提供者一样,创建一个consumer-server模块

2、导入和服务提供者一样的依赖

3、编写服务

本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入。我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同。即在和服务提供者相同的路径名下再创建TicketService接口

package com.jjh.service;

public interface TicketService {
    public String getTicket();
}

创建消费者服务接口

public interface UserService {

    public void buyTicket();

}

创建消费者服务实现类

import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;

@Service //注入到容器中
public class UserServiceImpl implements UserService {
    @Reference //远程引用指定的服务,他会按照全类名进行匹配,看谁给注册中心注册了这个全类名
    TicketService ticketService;
    public void buyTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到"+ticket);
    }
}

4、配置文件中进行配置

application.properties

#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

5、测试类编写

ConsumerServerApplicationTests

@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    public void contextLoads() {

        userService.buyTicket();

    }

}

远程服务调用成功

image-20231216122220666

微服务架构的问题及解决方案

微服务架构问题:分布式架构会遇到的四个核心问题

  • 这么多的服务,客户端该如何去访问?
  • 这么多的服务,服务器之间如何进行通信?
  • 这么多的服务,该如何管理和治理?
  • 服务挂了,怎么办?

解决方案:

SpringCloud,是一套生态,就是来解决以上分布式架构的四个问题

想使用SpringCloud,必须要掌握Springboot,因为它是基于SpirngBoot的

1、SpringCloud NetFlix,出了一套解决方案,一站式解决方案,我们可以直接去这里拿

  • Api网关,zuul组件
  • Feign-->HttpClient-->Http的通信方式,同步并阻塞
  • 服务注册与发现,Eureka
  • 熔炼机制:Hystrix

但是在2018年底,NetFlix宣布无限制停止维护,生态不在维护,就会脱节

2、Apcahe Dubbo zookeeper 第二套解决方案

  • 但是没有API,要么找第三方,要么自己实现
  • Dubbo是一个高性能的基于java实现的RPC通信框架
  • 服务注册与发现,zookeeper,没有熔断机制,借助了Hystrix

3、SpringCloud Alibaba 一站式解决方案!