查看原文
其他

SpringBoot概述

树莓蛋黄派 Java规途 2023-07-04


  • 一、SpringBoot概述

    • (一)什么是SpringBoot

    • (二)SpringBoot的主要优势

    • (三)微服务

  • 二、第一个SpringBoot程序

    • (一)方式一:官网下载

    • (二)方式二:直接在idea中创建

  • 三、SpringBoot自动装配原理

    • (一)自动装配

    • (二)启动器

    • (三)主程序

    • (四)主程序中的run方法

  • 四、SpringBoot配置

    • (一)配置文件

    • (二)YAML

    • (三)YAML给实体类赋值

    • (四)JSR303数据校验

    • (五)多环境配置与配置文件位置

    • (六)Conditional

    • (七)配置文件与spring.factories之间的联系

  • 五、SpringBoot Web开发

    • (一)静态资源

    • (二)首页定制

    • (三)图标自定义

    • (四)模板引擎

    • (五)SpringMVC的自动配置

  • 六、SpringData

    • (一)准备工作

    • (二)执行数据库操作—基于JDBC

    • (三)使用德鲁伊数据源

    • (四)整合MyBatis

  • 七、Spring Security

    • (一)环境准备

    • (二)用户权限的认证与授权(Thymeleaf与SpringSecurity整合)

    • (三)用户注销及权限控制

    • (四)记住信息功能

  • 八、Shiro

    • (一)什么是Shiro

    • (二)功能介绍

    • (三)Shiro架构

    • (四)第一个Shiro程序

    • (五)Shiro常见的过滤器

  • 九、Swagger

    • (一)Swagger简介

    • (二)SpringBoot集成Swagger

  • 十、任务

    • (一)注解开启异步任务

    • (二)邮件任务

    • (三)定时任务

  • 十一、SpringBoot整合

    • (一)SpringBoot相关配置回顾

    • (二)SpringBoot与Redis进行整合

    • (三)自定义配置RedisTemplate


SpringBoot及微服务架构

微服务架构:SpringBoot是Spring的再简化,打包方式为jar包,内嵌Tomcat

一、SpringBoot概述

(一)什么是SpringBoot

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。通过这种方式,Spring Boot致力于在蓬勃发展的快速应用开发领域(rapid application development)成为领导者。

核心思想:「约定大于配置」

官网:https://spring.io/projects/spring-boot

(二)SpringBoot的主要优势

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

(三)微服务

微服务是一种架构风格,要求我们在开发应用的时候,这个应用必须构建成一系列小服务的组合,可以通过http的方式进行互通。

  • 单体应用框架

所谓单体应用框架是指我们将一个应用中的所有应用服务都封装在一个应用中。无论是ERP、CRM或是其他什么系统,都把数据库访问、web访问等等各个功能放到一个war包内。

  1. 好处是:便于开发和测试,也十分方便部署,当需要扩展的时候,只需要将war复制多份,然后部署到多个服务器上,再做负载均衡即可。
  2. 单体应用框架的缺点是:一旦修改某一处地方,都需要停掉整个服务,再重新打包,部署这个应用的war包,对于一个大型应用,应该要实现分工和分离。
  • 微服务架构

单体应用框架:即我们将所有的功能单元放在一个应用里面,然后把整个应用部署到服务器上,「如果负载能力不行,就需要将整个应用进行水平复制,进行扩展,再次负载均衡。」

微服务架构就是将每个功能元素独立出来,把独立出来的功能元素的动态组合,需要的功能元素进行组合,需要多一些时可以整个多个功能元素。「微服务架构就是对功能元素进行复制,而没有对整个应用进行复制。」

好处是:

  • 节省了调用资源
  • 每个功能元素的服务都是一个可替换的、可独立升级的软件代码。

但是这种庞大的系统架构给部署和运维带来了很大的难度,于是,Spring带来了构建大型分布式微服务的全套,全程产品。

  • 构建一个个功能独立的微服务应用单元,可以使用SpringBoot,快速构建一个应用。
  • 大型分布式网络服务的调用,这部分由Spring Cloud来完成,实现分布式。
  • 在分布式中间,进行流式数据计算,批处理,有Spring Cloud data flow
  • Spring提供了整个从开始构建应用到大型分布式应用全流程方案。

二、第一个SpringBoot程序

基本环境:jdk1.8、maven、springboot、idea

官方提供了一个快速生成的网站,IDEA集成这个网站。

(一)方式一:官网下载

可以通过在官网下载对应的springboot项目,官网网址:https://start.spring.io/

之后导入idea即可。

(二)方式二:直接在idea中创建

一般开发直接在idea中创建即可。

SpringBoot包的结构如下:

  1. 测试编写一个controller层请求。
@RestController
public class HelloController {

    @RequestMapping("/h")
    public String hello(){
        //调用业务,接收前端参数
        return "hello SpringBoot!";
    }
}

访问页面展示:

无需再去装配dao层、service层以及controller层的相关配置文件,SpringBoot完全实现了「自动装配」

  1. 相关的依赖如下:
<dependencies>
  <!--以下都是相应的web依赖,Tomcat、DispatcherServlet和xml-->
  <dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

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

SpringBoot的所有依赖都是以spring-boot-starter开头的。

同时增加一个plugin支持(打包插件)

<build>
  <plugins>
   <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
   </plugin>
  </plugins>
 </build>

以上主要有4个部分

  • 「项目元数据信息」,创建的时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId,artifacted、version、name、description等
  • 「parent」:继承spring-boot-starter-parent的依赖管理,控制版本与打包等内容
  • 「dependencies」:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了SpringMVC),使用SpringMVC构建web(包括RESTful)应用程序的入口者,使用Tomcat作为默认嵌入式容器,spring-boot-starter-test用于编写单元测试的依赖包。
  • 「build」:构建配置部分,默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把SpringBoot应用打包成JAR来运行。
  1. 项目进行打包

选中maven的lifeCycle,选中package指令就可以完成项目的打包操作。

  1. SpringBoot直接更改Tomcat端口号
server.port=8081
  1. 自定义SpringBoot启动的动画

在resources目录下创建banner.txt,并将要替换的内容放入banner.txt中即可。

三、SpringBoot自动装配原理

(一)自动装配

pom.xml

  • spring-boot-dependencies:核心依赖在父工程中。

  • 对应的依赖不需要版本号,原因是因为相对应的版本号在父工程中进行了集中的管理。

(二)启动器

   <!--启动器-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

名称为启动器,就是springboot的启动场景。

如spring-boot-starter-web会帮助我们自动导入web环境下所有的依赖

springboot支持所有的功能场景,都变成一个一个的启动器,支持的场景如下所示:

 <properties>
    <activemq.version>5.16.4</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.95</appengine-sdk.version>
    <artemis.version>2.19.1</artemis.version>
    <aspectj.version>1.9.7</aspectj.version>
    <assertj.version>3.21.0</assertj.version>
    <atomikos.version>4.0.6</atomikos.version>
    <awaitility.version>4.1.1</awaitility.version>
    <build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
    <byte-buddy.version>1.11.22</byte-buddy.version>
    <caffeine.version>2.9.3</caffeine.version>
    <cassandra-driver.version>4.13.0</cassandra-driver.version>
    <classmate.version>1.5.1</classmate.version>
    <commons-codec.version>1.15</commons-codec.version>
    <commons-dbcp2.version>2.9.0</commons-dbcp2.version>
    <commons-lang3.version>3.12.0</commons-lang3.version>
    <commons-pool.version>1.6</commons-pool.version>
    <commons-pool2.version>2.11.1</commons-pool2.version>
    <couchbase-client.version>3.2.5</couchbase-client.version>
    <db2-jdbc.version>11.5.7.0</db2-jdbc.version>
    <dependency-management-plugin.version>1.0.11.RELEASE</dependency-management-plugin.version>
    <derby.version>10.14.2.0</derby.version>
    <dropwizard-metrics.version>4.2.8</dropwizard-metrics.version>
    <ehcache.version>2.10.9.2</ehcache.version>
    <ehcache3.version>3.9.9</ehcache3.version>
    <elasticsearch.version>7.15.2</elasticsearch.version>
    <embedded-mongo.version>3.0.0</embedded-mongo.version>
    <flyway.version>8.0.5</flyway.version>
    <freemarker.version>2.3.31</freemarker.version>
    <git-commit-id-plugin.version>4.9.10</git-commit-id-plugin.version>
    <glassfish-el.version>3.0.4</glassfish-el.version>
    <glassfish-jaxb.version>2.3.6</glassfish-jaxb.version>
    <glassfish-jstl.version>1.2.6</glassfish-jstl.version>
    <groovy.version>3.0.9</groovy.version>
    <gson.version>2.8.9</gson.version>
    <h2.version>1.4.200</h2.version>
    <hamcrest.version>2.2</hamcrest.version>
    <hazelcast.version>4.2.4</hazelcast.version>
    <hazelcast-hibernate5.version>2.2.1</hazelcast-hibernate5.version>
    <hibernate.version>5.6.5.Final</hibernate.version>
    <hibernate-validator.version>6.2.2.Final</hibernate-validator.version>
    <hikaricp.version>4.0.3</hikaricp.version>
    <hsqldb.version>2.5.2</hsqldb.version>
    <htmlunit.version>2.54.0</htmlunit.version>
    <httpasyncclient.version>4.1.5</httpasyncclient.version>
    <httpclient.version>4.5.13</httpclient.version>
    <httpclient5.version>5.1.3</httpclient5.version>
    <httpcore.version>4.4.15</httpcore.version>
    <httpcore5.version>5.1.3</httpcore5.version>
    <infinispan.version>12.1.11.Final</infinispan.version>
    <influxdb-java.version>2.22</influxdb-java.version>
    <jackson-bom.version>2.13.1</jackson-bom.version>
    <jakarta-activation.version>1.2.2</jakarta-activation.version>
    <jakarta-annotation.version>1.3.5</jakarta-annotation.version>
    <jakarta-jms.version>2.0.3</jakarta-jms.version>
    <jakarta-json.version>1.1.6</jakarta-json.version>
    <jakarta-json-bind.version>1.0.2</jakarta-json-bind.version>
    <jakarta-mail.version>1.6.7</jakarta-mail.version>
    <jakarta-management.version>1.1.4</jakarta-management.version>
    <jakarta-persistence.version>2.2.3</jakarta-persistence.version>
    <jakarta-servlet.version>4.0.4</jakarta-servlet.version>
    <jakarta-servlet-jsp-jstl.version>1.2.7</jakarta-servlet-jsp-jstl.version>
    <jakarta-transaction.version>1.3.3</jakarta-transaction.version>
    <jakarta-validation.version>2.0.2</jakarta-validation.version>
    <jakarta-websocket.version>1.1.2</jakarta-websocket.version>
    <jakarta-ws-rs.version>2.1.6</jakarta-ws-rs.version>
    <jakarta-xml-bind.version>2.3.3</jakarta-xml-bind.version>
    <jakarta-xml-soap.version>1.4.2</jakarta-xml-soap.version>
    <jakarta-xml-ws.version>2.3.3</jakarta-xml-ws.version>
    <janino.version>3.1.6</janino.version>
    <javax-activation.version>1.2.0</javax-activation.version>
    <javax-annotation.version>1.3.2</javax-annotation.version>
    <javax-cache.version>1.1.1</javax-cache.version>
    <javax-jaxb.version>2.3.1</javax-jaxb.version>
    <javax-jaxws.version>2.3.1</javax-jaxws.version>
    <javax-jms.version>2.0.1</javax-jms.version>
    <javax-json.version>1.1.4</javax-json.version>
    <javax-jsonb.version>1.0</javax-jsonb.version>
    <javax-mail.version>1.6.2</javax-mail.version>
    <javax-money.version>1.1</javax-money.version>
    <javax-persistence.version>2.2</javax-persistence.version>
    <javax-transaction.version>1.3</javax-transaction.version>
    <javax-validation.version>2.0.1.Final</javax-validation.version>
    <javax-websocket.version>1.1</javax-websocket.version>
    <jaxen.version>1.2.0</jaxen.version>
    <jaybird.version>4.0.5.java8</jaybird.version>
    <jboss-logging.version>3.4.3.Final</jboss-logging.version>
    <jdom2.version>2.0.6.1</jdom2.version>
    <jedis.version>3.7.1</jedis.version>
    <jersey.version>2.35</jersey.version>
    <jetty-el.version>9.0.52</jetty-el.version>
    <jetty-jsp.version>2.2.0.v201112011158</jetty-jsp.version>
    <jetty-reactive-httpclient.version>1.1.11</jetty-reactive-httpclient.version>
    <jetty.version>9.4.45.v20220203</jetty.version>
    <jmustache.version>1.15</jmustache.version>
    <johnzon.version>1.2.16</johnzon.version>
    <jolokia.version>1.7.1</jolokia.version>
    <jooq.version>3.14.15</jooq.version>
    <json-path.version>2.6.0</json-path.version>
    <json-smart.version>2.4.8</json-smart.version>
    <jsonassert.version>1.5.0</jsonassert.version>
    <jstl.version>1.2</jstl.version>
    <jtds.version>1.3.1</jtds.version>
    <junit.version>4.13.2</junit.version>
    <junit-jupiter.version>5.8.2</junit-jupiter.version>
    <kafka.version>3.0.0</kafka.version>
    <kotlin.version>1.6.10</kotlin.version>
    <kotlin-coroutines.version>1.5.2</kotlin-coroutines.version>
    <lettuce.version>6.1.6.RELEASE</lettuce.version>
    <liquibase.version>4.5.0</liquibase.version>
    <log4j2.version>2.17.1</log4j2.version>
    <logback.version>1.2.10</logback.version>
    <lombok.version>1.18.22</lombok.version>
    <mariadb.version>2.7.5</mariadb.version>
    <maven-antrun-plugin.version>3.0.0</maven-antrun-plugin.version>
    <maven-assembly-plugin.version>3.3.0</maven-assembly-plugin.version>
    <maven-clean-plugin.version>3.1.0</maven-clean-plugin.version>
    <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
    <maven-dependency-plugin.version>3.2.0</maven-dependency-plugin.version>
    <maven-deploy-plugin.version>2.8.2</maven-deploy-plugin.version>
    <maven-enforcer-plugin.version>3.0.0</maven-enforcer-plugin.version>
    <maven-failsafe-plugin.version>2.22.2</maven-failsafe-plugin.version>
    <maven-help-plugin.version>3.2.0</maven-help-plugin.version>
    <maven-install-plugin.version>2.5.2</maven-install-plugin.version>
    <maven-invoker-plugin.version>3.2.2</maven-invoker-plugin.version>
    <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
    <maven-javadoc-plugin.version>3.3.2</maven-javadoc-plugin.version>
    <maven-resources-plugin.version>3.2.0</maven-resources-plugin.version>
    <maven-shade-plugin.version>3.2.4</maven-shade-plugin.version>
    <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
    <maven-surefire-plugin.version>2.22.2</maven-surefire-plugin.version>
    <maven-war-plugin.version>3.3.2</maven-war-plugin.version>
    <micrometer.version>1.8.3</micrometer.version>
    <mimepull.version>1.9.15</mimepull.version>
    <mockito.version>4.0.0</mockito.version>
    <mongodb.version>4.4.2</mongodb.version>
    <mssql-jdbc.version>9.4.1.jre8</mssql-jdbc.version>
    <mysql.version>8.0.28</mysql.version>
    <nekohtml.version>1.9.22</nekohtml.version>
    <neo4j-java-driver.version>4.4.3</neo4j-java-driver.version>
    <netty.version>4.1.74.Final</netty.version>
    <netty-tcnative.version>2.0.50.Final</netty-tcnative.version>
    <okhttp3.version>3.14.9</okhttp3.version>
    <oracle-database.version>21.3.0.0</oracle-database.version>
    <pooled-jms.version>1.2.3</pooled-jms.version>
    <postgresql.version>42.3.3</postgresql.version>
    <prometheus-client.version>0.12.0</prometheus-client.version>
    <quartz.version>2.3.2</quartz.version>
    <querydsl.version>5.0.0</querydsl.version>
    <r2dbc-bom.version>Arabba-SR12</r2dbc-bom.version>
    <rabbit-amqp-client.version>5.13.1</rabbit-amqp-client.version>
    <rabbit-stream-client.version>0.4.0</rabbit-stream-client.version>
    <reactive-streams.version>1.0.3</reactive-streams.version>
    <reactor-bom.version>2020.0.16</reactor-bom.version>
    <rest-assured.version>4.4.0</rest-assured.version>
    <rsocket.version>1.1.1</rsocket.version>
    <rxjava.version>1.3.8</rxjava.version>
    <rxjava-adapter.version>1.2.1</rxjava-adapter.version>
    <rxjava2.version>2.2.21</rxjava2.version>
    <saaj-impl.version>1.5.3</saaj-impl.version>
    <selenium.version>3.141.59</selenium.version>
    <selenium-htmlunit.version>2.54.0</selenium-htmlunit.version>
    <sendgrid.version>4.7.6</sendgrid.version>
    <servlet-api.version>4.0.1</servlet-api.version>
    <slf4j.version>1.7.36</slf4j.version>
    <snakeyaml.version>1.29</snakeyaml.version>
    <solr.version>8.8.2</solr.version>
    <spring-amqp.version>2.4.2</spring-amqp.version>
    <spring-batch.version>4.3.5</spring-batch.version>
    <spring-data-bom.version>2021.1.2</spring-data-bom.version>
    <spring-framework.version>5.3.16</spring-framework.version>
    <spring-hateoas.version>1.4.1</spring-hateoas.version>
    <spring-integration.version>5.5.9</spring-integration.version>
    <spring-kafka.version>2.8.3</spring-kafka.version>
    <spring-ldap.version>2.3.6.RELEASE</spring-ldap.version>
    <spring-restdocs.version>2.0.6.RELEASE</spring-restdocs.version>
    <spring-retry.version>1.3.1</spring-retry.version>
    <spring-security.version>5.6.2</spring-security.version>
    <spring-session-bom.version>2021.1.2</spring-session-bom.version>
    <spring-ws.version>3.1.2</spring-ws.version>
    <sqlite-jdbc.version>3.36.0.3</sqlite-jdbc.version>
    <sun-mail.version>1.6.7</sun-mail.version>
    <thymeleaf.version>3.0.15.RELEASE</thymeleaf.version>
    <thymeleaf-extras-data-attribute.version>2.0.1</thymeleaf-extras-data-attribute.version>
    <thymeleaf-extras-java8time.version>3.0.4.RELEASE</thymeleaf-extras-java8time.version>
    <thymeleaf-extras-springsecurity.version>3.0.4.RELEASE</thymeleaf-extras-springsecurity.version>
    <thymeleaf-layout-dialect.version>3.0.0</thymeleaf-layout-dialect.version>
    <tomcat.version>9.0.58</tomcat.version>
    <unboundid-ldapsdk.version>4.0.14</unboundid-ldapsdk.version>
    <undertow.version>2.2.16.Final</undertow.version>
    <versions-maven-plugin.version>2.8.1</versions-maven-plugin.version>
    <webjars-locator-core.version>0.48</webjars-locator-core.version>
    <wsdl4j.version>1.6.3</wsdl4j.version>
    <xml-maven-plugin.version>1.0.2</xml-maven-plugin.version>
    <xmlunit2.version>2.8.4</xmlunit2.version>

当需要使用某项功能的时候,就只需要找到对应的启动器即可。starter

(三)主程序

@SpringBootApplication
public class Springboot1Application {

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

}

  • @SpringBootApplication注解标注这个类是一个SpringBoot的应用
  • SpringApplication.run(Springboot1Application.class, args);将SpringBoot应用启动
  • 其他注解:
    • @SpringBootConfiguration——>@Configuration——>@Component依次为SpringBoot的配置,Spring的配置类,Spring的组件
    • @EnableAutoConfiguration——>@AutoConfigurationPackage(——>@Import({Registrar.class}))——>@Import({AutoConfigurationImportSelector.class})依次表示为:自动配置——>自动配置包(——>自动配置包注册)——>自动配置导入选择
    • List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);可以获取到所有的配置
  • 获取候选的配置
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
        return configurations;
    }
  • META-INF/spring.factories自动配置的核心文件
  • 所有的资源都加载到对应的配置类中
Properties properties = PropertiesLoaderUtils.loadProperties(resource);

SpringBoot所有的自动配置都是在启动的时候扫描并加载 ,spring.factories所有的自动配置类都在这里面,但是不一定都会生效,需要进行相应的判断,只要导入了对应的start,就有了对应的启动器,有了启动器,自动装配就会生效,配置就成功了。

  1. SpringBoot在启动的时候,从类路径下/META-INF/spring.factories获取指定的值
  2. 将这些自动配置的类导入容器,自动配置就会生效,进行自动配置。
  3. 整合javaEE,解决方案和自动配置的东西都在spring-boot-autoconfigure-2.6.4.jar包下
  4. 其会将所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中去。
  5. 容器中有许多的xxxAutoConfiguration的文件(@Bean),就是这些类给容器中导入了这个场景所需要的所有组件,并自动配置。
  6. 自动配置的出现,就不需要再手写配置文件。

(四)主程序中的run方法

@SpringBootApplication
public class Springboot1Application {

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

}

该方法返回一个ConfigurableApplicationContext对象

run方法中的参数:参数一:应用入口的类,参数二:命令行参数

1.SpringApplication.run分析

该方法主要分为2部分,一部分是SpringApplication的实例化,二是run方法的执行

2. SpringApplication

这个类主要作了以下四件事

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

四、SpringBoot配置

(一)配置文件

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

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

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

(二)YAML

「YAML」(/ˈjæməl/,尾音类似camel骆驼)是一个可读性高,用来表达数据序列化的格式。YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子邮件的数据格式(RFC 2822)中获得灵感。Clark Evans在2001年首次发表了这种语言,另外Ingy döt Net与Oren Ben-Kiki也是这语言的共同设计者。当前已经有数种编程语言或脚本语言支持(或者说解析)这种语言。

格式如下:

server:
  port: 8081
  
server:
  port: 8081

  # 普通的kv键值对
  name: zhangsan

  #配置对象
  student:
    name: zhangsan
    age: 18
    sex: 

    # 行内写法
    student: {name: zhangsan,age: 19, sex: 男}

  # 数组
    pets:
      - pig
      - dog
      - cat

  pets: [cat,dog,pig]  

yaml的文件对「空格的要求极为严格」,必须要进行相应的缩进操作。并且注入至相应的配置类中。

(三)YAML给实体类赋值

  • 首先创建对应的实体类
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;

    public Person() {
    }

    public Person(String name, Integer age, boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Dog dog) {
        this.name = name;
        this.age = age;
        this.happy = happy;
        this.birth = birth;
        this.maps = maps;
        this.lists = lists;
        this.dog = dog;
    }

    public String getName() {
        return name;
    }

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

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public boolean isHappy() {
        return happy;
    }

    public void setHappy(boolean happy) {
        this.happy = happy;
    }

    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    public Map<String, Object> getMaps() {
        return maps;
    }

    public void setMaps(Map<String, Object> maps) {
        this.maps = maps;
    }

    public List<Object> getLists() {
        return lists;
    }

    public void setLists(List<Object> lists) {
        this.lists = lists;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", happy=" + happy +
                ", birth=" + birth +
                ", maps=" + maps +
                ", lists=" + lists +
                ", dog=" + dog +
                '}';
    }
}

  • 编写相应的yaml文件
person:
  name: Jack
  age: 18
  happy: false
  birth: 2022/03/15
  maps: {k1:v1,k2:v2}
  lists: [code,music]
  dog:
    name: wangcai
    age: 3

并且可以使用「EL表达式赋予随机的值」

  age: ${random.int}
  • 为实体类添加注解
@ConfigurationProperties(prefix = "person")

添加注解后,此处会爆红,提示需要配置,但是可以配置也可以不配值,不影响使用。

该注解的作用主要是:将配置文件中的每一个属性的值,映射到这个组件中,告诉springboot将本类中的所有属性和配置文件中相关的配置进行绑定。并且只有这个组件是容器中的组件,才能使用容器提供的@ConfigurationProperties功能(也就是说必须将实体类注册为组件。)

  • 进行相应的测试(使用自动注入)
@SpringBootTest
class Springboot2ConfigApplicationTests {
    @Autowired
    private Person person;

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

}

除此之外还可以自定义新的配置文件(与springboot官方要求不符合的,官方要求配置名为application),此时需要使用@propertySource(value=“classpath:配置文件”)

  • @Value与@ConfigurationProperties比较

@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个一个指定
松散绑定(松散语法)支持不支持
SpEL表达式不支持支持
JSR303数据校验支持不支持
复杂类型封装支持不支持
  • @ConfigurationProperties只需要写一次即可,@Value则需要每个字段都添加。
  • 松散绑定,比如在yaml中写的last-name,但处理的时候与lastName一样,后面跟着的字母默认是大写。
  • JSR303数据校验即在字段增加一层过滤器验证,可以保证数据的合法性。
  • 复杂类型的封装,yaml中可以封装对象,但是@Value不支持对象的封装。

最佳实践;

  1. 如果在业务中,只需要获取配置文件的某个值,可以使用@Value
  2. 但是如果专门编写了JavBean来与配置文件进行映射,就直接用@ConfigurationProperties(prefix=“xxxx”)

(四)JSR303数据校验

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

  • 以邮箱校验为例:

在JavaBean的类级别上加上@Validated注解,并且同时在name属性上添加@Email注解,进行JSR303校验

添加注解之前,先导入依赖

  <!-- https://mvnrepository.com/artifact/javax.validation/validation-api -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>
@Component
@Validated //进行相应的数据校验
@ConfigurationProperties(prefix = "person")
public class Person {
   @Email(message = "这不是一个合法的邮箱")
    private String name;
    private Integer age;
    private boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;

添加@Email注解之后,可以为其添加相应的提示信息message,也可以不添加。

运行之后,出错了

原因在于我们在类中添加了这两个注解,导致格式不匹配。

(五)多环境配置与配置文件位置

  • 配置文件的位置

file:./config/ >file:./ >classpath:./config/ >classpath:./

「默认情况下系统自带的yaml文件或者properties文件的优先级是最低的,其次是位于resources目录下的config目录中的yaml文件,再次是位于项目路径下的yaml文件,最后是位于项目下的config下的yaml文件。其优先级最高。」

但是不论优先级高低,配置文件的名字始终必须是「application」

  • 配置文件的多环境部署

可以在resources目录下创建多个application-xxx.yaml文件,默认的配置文件为application.yaml,默认的优先级别是最高的。

如图所示:创建了3个不同环境下的配置文件

主要有默认环境,dev开发环境,test测试环境。

在默认环境中设置配置文件的优先级

# Spring的多环境配置
spring:
  profiles:
    active: dev

此时就会采用dev环境下的配置信息来完成相应的操作。

除此之外,也可以使用一个yaml文件完成多环境的配置

# Spring的多环境配置
server:
  port: 8081
spring:
  profiles:
    active: dev
---
server:
  port: 8082
spring:
  profiles:dev
---
server:
  port: 8083
spring:
  profiles: test

此时表示指定了dev的开发环境。

(六)Conditional

自动配置类必须在一定的条件下才能生效,@Conditional派生注解(Spring注解版原生的@Conditional作用。)

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

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

(七)配置文件与spring.factories之间的联系

  • 在我们的配置文件中可以配置的东西,都存在一个固有的规律:

  • xxxproperties文件一般都被xxxAutoConfiguration装配

  • xxxproperties又与我们的配置文件进行绑定

  • xxxAutoConfiguration存在一定的默认值,我们可以通过xxxproperties文件与配置文件进行绑定,从而实现自定义。

  • 「原理」

  1. SpringBoot启动会加载大量的自动配置类
  2. 需要查看我们需要的功能有无在SpringBoot默认写好的自动配置类当中。
  3. 只要需要的组件存在于自动配置类中,就不需要手动配置。
  4. 给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,只要在配置文件中指定这些属性的值即可。

「xxxAutoConfiguration:自动配置类,给容器中添加组件」

「xxxproperties:封装配置文件中的相关属性」

  • 查看哪些类没有生效,哪些类生效

可以通过debug=true查看当前项目中哪些类生效了,哪些类依旧没有生效。

「Postive matches」:自动配置类启用的,正匹配

「Negative matches:」没有启动,没有匹配成功的自动配置类,负匹配

「Unconditional classes:」没有条件的类

五、SpringBoot Web开发

(一)静态资源

所有的webmvc的配置都在 WebMvcAutoConfiguration类中

  public void addResourceHandlers(ResourceHandlerRegistry registry) {
      //如果资源没有被注册,那么使用默认的资源
            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");
            } else {
                this.addResourceHandler(registry, "/webjars/**""classpath:/META-INF/resources/webjars/");
                this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
                    registration.addResourceLocations(this.resourceProperties.getStaticLocations());
                    if (this.servletContext != null) {
                        ServletContextResource resource = new ServletContextResource(this.servletContext, "/");
                        registration.addResourceLocations(new Resource[]{resource});
                    }

                });
  1. webjars(第一种拿到web资源的方式)
 this.addResourceHandler(registry, "/webjars/**""classpath:/META-INF/resources/webjars/");

要求我们去classpath:/META-INF/resources/webjars/下的目录去寻找对应的资源

启动项目,访问对应的路径http://localhost:8080/webjars/jquery/3.5.1/jquery.js

获取到了相应的内容

  1. 第二种方式,访问源码指定的路径
this.addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(),

通过getStaticPathPattern()可以获取到支持的所有的静态资源路径,访问可得:

 private String staticPathPattern = "/**";

/**表示当前目录下的所有路径都可以访问到静态资源

   registration.addResourceLocations(this.resourceProperties.getStaticLocations());

通过getStaticLocations());可以跳转到指定生效的静态资源路径

  private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String[]{"classpath:/META-INF/resources/""classpath:/resources/""classpath:/static/""classpath:/public/"};

主要有:classpath:/META-INF/resources/classpath:/resources/classpath:/static/以及classpath:/public/目录下,其中classpath表示目录中的resources目录。

  • 在public目录下:classpath:/public/

访问对应的地址http://localhost:8080/1.html

可以正常访问

  • 新建resources目录:classpath:/resources/

访问对应的地址:http://localhost:8080/2.html

可以正常访问

  • static目录:classpath:/static/

访问对应的地址:http://localhost:8080/3.html

可以正常访问

并且经过比较,对应三个目录的优先级为:resources>static>public

  1. 总结
  • 在SpringBoot中可以使用以下方式处理静态资源

    • webjars ——>localhost:8080/webjars/
    • public static /**,resources ——>localhost:8080/
  • 优先级:resources>static(默认)>public

(二)首页定制

主要的定制还是在WebMvcAutoConfiguration类中

 @Bean
        public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
            WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(new TemplateAvailabilityProviders(applicationContext), applicationContext, this.getWelcomePage(), this.mvcProperties.getStaticPathPattern());
            welcomePageHandlerMapping.setInterceptors(this.getInterceptors(mvcConversionService, mvcResourceUrlProvider));
            welcomePageHandlerMapping.setCorsConfigurations(this.getCorsConfigurations());
            return welcomePageHandlerMapping;
        }

this.mvcProperties.getStaticPathPattern())表示定制的主页可以存放的静态资源路径,主要有四个/** classpath:/public/、classpath:/resources/、classpath:/static/

在public目录下创建index.html,启动后访问:http://locahost:8080即可访问到index.html文件(其他路径也可以访问)

  • templates目录下的所有页面,只能通过controller来跳转

可以看出当将index.html页面放置在templates下,直接访问是不可行的

只能通过controller跳转。但同时需要模板引擎的支持。

「总的来说index.html主页最好放置在staticpath所罗列的目录下(public等目录)」

(三)图标自定义

新版SpringBoot只需要将图片放置在static文件夹下即可,命名为favicon.ico文件。

即可在访问主页的时候访问到图标。

(四)模板引擎

Templates模板引擎渲染,从后端获取数据进行渲染,最终输出为html页面

SpringBoot推荐使用Thymeleaf。

Thymeleaf官网:https://www.thymeleaf.org/

模板引擎的作用就是来写一个页面的模板。比如有些值是动态变化的,写一些表达式来表示这些值,我们需要组装一些数据,把这些数据找到,再将这个模板和这个数据交给模板引擎,模板引擎按照这个数据将表达式进行解析。填充到指定位置,最后把这个数据最终生成一个我们想要的页面内容。

1.Thymeleaf的基本使用

使用Thymeleaf

  1. 导入相关的依赖
   <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>
  1. 查看ThymeleafProperties
public class ThymeleafProperties {
    private static final Charset DEFAULT_ENCODING;
    public static final String DEFAULT_PREFIX = "classpath:/templates/";
    public static final String DEFAULT_SUFFIX = ".html";

可以看出Thymeleaf配置的相关细节,必须要在resources下的templates目录下,并且必须要以html为后缀。

  1. 在templates目录下创建test.html文件

需要在html标签后添加Thymeleaf约束``

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<h1>这是第一个Thymeleaf模板引擎页面</h1>

</body>
</html>
  1. 设置对应的controller跳转
@Controller
public class IndexController {

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


}

  1. 访问

访问地址为:http://localhost:8080/test

注意:Thymeleaf可以接管所有的html元素,格式为th:元素名

<!DOCTYPE html>
<html lang="en" xmlns:th:="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<h1>这是第一个Thymeleaf模板引擎页面</h1>
<div th:text="${msg}"></div>

</body>
</html>

设置对应的视图跳转

@Controller
public class IndexController {

    @RequestMapping("/test")
    public String test(Model model){
        model.addAttribute("msg","Hello World!");
        return "test";
    }


}

访问地址:http://localhost:8080/test

即可正常访问页面。

2. Thymeleaf基本语法

  • text与utext属性(文本与非文本)
@Controller
public class IndexController {

    @RequestMapping("/test")
    public String test(Model model){
        model.addAttribute("msg","<h1>Hello World!</h1>");
        return "test";
    }
}
<!DOCTYPE html>
<html lang="en"
      xmlns:th:="http://www.thymeleaf.org"
        >

<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<h1>这是第一个Thymeleaf模板引擎页面</h1>
<div th:text="${msg}"></div>
<div th:utext="${msg}"></div>
</body>
</html>

这里分别使用了text与utext属性,各自去获取后端传递的msg信息内容,访问http://localhost:8080/test

可以正常访问

可以看出使用非文本属性的内容就以一级标题的内容展现在页面上。不做字符串来处理。

  • th:each属性
@Controller
public class IndexController {
    @RequestMapping("/test")
    public String test(Model model){
        model.addAttribute("msg","<h1>Hello World!</h1>");
        //设置users数组信息
        model.addAttribute("users", Arrays.asList("张三","李四"));
        return "test";
    }
}
<!DOCTYPE html>
<html lang="en"
      xmlns:th:="http://www.thymeleaf.org"
        >

<head>
    <meta charset="UTF-8">
    <title>Test</title>
</head>
<body>
<h1>这是第一个Thymeleaf模板引擎页面</h1>
<div th:text="${msg}"></div>
<div th:utext="${msg}"></div>
<hr>
<h3 th:each="user:${users}" th:text="${user}"></h3>
    //第二种方式
 <h3 th:each="user:${users}" >[[${user}]]</h3>   
</body>
</html>

<h3 th:each="user:${users}" th:text="${user}"></h3>中th:each表示从users中遍历每个user,之后再将user以文本的形式显示。

<h3 th:each="user:${users}" >[[${user}]]</h3>表示取遍历出的user,需要用两个中括号来表示要取出的内容信息。

http://localhost:8080/test

正常访问

  • 简单表达式语法

    • 普通变量:${....}
    • 国际化消息:#{....}
    • 选择变量表达式:*{.....}
    • 链接表达式:@{.....}
    • 片段表达式:~{....}
  • 字面量

    • 文本字面量:one textanother one..(使用单引号写)
    • 数字字面量:0.340.3
    • 布尔字面量:truefalse
    • 空值字面量:null
    • 文字标记:onesometextmain….
  • 文本操作

    • 字符串连接:+
    • 文字替换:|The name is ${name}|
  • 算术操作

    • 二进制运算符:+-*/%
    • 减号(一元运算符):-
  • 布尔操作

    • 二进制操作:andor
    • 布尔排列(一元运算符):not
  • 比较与相等

    • 比较:><>=<=、(gt、lt、ge、le)
    • 相等操作:==!=(eq、ne)
  • 条件操作(三元条件运算符)

    • IF-then:(if)?(then)
    • IF-then-else:(if)?(then):(else)
    • Default:(value)?:(defaultvalue)

(五)SpringMVC的自动配置

Spring Boot 为 Spring MVC 提供了自动配置,适用于大多数应用程序。

自动配置在 Spring 的默认值之上添加了以下特性:

  • 包括ContentNegotiatingViewResolverBeanNameViewResolver豆类。
  • 支持提供静态资源,包括对 WebJars 的支持
  • 自动注册ConverterGenericConverterFormatterbean。
  • 支持HttpMessageConverters
  • 自动注册MessageCodesResolver
  • 静态index.html支持。
  • ConfigurableWebBindingInitializerbean的自动使用。

如果您想保留那些 Spring Boot MVC 自定义并进行更多MVC 自定义(拦截器、格式化程序、视图控制器和其他功能),您可以添加自己@Configuration的类型类WebMvcConfigurer「不」 添加@EnableWebMvc.

如果您想提供、 或的自定义实例RequestMappingHandlerMapping,并且仍然保留 Spring Boot MVC 自定义,您可以声明一个类型的 bean并使用它来提供这些组件的自定义实例。RequestMappingHandlerAdapter``ExceptionHandlerExceptionResolver``WebMvcRegistrations

如果你想完全控制 Spring MVC,你可以添加你自己的@Configuration注释@EnableWebMvc,或者添加你自己的@Configuration-annotated DelegatingWebMvcConfiguration,如@EnableWebMvc.

1.自定义视图解析器

@Configuration //将此类变成一个配置类
public class MyMVCConfig implements WebMvcConfigurer {
    /**使用此类来全面扩展SpringMVC*/
    //1.首先添加注解@Configuration,变为一个配置类
    //2.再实现一个接口WebMvcConfigurer

        //将自己设计的视图解析器注入到组件中(到bean中)
         @Bean
        public ViewResolver getViewResolver(){
            return new viewResolvers();
        }

    //自己设计一个视图解析器
    public static class  viewResolvers implements ViewResolver{

        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
}

  • 首先创建自定义SpringMVC类,并实现WebMvcConfigurer接口。
  • 其次添加注解@Configuration表示这是一个配置类
  • 最后自定义一个视图解析器并实现ViewResolver接口(重写resolverViewName方法)
  • 最后使用@Bean将创建的自定义视图解析器交给SpringBoot托管(注入到Bean组件中)

总的来说,如果想要自定义视图解析器,只要写一个类,完成上述操作,然后将其交给SpringBoot进行托管,就会自动进行装配。

「如下图所示:访问DispatcherServlet类下的 doDispatch方法,并进行断点调适」

2.扩展SpringMVC

  • 方式一

「如果需要扩展SpringMVC,官方建议我们这样做:」

自定义一个视图解析器之后可以实现WebMvcConfigurer接口,重写相应的不同方法,此处展示添加视图跳转的方法:

public class MyMVCConfig implements WebMvcConfigurer {
    /**此方法表示视图跳转*/
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        //表示定义视图跳转(访问/t路径就会访问test.html页面)
        registry.addViewController("/t").setViewName("test");

    }
    
}

通过访问/t路径就可以访问到test页面(并且需要注意的是,test.html必须要在templates目录下才可以访问到。因为跳转默认的是classpath:/templates/目录下)

@EnableWebMvc注解不建议添加到扩展SpringMVC的类中,因为它导入一个类:DelegatingWebMvcConfiguration,该类可以从容器中获取所有的webmvcconfig ,一旦添加了注解,那么就会使WebMvcAutoConfiguration下的所有默认配置失效,无法实现跳转。

@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})

@EnableWebMvc注解中的DelegatingWebMvcConfiguration类继承了WebMvcConfigurationSupport类,而在WebMvcAutoConfiguration中的注解@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})中注明了,没有WebMvcConfigurationSupport,以下配置生效。也就是说一旦使用了@EnableWebMvc注解,就会使默认配置全部失效。

SpringBoot中有很多的xxxConfiguration帮助我们进行扩展配置

六、SpringData

对于数据层的访问,无论是SQL还是NOSQL,SpringBoot的底层都是采用Spring Data的方式进行统一处理。

SpringData官网:https://spring.io/projects/spring-data

SpringData的运行:

(一)准备工作

  1. 首先创建新的SpringBoot项目,并且勾选相应的工具,这里选择JDBC与Mysql Driver,因为需要连接数据库驱动。

  2. 配置数据库的连接配置文件,此处使用yaml文件(application.yaml)

spring:
  datasource:
    username: root
    password:123456
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
    driver-class-name: com.mysql.cj.jdbc.Driver
  1. 测试数据源
@SpringBootTest
class Springboot04DataApplicationTests {
    @Autowired
    private DataSource dataSource;
    @Test
    void contextLoads() {
        //查看默认数据源
        System.out.println(dataSource.getClass());
    }

}

输出了class com.zaxxer.hikari.HikariDataSource,这是SpringBoot默认的数据源

  1. 查看数据库的连接
//查看连接的类型
        Connection connection = dataSource.getConnection();
        System.out.println(connection);
        connection.close();

HikariProxyConnection@1505964934 wrapping com.mysql.cj.jdbc.ConnectionImpl@571a9686此处显示了使用了jdbc来连接。

(二)执行数据库操作—基于JDBC

  1. 编写控制器类,访问数据库中的信息(查询数据)
@RestController
public class JdbcController {

    /**使用jdbc模板来执行相关操作DataSource dataSource, JdbcProperties properties*/
    @Autowired
    JdbcTemplate jdbcTemplate;

    /**
     * 当没有数据库的时候我们可以使用Map集合将数据库信息从中取出,Map就对应数据库中列与行的关系
     * @return
     */

    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql="SELECT * FROM mybatis.user";
        List<Map<String, Object>> list= jdbcTemplate.queryForList(sql);
        return list;

    }
}

  • 由于我们只需要访问数据库的信息,不使用页面展示,在controller中使用@RestController接口,并且同时引入web的依赖。
  • JdbcTemplate jdbcTemplateJDBC模板,用来执行JDBC的相关操作(作用类似于预处理语句),使用@Autowired自动装配(源码中初始化需要数据源与数据库配置),源码如下所示:
 @Bean
    @Primary
    JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
        JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
        Template template = properties.getTemplate();
        jdbcTemplate.setFetchSize(template.getFetchSize());
        jdbcTemplate.setMaxRows(template.getMaxRows());
        if (template.getQueryTimeout() != null) {
            jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
        }

        return jdbcTemplate;
    }

此处的jdbcTemplate使用了@Bean来标注,表示注入到容器中,可以直接使用。

  • 当要访问的数据库的字段时,没有对应的实体类与之对应,可以使用Map集合来对数据库中的列与行实现一一对应。

结果如下:

以上显示的是用数组的中括号括起来的一堆JS对象。

增删改的操作类似。

  1. 增加一个用户
    @GetMapping("/addUser")
    public String addUser(){
        String sql="INSERT INTO mybatis.user VALUES(4,'小明','9999')";
        int update = jdbcTemplate.update(sql);
        if (update>0) {
            return "执行添加成功";
        }else {
            return "执行添加失败";
        }

    }

由于是直接添加用户,所以并不需要添加任何的参数。

  1. 修改一个用户
    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") Integer id){
        String sql="UPDATE mybatis.user SET name=? ,pwd=? WHERE id="+id;
        //由于此处使用了预处理语句,因此需要对此进行相应的封装
        Object[] objects = new Object[2];
        objects[0]="小李";
        objects[1]="123456789";
        int update = jdbcTemplate.update(sql,objects);
        if (update>0) {
            return "执行修改成功";
        }else {
            return "执行修改失败";
        }
    }
}
  • 此处修改用户的某些信息,使用了Restful风格,需要用到@Pathvariable(url传入的参数名)
  • 此处使用了预处理的语句,并使用数组将要修改的相关信息存入数组,并在模板对象中与sql语句一并执行。
  1. 删除一个用户
 @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") Integer id){
        String sql="DELETE  FROM mybatis.user WHERE id=?";
        int update = jdbcTemplate.update(sql, id);
        if (update>0) {
            return "执行删除成功";
        }else {
            return "执行删除失败";
        }
    }

此处仍旧使用Restful格式来删除用户信息。传入相应的id,即可完成删除指定用户的信息。

(三)使用德鲁伊数据源

  • 导入依赖(log4j、德鲁伊、mybatis、mysql驱动等)
 <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
  </dependency>
   <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
</dependency>
 <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
</dependency>
 <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
 </dependency>
<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
        </dependency>

  • 更改相应的配置文件信息
    # 指定德鲁伊数据源
    type: com.alibaba.druid.pool.DruidDataSource
      #SpringBoot默认是不注入这些的,需要自己绑定
      #druid数据源专有配置
      initialSize: 5
      minIdle: 5
      maxActive: 20
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: SELECT 1 FROM DUAL
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true

      #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
      #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
      #则导入log4j 依赖就行
      filters: stat,wall,log4j
      maxPoolPreparedStatementPerConnectionSize: 20
      useGlobalDataSourceStat: true
      connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

德鲁伊在增删改查方面与SpringBoot默认的数据源相差不大,与之相比,德鲁伊的优势在于可以「自定义相应的配置。」

  • 自定义配置
  1. 数据源与配置文件绑定
@Configuration
public class DruidConfig {

    /**此时需要将数据源与配置文件进行绑定,
     * @Bean将返回的德鲁伊数据源对象注入到容器中交由SpringBoot进行管理
     * @ConfigurationProperties(prefix = "spring.datasource")是将该数据源与配置文件进行绑定,需要指定配置信息的前缀。
     * */

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource  druidDataSource(){
        return new DruidDataSource();
    }
  • 此处需要创建一个自定义的类,标注为@Configuration表示可以自定义,自行配置
  • 获取德鲁伊数据源的对象, @Bean将返回的德鲁伊数据源对象注入到容器中交由SpringBoot进行管理。 @ConfigurationProperties(prefix = "spring.datasource")是将该数据源与配置文件进行绑定,需要指定配置信息的前缀。
  • 最后返回一个新创键的德鲁伊数据源对象
  1. 自定义后台监控(德鲁伊数据源的优势)

    /**后台监控功能*/
@Bean
    public ServletRegistrationBean staticViewServlet(){
        ServletRegistrationBean<StatViewServlet> monitorBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
        /**后台需要有人登录帐号密码进行管理*/
        //设置初始化参数,并且.setInitParameters中需要传入map
        Map<String,String> initParameters=new HashMap<>();
        //向其中增加相应的配置,注意的是:loginUsername与loginUserPassword是固定的,不可更改。
            initParameters.put("loginUsername","admin");
            initParameters.put("loginPassword","123456");

        //设定访问规则,谁能访问(值为空,则表示谁都可以访问,否则指定某个主机名)
        initParameters.put("allow","");

        //禁止某些人访问(前者表示具体名字,后者表示对应的ip地址)

       // initParameters.put("xxx","10.200.0.48");
        monitorBean.setInitParameters(initParameters);
        //最后返回这个monitorBean
        return monitorBean;
    }

  • 后台监控的类是 ServletRegistrationBean,最终是将该对象注入至SpringBoot的容器中去。
  • 首先创建一个ServletRegistrationBean对象,其中对应的类型为StatViewServletServletRegistrationBean的构造函数中需要传入Servlet并且传入对应的过滤路径,一般为/druid/*,表示如果想要查看后台的监控数据,需要访问/druid/路径,才能够跳转到相应的页面。
  • 返回的ServletRegistrationBean类对象可以使用设置初始化值来设置相应的访问规则。
  • 需要将相应的权限设置放置在Map集合中,并设置相应的参数与值,就登录的用户名与密码的属性名是固定的loginUsernameloginPassword,与此同时可以设置访问的权限,对应的属性名为allow,属性值自定义,若为空,则表示开放,任何人都可以访问。禁止访问的设置是设置对应的「禁止访问用户名和对应的ip地址」
  • 对应的访问如下所示:
  1. 自定义过滤器(不加入后台监控统计)

/**配置过滤的功能*/
        @Bean
        public FilterRegistrationBean webStatFilter(){
            FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>();
            //设置对应的过滤器
            bean.setFilter(new WebStatFilter());
            //设置过滤的规则
            Map<String,String> initParameters=new HashMap<>();
            //排除哪些页面不过滤(即不进行统计)
            initParameters.put("exclusions","*.js,*.css,/druid/*");

            //为bean设置过滤条件
            bean.setInitParameters(initParameters);
            return bean;
        }
  • 首先进行过滤的类是 WebStatFilter,并且通过FilterRegistrationBean来设置对应的过滤器,与监控类似,需要在map集合中添加一些属性与属性值,表示相应的过滤规则。
  • 最后返回FilterRegistrationBean

(四)整合MyBatis

整合MyBatis需要导入相关的依赖

<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.2</version>
</dependency>

这不是官方的依赖,是mybatis的自研依赖

  • 准备环境
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

测试是否有默认的数据源与连接

@SpringBootTest
class Springboot05MybatisApplicationTests {
    @Autowired
    private  DataSource dataSource;

    @Test
    void contextLoads() throws SQLException {
        System.out.println(dataSource.getClass());
        System.out.println(dataSource.getConnection());
    }

}
  • 编写POJO类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer id;
    private String name;
    private String pwd;
}

  • 编写dao(mapper)层
  1. 编写增删改查的mapper接口
@Mapper
@Repository
public interface UserMapper {
    /**
     * 编写增删改查的相关方法 @return the list
     */

    List<User> queryUserList();

    /**
     * Query user by id user.
     *
     * @param id the id
     * @return the user
     */

    User queryUserById(@Param("id") Integer id);

    /**
     * Add user int.
     *
     * @param user the user
     * @return the int
     */

    int addUser(User user);

    /**
     * Update user int.
     *
     * @param user the user
     * @return the int
     */

    int updateUser(User user);

    /**
     * Delete user by id int.
     *
     * @param id the id
     * @return the int
     */

    int deleteUserById(@Param("id") Integer id);

}

  • @Mapper注解表示该类是MyBatis下的一个Mapper接口,也可以在主启动类下添加注解@MapperScan("com.example.mapper")表示将对应包下的所有的类都扫描成mapper接口类。
  • @Repository表示该类是位于dao层的一个类,表示分层的概念,,同时将该类注入Spring。
  1. 编写对应的mapper.xml文件
  • 在properties配置文件中编写相关别名和xml文件的配置
#整合MyBatis
# 设置别名
mybatis.type-aliases-package=com.example.pojo
# 设置对应mapper实现的地址
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
  • 扫描对应的别名包为com.example.pojo

  • 定位实现接口的xml文件classpath:mybatis/mapper/*.xml,其中classpth:表示的是resources目录,依次查找,找到对应的以xml结尾的所有文件。

  • 编写实现接口的xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">
    <select id="queryUserList" resultType="com.example.pojo.User">
        SELECT * FROM mybatis.user;
    </select>
    <select id="queryUserById" resultType="com.example.pojo.User">
        SELECT * FROM mybatis.user WHERE id=#{id};
    </select>

    <insert id="addUser" parameterType="com.example.pojo.User">
        INSERT INTO mybatis.user(id, name, pwd) VALUES (#{id},#{name},#{pwd});
    </insert>
    <update id="updateUser" parameterType="com.example.pojo.User">
        UPDATE mybatis.user SET name=#{name},pwd=#{pwd} WHERE id=#{id};
    </update>

    <delete id="deleteUserById" parameterType="int">
        DELETE FROM mybatis.user WHERE id=#{id};
    </delete>


</mapper>

该文件为UserMapper.xml,位于resources/mybatis/mapper目录之中。

  • 编写controller层内容

@RestController
public class UserController {
    @Autowired
    private UserMapper userMapper;

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

        return users;
    }

    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id")Integer id){
        User user = userMapper.queryUserById(id);
        return user;
    }
    
}

进行相关的测试

七、Spring Security

在web开发中,安全始终是第一位。是一个非功能性需求。

网站安全应当在结构确定的时候就应该考虑安全问题。常见的安全框架有shiro和SpringSercuity等,两者很像,只是名字不同,认证、授权

官方网站:https://spring.io/projects/spring-security

Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。

Spring Security 是一个专注于为 Java 应用程序提供身份验证和授权的框架。像所有 Spring 项目一样,Spring Security 的真正强大之处在于它可以轻松扩展以满足自定义需求

SpringSecurity的几个主要的类:

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

该框架的主要目标是认证和授权(访问控制)

(一)环境准备

  1. 准备相应的依赖
<!-- https://mvnrepository.com/artifact/org.thymeleaf/thymeleaf-spring5 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.11.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-java8time -->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
            <version>3.0.4.RELEASE</version>
        </dependency>
            
         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
            

同时创建一个springboot的项目,选中支持web模板,最后在pom.xml文件中导入相应的依赖。

  1. 准备相应的页面
  1. 修改相应的配置文件
spring.thymeleaf.cache=false
//关闭thymeleaf的缓存
  1. 编写视图控制器并访问
@Controller
public class RouterController {

    /**设置进入首页的路径*/
    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }

    /**设置去登录的请求路径*/
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    /**设置跳转到三个不同等级的页面
     * 此处使用restful风格的id来指定要跳转的对应的不同的页面。
     * 由于id需要从前端获取,所以本质上3个请求方法可以请求9个不同的页面*/

    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") Integer id){
        return "views/level1/"+id;
    }

    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id") Integer id){
        return "views/level2/"+id;
    }

    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") Integer id){
        return "views/level3/"+id;
    }
}
  • 此类用于控制视图的跳转,跳转到具体的哪个视图,由对应的id来指定。
  • 总共12个请求路径

访问http://localhost:8080/或者http://localhost:8080/index就可以进入主页面。

(二)用户权限的认证与授权(Thymeleaf与SpringSecurity整合)

  1. 使用内存中的用户信息来完成认证与授权
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
      /**定义规则,首页谁都可以访问,但是相应的功能页只给有权限的人访问*/
      //请求授权的规则
      //authorizeHttpRequests对所有的请求进行认证,
      //antMatchers("/")对应匹配的请求为/,首页
      //permitAll()表示允许所有的用户,即所有的用户均有权限。
      //接下去的依次类推,指定访问路径,并且指定相应的用户许可
      //此处拦截不写views是因为这是一个请求路径,所以只用写部分也可进行拦截
        http.authorizeHttpRequests().antMatchers("/","/idnex").permitAll()
                                    .antMatchers("/level1/**").hasRole("vip1")
                                    .antMatchers("/level2/**").hasRole("vip2")
                                    .antMatchers("/level3/**").hasRole("vip3");

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

    }

    /**重写相应的认证规则
     * 并且对于密码可以进行相应的编码*/

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //inMemoryAuthentication()表示从内容中获取认证信息,也可以从数据库中获取
        //withUser表示指定访问的用户,password表示用户访问密码,roles表示用户访问的权限高低
        //and表示添加另一个角色登录验证
        //passwordEncoder表示添加加密规则 传入一个加密规则对象即可,在后续的密码参数中需要添加
        //(new xxxPasswordEncoder().encode(原密码)即可完成加密
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("zhangsan").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123456789")).roles("vip1","vip2","vip3")
                .and()
                .withUser("guest").password(new BCryptPasswordEncoder().encode("123")).roles("vip1");
    }
}

  • 首先在创建安全配置类,并继承 WebSecurityConfigurerAdapter(网络安全配置适配器类),并在该类上添加注解@EnableWebSecurity表示开启了网络安全的模式。

  • 继承父类WebSecurityConfigurerAdapter可以重写很多方法

  • 此处重点重写configure(HttpSecurity http)configure(AuthenticationManagerBuilder auth) 前者表示授权,后者表示认证

  • configure(HttpSecurity http)授权方法需要的是:

    • authorizeHttpRequests对所有的请求进行认证
    • antMatchers("/xxx/**")对应匹配的请求为/xxx/下的所有请求(此处需要用**表示所有的请求),并设置相应的授权,比如hasRole("vip1")表示对应的路径/xxx/**下的请求只能给对应身份为vip1的用户才可以进行访问。
    • permitAll()表示允许所有的用户可以访问,也就不需要指定某一个角色来进行访问。
    • http.formLogin();表示如果用户没有进行相应的认证会自动跳转到框架已经写好的登录页面之中去。如下图所示:对应的访问地址为:http://localhost:8080/login

此时如果登录的用户密码错误,就会跳转到另一个错误的网址:http://localhost:8080/login?error

  • configure(AuthenticationManagerBuilder auth)方法需要注意:
    • 参数auth调用方法inMemoryAuthentication()表示的是从内存中获取相应的认证信息。当然也可以从数据库中进行获取,只是从内存中获取的速度更快,一般都是从数据库中获取相应的认证信息。
    • passwordEncoder(new xxxPasswordEncoder())表示采取相应的加密策略,因为明文密码安全性很低,可以通过反编译获取,因此需要设置相应的加密策略。
    • withUser("zhangsan")设置需要认证的用户名信息,此处可以进行相应的设置。
    • password(new xxxPasswordEncoder().encode("明文密码"))表示对密码采用上述设置的加密策略进行相应的加密操作,提高用户帐号的安全性。
    • roles(“xxx”,“xxx”)此处设置认证相同的权限,以上述的root帐号为例子,该roles方法表示root用户具有vip1、2、3三者的访问权限,即可以三个路径下的内容都可以访问。
    • .and()该方法表示添加相应的认证规则。

如果存在超越权限访问,则会进行相应的报错提示:

并且该类方法都是使用函数式编程范式来进行相应的配置。

  1. 使用数据库中的信息完成验证信息
  @Autowired
    private DataSource dataSource;  
  @Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception{
        //首先需要确认密码已经被加密了
        User.UserBuilder users=User.withDefaultPasswordEncoder();
        auth.jdbcAuthentication()
                .dataSource(dataSource)
                .withDefaultSchema()
                .withUser(users.username("user").password("password").roles("USER"))
                .withUser(users.username("admin").password("password").roles("USER","ADMIN"));
    }
    

(三)用户注销及权限控制

  1. 用户注销实现
@Override
    protected void configure(HttpSecurity http) throws Exception {
      /**定义规则,首页谁都可以访问,但是相应的功能页只给有权限的人访问*/
      //请求授权的规则
      //authorizeHttpRequests对所有的请求进行认证,
      //antMatchers("/")对应匹配的请求为/,首页
      //permitAll()表示允许所有的用户,即所有的用户均有权限。
      //接下去的依次类推,指定访问路径,并且指定相应的用户许可
      //此处拦截不写views是因为这是一个请求路径,所以只用写部分也可进行拦截
        http.authorizeHttpRequests().antMatchers("/","/idnex").permitAll()
                                    .antMatchers("/level1/**").hasRole("vip1")
                                    .antMatchers("/level2/**").hasRole("vip2")
                                    .antMatchers("/level3/**").hasRole("vip3");

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

      //注销功能,开启了注销的功能,注销后跳转到首页
      http.logout().logoutSuccessUrl("/");
  • 注销实现依赖于 http.logout().logoutSuccessUrl("/");,表示将该用户(已经登录)注销,并且跳转到主页(“/”表示跳转到主页index.html的请求。)
  • 注销如图所示:跳转到对应的http://localhost:8080/logout地址,进行注销的操作,注销之后跳转到主页
  1. 权限控制(Thymeleaf与SpringSecurity整合)
  • 导入相应的依赖
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

并且在需要使用到thymeleaf-extras-springsecurity4整合的页面上添加命名空间

xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"
  • 登录注销相关设置
 <!--登录注销-->
            <div class="right menu">

                <div sec:authorize="!isAuthenticated()">
                    <!--如果未登录,则需要显示登录按钮-->
                    <!--未登录-->
                    <a class="item" th:href="@{/toLogin}">
                        <i class="address card icon"></i> 登录
                    </a>
                </div>
                <!--如果登录应该显示用户的相关信息-->
                <div sec:authorize="isAuthenticated()">
                    <a class="item">
                        用户名:<span sec:authentication="name"></span>
                    </a>
                </div>

                <div sec:authorize="isAuthenticated()">
                    <!--如果已经认证了,则显示注销按钮-->
                    <!--注销-->
                    <a class="item" th:href="@{/logout}">
                        <i class="sign-out icon"></i> 注销
                    </a>
                </div>
  • sec:authorize="!isAuthenticated()表示如果用户没有通过认证,则需要显示某些内容
  • sec:authorize="isAuthenticated()表示如果用户已经完成认证,需要显示某些内容
  • sec:authentication="name"表示显示用户已经登录的用户名信息。
  1. CSRF

「跨站请求伪造」(英语:Cross-site request forgery),也被称为 「one-click attack」 或者 「session riding」,通常缩写为 「CSRF」 或者 「XSRF」, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,「XSS」 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

//关闭CSRF
http.csrf().disable();

由于SpringBoot是默认开启的,因此我们需要手动关闭CSRF。

(四)记住信息功能

  1. 开启记住信息的功能
//开启记住我的功能 ,本质上是cookie的实现,默认保存两周
http.rememberMe();

开启此项方法,即可实现记住信息的功能,它会将用户的信息封装成cookie,保存在浏览器中长达2周

八、Shiro

(一)什么是Shiro

什么是Shiro?

Apache Shiro是一个强大且易用的「Java安全框架」,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

官网地址:https://shiro.apache.org/

(二)功能介绍

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

(三)Shiro架构

1.从外部来看

  • 「subject」:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何都是Subject,如网络爬虫、机器人等,与Subject的所有交互都会委托给SercurityManager,Subject其实是一个门面,SercurityManager才是实际的执行者
  • 「SecurityManager」:安全管理器,即所有与安全有关的操作都会与SecurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色。
  • 「Realm」:Shiro从Realm获取安全数据(比如:用户、角色、权限),就是说SecurityManager要验证用户身份,那么它就需要从Realm获取相应的用户进行比较,来确定用户的身份是否合法,也需要从Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成是DatSource。

2.Shiro架构内部

  • 「Subject」:任何可以与应用交互的‘用户’
  • 「SecurityManager」:相当于SpringMVC中的DispatcherServlet,是Shiro的心脏,所有具体的交互都通过SercurityManager进行控制,它管理着所有的Subject,并且负责进行认证。
  • 「Authenticator」:负责Subject认证,是一个扩展点,可以自定义实现,可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
  • 「Authorizer」:授权器,即访问控制器,用来决定主体是否具有权限进行相应的操作,即控制着用户能访问应用中的哪些功能。
  • 「Realm」:可以有一个或者多个realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等。由用户提供,「所以一般在应用中都需要实现自己的realm。」
  • 「SessionManager」:管理session生命周期的组件,而Shiro并不仅仅可以用在web环境,也可以用在普通的JavaSE环境中。
  • 「CacheManager」:缓存控制器,来管理用户,角色、权限等缓存的,因为这些数据基本上很少改变,放到缓存中以后可以提高访问的性能。
  • 「Cryptography」:密码模块,Shiro提供了一些常见的加密组件用于密码的加密、解密等。

(四)第一个Shiro程序

1. maven实现

创建普通的maven项目,并添加相应的依赖

  • 导入依赖
  <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.1</version>
        </dependency>

        <!-- configure logging -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j-impl</artifactId>
            <version>2.17.1</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.17.1</version>
            <scope>runtime</scope>
        </dependency>
  • 编写log4j2配置文件
log4j.rootLogger=INFO,stdout
log4j.appender.stdout=prg.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n

#阿帕奇库
log4j.logger.org.apache=WARN

#Spring
log4j.logger.org.springframework=WARN

# 默认的shiro日志
log4j.logger.org.apache.shiro=INFO

#取消冗长的日志
log4j.logger.org.apache.shiro.util.ThreadContext=WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
<!--
  ~ Licensed to the Apache Software Foundation (ASF) under one
  ~ or more contributor license agreements.  See the NOTICE file
  ~ distributed with this work for additional information
  ~ regarding copyright ownership.  The ASF licenses this file
  ~ to you under the Apache License, Version 2.0 (the
  ~ "License"); you may not use this file except in compliance
  ~ with the License.  You may obtain a copy of the License at
  ~
  ~     http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing,
  ~ software distributed under the License is distributed on an
  ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  ~ KIND, either express or implied.  See the License for the
  ~ specific language governing permissions and limitations
  ~ under the License.
  -->


<Configuration name="ConfigTest" status="ERROR" monitorInterval="5">
    <!--
      ~ Licensed to the Apache Software Foundation (ASF) under one
      ~ or more contributor license agreements.  See the NOTICE file
      ~ distributed with this work for additional information
      ~ regarding copyright ownership.  The ASF licenses this file
      ~ to you under the Apache License, Version 2.0 (the
      ~ "License"); you may not use this file except in compliance
      ~ with the License.  You may obtain a copy of the License at
      ~
      ~     http://www.apache.org/licenses/LICENSE-2.0
      ~
      ~ Unless required by applicable law or agreed to in writing,
      ~ software distributed under the License is distributed on an
      ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
      ~ KIND, either express or implied.  See the License for the
      ~ specific language governing permissions and limitations
      ~ under the License.
      -->


    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
        </Console>
    </Appenders>
    <Loggers>
        <Logger name="org.springframework" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="org.apache" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="net.sf.ehcache" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false">
            <AppenderRef ref="Console"/>
        </Logger>
        <Root level="info">
            <AppenderRef ref="Console"/>
        </Root>
    </Loggers>
</Configuration>

  • shiro.ini文件
[users]
# user 'root' with password 'secret' and the 'admin' role
root = secret, admin
# user 'guest' with the password 'guest' and the 'guest' role
guest = guest, guest
# user 'presidentskroob' with password '12345' ("That's the same combination on
# my luggage!!!" ;)), and role 'president'
presidentskroob = 12345, president
# user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz'
darkhelmet = ludicrousspeed, darklord, schwartz
# user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz'
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
#
# Each line conforms to the format defined in the
# org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc
# -----------------------------------------------------------------------------
[roles]
# 'admin' role has all permissions, indicated by the wildcard '*'
admin = *
# The 'schwartz' role can do anything (*) with any lightsaber:
schwartz = lightsaber:*
# The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with
# license plate 'eagle5' (instance specific id)
goodguy = winnebago:drive:eagle5
  • quickstart文件
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.lang.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Simple Quickstart application showing how to use Shiro's API.
 *
 * @since 0.9 RC2
 */

public class Quickstart {

    private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);


    public static void main(String[] args) {

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

        // Use the shiro.ini file at the root of the classpath
        // (file: and url: prefixes load from files and urls respectively):
       DefaultSecurityManager securityManager = new DefaultSecurityManager();
        IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
        securityManager.setRealm(iniRealm);

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

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

        // get the currently executing user:
        Subject currentUser = SecurityUtils.getSubject();

        // Do some stuff with a Session (no need for a web or EJB container!!!)
        Session session = currentUser.getSession();
        session.setAttribute("someKey""aValue");
        String value = (String) session.getAttribute("someKey");
        if (value.equals("aValue")) {
            log.info("Retrieved the correct value! [" + value + "]");
        }

        // let's login the current user so we can check against roles and permissions:
        if (!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken("lonestarr""vespa");
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException uae) {
                log.info("There is no user with username of " + token.getPrincipal());
            } catch (IncorrectCredentialsException ice) {
                log.info("Password for account " + token.getPrincipal() + " was incorrect!");
            } catch (LockedAccountException lae) {
                log.info("The account for username " + token.getPrincipal() + " is locked.  " +
                        "Please contact your administrator to unlock it.");
            }
            // ... catch more exceptions here (maybe custom ones specific to your application?
            catch (AuthenticationException ae) {
                //unexpected condition?  error?
            }
        }

        //say who they are:
        //print their identifying principal (in this case, a username):
        log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");

        //test a role:
        if (currentUser.hasRole("schwartz")) {
            log.info("May the Schwartz be with you!");
        } else {
            log.info("Hello, mere mortal.");
        }

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

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

        //all done - log out!
        currentUser.logout();

        System.exit(0);
    }
}

2.SpringBoot实现

(1)环境准备
  • 导入依赖
<!--        导入shiro整合spring的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>
  • 编写config配置包

a、UserRealm,自定义的realm,自己进行相应的实现

public class UserRealm  extends AuthorizingRealm {

    /**
     * 授权
     * @param principalCollection
     * @return
     */

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

    /**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */

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

该类继承了AuthorizingRealm类,并重写了其中的两个方法,一个是授权方法,一个是认证方法。

b、定义ShiroConfig类

@Configuration
public class ShiroConfig {

    /**创建Realm对象 需要自定义
     * 此处添加bean是将其添加到spring容器中*/

    @Bean
    public UserRealm userRealm(){
        return new UserRealm();
    }

    /**
    DefaultWebSecurityManager 默认网页安全管理对象
     创建对应的对象
     此处参数@Qualifier("userRealm") UserRealm userRealm表示将当前参数与上一个方法的对象关联*/

    @Bean(name = "securityManager")
    public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
        //创建一个核心对象管理对象
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();

        //管理对象需要关联Realm对象
        securityManager.setRealm(userRealm);

        return securityManager;
    }

    /**
    ShiroFilterFactoryBean shiro过滤工厂对象*/

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

  • @Configuration标签表示此类是一个自定义的配置类,在该类中可以自定义一些方法的实现并且将其交给spring容器托管。
  • userRealm方法用户获取userRealm对象,返回一个userRealm对象,并在该方法上添加注解@Bean表示该类的对象交给spring容器进行托管。(该对象表示数据库的安全信息)
  • 同理defaultWebSecurityManager方法表示获取一个DefaultWebSecurityManager类对象,并且该对象还应该设置realm对象(即userRealm对象),因此需要从参数处获取,要设置对应的参数,需要使用指定对象的注解@Qualifier指定需要绑定的是哪个对象,一般是用方法名作为value值。
  • shiroFilterFactoryBean方法表示获取一个Subject对象,与获取安全管理对象的方法相同,都需要传入对应的参数(安全管理对象),使用注解@Qualifier指定参数要绑定的对象是哪个。
(2)登录验证
  • 首先编写登录页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<h1>登录</h1>
<hr>
<p style="color: red" th:text="${msg}"></p>
<form th:action="@{/login}" method="post">
    <p>用户名:<input type="text" name="username"></p>
    <p>密码:<input type="password" name="password"></p>
    <p><input type="submit" value="登录"></p>
</form>

</body>
</html>

使用thymeleaf模板进行操作,登录表单提交到/login请求

  • 编写认证过滤
  /**
    ShiroFilterFactoryBean shiro过滤工厂对象*/

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        /*添加shiro的内置过滤器
        * anno:无需认证即可访问
        * authc:必须认证之后才可以访问
        * user:必须拥有了记住我功能才能访问
        * perms:拥有对某个资源的权限才能访问
        * role:拥有某个角色权限才能访问
        *
        * */

        //定义一个过滤的链条
        Map<String, String> filterMap=new LinkedHashMap<>();
        //通过filterMap设置相应的权限
        //filterMap.put("/user/add","authc");
        //filterMap.put("/user/update","authc");
        //支持通配符
        filterMap.put("/user/*","authc");

        bean.setFilterChainDefinitionMap(filterMap);

        //如果没有权限,就会跳转到登录页面
        bean.setLoginUrl("/toLogin");

        return bean;
    }
  • 在设置过滤器对象的方法中,设置相应的过滤器和过滤方法。
  • 相应的规则参数为:
    • anno:无需认证即可访问
    • authc:必须认证之后才可以访问
    • user:必须拥有了记住我功能才能访问
    • perms:拥有对某个资源的权限才能访问
    • role:拥有某个角色权限才能访问
  • 定义一个过滤链条,添加对应的过滤规则,支持通配符(比如拦截某个路径的请求)
  • bean.setFilterChainDefinitionMap(filterMap);将该过滤规则添加到过滤器对象中。
  • 如果没有权限就会跳转到相应的页面(一个跳转请求)

  • 编写对应的控制请求语句
 @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }

    @RequestMapping("/login")
    public String login(String username,String password,Model model){
        //获取当前的用户
        Subject subject = SecurityUtils.getSubject();
        //封装用户的登录数据(封装成一个令牌)
        UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);
       try {
           //用户用令牌进行登录,如果没有异常,就说明登录成功了,
           subject.login(usernamePasswordToken);
           return "index";
       }catch (UnknownAccountException e){
           //用户名不存在异常
           model.addAttribute("msg","用户名错误");
           return "login";

       }catch (IncorrectCredentialsException e){
           //密码不存在
           model.addAttribute("msg","密码错误");
           return "login";
       }

    }

}
  • 首先定义了一个跳转方法,来接受默认登录页面的跳转,如果没有权限访问,就会跳转到该页面。
  • 设置一个登录表单提交的请求认证方法
  • Subject subject = SecurityUtils.getSubject();此举旨在获取一个正在登录的用户对象
  • UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(username,password);将从前端获取的帐号密码封装成一个账户密码令牌。将用户登录的信息进行相应的封装。
  • subject.login(usernamePasswordToken);表示正在执行登录操作,如果存在异常或者错误,则进行相应的反馈,反馈给前端页面。如果登录认证成功,则返回到index主页面,如果错误,则还是回到登录页面。

  • 对账户密码进行认证
/**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证的方法........");
        //然后认证用户名和密码,从数据库中取
        String username="root";
        String password="123456";
        //将参数中的转换为用户登录的令牌
        UsernamePasswordToken userToken=(UsernamePasswordToken) authenticationToken;
        //进行判断
        if (!userToken.getUsername().equals(username)){
            //如果不相同则抛出异常
            return null;
        }
      //密码认证shiro完成


        //由于AuthenticationInfo是个接口,所以需要获取实现类对象(参数为principal,password和realmName)
        return new SimpleAuthenticationInfo("",password,"");
    }
  • 此处执行认证信息,获取从数据库中取出的账户密码与令牌进行对比
  • UsernamePasswordToken userToken=(UsernamePasswordToken) authenticationToken;将认证令牌强制转换成为一个用户的账户密码认证令牌,以便用于认证
  • userToken.getUsername().equals(username)将令牌中的信息取出与从数据库中获取的信息进行对比,如果认证失败,即返回null值。
  • 相应的密码认证交由shiro自动完成。最后返回接口的实现类对象SimpleAuthenticationInfo,参数为principal,password和realmName。

3.MyBatis整合Shiro

(1)环境准备
  • 导入相应的依赖
 <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>
        </dependency>

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

<!--        导入shiro整合spring的包-->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring-boot-web-starter</artifactId>
            <version>1.8.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.8.0</version>
        </dependency>

        <!--整合数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.16</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.8</version>
        </dependency>
       <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
    </dependencies>
  • 编写application.yaml配置文件

配置信息参见:德鲁伊数据源

spring:
  datasource:
    username: root
    password: xielibin20001011
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT
    driver-class-name: com.mysql.cj.jdbc.Driver
    # 指定德鲁伊数据源
    type: com.alibaba.druid.pool.DruidDataSource
      #SpringBoot默认是不注入这些的,需要自己绑定
    #druid数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

      #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
      #如果允许报错,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
    #则导入log4j 依赖就行
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

mybatis:
  type-aliases-package: com.example.pojo
  mapper-locations: classpath:mapper/*.xml
(2)Shiro实现认证操作

连接到相应的数据库

  • 编写实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private Integer id;
    private String name;
    private String pwd;
}

此处的各字段与数据库中的各个字段一一对应

  • 编写Mapper接口与mapper实现类
@Repository
@Mapper
public interface UserMapper {

    /**
     * Query user by name user.
     * 通过用户名查询指定的用户
     *
     * @param name the name
     * @return the user
     */

    User queryUserByName(@Param("name") String name);

}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.mapper.UserMapper">
   <select id="queryUserByName" parameterType="string" resultType="User">
       SELECT * FROM mybatis.user WHERE name=#{name};
   </select>
</mapper>

此处的mapper接口的对应实现以xml格式来实现,位于resources目录下的mapper文件夹中,同时由于在yaml文件中配置了对应的mapper-locations属性,所以可以自动扫描关联。

  • 编写service层与对应的实现类
public interface UserService {
    /**
     * Query user by name user.
     * 通过用户名查询指定的用户
     *
     * @param name the name
     * @return the user
     */

    User queryUserByName(@Param("name") String name);
}

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserMapper userMapper;

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

需要注意的是必须要在其实现类上添加注解@Service将其标注为service层组件,交由spring托管。

  • 重新编写认证方法
 @Autowired
    private UserService userService; 
/**
     * 认证
     * @param authenticationToken
     * @return
     * @throws AuthenticationException
     */

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        System.out.println("执行了认证的方法........");
        //将参数中的对象转换为用户登录的令牌
        UsernamePasswordToken userToken=(UsernamePasswordToken) authenticationToken;
  //获取指定用户名的用户对象
        User user = userService.queryUserByName(userToken.getUsername());
        //此处通过查询真实数据库来获取到一个用户
        if (user==null){
            //如果用户为空,则表示登录失败
            return null;
        }
        ////密码认证shiro完成
        //由于AuthenticationInfo是个接口,所以需要获取实现类对象(参数为principal,password和realmName)
        return new SimpleAuthenticationInfo("",user.getPwd(),"");
    }
}

a、获取注入的service对象,调用相应的方法从令牌中获取用户输入的相关信息,再从数据库中查出一个对象。如果对象为空,则说明账户名错误了,返回空值,反馈错误。

b、在此基础上,密码可以再次加密,可以使用Md5继续加密。

(3)Shiro实现请求授权
  • 添加数据库的权限字段

添加了一个perms字段,表示各个用户的相应的不同权限。

其中的字段值为相应的权限信息。并且user:*表示拥有所有的认证权限

  • 实体类中添加相应的字段
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {

    private Integer id;
    private String name;
    private String pwd;
    private String perms;
}

新添加的字段为perms,与数据库中的信息对应。

  • 在Shiro过滤对象中设置认证信息
 /**
    ShiroFilterFactoryBean shiro过滤工厂对象*/

    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        //设置安全管理器
        bean.setSecurityManager(defaultWebSecurityManager);
        /*添加shiro的内置过滤器
        * anno:无需认证即可访问
        * authc:必须认证之后才可以访问
        * user:必须拥有了记住我功能才能访问
        * perms:拥有对某个资源的权限才能访问
        * role:拥有某个角色权限才能访问
        *
        * */

        //定义一个过滤的链条
        Map<String, String> filterMap=new LinkedHashMap<>();
        //通过filterMap设置相应的权限
        //filterMap.put("/user/add","authc");
        //filterMap.put("/user/update","authc");

        //通过filter设置相应的权限
       //此处表示设置了两种不同的权限,一种是访问add,一种表示访问update
        filterMap.put("/user/add","perms[user:add]");
        filterMap.put("/user/update","perms[user:update]");
        //如果未授权,可以跳转到未授权的页面

        //支持通配符
        filterMap.put("/user/*","authc");


        bean.setFilterChainDefinitionMap(filterMap);

        //如果没有权限,就会跳转到登录页面
        bean.setLoginUrl("/toLogin");

        //设置未授权的请求
        bean.setUnauthorizedUrl("/unauthorized");

        return bean;
    }

a、 filterMap.put("/user/add","perms[user:add]");设置了当访问/user/add的时候需要验证用户权限,对应的权限信息为user:add,如果没有对应的权限,则无法访问该页面。

b、bean.setUnauthorizedUrl("/unauthorized");设置当某个用户没有相应的权限的时候会执行的某些操作,此处设置跳转到一个新的页面并给出相应的提示信息。

  • 设置无权限的页面信息提示
  @RequestMapping("/unauthorized")
    @ResponseBody
    public String unauthorized(){

    return "未经授权,无法访问此页面!!!!!";
    }

此处表示如果对应的用户没有权限登录该路径的话,会跳转到这个页面,页面的信息内容显示为未经授权,无法访问此页面!!!!!

  • 编写认证与授权信息

        //由于AuthenticationInfo是个接口,所以需要获取实现类对象(参数为principal,password和realmName)
        //第一个参数为principal,表示相关的资源信息
        return new SimpleAuthenticationInfo(user,user.getPwd(),"");

在方法doGetAuthenticationInfo认证的最后,将从数据库中查询到的对象,传入SimpleAuthenticationInfo对象中,后续就可以通过当前用户对象,获取到对象的资源信息。

 @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("执行了授权方法........");
        //可以在此给用户授权

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //给予所有用户相应的权限
        //info.addStringPermission("user:add");

        //获取当前登录的对象
        Subject subject = SecurityUtils.getSubject();
        User currentUser = (User)subject.getPrincipal();

        //为当前用户添加权限
        info.addStringPermission(currentUser.getPerms());

        return info;
    }

a、此处表示认证之后的授权操作。

b、创建授权对象 SimpleAuthorizationInfo info,之后可以为其添加相应的权限信息。

c、由于不同的用户需要有不同的权限,所以需要获取当前用户的相关信息。再通过subject.getPrincipal();获取到用户资源。

d、最后为用户设置相应的权限 info.addStringPermission(currentUser.getPerms());,相应的权限信息都是从数据库中获取而来的。最后返回权限对象。

4. Shiro整合Thymeleaf

(1)环境准备
<!-- https://mvnrepository.com/artifact/com.github.theborakompanioni/thymeleaf-extras-shiro -->
<dependency>
    <groupId>com.github.theborakompanioni</groupId>
    <artifactId>thymeleaf-extras-shiro</artifactId>
    <version>2.0.0</version>
</dependency>

(2)添加用户的session
 //当此处的用户不为空的时候,为用户添加session信息
        Subject currentUser = SecurityUtils.getSubject();
        Session session = currentUser.getSession();
        //为对应的session设置属性
        session.setAttribute("loginUser",user);

doGetAuthenticationInfo方法中添加用户的session,便于前端获取相关信息。

(3)编写前端页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">

<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>

<h1>首页</h1>
<p th:text="${shiro}"></p>
<hr>
<!--此处表示如果session的loginUser属性为空,则显示登录按钮-->
<div th:if="${session.loginUser==null}">
  <a th:href="@{/toLogin}">登录</a>
</div>
<div shiro:hasPermission="user:add">
    <a th:href="@{/user/add}">add</a><br>
</div>

<div shiro:hasPermission="user:update">
    <a th:href="@{/user/update}">update</a>
</div>

</body>
</html>

<div th:if="${session.loginUser==null}">此处的div标签表示如果session中的loginUser属性为空,则不会显示某个元素。是与Thymeleaf结合。

(五)Shiro常见的过滤器

1.与身份验证有关的

「配置缩写」「对应的过滤器」「功能」
anonAnonymousFilter指定url可以匿名访问
authcFormAuthenticationFilter基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username);passwordParam:表单提交的密码参数名(password);rememberMeParam:表单提交的密码参数名(rememberMe);loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址;failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure)
authcBasicBasicHttpAuthenticationFilterBasic HTTP身份验证拦截器,主要属性:applicationName:弹出登录框显示的信息(application)
logoutauthc.LogoutFilter退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/)
userUserFilter用户拦截器,用户已经身份验证/记住我登录的都可

2.授权相关的

「配置缩写」「对应的过滤器」「功能」
rolesRolesAuthorizationFilter角色授权拦截器,验证用户是否拥有所有角色;主要属性:loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”
permsPermissionsAuthorizationFilter权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms[“user:create”]”
portPortFilter端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样
restHttpMethodPermissionFilterrest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll)
sslSslFilterSSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样
noSessionCreationNoSessionCreationAuthorizationFilter需要指定权限才能访问

九、Swagger

(一)Swagger简介

Swagger 是一款RESTFUL接口的文档在线自动生成+功能测试功能软件。

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。总体目标是使客户端和文件系统作为服务器以同样的速度来更新。文件的方法,参数和模型紧密集成到服务器端的代码,允许API来始终保持同步。Swagger 让部署管理和使用功能强大的API从未如此简单。

  • 号称世界上最流行的API框架
  • RestFul API文档在线自动生成-》API文档与API定义同步更新。
  • 直接运行,可以在线测试API接口
  • 支持多种语言

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

在项目中使用Swagger需要的springfox:

  • swagger2
  • swagger UI

(二)SpringBoot集成Swagger

1. 新建项目并搭建相关环境

  • 新建一个springboot的项目,勾选Spring web支持
  • 导入swagger相关依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

  • 测试相关环境
@RestController
public class HomeController {

    /**
     * 一共有两个请求,一个是hello,一个error请求
     * @return
     */

    @RequestMapping("/")
    public String home(){
        return "Hello Swagger!!!";
    }
}

可以正常访问。

  • 配置Swagger(未进行相关配置)
@Configuration
@EnableSwagger//开启swagger
public class SwaggerConfig {

}

@EnableSwagger2 表示开启swagger2

此时访问会引发空指针异常,所以需要添加相关配置才可以正确访问

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
  • 默认配置下访问页面

访问地址:http://localhost:8080/swagger-ui.html

此页面一共分四个主要模块:包括swagger信息、接口信息、实体类信息、组

2. 配置Swagger

Swagger的的bean实例叫做Docket

Docket源码如下:

public class Docket implements DocumentationPlugin {
    public static final String DEFAULT_GROUP_NAME = "default";
    private final DocumentationType documentationType;
    private final List<SecurityContext> securityContexts = Lists.newArrayList();
    private final Map<RequestMethod, List<ResponseMessage>> responseMessages = Maps.newHashMap();
    private final List<Parameter> globalOperationParameters = Lists.newArrayList();
    private final List<Function<TypeResolver, AlternateTypeRule>> ruleBuilders = Lists.newArrayList();
    private final Set<Class> ignorableParameterTypes = Sets.newHashSet();
    private final Set<String> protocols = Sets.newHashSet();
    private final Set<String> produces = Sets.newHashSet();
    private final Set<String> consumes = Sets.newHashSet();
    private final Set<ResolvedType> additionalModels = Sets.newHashSet();
    private final Set<Tag> tags = Sets.newHashSet();
    private PathProvider pathProvider;
    private List<? extends SecurityScheme> securitySchemes;
    private Ordering<ApiListingReference> apiListingReferenceOrdering;
    private Ordering<ApiDescription> apiDescriptionOrdering;
    private Ordering<Operation> operationOrdering;
    private ApiInfo apiInfo;
    private String groupName;
    private boolean enabled;
    private GenericTypeNamingStrategy genericsNamingStrategy;
    private boolean applyDefaultResponseMessages;
    private String host;
    private Optional<String> pathMapping;
    private ApiSelector apiSelector;
    private boolean enableUrlTemplating;
    private List<VendorExtension> vendorExtensions;

    public Docket(DocumentationType documentationType) {
        this.apiInfo = ApiInfo.DEFAULT;
        this.groupName = "default";
        this.enabled = true;
        this.genericsNamingStrategy = new DefaultGenericTypeNamingStrategy();
        this.applyDefaultResponseMessages = true;
        this.host = "";
        this.pathMapping = Optional.absent();
        this.apiSelector = ApiSelector.DEFAULT;
        this.enableUrlTemplating = false;
        this.vendorExtensions = Lists.newArrayList();
        this.documentationType = documentationType;
    }

DocumentationType源码

public class DocumentationType extends SimplePluginMetadata {
    public static final DocumentationType SWAGGER_12 = new DocumentationType("swagger""1.2");
    public static final DocumentationType SWAGGER_2 = new DocumentationType("swagger""2.0");
    public static final DocumentationType SPRING_WEB = new DocumentationType("spring-web""1.0");
    private final MediaType mediaType;

配置相关信息

主要是配置了apiInfo信息(加入了自身的定制化内容)

@Configuration
@EnableSwagger//开启swagger
public class SwaggerConfig {

    /**
     * 配置了swagger的bean实例
     * @return
     */

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

    /**配置swagger文档信息 apiInfo*/
    private ApiInfo apiInfo(){
        //定义作者信息
        Contact user = new Contact("张三""http://localhost:8089/""123456789@163.com");
        return  new ApiInfo("Swagger API文档",
                "愿乘长风,破万里浪!",
                "v1.0""http://localhost:8085/",
                user, "Apache 2.0",
                "http://www.apache.org/licenses/LICENSE-2.0"new ArrayList());
    }


}

.apiInfo(apiInfo())调用了自定义的apiInfo方法作为参数,在私有方法apiInfo中,返回了一个新的apiInfo,并返回给调用者,在对应的swagger实例化方法中,调用方法,进行定制化操作。

访问:http://localhost:8080/swagger-ui.html,页面如下

3.配置swagger的扫描接口及开关

swagger-ui.html的文件位置

(1)配置扫描接口
  /**
     * 配置了swagger的bean实例
     * @return
     */

    @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .select()
                //RequestHandlerSelectors配置要扫描接口的方式
                //1. basePackage,·配置基于包的扫描接口的方式(指定扫描的包)
                //2. any()表示扫描全部的包
                //3. none()表示不扫描包
                //4. withClassAnnotation表示扫描类上的注解,参数是一个注解的反射对象
                //5. withMethodAnnotation()表示扫描方法上的注解 参数是一个注解的反射对象
                //一般是使用包扫描的方式
                .apis(RequestHandlerSelectors.basePackage("com.example.controller"))
                //paths表示过滤某些路径
                //ant("/h/**")表示扫描过滤带有h请求下的接口
                .paths(PathSelectors.ant("/h/**"))
                .build();
    }
  • select方法表示选择swagger的配置的内容,之后需要以build方法结尾,表示结束,这是一个典型的工厂模式的例子。
  • apis方法中需要一个RequestHandler接口的实现类对象RequestHandlerSelectors,该对象有5个主要的配置扫描接口的方式
    • basePackage,配置基于包的扫描接口的方式(指定扫描的包)
    • any()表示扫描全部的包
    • none()表示不扫描包
    • withClassAnnotation表示扫描类上的注解,可以传入参数,参数是一个注解的反射对象(该对象必须位于类上,位于方法上无效)。
    • withMethodAnnotation表示扫描方法上的注解 可以传入参数,参数是一个注解的反射对象(该对象必须位于方法上,位于类上无效)。
  • paths表示过滤某些路径,其中需要传入Selector对象,此处传入的是路径选择对象PathSelectors,该对象可以调用以下方法对某些路径进行过滤,过滤完的路径,将不会被显示在swagger-ui.html的展示页面中。
    • any表示过滤任何路径,即任何路径下的接口都不会被显示在页面上。
    • none表示没有路径被过滤,即任何路径下的接口都会显示在页面上,没有任何的过滤。
    • regex可以使用正则表达式来过滤相匹配的路径
    • ant表示指定请求下的内容进行过滤。
  • 需要注意的是select().apis().paths().build往往是一同出现的,其中apis()paths()可以选择性添加。
(2)配置是否启动swagger

设置enable属性,将其设置为false,表示关闭swagger,swagger不能在浏览器中访问。(默认为true开启)

 @Bean
    public Docket getDocket(){
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(false);

此时再次访问http://localhost:8080/swagger-ui.html页面出现错误。

「如果只希望swagger只在生产环境中使用,发布的时候不使用?」

  • 判断是不是生产环境?
  • 注入enable()

a、首先在application.yaml文件中设置激活的相关环境

spring:
  mvc:
    pathmatch:
      matching-strategy: ant_path_matcher
 profiles:
   active: dev

此处表示生产环境dev,还可以指定testpro等环境。

b、在swagger的配置类中,利用环境对象设置对应要使用的环境

@Bean
    public Docket getDocket(Environment environment){
        //获取项目的环境
        //设置要显示的swagger环境
        Profiles profiles=Profiles.of("dev","test");
        //通过environment.acceptsProfiles判断当前是否处在自己设定的环境当中
        boolean flag= environment.acceptsProfiles(profiles);

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(flag)

在方法getDocket中传入参数Evironment,在通过Profiles设置要显示的swagger环境,最后通过environment.acceptsProfile方法将profiles对象传入,与配置文件中的环境进行比较,如果配置文件中的环境与这里设置的环境一致,那么swagger显示就会生效,否则就失效。

4.配置API文档分组

 @Bean
    public Docket getDocket(Environment environment){
        //获取项目的环境
        //设置要显示的swagger环境
        Profiles profiles=Profiles.of("dev","test");
        //通过environment.acceptsProfiles判断当前是否处在自己设定的环境当中
        boolean flag= environment.acceptsProfiles(profiles);

        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .groupName("xxx")

此处的groupName表示指定组,

如何配置多个分组

此时只需要配置多个Docket即可,每个Docket定制各自的专属内容即可。

@Configuration
@EnableSwagger//开启swagger
public class SwaggerConfig {

    @Bean
    public Docket docket1(){
        return new Docket(DocumentationType.SWAGGER_2).groupName("A");
    }

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

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

访问:http://localhost:8080/swagger-ui.html

5.实体类配置

(1)设置实体类

@ApiModel("实体类")
public class User {
    @ApiModelProperty("用户名")
    private String userName;
    @ApiModelProperty("密码")
    private String password;

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

  • 此处设置了两个私有属性,如果想要在swagger-ui.html中访问到,则需要添加相应的getter和setter方法。
  • 注解@ApiModel("实体类")表示为实体类添加相应的注释,以便更好地查看相关信息。
  • 注解 @ApiModelProperty("用户名")表示为实体类的属性添加相应的注释,起到解释说明的作用。
注解添加注释

上图表示用注解来给实体类的相关信息添加相应的解释说明注释。(此处需要先完成swagger与实体类的绑定。)

(2)实体类扫描绑定

要想实体类被swagger扫描,需要在接口中添加相应实体类的实例。

  /**
     * 此处表示只要我们的接口返回值中含有实体类,他就会被扫描到swagger中
     * @return
     */

    @PostMapping(value = "/user")
    public User user(){
        return new User();
    }

此时表示相应的实体类将会被swagger扫描,即可以在ui页面中查看到相关的实体类信息。

(3)为对应的控制器接口和参数添加注释


    @ApiOperation("Hello接口")
    @PostMapping("/h")
    public String hello(@ApiParam("用户名") String userName){
        return "hello"+userName;
    }
  • 使用 @ApiOperation("Hello接口")表示解释说明该请求接口。
  • 使用@ApiParam("用户名")表示解释说明参数的相关信息内容。

结果如下图所示:

请求接口与参数解释

总结:

  • 我们可以通过swagger给一些难以理解的属性或者接口(控制请求方法)添加相应的注释信息
  • 接口文档实时更新
  • 可以在线测试
  • 在正式发布的时候需要关闭swagger

十、任务

(一)注解开启异步任务

  1. 开启异步任务注解功能
//开启异步任务处理功能
@EnableAsync
@SpringBootApplication
public class Springboot09TaskApplication {

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

}

@EnableAsync表示开启异步任务注解处理的功能。

  1. 编写测试的service类
@Service
public class AsyncService {

    /**
     * 此时需要告诉spring这是一个异步的方法
     */

    @Async
    public void hello(){
        try {
            Thread.sleep(4000);
        }catch (Exception e){
            throw new RuntimeException(e);
        }
        System.out.println("消息处理中.......");
    }

}
  • @Service将该类交由Spring进行管理
  • @Async告诉spring这个方法必须是一个异步的方法
  1. 编写controller测试
@RestController
public class AsyncController {
    @Autowired
    private AsyncService service;
    @GetMapping("/hello")
    public String hello(){
        service.hello();
        return "ok";
    }
}

此时访问http://localhost:8080/hello会直接输出OK字符串。并不需要前台的响应等待,这是因为hello方法被声明为一个异步的方法。不需要等待其休眠4s之后才输出字符串内容。

(二)邮件任务

  1. 导入相关的依赖
<!--添加邮件任务-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
  1. 编写相应的yaml配置文件
spring:
  mail:
    username: 174944561121@qq.com
    password: kwhfpyzfjhkqecdf
    host: smtp.qq.com
    properties:                             
      mail:
        smtp:
          ssl:
            enable: true
# 开启加密验证(针对qq)

properties表示设置相应的加密验证,使用SSL进行加密。

  1. 简单邮件发送测试
@SpringBootTest
class Springboot09TaskApplicationTests {
    @Autowired
    private JavaMailSender javaMailSender;
    //此处表示创建了一个邮件发送者对象。

    @Test
    void contextLoads() {
        //创建一个邮件对象
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
        //为邮件对象设置相应的属性
        simpleMailMessage.setSubject("通知");
        simpleMailMessage.setText("这是我第一次使用Java来发送邮件......");
        simpleMailMessage.setFrom("174456114@qq.com");
        simpleMailMessage.setTo("1744546415@qq.com");
        javaMailSender.send(simpleMailMessage);
    }

}

  • 此处必须首先注入JavaMailSender 对象,表示用来发送邮件的对象。
  • 具体的邮件对象是SimpleMailMessage,可以为其设置不同的属性,有主题、文本、发送者与接收者等等。
  • 最后调用send方法发送邮件。
  1. 复杂邮件发送测试
  @Test
    public void sendTest() throws MessagingException {
        //此处测试发送一个复杂的邮件
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        //组装复杂的邮件
        //创建一个MimeMessage帮助对象,帮助组装(传入两个参数,一个是message对象,一个是否支持多文件传输)
        MimeMessageHelper helper = new MimeMessageHelper(mimeMessage,true);
        helper.setSubject("复杂邮件创建测试");
        //设置html文本,将属性设置为true
        helper.setText("<h1>这是我用Java发送的第一个复杂邮件测试</h1>",true);

        //可以为邮件添加附件
        helper.addAttachment("1.png",new File("/home/lambda/Desktop/1.png"));

        //添加发送者与接收者
        helper.setFrom("174944564114@qq.com");
        helper.setTo("17494598564@qq.com");
        
        javaMailSender.send(mimeMessage);

    }
  • 发送复杂邮件需要使用MimeMessage对象,表示能够发送的复杂邮件
  • MimeMessageHelper对象表示能够帮助复杂邮件对象设置相应的属性,如文本内容、文本主题、文本附件内容等等,还可以支持HTML5的格式语法。
  • 最后通过javaMailSender发送对应的创建好的复杂邮件对象。

(三)定时任务

  1. 主函数中添加开启定时任务的注解
//开启异步任务处理功能
@EnableAsync
//开启定时功能注解
@EnableScheduling
@SpringBootApplication
@SuppressWarnings("all")
public class Springboot09TaskApplication {

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

}

  • @EnableScheduling表示开启定时任务,能够使用定时任务来完成相应的操作。
  • @SuppressWarnings("all")表示忽略了所有的警告信息。
  1. 编写service层的定时任务方法
@Service
@SuppressWarnings("all")
public class ScheduledService {

    /**
     * 此方法在一个特定的时间来执行。
     */

    //此处需要使用cron表达式
    @Scheduled(cron = "0/2 * * * * ?")
    public void hello(){
        System.out.println("Hello,这个定时任务被执行了........");
    }

}
  • 此处的 @Scheduled(cron = "0/2 * * * * ?")注解表示设置定时任务的执行机制
  • cron表达式设置了相应任务的执行频率,上例子中表示每隔2s执行一次输出任务。
  • 需要特别指出的是这样的定时任务是「异步」
  1. 测试

上图中相应的信息每隔2s就会被输出在控制台上一次。

十一、SpringBoot整合

(一)SpringBoot相关配置回顾

SpringBoot操作数据:spring-data

SpringData也是和SpringBoot齐名的项目。

SpringData官网:https://spring.io/projects/spring-data

SpringBoot在2.x之后。原来使用的Jedis被替换成lettuce了。

  • 「Jedis」:采用的是直接连接server,多个线程操作的话是十分不安全的。如果想要避免不安全,则需要使用Jedis pool池。类似于BIO模式。
  • 「lettuce」:底层采用了netty,实例可以在多个线程中共享,不存在线程不安全。可以减少线程数。类似于NIO模式。

由于在springboot中每一个配置的类都有一个自动配置类xxxAutoConfiguration

自动配置类都会绑定一个xxxproperties类,该类与我们的配置文件绑定。

因此,redis与springboot整合,对应的会有一个RedisAutoConfiguration。该类会绑定一个配置文件类RedisProperties,该配置文件类中的属性与我们的application.properties中的文件可配置的属性一致。

  1. RedisAutoConfiguration源码
@EnableConfigurationProperties({RedisProperties.class})
@Import(
{LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration 
{
    public RedisAutoConfiguration() {
    }

    @Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) 
{
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) 
{
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

以上需要注意的如下:

  • @EnableConfigurationProperties({RedisProperties.class})表示导入了自动配置的Redis配置文件类。
  • redisTemplate方法表示默认初始化一个RedisTemplate模板对象用于数据库操作
  • @ConditionalOnMissingBean以及 @ConditionalOnSingleCandidate分别表示当没有bean对象的时候可以自定义模板类对象和表示字符串类型的模板对象(条件是单一的连接工厂对象)
  • 但是第一个方法默认的RedisTemplate是没有过多设置的,但Redis的对象都是需要「序列化」
  • 第一个方法的两个泛型参数都是Object类型,都需要强制类型转换。
  1. RedisProperties源码
@ConfigurationProperties(
    prefix = "spring.redis"
)
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String username;
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private Duration connectTimeout;
    private String clientName;
    private RedisProperties.ClientType clientType;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();

    public RedisProperties() {
    }
    ...一系列对应的setter和getter方法

  • @ConfigurationProperties表示当在配置文件中进行相关配置的时候,需要书写前缀spring.redis
  • 该类只有一个无参数的构造方法
  • 该类的属性的内容就是我们可以在application.properties文件中的配置的内容。其中host默认是本机。
  1. RedisConnectFactory源码
public interface RedisConnectionFactory extends PersistenceExceptionTranslator {
    RedisConnection getConnection();

    RedisClusterConnection getClusterConnection();

    boolean getConvertPipelineAndTxResults();

    RedisSentinelConnection getSentinelConnection();
}

这个是一个Redis的连接工厂类。是一个接口。主要方法是处理一系列的连接。

其有两个实现类:一个是JedisConncetionFactory,一个是lettuceConncetionFactory

其中SpringBoot在2.x之后主要使用的是lettuceConncetionFactory实现类来实现相关连接处理。

  1. RedisTemplate源码
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
    private boolean enableTransactionSupport = false;
    private boolean exposeConnection = false;
    private boolean initialized = false;
    private boolean enableDefaultSerializer = true;
    @Nullable
    private RedisSerializer<?> defaultSerializer;
    @Nullable
    private ClassLoader classLoader;
    // 此处表示默认的值的序列化
    @Nullable
    private RedisSerializer keySerializer = null;
    @Nullable
    private RedisSerializer valueSerializer = null;
    @Nullable
    private RedisSerializer hashKeySerializer = null;
    @Nullable
    private RedisSerializer hashValueSerializer = null;
    // 以上表示默认的值的序列化
    private RedisSerializer<String> stringSerializer = RedisSerializer.string();
    @Nullable
    private ScriptExecutor<K> scriptExecutor;
    private final ValueOperations<K, V> valueOps = new DefaultValueOperations(this);
    private final ListOperations<K, V> listOps = new DefaultListOperations(this);
    private final SetOperations<K, V> setOps = new DefaultSetOperations(this);
    private final StreamOperations<K, ?, ?> streamOps = new DefaultStreamOperations(this, ObjectHashMapper.getSharedInstance());
    private final ZSetOperations<K, V> zSetOps = new DefaultZSetOperations(this);
    private final GeoOperations<K, V> geoOps = new DefaultGeoOperations(this);
    private final HyperLogLogOperations<K, V> hllOps = new DefaultHyperLogLogOperations(this);
    private final ClusterOperations<K, V> clusterOps = new DefaultClusterOperations(this);

    public RedisTemplate() {
    }

其中可以注意到这四个初始值的序列化的实现方式默认采用JDK来实现序列化的:

但是更多时候我们需要的是使用json来实现序列化。

(二)SpringBoot与Redis进行整合

  1. 导入相关依赖
  <dependency>
        <!--  操作redis-->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
  1. 配置Redis
# 配置Redis
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 1
  1. 测试Redis操作
@SpringBootTest
class Springboot10RedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    void contextLoads() {
        // 操作字符串
        redisTemplate.opsForValue().set("hello","world");

        //操作list集合
        redisTemplate.opsForList().set("my",5,"java");

        //操作set集合
        redisTemplate.opsForSet().add("java","java",30,'c');

        //操作Hyperloglog
        redisTemplate.opsForHyperLogLog().add("city",5);
        //操作hash
        redisTemplate.opsForHash().put("myhash","hash1","value1");

        //......

        //可以直接操作事务
        redisTemplate.multi();
        //....相关事务操作
        redisTemplate.discard();

        //获取连接的相关操作
        RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
        //可以对数据库进行相关的操作
        connection.flushAll();
        connection.flushDb();
    }
}

(三)自定义配置RedisTemplate

查看RedisTemplate的源码可知,值的序列化默认是使用JDK的序列化方式来实现的。但很多时候我们需要使用「json来实现序列化」,所以我们可以自定义一个Redis的配置类,将相关的Redis配置在该配置类中实现。

总的来说,SpringBoot提供了7种基本的序列化方式:

1. 使用默认的RedisTemplate

使用默认的RedisTemplate实现对象的序列化(即使用默认JDK的序列化)

  • 创建实体类
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;

}
  • 进行相关测试操作

a、此时对象不进行序列化,将其进行存取操作

  @Autowired
    private User user;  
@Test
    public void test(){
        redisTemplate.getConnectionFactory().getConnection().flushAll();
        //此时对象尚未进行序列化
        user.setName("小明");
        user.setAge(15);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));

    }

最终序列化失败

「原因在于相关的实体类对象没有实现Java的序列化接口。」

如果实现序列化接口

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
    private String name;
    private Integer age;

}

再次运行,获得如下结果

此时实例对象序列化成功,主要原因是JDK序列化成功的一个条件是该实例对象实现了序列化接口,因此能够实例化成功。

但是由于实现序列化接口增强了耦合性,因此并不推荐使用。

很多时候在实战项目中,都是使用「json」来实现序列化。

  @Test
    public void test() throws JsonProcessingException {
        redisTemplate.getConnectionFactory().getConnection().flushAll();
        user.setName("小明");
        user.setAge(15);
        //对新的对象进行序列化操作
        String userJson = new ObjectMapper().writeValueAsString(user);
        redisTemplate.opsForValue().set("user",userJson);
        System.out.println(redisTemplate.opsForValue().get("user"));

    }

对应的结果如下:序列化成功

2. 使用自定义的RedisTemplate对象

  • 创建RedisConfig类,进行对Redis的自定义配置
@Configuration
public class RedisConfig {
    /**此处编写Redis的相关自定义配置实现。*/

    @Bean
    @ConditionalOnMissingBean(
            name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) 
{
        RedisTemplate<String, Object> template = new RedisTemplate();
        //使用jackson来实现序列化
        Jackson2JsonRedisSerializer<Object> objectJackson2 = new Jackson2JsonRedisSerializer<Object>(Object.class);
        //此时可以对json序列化进行相关的配置(主要使用的是ObjectMapper对象,该对象将java对象与json对象进行转换)
        ObjectMapper om=new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectJackson2.setObjectMapper(om);

        /**其他的序列化方式*/
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用string的序列化方式
        //template.setKeySerializer(stringRedisSerializer);
        //hash的key也采用string的序列化方式
        //template.setHashKeySerializer(stringRedisSerializer);
        //value采用json的序列化方式
        //template.setValueSerializer(objectJackson2);
        //hash的 value也采用json的序列化方式
        //template.setHashValueSerializer(objectJackson2);
        //具体的操作
        //使用json实现序列化
        template.setDefaultSerializer(objectJackson2);
        template.setConnectionFactory(redisConnectionFactory);
        //最后保存所有的设置
        template.afterPropertiesSet();
        return template;
    }

}

需要注意的是:template.setxxxSerializer表示为某一种类型的数据设置自定义的序列化方式。

此时需要保证在测试类中使用的是自定义的redisTemplate对象,需要在对应的对象上加入@Qualifier("redisTemplate"),由于在RedisAutoConfiguration类的源码中的redisTemplate方法上有注解@ConditionalOnMissingBean指定了自定义的name为redisTemplate,也就是说只有当自定义的类的对象名(bean的name)为redisTemplate时候,自定义的类对象才能生效,否则就会失效。

@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;

测试

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private Integer age;

}

 @Test
    public void test() throws JsonProcessingException {
        redisTemplate.getConnectionFactory().getConnection().flushAll();
        user.setName("小明");
        user.setAge(15);
        redisTemplate.opsForValue().set("user",user);
        System.out.println(redisTemplate.opsForValue().get("user"));

    }

此时序列化成功

使用的是自定义的SpringBoot提供的Jackson2JsonRedisSerializer序列化实例对象,替代了原先默认的JDK序列化方式,因此即使在User没有实现序列化接口的时候仍旧可以实现实例对象的序列化。

3. 编写RedisUtils工具类

在很多的实战场景下并不会使用原生的方式去书写代码,可以自定义一个工具类完成相应的基础操作(常见的增删改查)。

@Component
@SuppressWarnings("all")
public final class RedisUtils {

    @Autowired
    @Qualifier("redisTemplate")
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 设置缓存的过期时间
     * @param key 键
     * @param time 时间,单位秒
     * @return 布尔值
     */

    public boolean expire(String key,Long time){
        try {
            if (time>0){
                redisTemplate.expire(key,time, TimeUnit.SECONDS);
            }
            return true;
        }catch (Exception e) {
            e.printStackTrace();
            return false;
        }

    }

    /**
     * 获取指定key的过期时间
     * @param key 不能为null
     * @return Long 表示过期时间
     */

    public Long getExpireTime(String key){
        return redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }

    /**
     * 判断key的值是否存在
     * @param key 需要验证的key
     * @return 返回一个布尔值
     */

    public boolean hasKey(String key){
        try {
            return redisTemplate.hasKey(key);
        }catch (Exception exception){
            exception.printStackTrace();
            return false;
        }
    }
    .........此外还有一系列的方法

在使用的时候可以直接通过@Autowired注解直接注入

  @Autowired
  private RedisUtils redisUtils;

后续即可以正常使用了。

测试使用

  @Test
    public void testValue(){
        redisTemplate.getConnectionFactory().getConnection().flushAll();
        redisTemplate.opsForValue().set("name","zhangsan");
        System.out.println(redisUtils.getValue("name"));
    }



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存