SpringBoot 教程

SpringBoot

本项目参考 【狂神说Java】SpringBoot 最新教程 IDEA 版通俗易懂
Github 地址:https://github.com/Lockegogo/Java-Study

Hello, World!

什么是 Spring

  • Spring 是一个开源框架,2003 年兴起的一个轻量级的Java 开发框架,作者:Rod Johnson
  • Spring是为了解决企业级应用开发的复杂性而创建的,简化开发

Spring 是如何简化 Java 开发的

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

  1. 基于 POJO 的轻量级和最小侵入性编程,所有东西都是 bean;
  2. 通过 IOC,依赖注入(DI)和面向接口实现松耦合;
  3. 基于切面(AOP)和惯例进行声明式编程;
  4. 通过切面和模板减少样式代码:RedisTemplate,xxxTemplate;

什么是 SpringBoot

SpringBoot 是一个 javaweb 的开发框架,和 SpringMVC 类似,对比其他 javaweb 框架的好处,官方说是简化开发,约定大于配置,能迅速的开发 web 应用,几行代码开发一个 http 接口。

所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景,衍生一种规范框架,人们只需要进行各种配置而不需要自己去是实现它,这时候强大的配置功能成了优点;发展到一定程度后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架,之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡约定大于配置,进而衍生出一些一站式的解决方案,这就是 Java 企业级应用 \(\to\) J2EE \(\to\) spring \(\to\) springboot 的过程。

SpringBoot 基于 Spring 开发,SpringBoot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。SpringBoot 以约定大于配置的核心思想,默认帮我们进行了很多设置,多数 SpringBoot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 应用中这些第三方库几乎可以零配置的开箱即用。

SpringBoot 的主要优点:

  • 为所有 Spring 开发者更快的入门
  • 开箱即用,提供各种默认配置来简化项目配置
  • 内嵌式容器简化 Web 项目
  • 没有冗余代码生成和 XML 配置的要求

第一个 SpringBoot 程序

  1. HelloController.java
@RestController
public class HelloController {

    // 接口:http://localhost:8080/hello
    @RequestMapping("/hello")
    public String hello() {
        // 调用业务,接收前端的参数
        return "Hello World";
    }

}
  1. pom.xml:自动生成
<dependencies>
    <!--web 依赖:tomcat,dispatcherServlet,xml...-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
  1. 更改项目的端口号
# 更改项目的端口号
server.port=8081

运行原理初探

父依赖

  • spring-boot-dependencies:核心依赖在父工程中,管理项目的资源过滤及插件!
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.2</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
  • 点进去后发现还有一个父依赖,这才是真正管理 SpringBoot 应用里面所有版本依赖的地方
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>3.0.2</version>
</parent>
  • 我们在写或者引入一些 Springboot 依赖的时候,不需要指定版本,就因为有这些版本仓库

启动器 spring-boot-starter

springboot-boot-starter-xxx:Springboot 的启动场景,Springboot 会将所有的功能场景,都变成一个个的启动器,例如 spring-boot-starter-web 会帮我们自动导入 web 的所有依赖,我们需要使用什么功能,就只需要找到对应的启动器就好了。

主程序

默认的主启动类

// @SpringBootApplication 来标注一个主程序类
// 说明这是一个 SpringBoot 应用
@SpringBootApplication
public class SpringbootApplication {
   public static void main(String[] args) {
     // 以为是启动了一个方法,没想到启动了一个服务
     // 该方法返回一个 ConfigurableApplicationContext 对象
 	 // 参数一:应用入口的类; 参数二:命令行参数
      SpringApplication.run(SpringbootApplication.class, args);
   }
}
SpringApplication 的实例化

这个类主要做了以下四件事情:

  1. 推断应用的类型是普通的项目还是 Web 项目
  2. 查找并加载所有可用初始化器 , 设置到 initializers 属性中
  3. 找出所有的应用程序监听器,设置到 listeners 属性中
  4. 推断并设置 main 方法的定义类,找到运行的主类
run 方法的执行

img

注解(@SpringBootApplication)

作用:标注在某个类上说明这个类是 SpringBoot 的主配置
SpringBoot 就应该运行这个类的 main 方法来启动 SpringBoot 应用;
进入这个注解:可以看到上面还有很多其他注解!

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // ......
}
  1. @ComponentScan
    1. 对应 XML 配置中的元素
    2. 自动扫描并加载符合条件的组件或者 bean,将这个 bean 定义加载到 IOC 容器中
  2. @SpringBootConfiguration
    1. SpringBoot 的配置类,标注在某个类上,表示这是一个 SpringBoot 的配置类
  3. @EnableAutoConfiguration
    1. 开启自动配置功能
  4. @Import({AutoConfigurationImportSelector.class}):给容器导入组件
    1. AutoConfigurationImportSelector:自动配置导入选择器

自动配置真正实现是从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration 的 JavaConfig 形式的 IOC 容器配置类 , 然后将这些都汇总成为一个实例并加载到 IOC 容器中。

yaml 配置注入

yaml 语法学习

配置文件

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

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

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

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:
  port: 8080
基础语法

说明:语法要求严格!

  1. 空格不能省略

  2. 以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

  3. 属性和值的大小写都是十分敏感的。

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

  1. 字面量直接写在后面就可以,字符串默认不用加上双引号或者单引号;k: v
    1. 双引号不会转义字符串里面的特殊字符,特殊字符会作为本身想表示的意思;
      1. 比如 :name: "kuang \n shen" 输出 :kuang 换行 shen
    2. 单引号,会转义特殊字符,特殊字符最终会变成和普通字符一样输出
      1. 比如 :name: ‘kuang \n shen’ 输出 :kuang \n shen

对象、Map(键值对)

# 对象、Map格式
k:
    v1:
    v2:

在下一行来写对象的属性和值的关系,注意缩进;比如:

student:
    name: qinjiang
    age: 3

行内写法

student: {name: qinjiang,age: 3}

数组( List、set )

用 - 值表示数组中的一个元素,比如:

pets:
 - cat
 - dog
 - pig

行内写法

pets: [cat,dog,pig]

修改 SpringBoot 的默认端口号

配置文件中添加,端口号的参数,就可以切换端口;

server:
  port: 8082

注入配置文件

yaml 注入配置文件

yaml 文件更强大的地方在于,他可以给我们的实体类直接注入匹配值!

  1. 在 springboot 项目中的 resources 目录下新建一个文件 application.yaml

  2. 编写一个实体类 Dog;

@Component  // 注册 bean 到容器中
public class Dog {
    private String name;
    private Integer age;

    // 有参无参构造、get、set方法、toString()方法
}
  1. 给 bean 注入属性值:@Value
@Component // 注册 bean
public class Dog {
    @Value("阿黄")
    private String name;
    @Value("18")
    private Integer age;
}
  1. 在 SpringBoot 的测试类下注入狗狗输出一下
@SpringBootTest
class Springboot02ConfigApplicationTests {

    @Autowired
    private Dog dog;

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

}

@Autowired 是自动装配(按类型,这样就不用在代码中 new 了),@Component 是注册 bean 到容器中,注意这其中的概念差异。

  1. 再编写一个复杂一点的实体类:Person
@Component
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()方法
}
  1. 使用 yaml 配置的方式进行注入:
person:
  name: locke
  age: 3
  happy: false
  birth: 2000/01/01
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - girl
    - music
  dog:
    name: 旺财
    age: 1
  1. 我们刚才已经把 person 这个对象的所有值都写好了,我们现在来注入到我们的类中!
/*
@ConfigurationProperties 作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉 SpringBoot 将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的 person 下面的所有属性一一对应
*/
@Component
@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()方法
}
  1. IDEA 提示,配置注解器没有找到,查看文档,找到一个依赖
    1. 注解 @ConfigurationProperties(prefix = "person")
    2. 点击 open Decumentation 进入官网
    3. 在 pom 中导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  1. 测试类测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
    @Autowired
    private Person person;
    @Test
    void contextLoads() {
        System.out.println(person);
    }

}

加载指定的配置文件

两种注解

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

具体操作:

  1. 在 resources 目录下新建一个 person.properties 文件
name=locke
  1. 在代码中加载 person.properties 文件
@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {

    @Value("${name}")
    private String name;

    ......
}

配置文件占位符

配置文件还可以编写占位符生成随机数:

person:
  name: qinjiang${random.uuid}
  age: ${random.int}
  happy: false
  birth: 2020/07/13
  maps: {k1: v1,k2: v2}
  lists:
    - code
    - music
    - girl
  dog:
    name: ${person.hello:hello}_旺财
    age: 3

对比小结

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

@ConfigurationProperties @Value
功能 批量注入配置文件中的属性 一个个指定
松散绑定 支持 不支持
SpEL 不支持 支持
JSR303 数据校验 支持 不支持
复杂类型封装 支持 不支持
  1. @ConfigurationProperties 只需要写一次即可 , @Value 则需要每个字段都添加

  2. 松散绑定:这个什么意思呢? 比如我的 yaml 中写的 last-name,这个和 lastName 是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。

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

  4. 复杂类型封装,yaml 中可以封装对象 , 使用 value 就不支持。

JSR303 数据校验原理

JSR 303

Springboot 中可以用 @validated 来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。

我们这里来写个注解让我们的 name 只能支持 Email格式:

  1. 添加 validation 启动器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  1. @Email 添加
@Component // 注册 bean
@ConfigurationProperties(prefix = "person")
@Validated // 数据校验
public class Person {
    @Email(message="邮箱格式错误") // name 必须是邮箱格式
    private String name;
}
  1. 运行结果 :default message [不是一个合法的电子邮件地址]

使用数据校验,可以保证数据的正确性。

常见参数

@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;

空检查
@Null       验证对象是否为null
@NotNull    验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   检查约束元素是否为NULL或者是EMPTY.

Booelan检查
@AssertTrue     验证 Boolean 对象是否为 true
@AssertFalse    验证 Boolean 对象是否为 false

长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min=, max=) string is between min and max included.

日期检查
@Past       验证 Date 和 Calendar 对象是否在当前时间之前
@Future     验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern    验证 String 对象是否符合正则表达式的规则

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

多环境切换

profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;(不同位置的优先级如下图)

多配置文件

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

例如:

  • application-test.properties 代表测试环境配置
  • application-dev.properties 代表开发环境配置

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

我们需要通过一个配置来选择需要激活的环境:

# 比如在配置文件中指定使用 dev 环境,我们可以通过设置不同的端口号进行测试;
# 我们启动 SpringBoot,就可以看到已经切换到 dev 下的配置了;
spring.profiles.active=dev

yaml 的多文档块

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

server:
  port: 8081

# 选择要激活那个环境块
spring:
  profiles:
    active: test

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


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

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

配置文件加载方式

springboot 启动会扫描以下位置的 application.properties 或者 application.yaml 文件作为 SpringBoot 的默认配置文件:

img

优先级1:项目路径下的 config 文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的 config 文件夹配置文件
优先级4:资源路径下配置文件

运维小技巧

指定位置加载配置文件:通过 spring.config.location 来改变默认的配置文件位置。

项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置(这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高);

java -jar spring-boot-config.jar --spring.config.location=F:/application.properties

自动配置原理

分析自动配置原理

  1. SpringBoot 启动的时候加载主配置类,开启了自动配置功能 @EnableAutoConfiguration

  2. @EnableAutoConfiguration 作用

    1. 利用 EnableAutoConfigurationImportSelector 给容器中导入一些组件

    2. 可以查看 selectImports() 方法的内容,他返回了一个 autoConfigurationEnty,来自this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);这个方法我们继续来跟踪:

    3. 这个方法有一个值:List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);叫做获取候选的配置 ,我们点击继续跟踪

      1. SpringFactoriesLoader.loadFactoryNames()
      2. 扫描所有 jar 包类路径下META-INF/spring.factories
      3. image-20230201172623848
      4. 把扫描到的这些文件的内容包装成 properties 对象
      5. 从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中
    4. 在类路径下,META-INF/spring.factories 里面配置的所有 EnableAutoConfiguration 的值加入到容器中;

    5. 每一个这样的 xxxAutoConfiguration 类都是容器中的一个组件,都加入到容器中;用他们来做自动配置;

  3. 每一个自动配置类进行自动配置功能;

  4. 我们以 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 类中获取某些属性。我们只需要在配置文件中指定这些属性的值即可;

    1. xxxxAutoConfigurartion:自动配置类;给容器中添加组件
    2. xxxxProperties:封装配置文件中相关属性;

@Conditional

了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;

@Conditional派生注解(Spring注解版原生的 @Conditional作用

作用:必须是 @Conditional 指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;

@Conditional扩展注解 作用(判断是否满足当前指定条件)
@ConditionalOnJava 系统的 java 版本是否符合要求
@ConditionalOnJava 容器中存在指定 Bean ;
@ConditionalOnMissingBean 容器中不存在指定 Bean ;
@ConditionalOnExpression 满足 SpEL 表达式指定
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的 Bean,或者这个 Bean 是首选 Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionalOnWebApplication 当前是 web 环境
@ConditionalOnNotWebApplication 当前不是 web 环境
@ConditionalOnJndi JNDI 存在指定项

那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是所有的都生效了。

自动配置类是否生效

我们可以在 application.properties 通过启用 debug=true属性;

在控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效;

# 开启springboot的调试类
debug=true
  • Positive matches:(自动配置类启用的:正匹配)

  • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

  • Unconditional classes: (没有条件的类)

  • 演示:查看输出的日志

自定义 starter

分析完源码以及自动装配的过程,我们可以尝试自定义一个启动器来玩儿~

说明

启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;

命名归约:

  • 官方命名:

    • 前缀:spring-boot-starter-xxx

    • 比如:spring-boot-starter-web....

  • 自定义命名:

    • xxx-spring-boot-starter

    • 比如:mybatis-spring-boot-starter

编写启动器

  1. 在 IDEA 中新建一个空项目:spring-boot-starter-diy
  2. 新建一个普通的 Maven 模块:locke-spring-boot-starter
  3. 新建一个 SpringBoot 模块:locke-spring-boot-starter-autoconfigure
  4. 点击 apply 即可,基本结构
  5. 在 starter 中导入 autoconfigure 的依赖
<!-- 启动器 -->
<dependencies>
    <!--  引入自动配置模块 -->
    <dependency>
        <groupId>com.locke</groupId>
        <artifactId>locke-spring-boot-starter-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>
  1. autoconfigure 项目下多余的文件都删掉,Pom 中只留下一个 starter,这是所有的启动器基本配置!
  2. 编写一个自己的服务:
public class HelloService {

    HelloProperties helloProperties;

    public HelloProperties getHelloProperties() {
        return helloProperties;
    }

    public void setHelloProperties(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    public String sayHello(String name){
        return helloProperties.getPrefix() + name + helloProperties.getSuffix();
    }

}
  1. 编写 HelloProperties 配置类
// 前缀 locke.hello
@ConfigurationProperties(prefix = "locke.hello")
public class HelloProperties {

    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}
  1. 编写我们的自动配置类并注入 bean,测试
@Configuration
@ConditionalOnWebApplication // web 应用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {

    @Autowired
    HelloProperties helloProperties;

    @Bean
    public HelloService helloService(){
        HelloService service = new HelloService();
        service.setHelloProperties(helloProperties);
        return service;
    }

}
  1. 在 resources 编写一个自己的 META-INF\spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=nuc.ss.HelloServiceAutoConfiguration
  1. 编写完成后,可以安装到 maven 仓库中。

测试启动器

  1. 新建一个 SpringBoot 项目
  2. 导入我们自己写的启动器
<dependency>
    <groupId>nuc.ss</groupId>
    <artifactId>ss-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  1. 编写一个 HelloController 进行测试我们自己写的接口!
@RestController
public class HelloController {

    @Autowired
    HelloService helloService;

    @RequestMapping("/hello")
    public String hello(){
        return helloService.sayHello("zxc");
    }

}
  1. 编写配置文件:application.properties
locke.hello.prefix="ppp"
locke.hello.suffix="sss"
  1. 启动项目进行测试!

Web 开发

使用 SpringBoot 的步骤:

  1. 创建一个 SpringBoot 应用,选择我们需要的模块,SpringBoot 就会默认将我们的需要的模块自动配置好

  2. 手动在配置文件中配置部分配置项目就可以运行起来了

  3. 专注编写业务代码,不需要考虑以前那样一大堆的配置了。

    1. 向容器中自动配置组件 :*** Autoconfiguration
    2. 自动配置类,封装配置文件的内容:***Properties

静态资源处理

静态资源映射规则

SpringBoot 如何处理静态资源,如 css, js 等文件?

如果我们是一个 web 应用,main 下面会有一个 webapp,之前是将所有页面导入在这里面的,但现在的 pom 打包方式是 jar,那怎么办?

我们先来聊聊这个静态资源映射规则:

  • SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面;
  • WebMvcAutoConfigurationAdapter 有很多配置方法;有一个方法:addResourceHandlers 添加资源处理
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用默认资源处理
        logger.debug("Default resource handling disabled");
        return;
    }
    // 缓存控制
    Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
    CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
    // webjars 配置
    if (!registry.hasMappingForPattern("/webjars/**")) {
        customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
                                             .addResourceLocations("classpath:/META-INF/resources/webjars/")
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
    // 静态资源配置
    String staticPathPattern = this.mvcProperties.getStaticPathPattern();
    if (!registry.hasMappingForPattern(staticPathPattern)) {
        customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
                                             .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
                                             .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
    }
}

读一下源代码:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;

webjars

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

第一种静态资源映射规则

SpringBoot 导入静态资源需要使用 Webjars

  1. 引入 jQuery 对应版本的 pom 依赖:
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>
  1. 导入完毕,查看 webjars 目录结构,并访问 Jquery.js 文件!

img

  1. 访问:只要是静态资源,SpringBoot 就会去对应的路径寻找资源,我们这里访问:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

第二种静态资源映射规则

如果是自己的静态资源该怎么导入呢?

  1. 去找 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/"
};
  1. ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。
  2. 以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"    // localhost:8080/webjars
"classpath:/resources/"             // localhost:8080
"classpath:/static/"				// localhost:8080
"classpath:/public/"				// localhost:8080
  1. 我们可以在 resources 根目录下新建对应的文件夹,都可以存放我们的静态文件;

img

  1. 比如我们访问 http://localhost:8080/1.js , 他就会去这些文件夹中寻找对应的静态资源文件;

自定义静态资源路径

我们也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在 application.properties 中配置;

spring.resources.static-locations=classpath:/coding/,classpath:/ss/

但是最好不要这么做。

总结

  1. 在springboot,我们可以使用一下方式处理静态资源
    1. webjars localhost:8080/webjars/
    2. public,static,/**,resources localhost:8080/
  2. 优先级:resources > static(默认) > public

首页处理

首页定制

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

img

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

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

  • 新建一个 index.html,在我们上面的 3 个目录中任意一个;然后访问测试 http://localhost:8080/ 看结果!

首页图标

  1. 关闭 SpringBoot 默认图标:
# 关闭默认图标
spring.mvc.favicon.enabled=false
  1. 自己放一个图标在静态资源目录下,我放在 public 目录下

  2. 清除浏览器缓存 Ctrl + F5!刷新网页,发现图标已经变成自己的了!

2.2.x之后的版本(如2.3.0)直接执行 2 和 3 就可以了

Thymeleaf 模板引擎

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

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

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

现在就该轮到模板引擎出场了:Thymeleaf

img

模板引擎的作用:接收后台传递的模板和数据,将数据进行解析,填充到指定位置并进行展示。

引入 Thymeleaf

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

分析

引入之后如何使用?

首先按照 SpringBoot 的自动配置原理看一下 Thymeleaf 的自动配置规则,然后根据规则使用。

  1. 找到 Thymeleaf 的自动配置类:ThymeleafProperties
@ConfigurationProperties(
    prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";
    private boolean checkTemplate = true;
    private boolean checkTemplateLocation = true;
    private String prefix = "classpath:/templates/";
    private String suffix = ".html";
    private String mode = "HTML";
    private Charset encoding;
}
  1. 我们可以在其中看到默认的前缀和后缀!只需要把 html 页面放在类路径下的 templates 下,就可以自动渲染了

测试

  1. 编写一个 TestController
@Controller
public class TestController {

    @RequestMapping("/test")
    public String test1(){
        //classpath:/templates/test.html
        return "test";
    }
}
  1. 编写一个测试页面 test.html 放在 templates 目录下
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <h1>Test 页面</h1>
    </body>
</html>
  1. 启动项目请求测试

语法学习

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

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

入门

  1. 修改测试请求,增加数据传输
@RequestMapping("/t1")
public String test1(Model model){
    // 存入数据
    model.addAttribute("msg","Hello,Thymeleaf");
    // classpath:/templates/test.html
    return "test";
}
  1. 在 html 文件中导入命名空间的约束,方便提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  1. 编写前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>狂神说</title>
    </head>
    <body>
        <h1>测试页面</h1>

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

进阶

  1. 我们可以使用任意的 th:attr 来替换 html 中原生属性的值
  2. 可以写的表达式如下:
Simple expressions:(表达式语法)
Variable Expressions: ${...}:获取变量值;OGNL;
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象:#18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.

    3)、内置的一些工具对象:
      #execInfo : information about the template being processed.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any).
      #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
==================================================================================

  Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
  Message Expressions: #{...}:获取国际化内容
  Link URL Expressions: @{...}:定义URL;
  Fragment Expressions: ~{...}:片段引用表达式

Literals(字面量)
      Text literals: 'one text' , 'Another one!' ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…

Text operations:(文本操作)
    String concatenation: +
    Literal substitutions: |The name is ${name}|

Arithmetic operations:(数学运算)
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -

Boolean operations:(布尔运算)
    Binary operators: and , or
    Boolean negation (unary operator): ! , not

Comparisons and equality:(比较运算)
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )

Conditional operators: 条件运算(三元运算符)
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)

Special tokens:
    No-Operation: _

测试

  1. 编写一个Controller,放一些数据
@RequestMapping("/test2")
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";
}
  1. 测试页面读取数据
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Title</title>
    </head>
    <body>
        <div>
            <h1>Test页面</h1>
            <!--不转义-->
            <div th:text="${msg}"></div>
            <!--转义-->
            <div th:utext="${msg}"></div>

            <hr>
            <!--遍历数据-->
            <!--th:each每次遍历都会生成当前这个标签:官网#9-->
            <h3 th:each="user:${users}" th:text="${user}"></h3>
            <hr>
            <!--行内写法:官网#12-->
            <h3 th:each="user:${users}">[[ ${user} ]]</h3>
        </div>
    </body>
</html>

MVC 自动配置原理

在进行项目编写前,我们还需要知道一个东西,就是 SpringBoot 对我们的 SpringMVC 还做了哪些配置,包括如何扩展,如何定制。

只有把这些都搞清楚了,我们在之后使用才会更加得心应手。途径一:源码分析,途径二:官方文档!

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

内容协商视图解析器

ContentNegotiatingViewResolver

  1. 自动配置了 ViewResolver,就是我们之前学习的 SpringMVC 的视图解析器
  2. 即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。

转发和重定向的区别:转发是服务器行为;重定向是客户端行为。

  • 地址栏显示
    • 转发 forward:服务器请求资源,服务器直接访问目标地址的 URL, 把那个 URL 的响应内容读取过来,然后把这些内容再发给浏览器。浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址。
    • 重定向 redirect:服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址。所以地址栏显示的是新的 URL。
  • 数据共享
    • 转发 forward:转发页面和转发到的页面可以共享 request 里面的数据。
    • 重定向 redirect:不能共享数据。
  • 运用地方
    • 转发 forward:一般用于用户登录的时候,根据角色转发到相应的模块。
    • 重定向 redirect:一般用于用户注销登录返回主页面和跳转到其他的网站等。
  • 效率
    • 转发 forward:效率高。
    • 重定向 redirect:效率低。
  1. 我们去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索 ContentNegotiatingViewResolver。找到 viewResolver,继续点进去查看找到对应的解析视图的代码:resolveViewName,继续点看看其是如何获得候选的视图的:Iterator var5 = this.viewResolvers.iterator();
  2. 得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
  3. 继续研究组合逻辑,看到有个属性 viewResolvers,它是在哪里进行赋值的?
protected void initServletContext(ServletContext servletContext) {
    // 这里它是从 beanFactory 工具中获取容器中的所有视图解析器
    // ViewRescolver.class 把所有的视图解析器来组合的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
    }
    // ...............
}
  1. 既然它是在容器中去找视图解析器,那我们也可以去实现一个视图解析器了

自定义视图解析器

  1. 在主程序中写一个视图解析器:
// 扩展 springmvc      DispatchServlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    // public interface ViewResolver 实现了视图解析器接口的类,我们就可以把它看做视图解析器

    @Bean
    public ViewResolver myViewResolver() {
        return new MyViewResolver();
    }
    // 自定义了一个自己的视图解析器
    public static class MyViewResolver implements ViewResolver {

        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {

            return null;
        }
    }
}

注意 @Bean@ Component (@Controller / @Service / @Repository) 的区别:

  • @Component: 作用于类上,告知 Spring,为这个类创建 Bean,通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中
  • @Bean:主要作用于方法上 ,告知 Spring,这个方法会返回一个对象,且要注册在 Spring 的上下文中。通常方法体中包含产生 Bean 的逻辑,相当于 xml 文件中的 标签
  1. 给 DispatcherServlet 中的 doDispatch方法 加个断点进行调试一下,因为所有的请求都会走到这个方法中

img

  1. 启动我们的项目(以 debug 的方式),然后随便访问一个页面,回到 IDEA 看一下 Debug 信息,找到 this (DispatcherServlet)

img

  1. 找到视图解析器 (viewResolvers),就可以看到自己定义的

img

如果我们想要使用自己定制化的东西,只需要给容器在添加这个组件就好了,剩下的事情 SpringBoot 会帮我们做。

转换器和格式化器

  1. WebMvcAutoConfiguration 中找到格式化转换器:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 拿到配置文件中的格式化规则
    WebConversionService conversionService =
        new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}
  1. 点击去:可以看到在我们的 Properties 文件中,我们可以进行自动配置它!
public String getDateFormat() {
    return this.format.getDate();
}

public String getDate() {
    return this.date;
}

/**
	* Date format to use, for example `dd/MM/yyyy`.默认的
*/
private String date;
  1. 如果配置了自己的格式化方式,就会注册到 Bean 中生效,我们可以在配置文件中配置日期格式化的规则:
spring.mvc.date=
@Deprecated
public void setDateFormat(String dateFormat) {
    this.format.setDate(dateFormat);
}

public void setDate(String date) {
    this.date = date;
}

修改默认配置

学习心得

  1. 通过 WebMVC 的自动配置原理分析,我们要学会通过源码探究,得出结论;
  2. SpringBoot 的底层,大量地用到了这些设计思想,是很好的学习资料;
  3. SPringleBoot 在自动配置很多组件的时候,先看容器中有没有用户自己配置的,如果没有就用自动配置的;
  4. 如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来;
  5. 扩展使用 SpringMVC,我们需要编写一个 @Configuration 注解类,并且类型要为 WebMvcConfigurer,还不能标注 @EnableWebMvc 注解

@Configuration 用于定义配置类,可替换 xml 配置文件,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,并用于构建 bean 定义,初始化 Spring 容器。

具体实现

  1. 新建一个 config 包,写一个类 MyMvcConfig
// 如果我们要扩展 springmvc,官方建议我们这样去做 @Configuration
// 应为类型要求为 WebMvcConfigurer,所以我们实现其接口
// 扩展 springmvc      DispatchServlet
// @EnableWebMvc // 这玩意就是导入了一个类,DelegatingWebMvcConfiguration,从容器中获取所有的 webMvcConfig
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 浏览器发送 /test2,就会跳转到 test 页面;
        registry.addViewController("/test2").setViewName("test");
    }
}

全面接管 SpringMVC

  1. 全面接管即:SpringBoot 对 SpringMVC 的自动配置不需要了,所有都是我们自己去配置!
  2. 只需在我们的配置类中要加一个 @EnableWebMvc
  3. 如果我们全面接管 SpringMVC,之前 SpringBoot 给我们配置的静态资源映射一定会无效,可以测试一下;

当然在开发中,我们不推荐全面接管 SpringMVC。

员工管理:重要

准备工作

前端页面

  • 将 html 页面放入 templates 目录

  • 将 css,js,img 放入到 static 目录

实体类的编写

  1. Department
// 部门表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
    private Integer id;
    private String departmentName;
}
  1. Employee
// 员工表
@Data
@NoArgsConstructor
public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender;  // 0:女,1:男

    private Department department;
    private Date birth;

    public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        // 默认的创建日期
        this.birth = new Date();
    }
}

dao 层模拟数据库

  1. DepartmentDao
// 部门 Dao
@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,"后勤部"));
    }

    // 获得所有部门信息
    public Collection<Department> getDepartment() {
        return departments.values();
    }

    // 通过 id 得到部门
    public Department getDepartmentById(Integer id) {
        return departments.get(id);
    }
}
  1. EmployeeDao
// 员工 Dao
@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);
    }

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

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

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

img

首页实现

第一种方式

创建一个 IndexController,写一个返回首页的方法(不建议使用

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

第二种方式

创建一个 config 目录,在里面写一个 MyMvcConfig,里面重写 addViewControllers 方法:

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

img

加载静态资源

  1. 导入 thymeleaf 包
<html lang="en" xmlns:th="http://www.thymeleaf.org">
  1. 将所有页面的静态资源使用 thymeleaf 接管:注意在 application.properties 中关掉模板引擎的缓存 spring.thymeleaf.cache=false
<!-- 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>

下面是完整的 index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>Signin Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link th:href="@{/css/signin.css}" rel="stylesheet">
    </head>

    <body class="text-center">
        <form class="form-signin" action="dashboard.html">
            <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
            <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
            <label class="sr-only">Username</label>
            <input type="text" class="form-control" placeholder="Username" required="" autofocus="">
            <label class="sr-only">Password</label>
            <input type="password" class="form-control" placeholder="Password" required="">
            <div class="checkbox mb-3">
                <label><input type="checkbox" value="remember-me">Remember me</label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2022-2023</p>
            <a class="btn btn-sm">中文</a>
            <a class="btn btn-sm">English</a>
        </form>
    </body>

</html>

再次看一下首页页面:

img

页面国际化

  1. resources 文件夹下新建 i18n (internationalization 的简称)

img

注意名字千万不要写错!!!

  1. application.properties 中配置路径:
spring.thymeleaf.cache=false

# server.servlet.context-path=""

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

# 时间日期格式化
spring.mvc.format.date=yyyy-MM-dd
  1. index.html 中进行修改:使用 #{} 号进行取值
  • @{}:thymeleaf 中的超链接表达式
  • #{}:thymeleaf 中的消息表达式,或者资源表达式,一般和 th:text 一起使用
  • ${}:变量表达式,用于访问的是容器上下文的变量,比如域变量:request 域,session 域
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>

下面是完整的 index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:h="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <meta name="description" content="">
        <meta name="author" content="">
        <title>Signin Template for Bootstrap</title>
        <!-- Bootstrap core CSS -->
        <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
        <!-- Custom styles for this template -->
        <link th:href="@{/css/signin.css}" rel="stylesheet">
    </head>

    <body class="text-center">
        <form class="form-signin" action="dashboard.html">
            <img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
            <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
            <input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
            <input type="password" class="form-control" th:placeholder="#{login.password}" required="">
            <div class="checkbox mb-3">
                <label>
                    <input type="checkbox" value="remember-me" th:text="#{login.remember}">
                </label>
            </div>
            <button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
            <p class="mt-5 mb-3 text-muted">© 2022-2023</p>
            <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>
        </form>

    </body>

</html>
  1. 在 config 文件夹中新建 MyLocalResolver
public class MyLocalResolver implements LocaleResolver {
    // 解析请求
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        // 获取请求中的语言参数
        String language = request.getParameter("l");
        Locale locale = Locale.getDefault(); // 如果没有就使用默认的
        // 如果请求的链接携带了国家化的参数
        if (!StringUtils.isEmpty(language)) {
            // zh_CN
            String[] split = language.split("_");
            // 国家,地区
            locale = new Locale(split[0], split[1]);
        }
        return locale;
    }

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

    }
}
  1. 在 MyMvcConfig 中将自己写的组件配置到 spring 容器中:
// 自定义的国际化组件就生效了
@Bean
public LocaleResolver localeResolver() {
    return new MyLocalResolver();
}

登录页面

  1. 首页登录页面表单的修改
<form class="form-signin" th:action="@{/user/login}">
    ......
    <!--如果 msg 的消息不为空,则显示这个消息-->
    <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty({msg})}"></p>

    <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="">
	......
</form>
  1. 写一个 LoginController 登录验证
@Controller
public class LoginController {

    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password") String password,
                        Model model,
                        HttpSession session) {
        // 具体的业务,登录成功跳转到 dashboard 页面
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            session.setAttribute("loginUser", username);
            return "redirect:/main.html";
        } else {
            model.addAttribute("msg", "用户名或者密码错误");
            return "index";
        }
    }
}

注意这里 session 和 model 两个属性的区别

  1. session:即会话,是客户为实现特定应用目的与系统的多次交互请求。Http 协议是一种 无状态 的协议,客户端每打开一个 web 页面,它就会与服务器建立一个新连接,发送新请求到服务器,服务器处理请求并将该请求返回到客户端。服务器不记录任何客户端信息,session 是一种能将信息保存于服务器端的技术,能记录特定的客户端到服务器的一系列请求。session 里放的数据保存在服务器,可以供其他页面使用,只要用户不退出或者 SESSION 过期,这个值就一直可以保留。在当前的 request 周期之内,调用 getAttribute 方法同样也可以得到。
  2. model:一个 request 级别的接口,可以将数据放入视图中。model 的数据,只能在 Controller 返回的页面使用,其他页面不能使用
  1. 登录页面不友好(密码泄露)

img

  1. 解决密码泄露的问题

    1. 加一个 main 映射在 MyMvcConfig
    public class MyMvcConfig implements WebMvcConfigurer{
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/").setViewName("index");
            registry.addViewController("/index.html").setViewName("index");
            registry.addViewController("/main.html").setViewName("dashboard");
        }
    }
    
    1. 修改 LoginController 跳转页面代码 ( redirect 跳转,这样地址栏就会显示新地址)
    @Controller
    public class LoginController {
    
        @RequestMapping("/user/login")
        public String login(@RequestParam("username") String username,
                            @RequestParam("password") String password,
                            Model model,
                            HttpSession session) {
            // 具体的业务, 登录成功跳转到 dashboard 页面
            if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
                return "redirect:/main.html";
            } else {
                model.addAttribute("msg", "用户名或者密码错误");
                return "index";
            }
        }
    }
    
  2. 是否存在问题?

    1. 登录成功才可以进入 main 页面,否则直接输入 http://localhost:8080/main.html 就可以访问首页了,需要拦截器实现

登录拦截器

  1. LoginController 中添加一个 session 判断登录
@Controller
public class LoginController {

    @RequestMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password")String password,
                        Model model,
                        HttpSession session) {
        // 具体的业务, 登录成功跳转到 dashboard 页面
        if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
            session.setAttribute("loginUser",username);
            return "redirect:/main.html";
        } else {
            model.addAttribute("msg","用户名或者密码错误");
            return "index";
        }
    }
}
  1. config 页面写一个 LoginHandlerInterceptor 拦截器
public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 登录成功之后,应该有用户的 session
        Object loginUser = request.getSession().getAttribute("loginUser");

        if (loginUser == null) {
            request.setAttribute("msg","没有权限,请先登录");
            // 转发,服务器跳转
            request.getRequestDispatcher("/index.html").forward(request,response);
            return false;
        } else {
            return true;
        }
    }
}
  1. MyMvcConfig 页面重写拦截器方法
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoginHandlerInterceptor())
        .addPathPatterns("/**")
        .excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
}

注意:静态资源的过滤,否则页面渲染效果会消失。

  1. dashboard.html 页面修改登录信息为 session[[ ${session.loginUser} ]],登录成功之后会显示用户名

员工列表显示

后台编写

后台编写 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";
    }
}

提取公共页面

  1. 员工管理前端页面地址的修改(list.html 和 dashboard.html)@{/emps}
<li class="nav-item">
    <a class="nav-link" th:href="@{/emps}">
        ......
        员工管理
    </a>
</li>
  1. 抽取公共的代码(list.html 和 dashboard.html)

    1. dashboard.html 页面
    <!--顶部导航栏-->
    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
        <!--...-->
    </nav>
    
    <!--侧边栏-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
       <!--...-->
    </nav>
    
    1. list.html 页面
    <!--顶部导航栏-->
    <div th:insert="~{dashboard::topbar}"></div>
    
    
    <!--侧边栏-->
    <div th:insert="~{dashboard::sidebar}"></div>
    
  2. 进一步抽取公共的代码

    1. templates 目录下面创建 commons 目录,在 commons 目录下面创建 commons.html 放公共代码
    <!--只写改变的代码-->
    
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    
    <!--顶部导航栏-->
    <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar">
        .............
    </nav>
    
    <!--侧边栏-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
        <div class="sidebar-sticky">
            <ul class="nav flex-column">
                <li class="nav-item">
                    <a class="nav-link active" th:href="@{/index.html}">
                      	.............
                        首页 <span class="sr-only">(current)</span>
                    </a>
                </li>
                .............
                <li class="nav-item">
                    <a class="nav-link" th:href="@{/emps}">
                        .............
                        员工管理
                    </a>
                </li>
                .............
            </ul>
            .............
        </div>
    </nav>
    </html>
    
    1. dashboard.htmllist.html 页面一样
    <!--顶部导航栏-->
    <div th:replace="~{commons/commons::topbar}"></div>
    
    <!--侧边栏-->
    <div th:replace="~{commons/commons::sidebar}"></div>
    
  3. 添加侧边栏点亮

    1. 在 dashboard.html 和 list.html 页面中侧边栏传参(在括号里面直接传参)
    <!--侧边栏-->
    <div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>
    
    1. 在 commons.html 中接收参数并判断
    <!--侧边栏-->
    <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
        <div class="sidebar-sticky">
            <ul class="nav flex-column">
                <li class="nav-item">
                   <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
    
                      	.............
                        首页 <span class="sr-only">(current)</span>
                    </a>
                </li>
                .............
                <li class="nav-item">
                	<a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
    
                        .............
                        员工管理
                    </a>
                </li>
                .............
            </ul>
            .............
        </div>
    </nav>
    </html>
    

列表循环展示

员工列表循环(list.html)

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

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <h2>Section title</h2>
    <div class="table-responsive">
        <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>
                <tr th:each="emp:${emps}">
                    <td th:text="${emp.getLastName()}"></td>
                    <td th:text="${emp.getEmail()}"></td>
                    <td th:text="${emp.getGender()==0?'女':'男'}"></td>
                    <td th:text="${emp.department.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>
    </div>
</main>

img

添加员工信息

按钮提交

list.html 页面编写:

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

img

跳转到添加页面

  1. 后台页面的编写(跳转到 add.html 页面)
// 一定别忘记注入!!!!
@Autowired
DepartmentDao departmentDao;

@GetMapping("/emp")
public String toAddPage(Model model) {
    // 查出所有部门的信息
    Collection<Department> department = departmentDao.getDepartment();
    model.addAttribute("departments",department);
    return "emp/add";
}
  1. add.html 页面的编写(其他部分和 list.html 页面一样,只改 main 中的代码即可)
<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="1176244270@qq.com">
        </div>
        <div class="form-group">
            <label>Gender</label><br>
            <div class="form-check form-check-inline">
                <input class="form-check-input" type="radio" name="gender" value="1">
                <label class="form-check-label">男</label>
            </div>
        </div>
        <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 class="form-group">
            <label>department</label>
            <select class="form-control" name="department.id">
                <!--我们在 controller 接收的是一个 Employee,所以我们需要提交的是其中的一个属性-->
                <!--前端无法直接提交一个对象!-->
                <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 18:00:00">
        </div>
        <button type="submit" class="btn btn-primary">添加</button>
    </form>
</main>

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

添加员工成功

后台页面的编写:

@Autowired
DepartmentDao departmentDao;

@PostMapping("/emp")
public String addEmp(Employee employee) {
    employeeDao.save(employee); // 调用底层业务方法保存员工信息
    return "redirect:/emps";

注意 return "redirect:/emps";return "redirect:/index.html"; 区别!

日期格式的修改

  1. 如果输入的日期格式为 2020-01-01,则报错
  2. application.properties 文件中添加配置
spring.mvc.format.date=yyyy-MM-dd

修改员工信息

按钮提交

list.html 页面编辑按钮的编写(’+‘ 报红别管)

<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">编辑</a>

跳转到修改页面

  1. 后台页面的接收参数(Restful 风格)
// 去到员工的修改页面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id, Model model) {
    // 查出原来的数据
    Employee employee = employeeDao.getEmployeeById(id);
    model.addAttribute("emp",employee);
    // 查出所有部门的信息
    Collection<Department> department = departmentDao.getDepartment();
    model.addAttribute("departments",department);
    return "emp/update";
}
  1. update.html 页面(main 里面修改,其他和 list.html 页面一样)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
    <form th:action="@{/updateEmp}" method="post">
        <input type="hidden" name="id" th:value="${emp.getId()}">
        <div class="form-group">
            <label>LastName</label>
            <input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="海绵宝宝">
        </div>
        <div class="form-group">
            <label>Email</label>
            <input th:value="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="1176244270@qq.com">
        </div>
        <div class="form-group">
            <label>Gender</label><br>
            <div class="form-check form-check-inline">
                <input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
                <label class="form-check-label">男</label>
            </div>
        </div>
        <div class="form-check form-check-inline">
            <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
            <label class="form-check-label">女</label>
        </div>
        <div class="form-group">
            <label>department</label>
            <select class="form-control" name="department.id">
                <!--我们在controller接收的是一个Employee,所以我们需要提交的是其中的一个属性-->
                <option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
            </select>
        </div>
        <div class="form-group">
            <label>Birth</label>
            <input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" type="text" name="birth" class="form-control" placeholder="2020-07-25 00:00:00">
        </div>
        <button type="submit" class="btn btn-primary">修改</button>
    </form>
</main>

注意 update.htmladd.html 的区别!

  • update.html 需要把待修改的员工信息展示出来!
<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control">
  • add.html 只需要显示默认值!
<input type="text" name="lastName" class="form-control" placeholder="海绵宝宝">

修改员工成功

修改员工信息成功:

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

删除员工信息

按钮提交

list.html 页面删除按钮的修改:

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

接收参数删除用户信息

// 删除员工
@GetMapping("/delemp/{id}")
// @PathVariable 映射 URL 绑定的占位符
public String deleteEmp(@PathVariable("id") Integer id) {
    employeeDao.delete(id);
    return "redirect:/emps";
}

404 页面

  1. 404.html 页面放入到 templates 目录下面的 error 目录中
  2. 错误运行页面:

img

注销功能的实现

  1. commons.html 中修改注销按钮
<a class="nav-link" th:href="@{/user/logout}">注销</a>
  1. LoginController.java 中编写注销页面代码
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
    session.invalidate();
    return "redirect:/index.html";
}

如何写一个网站

步骤

  1. 搞定前端:页面长什么样子
  2. 设计数据库(难点)
  3. 前端让他能自动运行,独立化工程
  4. 数据接口如何对接:json,对象,all in one
  5. 前后端联调测试

模板

  1. 有一套自己熟悉的后台模板:工作必要 x-admin
  2. 前端页面:至少自己能够通过前端框架(Bootstrap | Layui | semantic-ui),组合出来一个网站页面
    1. 栅格系统
    2. 导航栏
    3. 侧边栏
    4. 表单
  3. 让这个网站能够独立运行

整合 JDBC

创建测试项目测试数据源

  1. 新建一个项目测试:引入相应的模块

img

  1. 项目建好之后,发现自动帮我们导入了如下的启动器:
<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>
  1. 编写 yaml 配置文件连接数据库:
spring:
  datasource:
    username: root
    password: 130914
    # ?serverTimezone=UTC 解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
  1. 测试类测试
@SpringBootTest
class SpringbootDataJdbcApplicationTests {

    // DI 注入数据源
    @Autowired
    DataSource dataSource;

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

JDBCTemplate

  1. 有了数据源 (com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接 (java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;

  2. 即使不使用第三方第数据库操作框架,如 MyBatis 等,Spring 本身也对原生的 JDBC 做了轻量级的封装,即 JdbcTemplate。

  3. 数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。

  4. Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用

  5. JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类

JdbcTemplate主要提供以下几类方法:

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

测试

  1. 编写一个 Controller,注入 jdbcTemplate,编写测试方法进行访问测试;
@RestController
public class JDBCController {

    final
    JdbcTemplate jdbcTemplate;

    public JDBCController(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    // 查询数据库的所有信息
    // 没有实体类,获取数据库的东西,怎么获取? Map
    @GetMapping("/userList")
    public List<Map<String, Object>> userList() {
        String sql = "select * from user";
        return jdbcTemplate.queryForList(sql);
    }

    @GetMapping("/addUser")
    public String addUser() {
        String sql = "insert into mybatis.user(id, name, pwd) values(7,'小明','123456')";
        jdbcTemplate.update(sql);
        return "update-ok";
    }

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id) {
        String sql = "update mybatis.user set name  = ?,pwd = ? where id = " + id;
        //封装
        Object[] objects = new Object[2];

        objects[0] = "小明2";
        objects[1] = "aaaaaaa";

        jdbcTemplate.update(sql, objects);
        return "update-ok";
    }

    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id) {
        String sql = "delete from mybatis.user where id = ?";
        jdbcTemplate.update(sql, id);
        return "delete-ok";
    }
}

整合 Druid

Java 程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。

Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。

Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。

Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。

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

配置数据源

  1. 添加上 Druid 数据源依赖:这个依赖可以从 Maven 仓库官网Maven Respository中获取,注意,如果使用 springboot 3.x,artifactId 一定要改为 druid-spring-boot-3-starter,若使用 druid-spring-boot-starter 会报错
<!-- springboot 版本为 3.0.2 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-3-starter</artifactId>
    <version>1.2.20</version>
</dependency>
  1. 切换数据源:之前已经说过 Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源。
spring:
  datasource:
    username: root
    password: 130914
    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 # 自定义数据源
  1. 在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换

  2. 切换成功后,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数,参考文档:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter

spring:
  datasource:
    username: root
    password: 130914
    # ?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

    # druid 数据源专有配置
    druid:
      # 初始化时建立物理连接的个数
      initial-size: 5
      # 连接池的最小空闲数
      min-idle: 5
      # 连接池最大连接数
      max-active: 20
      # 获取连接时最大等待时间,单位毫秒
      max-wait: 60000
      # 既作为检测的间隔时间又作为 test-while-idle 执行的依据
      time-between-eviction-runs-millis: 60000
      # 连接保持空闲而不被驱逐的最小时间
      min-evictable-idle-time-millis: 300000
      # 用来检测连接是否有效的 sql,要求是一个查询语句
      validation-query: SELECT 1 FROM DUAL
      # 申请连接时执行 validationQuery 检测连接是否有效,开启会降低性能
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      pool-prepared-statements: true

      # 配置监控统计拦截的filters,stat:监控统计、slf4j:日志记录、wall:防御 sql 注入
      filters: stat,wall,slf4j
      Max-pool-prepared-statement-per-connection-size: 20
      use-global-data-source-stat: true
      connect-properties:
        druid.stat.mergeSql: true
        druid.stat.slowSqlMillis: 500
  1. 现在需要程序员自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 SpringBoot 的自动生成;我们需要自己添加 DruidDataSource 组件到容器中,并绑定属性:
@Configuration
public class DruidConfig {
    /*
       将自定义的 Druid 数据源添加到容器中,不再让 Spring Boot 自动创建
       绑定全局配置文件中的 druid 数据源属性到 com.alibaba.druid.pool.DruidDataSource 从而让它们生效
       @ConfigurationProperties(prefix = "spring.datasource.druid"):作用就是将全局配置文件中
       前缀为 spring.datasource 的属性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名参数中
     */
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    @Bean
    public DruidDataSource druidDataSource() {
        return DruidDataSourceBuilder.create().build();
    }
}
  1. 去测试类中测试一下,看是否成功
@SpringBootTest
class SpringbootDataJdbcApplicationTests {

    // DI 注入数据源
    @Autowired
    DataSource dataSource;

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

        DruidDataSource druidDataSource = (DruidDataSource) dataSource;
        System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
        System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());

        // 关闭连接
        connection.close();
    }
}
  1. 输出结果:可见配置类已经生效

img

配置 Druid 数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装路由器时,人家也提供了一个默认的 web 页面。所以第一步需要设置 Druid 的后台管理页面,比如登录账号、密码等;配置后台管理等

继续配置 appliacation.yaml 文件:

spring:
  datasource:
    ...
    # druid 数据源专有配置
    druid:
      ...
      # StatViewServlet 配置:启用内置的监控页面
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        login-username: admin
        login-password: 123
        allow: 127.0.0.1  # 为空或者为 null 时,表示允许所有访问
        deny: 192.168.1.20  # 拒绝此 ip 访问
        reset-enable: false

配置完毕后,访问 :http://localhost:8080/druid/login.html

img

配置 Druid web 监控 filter 过滤器:

spring:
  datasource:
    ...
    # druid 数据源专有配置
    druid:
      ...
      # WebStatFilter 配置
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/jdbc/*'
        session-stat-enable: true

执行一条 SQL 语句后:http://localhost:8080/queryUserList,查看 Druid 界面的 SQL 监控:

img

整合 MyBatis

官方文档

Maven 仓库地址

  1. 导入 MyBatis 所需要的依赖:直接浏览器搜索 mybatis-spring-boot-starter 中文文档,查看版本依赖
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.1</version>
</dependency>
  1. 配置数据库连接信息(不变)
spring:
  datasource:
    username: root
    password: 130914
    #?serverTimezone=UTC 解决时区的报错
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver


# 整合 mybatis
mybatis:
  type-aliases-package: com.locke.pojo
  mapper-locations: classpath:mybatis/mapper/*.xml
  1. 测试数据库是否连接成功
  2. 创建实体类,导入 lombok
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
}
  1. 创建 mapper 目录以及对应的 Mapper 接口:UserMapper.java
// 这个注解表示了这是一个 mybatis 的 mapper 类
@Mapper
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}
  1. 在 resources 文件夹下建立 mybatis.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">
<!--namespace=绑定一个对应的 Dao/Mapper接口-->
<mapper namespace="com.locke.mapper.UserMapper">

    <select id="queryUserList" resultType="User">
        select * from mybatis.user;
    </select>

    <select id="queryUserById" resultType="User">
        select * from mybatis.user where id = #{id};
    </select>

    <insert id="addUser" parameterType="User">
        insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
    </insert>

    <update id="updateUser" parameterType="User">
        update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
    </update>

    <delete id="deleteUser" parameterType="int">
        delete from mybatis.user where id = #{id}
    </delete>
</mapper>
  1. maven 配置资源过滤问题
<resources>
    <resource>
        <directory>src/main/java</directory>
        <includes>
            <include>**/*.xml</include>
        </includes>
        <filtering>true</filtering>
    </resource>
</resources>
  1. 创建 controller 文件夹,编写 UserController 进行测试
@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

    @GetMapping("/queryUserList")
    public List<User> queryUserList() {
        List<User> userList = userMapper.queryUserList();

        for (User user : userList) {
            System.out.println(user);
        }

        return userList;
    }

    // 添加一个用户
    @GetMapping("/addUser")
    public String addUser() {
        userMapper.addUser(new User(7, "阿毛", "123456"));
        return "ok";
    }

    // 修改一个用户
    @GetMapping("/updateUser")
    public String updateUser() {
        userMapper.updateUser(new User(7, "阿毛", "123456"));
        return "ok";
    }

    @GetMapping("/deleteUser")
    public String deleteUser() {
        userMapper.deleteUser(7);

        return "ok";
    }
}
  1. 启动项目访问进行测试!

SpringSecurity

安全简介

在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。

市面上存在比较有名的:Shiro,Spring Security

每一个框架的出现都是为了解决某一个问题,Spring Security 是为了解决什么问题?

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架,侧重于为 java 应用程序提供身份验证和授权,与所有 Spring 项目一样,Spring 安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。

一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。

  • 用户认证:指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
  • 用户授权:指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

对于上面提到的两者应用场景,Spring Security 框架都有很好的支持:

  • 用户认证:Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。
  • 用户授权:Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。

实战测试

实验环境搭建

  1. 新建一个初始的 springboot 项目 web 模块,thymeleaf 模块,注意 springboot 降级到 2.6.0
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
  1. 为了有一个更好的学习体验,先不要引入 spring-boot-starter-security

  2. 导入静态资源:

img

  1. controller 跳转:RouterController.java
@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;
    }
}
  1. 测试实验环境是否 OK
    1. 首页 & 登录

img

img

认识 Spring Security

对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!

记住几个类:

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

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

认证和授权

目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能。

  1. 引入 Spring Security 模块
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 编写 Spring Security 配置类

    1. 参考官网:https://spring.io/projects/spring-security

    2. 查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5

    3. servlet-applications 8.16.4

  2. 编写基础配置类:

img

// 开启 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}
  1. 定制请求的授权规则:看源码 + 仿写
// 链式编程
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 首页所有人都可以访问,功能也只有对应有权限的人才能访问到
    // 请求授权的规则
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/level1/**").hasRole("vip1")
        .antMatchers("/level2/**").hasRole("vip2")
        .antMatchers("/level3/**").hasRole("vip3");

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

img

  1. configure() 方法中加入以下配置,开启自动配置的登录功能!
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
  1. 测试一下:发现没有权限的时候,会跳转到登录的页面
  2. 查看刚才登录页的注释信息:我们可以定义认证规则,重写 configure 的另一个方法

img

// 认证,springboot 2.1.x 可以直接使用
// 密码编码:PasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    // 这些数据正常应该中数据库中读

    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");
}
  1. 测试:我们可以使用这些账号登录进行测试,发现会报错

img

  1. 原因:我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码
// 认证,springboot 2.1.x 可以直接使用
// 密码编码:PasswordEncoder
// 在spring Secutiry 5.0+ 新增了很多加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

    // 这些数据正常应该中数据库中读
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
        .withUser("kuangshen").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");
}
  1. 测试:发现登录成功,并且每个角色只能访问自己认证下的规则

权限控制和注销

  1. 开启自动配置的注销的功能
// 定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
   // ....
   // 开启自动配置的注销的功能
   // /logout 注销请求
   http.logout();
}
  1. 在前端,增加一个注销的按钮,index.html 导航栏中
<!--注销-->
<a class="item" th:href="@{/logout}">
    <i class="sign-out icon"></i> 注销
</a>
  1. 我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!

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

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

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

      1. sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面
      2. Maven 依赖
      <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
      <dependency>
          <groupId>org.thymeleaf.extras</groupId>
          <artifactId>thymeleaf-extras-springsecurity5</artifactId>
      </dependency>
      
  2. 修改前端页面

    1. 导入命名空间
    <html lang="en" xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
    
    1. 修改导航栏,增加认证判断
    <!--登录注销-->
    <div class="right menu">
    
        <!--如果未登录-->
        <div sec:authorize="!isAuthenticated()">
            <a class="item" th:href="@{/login}">
                <i class="address card icon"></i> 登录
            </a>
        </div>
    
        <!--如果已登录-->
        <div sec:authorize="isAuthenticated()">
            <a class="item">
                <i class="address card icon"></i>
                用户名:<span sec:authentication="principal.username"></span>
                角色:<span sec:authentication="principal.authorities"></span>
            </a>
        </div>
    
        <div sec:authorize="isAuthenticated()">
            <a class="item" th:href="@{/logout}">
                <i class="sign-out  icon"></i> 注销
            </a>
        </div>
    </div>
    
  3. 重启测试

  4. 点击注销产生的问题

    1. 整合包 4(springsecurity4)
    2. 整合包 5(springsecurity5)
    3. 解决问题:
      1. 默认防止 csrf 跨站请求伪造,因为会产生安全问题
      2. 将请求改为 post 表单提交
      3. 在 spring security 中关闭 csrf 功能 http.csrf().disable();
  5. 继续将下面的角色功能块认证完成:

<!--菜单根据用户的角色动态的实现-->
<div class="column"  sec:authorize="hasRole('vip1')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 1</h5>
                <hr>
                <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
            </div>
        </div>
    </div>
</div>

<div class="column"  sec:authorize="hasRole('vip2')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 2</h5>
                <hr>
                <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
            </div>
        </div>
    </div>
</div>

<div class="column"  sec:authorize="hasRole('vip3')">
    <div class="ui raised segment">
        <div class="ui">
            <div class="content">
                <h5 class="content">Level 3</h5>
                <hr>
                <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
            </div>
        </div>
    </div>
</div>

记住我

  1. 开启记住我功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
   //开启记住我功能: cookie,默认保存两周
   http.rememberMe();
}
  1. 再次测试

  2. 如何实现的?

    1. 查看浏览器 cookie

    img

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

    img

    1. cookie 发送给浏览器保存,以后登录带上这个 cookie,只要通过检查就可以免登陆了,如果点注销,则会删除这个 cookie

定制登录页

如何实现自己的 Login 界面?

  1. 在刚才的登录页配置后面指定 loginpage
protected void configure(HttpSecurity http) throws Exception {
    //......

    // 没有权限默认会到登录页面,需要开启登录的页面
    // /login页面
    http.formLogin().loginPage("/toLogin");

    //......
}
  1. 然后前端也需要指向我们自己定义的 login 请求
<div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLogin}">
        <i class="address card icon"></i> 登录
    </a>
</div>
  1. 登录后需要将这些信息发送到哪里?我们也需要配置,login.html 配置提交请求及方式,方式必须为 post:
protected void configure(HttpSecurity http) throws Exception {
    //......

    // 没有权限默认会到登录页面,需要开启登录的页面
    // /login 页面
    http.formLogin()
      .usernameParameter("username")
      .passwordParameter("password")
      .loginPage("/toLogin")
      .loginProcessingUrl("/login"); // 登陆表单提交请求

    //......
}
  1. 在登录页增加我的多选框
<input type="checkbox" name="remember"> 记住我
  1. 后端验证处理
protected void configure(HttpSecurity http) throws Exception {
    //......
    //开启记住我功能: cookie,默认保存两周,自定义接收前端的参数
    http.rememberMe().rememberMeParameter("remember");
}

完整配置

// AOP:拦截器
// 开启 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

        // 没有权限默认会到登录页面,需要开启登录的页面
        // /login页面
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginPage("/toLogin")
                .loginProcessingUrl("/login");

        // 注销,开启了注销功能,跳到首页
        http.logout().logoutSuccessUrl("/");

        // 防止网站工具:get,post
        http.csrf().disable();//关闭csrf功能,登录失败肯定存在的原因

        // 开启记住我功能: cookie,默认保存两周,自定义接收前端的参数
        http.rememberMe().rememberMeParameter("remember");


    }

    // 认证,springboot 2.1.x 可以直接使用
    // 密码编码: PasswordEncoder
    // 在spring Secutiry 5.0+ 新增了很多加密方法
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        // 这些数据正常应该中数据库中读
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("kuangshen").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");
    }

}

Shiro

概述

功能

Apache Shiro 是一个强大且易用的 Java 安全框架,可以完成身份验证、授权、密码和会话管理。

  • Authentication:身份认证 / 登录,验证用户是不是拥有相应的身份;
  • Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
  • Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的;
  • Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
  • Web Support:Web 支持,可以非常容易的集成到 Web 环境;
  • Caching:缓存,比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率;
  • Concurrency:shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
  • Testing:提供测试支持;
  • Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
  • Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。

从外部看

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

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

认证流程

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

img

快速入门

拷贝案例

  1. 按照官网提示找到快速入门案例:shiro/samples/quickstart/
  2. 复制快速入门案例 POM.xml 文件中的依赖分析案例
<dependencies>
    <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.11.0</version>
    </dependency>

    <!-- configure logging -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-core</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
  1. 把快速入门案例中的 resource 下的 log4j.properties 复制下来
  2. 复制一下 shiro.ini 文件
  3. 复制一下 Quickstart.java 文件
  4. 运行 Quickstart.java,得到结果

分析案例

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

    Subject currentUser = SecurityUtils.getSubject();
    
  2. 通过当前用户拿到 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.hasRole()
    
  8. 注销

    currentUser.logout();
    

SpringBoot 集成 Shiro

注意:SringBoot 的版本为 2.7.3,java 版本为 11

编写配置文件

  1. 在刚刚的父项目中新建一个 springboot 模块
  2. 导入 SpringBoot 和 Shiro 整合包的依赖
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.6.0</version>
</dependency>
  1. 编写配置的三大要素:

    1. subject \(\to\) ShiroFilterFactoryBean
    2. securityManager \(\to\) DefaultWebSecurityManager
    3. realm
  2. 实际操作中对象创建的顺序:realm \(\to\) securityManager \(\to\) subject

  3. 编写自定义的 UserRealm.java,需要继承 AuthorizingRealm

// 自定义的 Realm
public class UserRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 打印一个提示
        System.out.println("执行了授权方法");
        return null;
    }

    // 认证
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 打印一个提示
        System.out.println("执行了认证方法");
        return null;
    }
}
  1. 新建一个 ShiroConfig 配置文件:ShiroConfig.java
@Configuration
public class ShiroConfig {
    // 第三步:ShiroFilterFactoryBean
    @Bean(name = "shiroFilterFactoryBean") // 务必设置 Bean name 为 shiroFilterFactoryBean,否则会报错
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 设置安全管理器
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        return bean;
    }

    // 第二步:securityManager -> DefaultWebSecurityManager
    // @Qualifier("userRealm") 指定 Bean 的名字为 userRealm
    // spring 默认的 BeanName 就是方法名
    // name 属性指定 BeanName
    @Bean(name = "SecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurity(@Qualifier("userRealm") UserRealm userRealm){
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 需要关联自定义的 Realm,通过参数把 Realm 对象传递过来
        securityManager.setRealm(userRealm);
        return securityManager;
    }

    // 第一步:创建 realm 对象,需要自定义类
    // 让 spring 托管自定义的 realm 类
    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }
}

搭建简单测试环境

  1. 新建一个登录页面
  2. 新建一个首页:三个链接,一个登录、一个增加用户、一个删除用户
  3. 新建一个增加用户页面
  4. 新建一个删除用户页面
  5. 编写对应的 Controller
@Controller
public class MyController {
    @RequestMapping({"/", "/index"})
    public String toIndex(Model model) {
        model.addAttribute("msg", "hello, Shiro");
        return "index";
    }

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

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

    @RequestMapping("/tologin")
    public String toLogin() {
        return "login";
    }
}
  1. 登录页面如下:注意在 pom 中加上 thymeleaf 依赖
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>登录页面</title>
    </head>
    <body>
        <h1>登录</h1>
        <hr>

        <form th:action="@{/login}">
            <p>用户名:<input type="text" name="username"></p>
            <p>密码:<input type="text" name="password"></p>
            <p>提交:<input type="submit"></p>
            <p style="color:red;" th:text="${msg}"></p>
        </form>
    </body>
</html>

使用

登录拦截

在上面的 getShiroFilterFactoryBean 方法中加上需要拦截的登录请求:

@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager){
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);

	//添加 Shiro 的内置过滤器=======================
    /*
        anon : 无需认证,就可以访问
        authc : 必须认证,才能访问
        user : 必须拥有 “记住我”功能才能用
        perms : 拥有对某个资源的权限才能访问
        role : 拥有某个角色权限才能访问
     */
    Map<String, String> map = new LinkedHashMap<>();
    // 设置 /user/addUser 这个请求,只有认证过才能访问
	// map.put("/user/addUser","authc");
	// map.put("/user/deleteUser","authc");
    // 设置 /user/ 下面的所有请求,只有认证过才能访问
    map.put("/user/*","authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    // 设置登录的请求
    shiroFilterFactoryBean.setLoginUrl("/tologin");
	//============================================
    return shiroFilterFactoryBean;
}
用户认证
  1. 在 Controller 中写一个登录的控制器:
// 登录的方法
@RequestMapping("/login")
public String login(String username, String password, Model model) {
    // 获取当前用户
    Subject subject = SecurityUtils.getSubject();
    // 没有认证过
    // 封装用户的登录数据,获得令牌
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);

    // 登录及异常处理
    try {
        // 用户登录
        subject.login(token);
        return "index";
    } catch (UnknownAccountException uae) {
        // 如果用户名不存在
        System.out.println("用户名不存在");
        model.addAttribute("exception", "用户名不存在");
        return "login";
    } catch (IncorrectCredentialsException ice) {
        // 如果密码错误
        System.out.println("密码错误");
        model.addAttribute("exception", "密码错误");
        return "login";
    }
}
  1. 重启并测试
    1. 可以看出,是先执行了自定义的 UserRealm 中的 AuthenticationInfo 方法,再执行了登录的相关操作
    2. 下面去自定义的 UserRealm 中的 AuthenticationInfo 方法中去获取用户信息
  2. 修改 UserRealm 中的 AuthenticationInfo
// 自定义的 Realm
public class UserRealm extends AuthorizingRealm {
    // 授权
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        // 打印一个提示
        System.out.println("执行了授权方法");
        return null;
    }

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

        // 用户名密码 (暂时先自定义一个做测试)
        String name = "root";
        String password = "1234";

        // 1. 用户名认证
        // 通过参数获取登录的控制器中生成的令牌
        UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
        // 用户名认证
        if (!token.getUsername().equals(name)){
            // 用户名不存在,shiro 底层就会抛出 UnknownAccountException 异常
            // return null 就表示控制器中抛出的相关异常
            return null;
        }
        // 2. 密码认证, Shiro 自己做,为了避免和密码的接触
        // 最后返回一个 AuthenticationInfo 接口的实现类,这里选择 SimpleAuthenticationInfo
        // 三个参数:获取当前用户的认证, 密码, 认证名
        return new SimpleAuthenticationInfo("", password, "");
    }
}
退出登录
  1. 在控制器中添加一个退出登录的方法
// 退出登录
@RequestMapping("/logout")
public String logout(){
    Subject subject = SecurityUtils.getSubject();
    subject.logout();
    return "login";
}

Swagger

  1. 前后端分离

    1. Vue + SpringBoot
    2. 后端时代:前端只用管理静态页面;html \(\to\) 后端。模板引擎 JSP \(\to\) 后端才是主力
  2. 前后端分离时代

    1. 前端 \(\to\) 前端控制层、视图层
    2. 伪造后端数据,json。已经存在了,不需要后端,前端工程队依旧能够跑起来
    3. 后端 \(\to\) 后端控制层、服务层、数据访问层
    4. 前后端通过 API 进行交互
    5. 前后端相对独立且松耦合
  3. 产生的问题

    1. 前后端集成联调,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发
  4. 解决方案

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

Swagger

  1. 号称世界上最流行的 API 框架

  2. Restful Api 文档在线自动生成器 \(\to\) API 文档 与API 定义同步更新

  3. 直接运行,在线测试 API

  4. 支持多种语言 (如:Java,PHP 等)

  5. 官网:https://swagger.io/

Spring 集成

版本依赖问题很大!

SpringBoot 集成 Swagger \(\to\) springfox,两个 jar 包

使用 Swagger 步骤:

  1. 新建一个 SpringBoot-web 项目,注意 springboot 版本要降到 2.5.6

  2. 添加 Maven 依赖(注意:2.9.2 版本之前,之后的不行

<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.9.2</version>
</dependency>

<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
</dependency>
  1. 编写 HelloController,测试确保运行成功!
@RestController
public class HelloController {
    @RequestMapping("/hello")
    public String hello() {
        return "hello!";
    }
}
  1. 要使用 Swagger,我们需要编写一个配置类 SwaggerConfig 来配置 Swagger
@Configuration  // 配置类
@EnableSwagger2 // 开启Swagger2的自动配置
public class SwaggerConfig {
}
  1. 访问测试 :Swagger UI ,可以看到 swagger 的界面;
    1. Swagger 信息
    2. 接口信息
    3. 实体类信息

img

配置 Swagger

  1. Swagger 实例 Bean 是 Docket,所以通过配置 Docket 实例来配置 Swaggger。

    @Configuration
    // 开启 Swagger
    @EnableSwagger2
    public class SwaggerConfig {
        @Bean // 配置 docket 以配置 Swagger 具体参数
        public Docket docket() {
            return new Docket(DocumentationType.SWAGGER_2);
        }
    }
    
  2. 可以通过 apiInfo() 属性配置文档信息

    // 配置文档信息
    private ApiInfo apiInfo() {
        Contact contact = new Contact("Lockegogo", "https://www.cnblogs.com/lockegogo/", "联系人邮箱");
        return new ApiInfo(
                "LK's Swagger", // 标题
                "Life is like a Markov chain.", // 描述
                "v1.0", // 版本
                "http://terms.service.url/组织链接", // 组织链接
                contact, // 联系人信息
                "Apach 2.0 许可", // 许可
                "许可链接", // 许可连接
                new ArrayList<>()// 扩展
        );
    
  3. Docket 实例关联上 apiInfo()

    @Bean
    public Docket docket() {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
    }
    
  4. 重启项目,访问测试 http://localhost:8080/swagger-ui.html 看下效果;

img

配置扫描接口

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

    @Bean
    public Docket docket() {
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select() // 通过.select()方法,去配置扫描接口, RequestHandlerSelectors 配置如何扫描接口
                .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller"))
                .build();
    }
    
  2. 重启项目测试,由于我们配置根据包的路径扫描接口,所以我们只能看到一个类

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

    // 根据包路径扫描接口
    basePackage(final String basePackage)
    // 扫描所有,项目中的所有接口都会被扫描到
    any()
    // 不扫描接口
    none()
    // 通过方法上的注解扫描,如 withMethodAnnotation(GetMapping.class) 只扫描 get 请求
    withMethodAnnotation(final Class<? extends Annotation> annotation)
    // 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有 controller 注解的类中的接口
    withClassAnnotation(final Class<? extends Annotation> annotation)
    
  4. 除此之外,我们还可以配置接口扫描过滤:

    @Bean
    public Docket docket() {
       return new Docket(DocumentationType.SWAGGER_2)
          .apiInfo(apiInfo())
          .select()
          .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller"))
           // 配置如何通过 path 过滤, 即这里只扫描请求以 /locke 开头的接口(网站上的路径)
          .paths(PathSelectors.ant("/locke/**"))
          .build();
    }
    
  5. 这里的可选值还有

    any() // 任何请求都扫描
    none() // 任何请求都不扫描
    regex(final String pathRegex) // 通过正则表达式控制
    ant(final String antPattern) // 通过 ant() 控制
    

配置 Swagger 开关

  1. 通过 enable() 方法配置是否启用 swagger,如果是 false,swagger 将不能在浏览器中访问了

    @Bean
    public Docket docket() {
       return new Docket(DocumentationType.SWAGGER_2)
          .apiInfo(apiInfo())
          // 配置是否启用 Swagger,如果是 false,在浏览器将无法访问
          .enable(false)
          .select()
          .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller"))
          .paths(PathSelectors.ant("/locke/**"))
          .build();
    }
    
  2. 如何动态配置当项目处于 test、dev 环境时显示 swagger,处于 prod 时不显示?

    @Bean
    // 获得当前的生产环境
    public Docket docket(Environment environment) {
        // 设置要显示 swagger 的环境
        Profiles of = Profiles.of("dev", "test");
        // 判断当前是否处于该环境
        // 通过 enable() 接收此参数判断是否要显示
        boolean b = environment.acceptsProfiles(of);
    
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(b)
                .select() // 通过.select()方法,去配置扫描接口, RequestHandlerSelectors 配置如何扫描接口
                .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller"))
                .build();
    }
    
  3. 可以在项目中增加配置文件

    1. dev 测试环境
    server.port=8081
    

    img

    项目运行结果:

    img

    1. pro 测试环境
    server.port=8082
    

img

项目运行结果

img

配置 API 分组

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

    @Bean
    public Docket docket(Environment environment) {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
          .groupName("狂神") // 配置分组
           // 省略配置....
    }
    
  2. 重启项目查看分组

    image-20200731195354714

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

    @Bean
    public Docket docket1(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
    }
    @Bean
    public Docket docket2(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
    }
    @Bean
    public Docket docket3(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
    }
    
  4. 重启项目查看即可

    img

实体配置

  1. 新建一个实体类

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    // @Api("注释")
    @ApiModel("用户实体")
    public class User {
        @ApiModelProperty("用户名")
        private String username;
        @ApiModelProperty("密码")
        private String password;
    }
    
  2. 只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:

    @RestController
    public class HelloController {
    
        // /error 默认错误请求
        @GetMapping("/hello")
        public String hello() {
            return "hello";
        }
    
        // 只要我们的接口中,返回值中存在实体类,他就会被扫描到 Swagger 中
        @PostMapping("/user")
        public User user() {
            return new User();
        }
    }
    
  3. 重启查看测试:

    image-20230219131116742

注:并不是因为 @ApiModel 这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而 @ApiModel 和 @ApiModelProperty 这两个注解只是为实体添加注释的

  • @ApiModel 为类添加注释
  • @ApiModelProperty 为类属性添加注释
  • @ApiOperation 为方法添加注释
  • @ApiParam 给参数加上注释

总结:

  • 我们可以通过 Swagger 给一些比较难理解的接口或者属性,增加注释信息
  • 接口文档实时更新
  • 可以在线测试

Swagger 是一个优秀的工具,几乎所有大公司都有使用它

【注意点】:在正式发布的时候,关闭 Swagger!!!

  • 出于安全考虑
  • 而且节省内存

常用注解

Swagger 的所有注解定义在 io.swagger.annotations 包下

下面列一些经常用到的,未列举出来的可以另行查阅说明:

Swagger注解 简单说明
@Api(tags = "xxx模块说明") 作用在模块类上
@ApiOperation("xxx接口说明") 作用在接口方法上
@ApiModel("xxxPOJO说明") 作用在模型类上:如VO、BO
@ApiModelProperty(value = "xxx属性说明",hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性
@ApiParam("xxx参数说明") 作用在参数、方法和字段上,类似@ApiModelProperty

我们也可以给请求的接口配置一些注释

  1. 在 HelloController 控制类中的接口添加 api 接口注释

    @RestController
    public class HelloController {
        ......
        @ApiOperation("Hello 控制接口")
        @GetMapping("/hello")
        public String hello2(@ApiParam("用户名") String username) {
            return "hello" + username;
        }
    
        @ApiOperation("get 测试")
        @GetMapping("/get")
        public User hello2(@ApiParam("用户") User user) {
            return user;
        }
    }
    

    img

  2. 进行 try it out 测试

    img

    测试结果

    img

总结:

  1. 这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!

  2. 相较于传统的 Postman 或 Curl 方式测试接口,使用 swagger 简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击 Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。

  3. Swagger 是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出 Word 接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭 Swagger,一来出于安全考虑二来也可以节省运行时内存。

拓展

我们可以导入不同的包实现不同的皮肤定义:

1、默认的 访问 http://localhost:8080/swagger-ui.html

<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
</dependency>

img

2、bootstrap-ui 访问 http://localhost:8080/doc.html

<!-- 引入 swagger-bootstrap-ui包 /doc.html-->
<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>swagger-bootstrap-ui</artifactId>
   <version>1.9.1</version>
</dependency>

img

3、Layui-ui 访问 http://localhost:8080/docs.html

<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
   <groupId>com.github.caspar-chen</groupId>
   <artifactId>swagger-ui-layer</artifactId>
   <version>1.1.3</version>
</dependency>

4、mg-ui 访问 http://localhost:8080/document.html

<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
   <groupId>com.zyplayer</groupId>
   <artifactId>swagger-mg-ui</artifactId>
   <version>1.0.6</version>
</dependency>

img

异步、定时、邮件任务

异步任务

  1. 创建一个 service

  2. 创建一个类 AsyncService

异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。

编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;

@Service
public class AsyncService {

    public void hello() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("业务进行中....");
    }
}
  1. 编写 controller
  2. 编写 AsyncController
@RestController
public class AsyncController {

    @Autowired
    AsyncService asyncService;

    @GetMapping("/hello")
    public String hello() {
        asyncService.hello(); // 停止 3 秒
        return "OK";
    }

}
  1. 访问 http://localhost:8080/hello 进行测试,3 秒后出现 OK,这是同步等待的情况。

问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:

  1. 给 hello 方法添加 @Async 注解;
// 告诉 Spring 这是一个异步方法
@Async
public void hello() {
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("业务进行中....");
}

SpringBoot 就会自己开一个线程池,进行调用!但是要让这个注解生效,我们还需要在主程序上添加一个注解 @EnableAsync ,开启异步注解功能;

//开启异步注解功能
@EnableAsync
@SpringBootApplication
public class SpringbootTaskApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootTaskApplication.class, args);
  }

}
  1. 重启测试,网页瞬间响应,后台代码依旧执行!

邮件任务

邮件发送,在我们的日常开发中,也非常的多,Springboot 也帮我们做了支持

  • 邮件发送需要引入 spring-boot-start-mail
  • SpringBoot 自动配置 MailSenderAutoConfiguration
  • 定义 MailProperties 内容,配置在 application.yaml
  • 自动装配 JavaMailSender
  • 测试邮件发送

测试:

  1. 引入 pom 依赖

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

    看它引入的依赖,可以看到 jakarta.mail

    <dependency>
       <groupId>com.sun.mail</groupId>
       <artifactId>jakarta.mail</artifactId>
       <scope>compile</scope>
    </dependency>
    
  2. 查看自动配置类:MailSenderAutoConfiguration

    img

    这个类中存在bean,JavaMailSenderImpl

    img

    然后我们去看下配置文件

    @ConfigurationProperties(prefix = "spring.mail")
    public class MailProperties {
    
    	private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    	private String host;
    	private Integer port;
    	private String username;
    	private String password;
    	private String protocol = "smtp";
    	private Charset defaultEncoding = DEFAULT_CHARSET;
    	private Map<String, String> properties = new HashMap<>();
    	private String jndiName;
    	//set、get方法省略。。。
    }
    
    
  3. 配置文件:

    spring.mail.username=1710841251@qq.com
    spring.mail.password=你的qq授权码
    spring.mail.host=smtp.qq.com
    # qq 需要配置 ssl
    spring.mail.properties.mail.smtp.ssl.enable=true
    

    获取授权码:在QQ邮箱中的设置 \(\to\) 账户 \(\to\) 开启 pop3 和 smtp 服务

    img

  4. Spring 单元测试

    @Autowired
    JavaMailSenderImpl javaMailSender;
    @Test//邮件设置1:一个简单的邮件
    void contextLoads() {
        SimpleMailMessage mailMessage = new SimpleMailMessage();
        mailMessage.setSubject("狂神,你好");
        mailMessage.setText("谢谢你的狂神说Java系列课程");
    
        mailMessage.setTo("24736743@qq.com");
        mailMessage.setFrom("1710841251@qq.com");
        javaMailSender.send(mailMessage);
    }
    
    @Test// 一个复杂的邮件
    void contextLoads2() throws MessagingException {
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        //组装
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    
        //正文
        helper.setSubject("狂神,你好~plus");
        helper.setText("<p style='color:red'>谢谢你的狂神说Java系列课程</p>", true);
    
        //附件
        helper.addAttachment("1.jpg", new File(""));
        helper.addAttachment("2.jpg", new File(""));
    
        helper.setTo("24736743@qq.com");
        helper.setFrom("1710841251@qq.com");
    
        javaMailSender.send(mimeMessage);
    
    }
    
  5. 查看邮箱,邮件接收成功!

我们只需要使用 Thymeleaf 进行前后端结合即可开发自己网站邮件收发功能了!

定时任务

项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring 为我们提供了异步执行任务调度的方式,提供了两个接口。

  • TaskExecutor 接口(任务执行者)
  • TaskScheduler 接口(任务调度者)

两个注解:

  • @EnableScheduling:开启定时功能的注解
  • @Scheduled:什么时候执行

cron 表达式:

字段 允许值 允许特殊字符
0-59 , - * /
0-59 , - * /
小时 0-23 , - * /
日期 1-31 , - * / ? L W C
月份 1-12 , - * /
星期 0-1 或 SUN-SAT 0,7 是 SUN , - * / ? L W C
特殊字符 代表含义
, 枚举
- 区间
* 任意
/ 步长
? 日/星期冲突匹配
L 最后
W 工作日
C 和 calendar 练习后计算过的值
# 星期,4#2 第二个星期三

测试步骤:

  1. 创建一个 ScheduledService

我们里面存在一个 hello 方法,他需要定时执行,怎么处理呢?

@Service
public class ScheduledService {
    // 在一个特定的时间执行这个方法——Timer
    //cron表达式
    // 秒 分 时 日 月 周几

    /*
        0 49 11 * * ?       每天的11点49分00秒执行
        0 0/5 11,12 * * ?   每天的11点和12点每个五分钟执行一次
        0 15 10 ? * 1-6     每个月的周一到周六的10点15分执行一次
        0/2 * * * * ?       每2秒执行一次
     */
    @Scheduled(cron = "0/2 * * * * ?")
    public void hello() {
        System.out.println("hello, 你被执行了");
    }
}
  1. 这里写完定时任务之后,我们需要在主程序上增加 @EnableScheduling 开启定时任务功能
// 开启异步注解功能
@EnableAsync
// 开启基于注解的定时任务
@EnableScheduling
@SpringBootApplication
public class SpringbootTaskApplication {

   public static void main(String[] args) {
       SpringApplication.run(SpringbootTaskApplication.class, args);
  }
}
  1. 我们来详细了解下cron表达式:http://www.bejson.com/othertools/cron/

  2. 常用的表达式

(1)0/2 * * * * ?   表示每2秒 执行任务
(1)0 0/2 * * * ?   表示每2分钟 执行任务
(1)0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行作
(4)0 0 10,14,16 * * ?   每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ?   朝九晚五工作时间内每半小时
(6)0 0 12 ? * WED   表示每个星期三中午12点
(7)0 0 12 * * ?   每天中午12点触发
(8)0 15 10 ? * *   每天上午10:15触发
(9)0 15 10 * * ?     每天上午10:15触发
(10)0 15 10 * * ?   每天上午10:15触发
(11)0 15 10 * * ? 2005   2005年的每天上午10:15触发
(12)0 * 14 * * ?     在每天下午2点到下午2:59期间的每1分钟触发
(13)0 0/5 14 * * ?   在每天下午2点到下午2:55期间的每5分钟触发
(14)0 0/5 14,18 * * ?     在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
(15)0 0-5 14 * * ?   在每天下午2点到下午2:05期间的每1分钟触发
(16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44触发
(17)0 15 10 ? * MON-FRI   周一至周五的上午10:15触发
(18)0 15 10 15 * ?   每月15日上午10:15触发
(19)0 15 10 L * ?   每月最后一日的上午10:15触发
(20)0 15 10 ? * 6L   每月的最后一个星期五上午10:15触发
(21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最后一个星期五上午10:15触发
(22)0 15 10 ? * 6#3   每月的第三个星期五上午10:15触

Dubbo 和 Zookeeper 集成

分布式理论

什么是分布式系统

在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;

分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据

分布式系统(distributed system)是建立在网络之上的软件系统。

首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。

Dubbo 文档

随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。

在 Dubbo 的官网文档有这样一张图:

img

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架 (ORM) 是关键。

img

适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。

缺点:

  1. 性能扩展比较难
  2. 协同开发问题
  3. 不利于升级维护

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的 Web 框架 (MVC) 是关键。

img

通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职,更易于管理,性能扩展也更方便,更有针对性。

缺点

  1. 公用模块无法重复利用,开发性的浪费。

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架 (RPC)是关键。

img

优点

  1. 每个子系统变成小型系统,功能简单,前期开发成本低,周期短
  2. 每个子系统可按需伸缩
  3. 每个子系统可采用不同的技术

缺点

  1. 子系统之间存在数据冗余、功能冗余,耦合性高
  2. 按需伸缩粒度不够,对同一个子系统中的不同的业务无法实现,比如订单管理和用户管理

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心 (SOA) [ Service Oriented Architecture]是关键。

img

特点

  1. 基于 SOA 的架构思想,将重复公用的功能抽取为组件,以服务的方式向各各系统提供服务
  2. 各各系统与服务之间采用 webservice、rpc 等方式进行通信
  3. ESB 企业服务总线作为系统与服务之间通信的桥梁

优点

  1. 将重复的功能抽取为服务,提高开发效率,提高系统的可重用性、可维护性
  2. 可以针对不同服务的特点按需伸缩
  3. 采用 ESB 减少系统中的接口耦合

缺点

  1. 系统与服务的界限模糊,会导致抽取的服务的粒度过大,系统与服务之间耦合性高
  2. 虽然使用了 ESB,但是服务的接口协议不固定,种类繁多,不利于系统维护。

微服务架构

基于 SOA 架构的思想,为了满足移动互联网对大型项目及多客户端的需求,对服务层进行细粒度的拆分,所拆分的每个服务只完成某个特定的业务功能,比如订单服务只实现订单相关的业务,用户服务实现用户管理相关的业务等等,服务的粒度很小,所以称为微服务架构。

img

特点

  1. 服务层按业务拆分为一个一个的微服务
  2. 微服务的职责单一
  3. 微服务之间采用 RESTful、RPC 等轻量级协议传输
  4. 有利于采用前后端分离架构。

优点

  1. 服务拆分粒度更细,有利于资源重复利用,提高开发效率
  2. 可以更加精准的制定每个服务的优化方案,按需伸缩
  3. 适用于互联网时代,产品迭代周期更短

缺点

  1. 开发的复杂性增加,因为一个业务流程需要多个微服务通过网络交互来完成
  2. 微服务过多,服务治理成本高,不利于系统维护

什么是 RPC

RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。

也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;

推荐阅读文章:https://www.jianshu.com/p/2accc2840a1b

RPC基本原理

img

img

RPC 两个核心模块:通讯,序列化。

  • 序列化:数据传输需要转换

测试环境搭建

Dubbo

Apache Dubbo 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。

img

服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

注册中心(Registry):注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者

监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心

调用关系说明

  1. 服务容器负责启动,加载,运行服务提供者。
  2. 服务提供者在启动时,向注册中心注册自己提供的服务。
  3. 服务消费者在启动时,向注册中心订阅自己所需的服务。
  4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
  5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
  6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

安装 zookeeper

https://juejin.cn/post/6974556676816896030

在 zoo.cfg 中添加:admin.serverPort=2182

  1. 双击 zkServer.cmd 开启服务:端口号是 2181
  2. 使用 zkCli.cmd 测试
    1. ls /:列出 zookeeper 根下保存的所有节点
    2. create –e /locke 123:创建一个 locke 节点,值为 123
    3. get /locke:获取 /locke 节点的值

安装 dubbo-admin

dubbo 本身并不是一个服务软件。它其实就是一个 jar 包,能够帮你的 java 程序连接到 zookeeper,并利用 zookeeper 消费、提供服务;

但是为了让用户更好的管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin(监控管理后台),不过这个监控即使不装也不影响使用。

  1. 下载 dubbo-admin
  2. 在项目目录下 cmd 打包:mvn clean package -Dmaven.test.skip=true,也可以参考该项目的 README 文件打包

img

  1. 启动后端:
# 切换到目录:直接在路径前输入 cmd
dubbo-Admin-develop/dubbo-admin-distribution/target>
# 执行下面的命令启动 dubbo-admin,dubbo-admin 后台由 SpringBoot 构建
java -jar ./dubbo-admin-0.5.0-SNAPSHOT.jar
  1. 执行完毕后,访问 http://localhost:38080/, 用户名和密码都是 root

  2. 登录成功后查看界面:

img

总结

  1. zookeeper:注册中心
  2. dubbo-admin:其实就是一个监控管理后台,可以查看我们注册了那些服务,哪些服务被消费了
  3. Dubbo:jar 包

SpringBoot + Dubbo + zookeeper

注意:SringBoot 的版本为 2.7.3

框架搭建

  1. 启动 zookeeper

  2. IDEA 创建一个空项目

  3. 创建一个模块,实现服务提供者 provider-server,选择 web 依赖即可,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.3</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.locke</groupId>
        <artifactId>provider-server</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <name>provider-server</name>
        <description>provider-server</description>
        <properties>
            <java.version>11</java.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <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>
    
  4. 项目创建完毕,我们写一个服务,比如买票的服务

    1. 编写接口:
    public interface TicketService {
       public String getTicket();
    }
    
    1. 编写实现类:
    public class TicketServiceImpl implements TicketService {
        @Override
        public String getTicket() {
            return "《狂神说Java》";
        }
    }
    
  5. 创建一个模块,实现服务消费者 consumer-server,选择 web 依赖即可,pom.xml 文件同上

  6. 项目创建完毕,我们写一个服务,比如用户的服务

    1. 编写 service
    public interface UserService {
       // 我们需要去拿去注册中心的服务
    }
    

需求:现在我们的用户想使用买票的服务,如何实现?

服务提供者

  1. 将服务提供者注册到注册中心,我们需要整合 Dubbo 和 zookeeper,所以需要导包
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>3.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-zookeeper -->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-registry-zookeeper</artifactId>
    <version>3.1.0</version>
</dependency>
  1. 在 springboot 配置文件中配置 Dubbo 的相关属性
server.port=8001
# 当前应用名字
dubbo.application.name=provider-server
# 注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 扫描指定包下服务
dubbo.scan.base-packages=com.locke.service

注意,如果在服务提供者的启动类 ProviderServerApplication 前加上 @EnableDubbo 注解,则配置文件中的 dubbo.scan.base-packages 可以不用加,@EnableDubbo 会自动将服务注册到 Dubbo 中。

  1. 在 service 的实现类中配置服务注解,发布服务!注意导包问题,不要导入spring 的包
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;

// 服务注册与发现
@DubboService  // 可以被扫描到,在项目一启动就自动注册到注册中心 zookeeper
@Component  // 使用 Dubbo 后尽量不要用 Service 注解
@Slf4j
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        log.info("买票服务被调用");
        return "《狂神说Java》";
    }
}

逻辑理解 :应用启动起来,dubbo 就会扫描指定的包下带有 @component 注解的服务,将它发布在指定的注册中心中!

  1. 运行测试

img

  1. 查看服务是否在 zookeeper 中注册成功:
    1. 在可视化界面上查看:http://localhost:38080/, 用户名和密码都是 root
      img
    2. zkCli.cmd 中查看:在 /services/provider-server 下面看到了一个 IP+ 端口的地址,说明该服务存在注册的实例,可以使用
    [zk: localhost:2181(CONNECTED) 0] ls
    ls [-s] [-w] [-R] path
    [zk: localhost:2181(CONNECTED) 1] ls /
    [dubbo, services, zookeeper]
    [zk: localhost:2181(CONNECTED) 2] ls /services
    [dubbo-admin, provider-server]
    [zk: localhost:2181(CONNECTED) 3] ls /services/provider-server
    [192.168.31.103:20880]
    [zk: localhost:2181(CONNECTED) 4]
    

服务消费者

  1. 导入依赖:和之前一样
  2. 配置参数
server.port=8002

#当前应用名字
dubbo.application.name=consumer-server
#注册中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
  1. 本来正常步骤是需要将服务提供者的接口打包,然后用 pom 文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;

img

  1. 完善消费者的服务类:注意导入的包
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;

@Service // 注入到容器中
public class UserService {

    // 想拿到 provider-server 提供的票,要去注册中心拿到服务
    // 引用,Pom 坐标,可以定义路径相同的接口名
    @DubboReference
    TicketService ticketService;

    public void bugTicket() {
        String ticket = ticketService.getTicket();
        System.out.println("在注册中心买到" + ticket);
    }

}
  1. 测试类编写
@SpringBootTest
public class ConsumerServerApplicationTests {

   @Autowired
   UserService userService;

   @Test
   public void contextLoads() {
       userService.bugTicket();
  }
}

启动测试

  1. 开启 zookeeper
  2. 打开 dubbo-admin 实现监控
  3. 开启服务者
  4. 消费者消费测试

img

以上就是 SpingBoot + dubbo + zookeeper 实现分布式开发的应用,其实就是一个服务拆分的思想。

posted @ 2024-02-14 20:41  Lockegogo  阅读(168)  评论(0编辑  收藏  举报