讲师简介

smart哥,互联网悍将,历经从传统软件公司到大型互联网公司的洗礼,入行即在中兴通讯等大型通信公司担任项目leader,后随着互联网的崛起,先后在美团支付等大型互联网公司担任架构师,公派旅美期间曾与并发包大神Doug Lea探讨java多线程等最底层的核心技术。对互联网架构底层技术有相当的研究和独特的见解,在多个领域有着丰富的实战经验。

一、Spring Boot简介

Spring Boot是一个基于Java的开源框架,用于创建微服务,它由Pivotal Team开发,用于构建独立的生产就绪Spring应用,本章将介绍Spring Boot,并熟悉基本概念。

1、SpringBoot和微服务

微服务(Micro Service)是一种允许开发人员独立开发和部署服务的体系结构,每个运行的服务都有自己的流程,这实现了轻量级模型以支持业务应用程序。

微服务整个体系非常庞大,由各个组件:例如:网关,注册中心,链路跟踪等相互协调完成。所有这些组件的存在都是为了保证微服务的正常运行,那么SpringBoot就是用来快速构建微服务的框架(这里的微服务指的就是构建纯接口RPC服务之间的调用,并没有界面等其他元素)

单体架构:ssm=springMVC+spring+mybatis

总结:

(1)spring boot不是微服务技术,只属于微服务中的一部分
(2)spring boot只是一个用于加速开发spring应用的基础框架,简化工作,开发单块应用(单个服务)很适合
(3)如果要直接基于spring boot做微服务,相当于需要自己开发很多微服务的基础设施,比如基于zookeeper来实现服务注册和发现
(4)spring cloud才是微服务技术

2、Spring Boot是什么?

Spring Boot作为一款快速开发框架,为Java开发人员提供了一个很好的平台,可以开发一个可以运行的独立和生产级Spring应用程序,可以开始使用最少的配置或者0配置(利用spring3之后的注解化),而无需进行整个Spring配置设置。

  • 快速开发spring应用的框架

    传统项目使用:spring mvc+spring+mybatis,首先配置一大堆xml配置文件,其次部署和安装tomcat,jetty等容器,跟java web打交道,跟servlet,listener,filter等打交道。

    最后,手工部署到tomcat或者jetty等容器中,发布一个web应用。

    spring boot,简单来说,就是看中了这种java web应用繁琐而且重复的开发流程,采用了spring之上封装的一套框架,spring boot,简化整个这个流程。

    尽可能提升我们的开发效率,让我们专注于自己的业务逻辑即可

  • 内嵌tomcat和jetty容器,不需要单独安装容器,jar包直接发布一个web应用,以普通的java程序形式运行。

  • 简化maven配置,采用parent继承模式,一站式引入需要的各种依赖,而且自动解决jar冲突问题。

  • 基于注解的零配置思想

  • 和各种流行框架,spring web mvc,mybatis,spring cloud无缝整合

3、为什么选择Spring Boot?

选择Spring Boot,因为它提供的功能和优点如下 -

  • 它提供了一种灵活的方法来配置Java Bean,XML配置和数据库事务。
  • 在Spring Boot中,一切都是自动配置的,无需手动配置。
  • 它提供基于注解的spring应用程序。
  • 简化依赖管理(整合第三方框架使用maven依赖的继承关系,starter形式)。
  • 它包括嵌入式Servlet容器(易于部署,最终以java应用程序的形式运行)。

4、Spring Boot是如何工作的?

Spring Boot会根据使用@EnableAutoConfiguration注解添加到项目中的依赖项自动配置应用程序。

例如,如果MySQL数据库在类路径上,但尚未配置任何数据库连接,则Spring Boot会自动配置内存数据库。

spring boot应用程序的入口点是包含@SpringBootApplication注释和main方法的类。
Spring Boot使用@ComponentScan注释自动扫描项目中包含的所有组件。

Spring Boot Starters

处理依赖管理对于大项目来说是一项艰巨的任务。 Spring Boot通过提供一组依赖项来解决此问题,以方便开发人员。

例如,如果要使用Spring和JPA进行数据库访问,则在项目中包含spring-boot-starter-data-jpa依赖项就足够了。

请注意,所有Spring Boot启动程序都遵循相同的命名模式spring-boot-starter-*,其中*表示它是应用程序的一种类型。

例子

请看下面的Spring Boot启动器,以便更好地理解 -
Spring Boot Starter Actuator依赖关系用于监视和管理应用程序。 其代码如下所示 -

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

Spring Boot Starter Security依赖项用于Spring Security。 其代码如下所示 -

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

Spring Boot Starter Web依赖项用于编写Rest端点。 其代码如下所示 -

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

Spring Boot Starter ThymeLeaf依赖项用于创建Web应用程序。 其代码如下所示 -

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

Spring Boot Starter Test依赖项用于编写测试用例。 其代码如下所示 -

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

springboot自己提供的starter:spring-boot-starter-*

第三方提供的:*-spring-boot-starter

5、自动配置

Spring Boot Auto Configuration会根据在项目中添加的JAR依赖项自动配置Spring应用程序。例如,如果MySQL数据库在类路径上,但尚未配置任何数据库连接,则Spring Boot会自动配置内存数据库。

为此,需要将@EnableAutoConfiguration批注或@SpringBootApplication批注添加到主类文件中。然后,将自动配置Spring Boot应用程序。

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@EnableAutoConfiguration
@ComponentScan("com.maxuan")
//@SpringBootApplication=EnableAutoConfiguration+ComponentScan
//@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello maxuan!";
    }
}

6、Spring Boot应用程序

Spring Boot Application的入口点是包含@SpringBootApplication注释的类。该类应具有运行Spring Boot应用程序的主要方法。 @SpringBootApplication注释包括自动配置,组件扫描和Spring Boot配置。

如果将@SpringBootApplication批注添加到类中,则无需添加@EnableAutoConfiguration@ComponentScan@SpringBootConfiguration批注。

因为@SpringBootApplication注释包括所有其他注释。

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
//@SpringBootApplication=EnableAutoConfiguration+ComponentScan
@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello maxuan!";
    }
}

7、组件扫描

Spring Boot应用程序在应用程序初始化时扫描所有bean和包声明。需要为类文件添加@ComponentScan批注,以扫描项目中添加的组件。

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
//@EnableAutoConfiguration
@ComponentScan("com.maxuan")
//@SpringBootApplication=EnableAutoConfiguration+ComponentScan
//@SpringBootApplication
@RestController
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello maxuan!";
    }
}

二、Spring Boot引导过程

1、Spring Initializer

引导Spring Boot应用程序的一种方法是使用Spring Initializer。 为此需要访问Spring Initializer 网页 https://start.spring.io/ 并选择 Build,Spring Boot版本和平台。 此外还需要提供组,工件和所需的依赖项来运行应用程序。

请注意以下屏幕截图,其中显示了添加spring-boot-starter-web依赖项以编写REST端点的示例

image-20201013083605689

提供组,工件,依赖关系,构建项目,平台和版本后,单击“Generate Project”按钮。 将下载zip文件并提取文件。

下载项目后,解压缩文件。pom.xml 文件的内容如下所示 -

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.maxuan</groupId>
    <artifactId>springboot-pro</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot-pro</name>
    <description>springboot-pro project for Spring Boot</description>

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

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

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

</project>

2、类路径依赖性

Spring Boot提供了许多Starters来在类路径中添加jar。 例如,要编写Rest Endpoint,需要在类路径中添加spring-boot-starter-web依赖项。请遵守下面显示的代码以便更好地理解 -

Maven依赖

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

3、Main方法

Main方法应该是编写Spring Boot Application类。 该类应使用@SpringBootApplication进行注释。这是启动Spring启动应用程序的入口点。以在src/java/main目录下找到主类文件。

在此示例中,主类文件位于src/java/main目录中,其默认包为com.maxuan

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {

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

4、编写一个Rest端点

要在Spring Boot Application主类文件本身中编写一个简单的Hello World Rest 端点,请按照以下步骤操作 -

  • 首先,在类的顶部添加@RestController注释。
  • 使用@RequestMapping注释编写Request URI方法。
  • Request URI方法应该返回Hello World字符串。
  • 现在,Spring Boot Application类文件将如下面的代码所示 -
package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App {

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

    @RequestMapping(value="/")
    public String hello(){
       return "hello world";
    }
}

5、创建一个可执行的JAR

创建一个可执行的JAR文件,工程名为springboot-pro,在命令提示符下使用Maven命令运行Spring Boot应用程序,如下所示 -

使用maven命令mvn clean install,如下所示 -

mvn clean install

执行命令后,开始编译,下载依赖包,如果编译成功,可以在命令提示符下看到 BUILD SUCCESS 的消息,如下所示 -

image-20201015112125314

6、用Java运行Hello World

创建可执行JAR文件后,可以在target目录中找到它。然后执行 java -jar springboot-pro-0.0.1-SNAPSHOT.jar,如下:

D:\360Downloads\soft\springboot-pro>cd target

D:\360Downloads\soft\springboot-pro\target>java -jar springboot-pro-0.0.1-SNAPSHOT.jar

出现了我们熟悉的springboot图标-

image-20201015115237832

打开浏览器,输入: http://localhost:8080/ 显示如下信息:

image-20201013084636179

成功打印hello world。

7、加餐课程(抽取run方法中tomcat容器的启动源码自定义启动容器)

具体讲解见视频教程,视频涉及源码讲解,尽量理解,若不理解可以跳过。

三、Spring Boot WAR包部署

通过使用Spring Boot应用程序,可以创建一个war文件以部署到Web服务器中。在本章中将学习如何创建WAR文件并在Tomcat Web服务器中部署Spring Boot应用程序。

1、Spring Boot Servlet初始化程序

传统的部署方式是使Spring Boot应用程序@SpringBootApplication类扩展SpringBootServletInitializer类。 SpringBootServletInitializer类文件允许在使用Servlet容器启动时配置应用程序。

下面给出了用于JAR文件部署的Spring Boot应用程序类文件的代码 -

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@SpringBootApplication
public class App {
    public static void main(String[] args) {
        SpringApplication.run(App.class,args);
    }
}

需要扩展类SpringBootServletInitializer以支持WAR文件部署。 Spring Boot应用程序类文件的代码如下 -

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;

@SpringBootApplication
public class App extends SpringBootServletInitializer {
   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(App.class);
   }
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
}

2、将打包JAR更新为WAR

使用以下代码将包装JAR更新为WAR。

对于Maven,在pom.xml 中将包装添加为WAR,如下所示 -

<packaging>war</packaging>

编写一个简单的Rest端点来返回字符串:"Hello World from Tomcat"。 要编写Rest端点,需要将Spring Boot Web starter依赖项添加到构建文件中。

对于Maven,使用如下所示的代码在pom.xml 中添加Spring Boot启动程序依赖项 -

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

现在,使用如下所示的代码在Spring Boot Application类文件中编写一个简单的Rest端点 -

package com.maxuan;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App extends SpringBootServletInitializer {
   @Override
   protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
      return application.sources(App.class);
   }
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }

   @RequestMapping(value = "/")
   public String hello() {
      return "Hello World from Tomcat";
   }
}

4、打包应用程序

现在,使用Maven命令创建一个WAR文件以部署到Tomcat服务器中,以打包应用程序,如下所示。

对于Maven,使用命令mvn package打包应用程序。 然后创建WAR文件,可以在目标目录中找到它,如下图:

image-20201017102351072

打完包之后在target目录中找到war,如下图:

image-20201017102408623

5、部署到Tomcat

现在,运行Tomcat服务器,并在webapps目录下部署WAR文件,如图:

image-20201017103354883

成功部署后,点击网页浏览器中的URL => http://localhost:9002/sbp/,观察输出结果如下图所示:

image-20201017102444258

四、Spring Boot构建系统

在Spring Boot中,选择构建系统是一项重要任务,建议使用MavenGradle,因为它们可以为依赖关系管理提供良好的支持, Spring不支持其他构建系统。

1、依赖管理

Spring Boot团队提供了一个依赖项列表,以支持每个版本的Spring Boot版本,无需在构建配置文件中提供依赖项版本,Spring Boot会根据发行版自动配置依赖项版本。注意,升级Spring Boot版本时,依赖项也会自动升级。

注 - 如果要指定依赖项的版本,可以在配置文件中指定它。 但是,Spring Boot团队强烈建议不要指定依赖项的版本。

2、Maven依赖

对于Maven配置,应该继承Spring Boot Starter父项目来管理Spring Boot Starters依赖项。 因此只需在pom.xml 文件中继承启动父级,如下所示。

<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-parent</artifactId>
   <version>2.3.4.RELEASE</version>
</parent>

应该指定Spring Boot父 Starter依赖项的版本号。 然后,对于其他启动器依赖项,不需要指定Spring Boot版本号

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

五、Spring Boot代码结构

Spring Boot没有任何代码布局可供使用,但是,有一些最佳实践可以帮助我们简化代码布局,本章中将详细讨论它们。

1、默认包

没有任何包声明的类被视为默认包, 请注意,通常不建议使用默认包声明, 使用默认包时,Spring Boot将导致自动配置或组件扫描出现故障等问题。

注 - Java推荐的包声明命名约定是反向域名。 例如 - com.maxuan.myproject

2、典型布局

Spring Boot应用程序的典型布局如下图所示 -

com
    +- maxuan
        +- myproject
            +- Application.java
            |
            +- entry
            |    +- Product.java
            +- dao
            |    +- ProductRepository.java
            +- controller
            |    +- ProductController.java
            +- service
            |    +- ProductService.java

code>Application.java文件应该声明main方法和@SpringBootApplication。 请遵守下面给出的代码以便更好地理解 -

package com.maxuan.myproject;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

六、Spring Boot Bean和依赖注入

在Spring Boot中,如何体现IOC?

  • 在xml中进行显式配置

    在xml配置文件中使用bean标签声明

  • 在java中进行显式配置

    @Configuration+@Bean

  • 隐式bean发现机制和自动装配

    隐式bean发现机制就是Spring支持扫描使用@Service、@Compent、@Repository、@Controller的类,并注册成Bean。

springboot中这三种方式都是可以的。

1、在xml中进行显式配置

在xml中配置是早期spring bean的注入方式,在spring3之前常用,在springboot中依然是支持这种方式的。

可以使用@Configuration+@ImportResource的方式完成注入,代码如下:

package com.maxuan;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Configuration
@ImportResource("classpath:bean.xml")
public class Config {

//    @Bean
//    public Person person(){
//        return new Person();
//    }
}

resources目录下创建bean.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:task="http://www.springframework.org/schema/task"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/task
        http://www.springframework.org/schema/task/spring-task.xsd
        ">
<!--    <context:component-scan base-package="com.maxuan" />-->
    <bean id="person" class="com.maxuan.Person">
        <property name="id" value="1"/>
        <property name="name" value="maxuan"/>
    </bean>
</beans>

2、在java中进行显式配置

可以使用Spring Framework来定义bean及其依赖注入。

@ComponentScan注释用于查找bean以及使用@Autowired注释注入的相应内容。

如果遵循Spring Boot典型布局,则无需为@ComponentScan注释指定任何参数, 所有组件类文件都自动注册到Spring Beans。

@Configuration+@Bean

以下示例提供了有关自动连接Rest Template对象并为其创建Bean代码片段 -

package com.maxuan;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Configuration
//@ImportResource("classpath:bean.xml")
public class Config {

    @Bean
    public Person person(){
        return new Person("2","maxuan1");
    }
}

以下代码显示UserController类文件中自动连接的Person对象和Bean创建对象的代码 -

package com.maxuan;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class UserController {

    @Autowired
    private PersonService personService;

    @Autowired
    private Person person;

    @GetMapping("/getUser")
    public String getUser() {
//        return "maxuan course";
        return personService.getUser();
    }

    @GetMapping("/getPerson")
    public String getPerson(){
        return person.getName();
    }
}

以上两种方式启动后在浏览器中输入:http://localhost:8080/getPerson 返回如下:

image-20201019101002560

3、隐式bean发现机制和自动装配

隐式bean发现机制就是Spring支持扫描使用@Service、@Compent、@Repository、@Controller的类,并注册成Bean,不需要在xml中或者java中加额外的配置。

比如controller调用service,如下代码:

package com.maxuan;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public interface PersonService {
    public String getPerson();
}
package com.maxuan;

import org.springframework.stereotype.Service;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Service
public class PersonServiceImpl implements PersonService{

    @Override
    public String getPerson() {
        return "my name is maxuan";
    }
}

创建PersonController调用service,代码如下:

package com.maxuan;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class PersonController {

    @Autowired
    private PersonService personService;

    @GetMapping("/getPerson")
    public String getPerson() {
        return personService.getUser();
    }   
}

在浏览器中输入:http://localhost:8080/getPerson,回车,显示结果如下

image-20201019101632290

七、Spring Boot运行器(Runner)

一般项目在部署启动后,需要执行一些诸如清除缓存等初始化的工作,虽然可以通过人工手动调用接口等方式完成,但是会容易遗漏,且不够优雅。这里推荐使用SpringBoot的Runner启动器,其会在服务启动后自动地执行相关初始化任务。

SpringBoot提供了两个Runner启动器——CommandLineRunner、ApplicationRunner接口:

1)、应用程序运行器(Runner)-ApplicationRunner

2)、命令行Runner接口-CommandLineRunner

二者实际上并无太大区别,只是前者是将启动参数封装到ApplicationArguments对象中,而后者是通过数组接收启动参数,实际开发过程中只需在接口的run方法中实现我们的初始化操作即可,当然不要忘了在启动器类上添加@Component注解(非入口类)

1、应用程序运行器

应用程序运行器(Runner)是一个用于在Spring Boot应用程序启动后执行代码的接口, 下面给出的示例显示了如何在主类文件上实现Application Runner接口。

package com.maxuan.springbootpro;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App implements ApplicationRunner {
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
   @Override
   public void run(ApplicationArguments arg0) throws Exception {
      System.out.println("Hello World from Application Runner");
   }
}

现在,如果从Application Runner观察Hello World下面的控制台窗口,则在Tomcat启动后执行println语句。以下屏幕截图所示:

图:

image-20201019152600153

2、命令行运行器

控制台窗口Runner是一个接口。 它用于在Spring Boot应用程序启动后执行代码。下面给出的示例显示了如何在主类文件上实现控制台窗口Runner接口。

package com.maxuan.springboot-pro;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App implements CommandLineRunner {
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
   @Override
   public void run(String... arg0) throws Exception {
      System.out.println("Hello world from Command Line Runner");
   }
}

查看下面的控制台窗口可以看到打印的字符串:"Hello world from Command Line Runner",它在Tomcat启动后执行println语句。

图:

image-20201019152711160

3、指定执行顺序

我们以项目启动后打印相关信息为例介绍其用法,这里我们有3个Runner启动器类,当有多个Runner启动器时,可通过@Order注解指定执行的优先级顺序,数值越小,优先级越高,越先被执行。

@Component
@Order(1)
public class SystemPrompt1 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("SystemPrompt1: 系统初始化完成");

        // 获取参数 方式1
        System.out.println("------- 获取参数 方式1 -------");
        for(String str : args) {
            System.out.println("str: " + str);
        }
    }
}
@Component
@Order(value = 2)
public class SystemPrompt2 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("SystemPrompt2: 系统初始化完成");
    }
}
@Component
@Order(value = 3)
public class SystemPrompt3 implements ApplicationRunner {
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("SystemPrompt3: 系统初始化完成");

        // 获取参数 方式1
        System.out.println("------- 获取参数 方式1 -------");
        for(String str : args.getSourceArgs()) {
            System.out.println("str: " + str);
        }

        // 获取参数 方式2
        System.out.println("------- 获取参数 方式2 -------");
        for(String str : args.getOptionNames()) {
            System.out.println("key: " + str + " value: " + args.getOptionValues(str));
        }
    }
}

在idea中配置程序入口参数:

image-20201019151434646

执行结果如下:

image-20201019153929229

在ApplicationRunner中,main参数即可通过getSourceArgs()来获取原始的字符串数组,也可以通过getOptionNames()、getOptionValues(String name)方法分别获取参数的名称集合、指定名称参数的值(值的类型为List

4、实战: 容器加载后自动监听

新建一个类,写一个监听器,监听项目加载完成后执行的操作,例如:监听项目启动后自动打开浏览器访问主页

package com.maxuan.springbootpro.component;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Component
public class MyCommandRunner implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        Runtime.getRuntime().exec("cmd /c start http://localhost:9002/getUser");
    }
}

启动springboot后自动打开默认浏览器执行:http://localhost:8080/getUser 如下图:

image-20201019151248920

八、Spring Boot应用程序属性

应用程序属性用于支持在不同的环境中工作。 在本章中,将学习如何配置和指定Spring Boot应用程序的属性。

1、命令行属性

Spring Boot应用程序将命令行属性转换为Spring Boot环境属性。命令行属性优先于其他属性源。 默认情况下,Spring Boot使用9002端口号来启动Tomcat。接下来将学习如何使用命令行属性更改端口号。

步骤1 - 创建可执行JAR文件后,使用命令java -jar <JARFILE>运行它。
步骤2 - 使用下面给出的屏幕截图中给出的命令,使用命令行属性更改Spring Boot应用程序的端口号。

image-20201019173824580

注 - 可以使用分隔符 - 提供多个应用程序属性。

2、properties文件

属性文件用于在单个文件中保留N个属性,以便在不同的环境中运行应用程序。 在Spring Boot中,属性保存在类路径下的application.properties文件中。
application.properties文件位于src/main/resources目录中,示例application.properties文件的代码如下 -

server.port = 9090
spring.application.name = springbootpro

请注意,在上面显示的代码中,Spring Boot应用程序springbootpro在端口9090上启动。

3、yml文件

Spring Boot支持基于yml的属性配置来运行应用程序,可以使用application.yml文件代替application.properties。 此YML文件也应保留在类路径中, application.yml文件示例如下 -

spring:
  application:
    name: springbootpro
server:
  port: 9091

注意:前面的属性会把后面的覆盖

4、yaml文件

springboot文件也支持.yaml的属性配置,application.yaml内容如下:

spring:
  application:
    name: springbootpro
server:
  port: 9092

5、外部化属性

可以将属性保存在不同的位置或路径中,而不是将属性文件保存在类路径下。 在运行JAR文件时,可以指定属性文件路径。 可以使用以下命令在运行JAR时指定属性文件的位置 -

--spring.config.location = d:\application.properties

image-20201020104530960

问题:以上属性的优先级顺序是什么样的?

优先级 peroperties>yml>yaml

6、使用@Value注解

@Value注释用于读取Java代码中的环境或应用程序属性值。读取属性值的语法如下所示 -

@Value("${property_key_name}")

请看下面的示例,它显示了如何使用@Value批注读取Java变量中的spring.application.name属性值的语法。

@Value("${spring.application.name}")

请遵守下面给出的代码以便更好地理解 -

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App {

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

   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
   @RequestMapping(value = "/")
   public String name() {
      return name;
   }
}

注 - 如果在运行应用程序时未找到该属性,则Spring Boot将抛出非法参数异常,因为无法在值${spring.application.name}中解析占位符'spring.application.name'
要解决占位符问题,可以使用下面给出的thr语法设置属性的默认值 -

@Value("${property_key_name:default_value}")
@Value("${spring.application.name:springbootpro}")

注意点:属性key值必须在配置文件中相对应,中间连接符合正常来说就是"."号和“:”号。

7、SpringBoot活动配置文件:application.properties

Spring Boot支持基于Spring活动配置文件的不同属性。 例如,可以保留两个单独的文件进行开发和生产,以运行Spring Boot应用程序。

application.properties中的Spring活动配置文件

下面来了解如何在application.properties 中使用Spring活动配置文件。 默认情况下,application.属性将用于运行Spring Boot应用程序。 如果想使用基于配置文件的属性,可以为每个配置文件保留单独的属性文件,如下所示 -

文件:application.properties -

server.port = 9002
spring.application.name = springbootpro

文件:application-dev.properties -

server.port = 9090
spring.application.name = springbootpro

文件:application-prod.properties -

server.port = 9999
spring.application.name = springbootpro

在运行JAR文件时,需要根据每个属性文件指定spring活动配置文件。

默认情况下,Spring Boot应用程序使用application.properties 文件,如要指定活动文件则使用参数 --spring.profiles.active=dev 。设置Spring活动文件的命令如下所示 -

image-20201020135909124

在控制台日志中看到活动的配置文件名称,如下所示 -

08:13:16.322  INFO 14028 --- [           
   main] com.maxuan.springbootpro.App  :
   The following profiles are active: dev

现在,Tomcat已经开始使用端口9090(http),如下所示 -

08:13:20.185  INFO 14028 --- [           
   main] s.b.c.e.t.TomcatEmbeddedServletContainer : 
   Tomcat started on port(s): 9090 (http)

可以设置生产活动配置文件,如下所示 -

image-20201020140109695

在控制台日志中看到活动的配置文件名称,如下所示 -

11:13:16.322  INFO 14028 --- [           
   main] com.maxuan.springbootpro.App  :
   The following profiles are active: prod

现在,Tomcat开始使用4431端口(http),如下所示 -

10:13:20.185  INFO 14028 --- [          
   main] s.b.c.e.t.TomcatEmbeddedServletContainer :
   Tomcat started on port(s): 9999 (http)

8、SpringBoot活动配置文件:application.yml

下面来了解如何为application.yml保留Spring活动配置文件。可以将Spring活动配置文件属性保留在单个application.yml文件中。无需使用像application.properties这样的单独文件。
以下是将Spring活动配置文件保留在application.yml文件中的示例代码。 请注意,分隔符(---)用于分隔application.yml文件中的每个配置文件。

spring:
   application:
      name: springbootpro
server:
   port: 9002

---
spring:
   profiles: dev
   application:
      name: springbootpro
server:
   port: 9090

---
spring: 
   profiles: prod
   application:
      name: springbootpro
server: 
   port: 9999

命令设置开发活动配置文件如下 -

image-20201020142615023

在控制台日志中看到活动的配置文件名称,如下所示 -

10:41:37.202  INFO 14104 --- [           
   main] com.maxuan.springbootpro.App  : 
   The following profiles are active: dev

现在,Tomcat开始使用端口9090(http),如下所示 -

09:41:46.650  INFO 14104 --- [           
   main] s.b.c.e.t.TomcatEmbeddedServletContainer : 
   Tomcat started on port(s): 9090 (http)

设置生产活动配置文件的命令如下 -

image-20201020142636175

在控制台日志中看到活动的配置文件名称,如下所示 -

12:43:10.743  INFO 13400 --- [    
   main] com.maxuan.springbootpro.App  : 
   The following profiles are active: prod

这将在端口4431(http)上启动Tomcat,如下所示:

12:43:14.473  INFO 13400 --- [     
   main] s.b.c.e.t.TomcatEmbeddedServletContainer : 
   Tomcat started on port(s): 9999 (http)

九、Spring Boot日志

日志,通常不会在需求阶段作为一个功能单独提出来,也不会在产品方案中看到它的细节。但是,这丝毫不影响它在任何一个系统中的重要的地位。

为了保证服务的高可用,发现问题一定要即使,解决问题一定要迅速,所以生产环境一旦出现问题,预警系统就会通过邮件、短信甚至电话的方式实施多维轰炸模式,确保相关负责人不错过每一个可能的bug。

预警系统判断疑似bug大部分源于日志。比如某个微服务接口由于各种原因导致频繁调用出错,此时调用端会捕获这样的异常并打印ERROR级别的日志,当该错误日志达到一定次数出现的时候,就会触发报警。

而市面上常见的日志框架有很多,比如:JCL、SLF4J、Jboss-logging、jUL、log4j、log4j2、logback等等,我们该如何选择呢?

通常情况下,日志是由一个抽象层+实现层的组合来搭建的。

日志-抽象层 日志-实现层
JCL(Jakarta Commons Logging)、SLF4J(Simple Logging Facade for Java)、jboss-logging jul、log4j、log4j2、logback

而SpringBoot机智的选择了SLF4J+Logback的组合,这种实现类似于JDBC + 数据库驱动(统一接口+实现类)。这个组合是当下比较合适的一组。

slf4j叫做日志门面,是一个统一的日志接口层,各种具体的日志实现都可以通过slf4j来实现,比如logback就是一个具体的日志门面的实现。

1、Spring Boot默认日志系统

Spring Boot默认使用LogBack日志系统,如果不需要更改为其他日志系统如Log4j2等,则无需多余的配置,LogBack默认将日志打印到控制台上。

如果要使用LogBack,原则上是需要添加dependency依赖的

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

但是因为新建的Spring Boot项目一般都会引用spring-boot-starter或者spring-boot-starter-web,而这两个起步依赖中都已经包含了对于spring-boot-starter-logging的依赖,所以,无需额外添加依赖。

2、日志格式

默认的Spring Boot Log格式显示在下面给出的屏幕截图中。

image-20201021104124584

它提供以下信息 -

  • 提供日志日期和时间的日期和时间。
  • 日志级别显示有:DEBUG,INFO,ERROR或WARN。
  • 进程ID。
  • ---是一个分隔符。
  • 线程名称括在方括号[]中。
  • 记录器名称,显示源类名称。
  • 日志消息。

3、文件日志输出

默认情况下,所有日志都将在控制台窗口中打印,而不是在文件中打印, 如果要在文件中打印日志,则需要在配置文件中设置属性logging.file.namelogging.file.path

##日志配置
logging:
  file:
    name: d:/mylog.log
    ##这个path是为默认spring.log准备的
    path: d:/

注意

  • name和path用法:
    • 如果只配置path,那么会在path下自动生成spring.log的文件用来记录日志
    • 如果只配置name,那么必须是文件的全路径名
    • 如果同时配置,则name生效
  • 文件将在达到10MB后自动旋转生成。

4、日志级别

Spring Boot支持所有记录器级别,例如:TRACEDEBUGINFOWARNERRORFATALOFF

日志级别总共有TRACE < DEBUG < INFO < WARN < ERROR < FATAL ,且级别是逐渐提高,如果日志级别设置为INFO,则意味TRACE和DEBUG级别的日志都看不到。

上例中我们打印了一个INFO级别的日志,因为Spring Boot默认级别就是INFO,如果我们改为WARN,是否还能看到这行日志信息?

在配置文件中定义Root logger,如下所示 -

logging:
   level:
     'root': info

注 - Logback不支持“FATAL”级别日志。 它映射到“ERROR”级别日志。

若yaml文件中提示没有root,可以在properties文件中配置。

特殊包级别?

5、配置Logback

Logback支持基于XML的配置来处理Spring Boot Log配置。日志配置详细信息在logback.xml文件中配置。logback.xml文件应放在classpath下。
可以使用下面给出的代码在Logback.xml文件中配置ROOT级别日志 -

<?xml version = "1.0" encoding = "UTF-8"?>
<configuration>
   <root level = "INFO">
   </root>
</configuration>

在下面给出的Logback.xml文件中配置控制台appender

<?xml version = "1.0" encoding = "UTF-8"?>
<configuration>
   <appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender"></appender>
   <root level = "INFO">
      <appender-ref ref = "STDOUT"/> 
   </root>
</configuration>

使用下面给出的代码在Logback.xml文件中配置文件appender。 请注意,需要在文件追加器中指定日志文件路径。

<?xml version = "1.0" encoding = "UTF-8"?>
<configuration>
   <appender name = "FILE" class = "ch.qos.logback.core.FileAppender">
      <File>/var/tmp/mylog.log</File>
   </appender>   
   <root level = "INFO">
      <appender-ref ref = "FILE"/>
   </root>
</configuration>

使用下面给出的代码在logback.xml文件中定义日志模式。还使用下面给出的代码在控制台或文件日志附加程序中定义支持的日志模式集 -

<pattern>[%d{yyyy-MM-dd'T'HH:mm:ss.sss'Z'}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
Shell

完整的logback.xml文件的代码如下所示。必须将其放在类路径中。

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration scan="true" scanPeriod="10 seconds">

    <!--<include resource="org/springframework/boot/logging/logback/base.xml" />-->

    <contextName>logback</contextName>
    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="D:/mylog/mymodule"/>

    <conversionRule conversionWord="ip" converterClass="com.maxuan.springbootpro.component.LogUtil" />
    <!-- 彩色日志 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%ip-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>info</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--输出到文件-->

    <!-- 时间滚动输出 level为 DEBUG 日志 -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_debug.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${log.path}/debug/log-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 INFO 日志 -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_info.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 WARN 日志 -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_warn.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 时间滚动输出 level为 ERROR 日志 -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文件的路径及文件名 -->
        <file>${log.path}/log_error.log</file>
        <!--日志文件输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文件只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
        以及指定<appender>。<logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              还有一个特俗值INHERITED或者同义词NULL,代表强制执行上级的级别。
              如果未设置此属性,那么当前logger将会继承上级的级别。
        addtivity:是否向上级logger传递打印信息。默认是true。
    -->
    <!--<logger name="org.springframework.web" level="info"/>-->
    <!--<logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>-->
    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
     -->

    <!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
        不能设置为INHERITED或者同义词NULL。默认是DEBUG
        可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->

    <!--开发环境:打印控制台-->
    <springProfile name="dev">
        <logger name="com.maxuan.springbootpro.controller" level="debug"/>
        <root level="info">
            <appender-ref ref="CONSOLE"/>
<!--            <appender-ref ref="DEBUG_FILE"/>-->
<!--            <appender-ref ref="INFO_FILE"/>-->
<!--            <appender-ref ref="WARN_FILE"/>-->
<!--            <appender-ref ref="ERROR_FILE"/>-->
        </root>
    </springProfile>

    <!--生产环境:输出到文件-->
    <springProfile name="prod">
        <root level="info">
<!--            <appender-ref ref="CONSOLE"/>-->
            <appender-ref ref="DEBUG_FILE"/>
            <appender-ref ref="INFO_FILE"/>
            <appender-ref ref="ERROR_FILE"/>
            <appender-ref ref="WARN_FILE"/>
        </root>
    </springProfile>

</configuration>

下面给出的代码显示了如何在UserController类文件中添加slf4j logger。

package com.maxuan.springbootpro.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class UserController {
    Logger logger = LoggerFactory.getLogger(UserController.class);

    @GetMapping("/getUser")
    public String getUser() {
        logger.info("==========getUser===========");
        logger.error("==========getUser===========");
        logger.warn("==========getUser===========");
        logger.debug("==========getUser===========");
        return "maxuan course";
    }
}

生产环境中的logback配置文件,在工程里讲解!

分布式环境下的logback文件注意点:

十、Spring Boot构建RESTful Web服务

1、什么是 RESTful API

Rest 是一种规范,符合 Rest 的 Api 就是 Rest Api。简单的说就是可联网设备利用 HTTP 协议通过 GET、POST、DELETE、PUT、PATCH 来操作具有URI标识的服务器资源,返回统一格式的资源信息,包括 JSON、XML、CSV、ProtoBuf、其他格式。

2、RESTful API 设计规范

o_restful1_1

传统风格与RESTful风格简单对比

操作 传统风格URL method ------------- RESTful风格URL method
增加 /addUser?name=xxx POST /user POST
删除 /deleteUser?id=1 GET /user/1 DELETE
修改 /updateUser?id=123&name=test POST /user/123 PUT
查询 /listUser GET /user GET
指定查询 /getUser?id=456 GET /user/456 GET

特点:

  1. URL描述资源

  2. 使用HTTP方法描述行为,使用HTTP状态码来表示不同的结果

  3. 使用json交互数据

  4. RESTful只是一种风格,并不是强制的标准

  5. 在RESTful架构中,每个网址代表一种资源(resource),所以网址中不能有动词,只能有名词,而且所用的名词往往与数据库的表格名对应。常见动词就是

    • GET(SELECT):从服务器取出资源(一项或多项)
    • POST(CREATE):在服务器新建一个资源
    • PUT(UPDATE):在服务器更新资源(客户端提供改变后的完整资源)
    • PATCH(UPDATE):在服务器更新资源(客户端提供改变的属性)
    • DELETE(DELETE):从服务器删除资源

常见的状态码:

状态码 表示的状态
200(OK) 现有资源已被更改
201(created) 新资源被创建
301(Moved Permanently) 资源的URI已被更新
303(See Other) 其他(如,负载均衡)
400 (bad request) 指代坏请求(如,参数错误)
404 (not found) 资源不存在
406 (not acceptable) 服务端不支持所需表示
409 (conflict) 通用冲突
412(Precondition Failed) 前置条件失败(如执行条件更新时的冲突)
415 (unsupported media type) 接受到的表示不受支持
500 (internal server error) 通用错误响应
503 (Service Unavailable) 服务端当前无法处理请求

完整的构建配置文件Maven build - pom.xml 的代码如下 -

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

    <groupId>com.maxuan</groupId>
    <artifactId>springboot-pro</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!--        <spring.version>5.2.9.RELEASE</spring.version>-->
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

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

在继续构建RESTful Web服务之前,建议了解以下注释 -

1、Rest控制器

@RestController注释用于定义RESTful Web服务,它提供JSON,XML和自定义响应,其语法如下所示 -

@RestController
public class UserController {
}

2、请求映射

@RequestMapping注解用于定义访问REST端点的Request URI,可以定义Request方法来使用和生成对象,默认请求方法是:GET

@RequestMapping(value = "/users")
public ResponseEntity<Object> getUsers() { 
}

3、请求主体

@RequestBody注解用于定义请求正文内容类型。

public ResponseEntity<Object> createUser(@RequestBody User user) {
}

4、路径变量

@PathVariable注解用于定义自定义或动态请求URI。 请求URI中的Path变量定义为花括号{},如下所示 -

public ResponseEntity<Object> updateUser(@PathVariable("id") String id) {
}

5、请求参数

@RequestParam注释用于从请求URL读取请求参数。默认情况下,它是必需参数。还可以为请求参数设置默认值,如下所示 -

public ResponseEntity<Object> getUser(
   @RequestParam(value = "name", required = false, defaultValue = "maxuan") String name) {
}

6、GET API

默认的HTTP请求方法是GET。此方法不需要任何请求主体。可以发送请求参数和路径变量来自定义或动态URL。
用于定义HTTP GET请求方法的示例代码如下所示。 在此示例中使用Person作为实体类。
这里,请求URI是/user,它将返回Person列表。下面给出了包含GET方法REST端点的控制器类文件。

package com.maxuan.springbootpro.controller;

import com.maxuan.springbootpro.entry.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/

@RestController
public class UserController {
   @RequestMapping(value="/user",method = RequestMethod.GET)
    public List<Person> query(@RequestParam(name = "id") String id,@RequestParam(name="name") String name){
        ArrayList<Person> list = new ArrayList<>();
        ArrayList<Person> ret =null;
        list.add(new Person("001","maxuan1"));
        list.add(new Person("002","maxuan2"));
        list.add(new Person("003","maxuan3"));
        for(Person person:list){
            if(person.getId()!=null&&person.getId().equals(id)){
                ret= new ArrayList<>();
                ret.add(person);
                return ret;
            }
        }
        return null;
    }
}

7、POST API

HTTP POST请求用于创建资源, 此方法包含请求正文,可以发送请求参数和路径变量来定义自定义或动态URL。
以下示例显示了用于定义HTTP POST请求方法的示例代码。
这里,请求URI是/user,它会一个用户后返回字符串。

package com.maxuan.springboot-pro.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.maxuan.springboot-pro.entry.Product;

@RestController
public class UserController {
   private static Map<String, Product> productRepo = new HashMap<>();

   @RequestMapping(value = "/products", method = RequestMethod.POST)
   public ResponseEntity<Object> createProduct(@RequestBody Product product) {
      productRepo.put(product.getId(), product);
      return new ResponseEntity<>("Product is created successfully", HttpStatus.CREATED);
   }
}

8、PUT API

HTTP PUT请求用于更新现有资源,此方法包含请求正文。可以发送请求参数和路径变量来自定义或动态URL。
下面给出的示例显示了如何定义HTTP PUT请求方法。 在此示例中使用HashMap更新现有产品,其中产品是POJO类。
这里的请求URI是/products/{id},它将产品存储到HashMap库后返回String。 请注意,使用路径变量{id}来定义需要更新的产品ID。

package com.maxuan.springboot-pro.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import com.maxuan.springboot-pro.entry.Product;

@RestController
public class UserController {
   private static Map<String, Product> productRepo = new HashMap<>();

   @RequestMapping(value = "/products/{id}", method = RequestMethod.PUT)
   public ResponseEntity<Object> updateProduct(@PathVariable("id") String id, @RequestBody Product product) { 
      productRepo.remove(id);
      product.setId(id);
      productRepo.put(id, product);
      return new ResponseEntity<>("Product is updated successsfully", HttpStatus.OK);
   }   
}

9、DELETE API

HTTP删除请求用于删除现有资源。此方法不包含任何请求正文。可以发送请求参数和路径变量来自定义或动态URL。
下面给出的示例显示了如何定义HTTP DELETE请求方法。 在此示例中,使用HashMap删除现有产品,即POJO类。
请求URI是/products/{id},它将在从HashMap存储库中删除产品后返回字符串。使用路径变量{id}来定义需要删除的产品ID。

package com.maxuan.springboot-pro.controller;

import java.util.HashMap;
import java.util.Map;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.maxuan.springboot-pro.entry.Product;

@RestController
public class UserController {
   private static Map<String, Product> productRepo = new HashMap<>();

   @RequestMapping(value = "/user/{id}", method = RequestMethod.DELETE)
   public ResponseEntity<Object> delete(@PathVariable("id") String id) { 
      productRepo.remove(id);
      return new ResponseEntity<>("Product is deleted successsfully", HttpStatus.OK);
   }
}

本节提供完整的源代码集。请遵守以下代码了解其各自的功能 -

Spring Boot主应用程序类 - App.java :

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
}
Java

entry类 - Person.java

package com.maxuan.springbootpro.entry;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class Person {
    private String id;
    private String name;

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

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public Person() {
    }
}

Rest Controller类 - UserController.java

package com.maxuan.springbootpro.controller;

import com.maxuan.springbootpro.entry.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class UserController {
    Logger logger = LoggerFactory.getLogger(UserController.class);

    @GetMapping("/getUser")
    public String getUser() {
        logger.debug("=========getUser=========");
        logger.info("=========getUser=========");
        logger.warn("=========getUser=========");
        logger.error("=========getUser=========");
        return "maxuan course";
    }

    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public List<Person> queryUser(@RequestParam(name = "id", required = false) String id,
                                  @RequestParam(name = "name", required = false) String name) {
        ArrayList<Person> list = new ArrayList<>();
        ArrayList<Person> ret = null;
        list.add(new Person("001", "maxuan1"));
        list.add(new Person("002", "maxuan2"));
        list.add(new Person("003", "maxuan3"));
        if (id == null && name == null) {
            return list;
        }
        for (Person person : list) {
            if (person.getId() != null && person.getId().equals(id)) {
                ret = new ArrayList<>();
                ret.add(person);
                return ret;
            }
        }
        return null;
    }

    @RequestMapping(value = "/user", method = RequestMethod.POST)
    public List<Person> createUser(@RequestBody Person person) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person("001", "maxuan1"));
        list.add(new Person("002", "maxuan2"));
        list.add(new Person("003", "maxuan3"));
        person.setId("004");
        list.add(person);
        for (Person person1 : list) {
            System.out.println(person1.toString());
        }
        return list;
    }

    @PutMapping("/user/{id}")
    public Person updateUser(@PathVariable("id") String id) {
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person("001", "maxuan1"));
        list.add(new Person("002", "maxuan2"));
        list.add(new Person("003", "maxuan3"));
        for (Person person : list) {
            if (person.getId() != null && person.getId().equals(id)) {
                person.setName("maxuannew");
                return person;
            }
        }
        return null;
    }

    @DeleteMapping("/user/{id}")
    public List<Person> deleteUser(@PathVariable("id") String id){
        ArrayList<Person> list = new ArrayList<>();
        list.add(new Person("001", "maxuan1"));
        list.add(new Person("002", "maxuan2"));
        list.add(new Person("003", "maxuan3"));
        for (Person person : list) {
            if (person.getId() != null && person.getId().equals(id)) {
                if(list.remove(person)){
                    return list;
                }
            }
        }
        return null;
    }
}

现在点击POSTMAN应用程序中显示的URL,查看输出。

GET API URL为:http://localhost:9001/user

图:

image-20201022155131356

GET API URL为:http://localhost:9001/user

图:

image-20201022155222755

PUT API URL为:http://localhost:9001/user/003

图:

image-20201022155247695

DELETE API URL为:http://localhost:9001/user/003

图:

image-20201022155316233

十一、Spring Boot异常处理

处理API中的异常和错误并向客户端发送适当的响应对企业应用程序有利。在本章中,将学习如何在Spring Boot中处理异常。

针对代码中的异常,常规有两种处理方式:一种throws直接抛出,另一种try..catch捕获

在java项目中,有可能存在人为逻辑的异常,也可能为取得异常的详情,或是保证程序在异常时继续向下执行,会采用第二种处理方式。但是,代码中每一处异常都来捕获,会使代码什么冗余且不利于维护,所以我们采用全局处理方式来做。

具体思路:

  • 定义一个全局异常处理类,返回统一规范的异常信息
  • 处理逻辑是,先判定是否会出现异常,再执行后续具体的业务

在继续进行异常处理之前,了解以下注解。

1、控制器Advice

@ControllerAdvice是一个注解,用于全局处理异常。

2、异常处理程序

@ExceptionHandler是一个注解,用于处理特定异常并将自定义响应发送到客户端。使用以下代码创建@ControllerAdvice类来全局处理异常 -

package com.maxuan.springbootpro.exception;

import org.springframework.web.bind.annotation.ControllerAdvice;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@ControllerAdvice
public class BizExceptionHandler {
}

定义一个扩展RuntimeException类的子类。

package com.maxuan.springbootpro.exception;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class BizException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    /**
     * 错误码
     */
    protected String errorCode;
    /**
     * 错误信息
     */
    protected String errorMsg;
}

可以定义@ExceptionHandler方法来处理异常,如图所示。 此方法应用于编写Controller Advice类文件。

@ExceptionHandler(value = BizException.class)
@ResponseBody
public ResponseResult bizExceptionHandler(HttpServletRequest req, BizException e){
    logger.error("发生业务异常!原因是:{}",e.getErrorMsg());
    return ResponseResult.error(e.getErrorCode(),e.getErrorMsg());
}

现在,使用下面给出的代码从service中抛出异常。

@Override
public int addPerson(Person person) {
    //Person personFromDB=mapper.selectFromDB(person.getId);
    for(Person personFromDB:list){
        if(personFromDB.getId().equals(person.getId())){
            // 已有,抛出异常,异常信息为已有该员工
            throw new BizException("-1","该员工已经存在");
        }
    }

    return list.add(person)?1:0;
    // return mapper.insertDB();
}

controller层正常处理

@RequestMapping("/addPerson")
public ResponseResult addPerson(@RequestBody Person person) {
    return personService.addPerson(person) == 1 ? ResponseResult.success() : ResponseResult.error("-1", "新增员工操作失败!");
}

下面给出了用于全局处理异常的ControllerAdvice类。在这个类文件中定义任何异常处理方法。

package com.maxuan.springbootpro.exception;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@ControllerAdvice
public class BizExceptionHandler {

    private static final Logger logger = LoggerFactory.getLogger(BizExceptionHandler.class);

    /**
     * 处理自定义的业务异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value = BizException.class)
    @ResponseBody
    public ResponseResult bizExceptionHandler(HttpServletRequest req, BizException e){
        logger.error("发生业务异常!原因是:{}",e.getErrorMsg());
        return ResponseResult.error(e.getErrorCode(),e.getErrorMsg());
    }

    /**
     * 处理空指针的异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =NullPointerException.class)
    @ResponseBody
    public ResponseResult exceptionHandler(HttpServletRequest req, NullPointerException e){
        logger.error("发生空指针异常!原因是:",e);
        return ResponseResult.error(CommonEnum.BODY_NOT_MATCH);
    }

    /**
     * 处理其他异常
     * @param req
     * @param e
     * @return
     */
    @ExceptionHandler(value =Exception.class)
    @ResponseBody
    public ResponseResult exceptionHandler(HttpServletRequest req, Exception e){
        logger.error("未知异常!原因是:",e);
        return ResponseResult.error(CommonEnum.INTERNAL_SERVER_ERROR);
    }

}

下面给出了Controller层的新增一个用户的代码, 如果用户已经存在,则抛出BizException。

@RestController
public class UserController {
    Logger logger = LoggerFactory.getLogger(UserController.class);
    @RequestMapping("/addPerson")
    public ResponseResult addPerson(@RequestBody Person person) {
        return personService.addPerson(person) == 1 ? ResponseResult.success() : ResponseResult.error("-1", "新增员工操作失败!");
    }
}

Spring Boot应用程序主类文件的代码如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

产品POJO类的代码如下 -

package com.maxuan.springbootpro.entry;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class Person {
    private static final long serialVersionUID = 1L;

    private String id;
    private String name;

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

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public Person() {
    }
}

Maven build的代码 - 文件:pom.xml 如下所示 -

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

   <modelVersion>4.0.0</modelVersion>
   <groupId>com.maxuan</groupId>
   <artifactId>springboot-pro</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <packaging>jar</packaging>
   <name>springboot-pro</name>
   <description>springboot-pro project for Spring Boot</description>

   <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-parent</artifactId>
      <version>1.5.8.RELEASE</version>
      <relativePath/> 
   </parent>

   <properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
      <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
      <java.version>1.8</java.version>
   </properties>

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

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

</project>

启动后用postman进行验证如下:

新增用户:maxuan6,成功!

image-20201023121400580

新增用户:maxuan1,失败!

image-20201023121529353

十二、Spring Boot拦截器

在Spring Boot中使用拦截器,可在以下情况下执行操作 -

  • 在将请求发送到控制器之前 在controller之前拦截
  • 在将响应发送给客户端之前 在controller return之前
  • 在全部请求和响应完成之后 在前端请求完全完毕之后

例如:使用拦截器在将请求发送到控制器之前添加请求标头,并在将响应发送到客户端之前添加响应标头。

img

使用场景:

  1. 日志记录 :几率请求信息的日志,以便进行信息监控、信息统计等等

  2. 权限检查 :对用户的访问权限,认证,或授权等进行检查

  3. 性能监控 :通过拦截器在进入处理器前后分别记录开始时间和结束时间,从而得到请求的处理时间

  4. 通用行为 :读取 cookie 得到用户信息并将用户对象放入请求头中,从而方便后续流程使用

要使用拦截器,需要创建支持它的@Component类,它应该实现HandlerInterceptor接口。
以下是在拦截器上工作时应该了解的三种方法 -

  • preHandle()方法 - 用于在将请求发送到控制器之前执行操作。此方法应返回true,以将响应返回给客户端。
  • postHandle()方法 - 用于在将响应发送到客户端之前执行操作。
  • afterCompletion()方法 - 用于在完成请求和响应后执行操作。

请注意以下代码以便更好地理解 -

@Component
public class UserAccessInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(
      HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

      return true;
   }
   @Override
   public void postHandle(
      HttpServletRequest request, HttpServletResponse response, Object handler, 
      ModelAndView modelAndView) throws Exception {}

   @Override
   public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
      Object handler, Exception exception) throws Exception {}
}

必须使用WebMvcConfigurerInterceptorRegistry注册此Interceptor,如下所示 -

package com.maxuan.springbootpro.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InteceptorConfig implements WebMvcConfigurer {

   @Autowired
   private UserAccessInterceptor userAccessInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(userAccessInterceptor)
              .addPathPatterns("/addPerson")
              .excludePathPatterns("/user/login");
   }
}

在下面给出完整的示例
InterceptorUserAccessInterceptor.java的代码如下 -

package com.maxuan.springbootpro.component;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Component
public class UserAccessInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle
            (HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {

        System.out.println("Pre Handle method is Calling");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response,
                           Object handler, ModelAndView modelAndView) throws Exception {

        System.out.println("Post Handle method is Calling");
    }

    @Override
    public void afterCompletion
            (HttpServletRequest request, HttpServletResponse response, Object
                    handler, Exception exception) throws Exception {

        System.out.println("Request and Response is completed");
    }
}

应用程序配置类文件的代码将拦截器注册到拦截器注册表,InteceptorConfig.java如下 -

package com.maxuan.springbootpro.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InteceptorConfig implements WebMvcConfigurer {

   @Autowired
   private UserAccessInterceptor userAccessInterceptor;

   @Override
   public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(userAccessInterceptor)
              .addPathPatterns("/addPerson")
              .excludePathPatterns("/user/login");
   }
}

控制器类文件UserController.java的代码如下 -

package com.maxuan.springbootpro.controller;

import com.maxuan.springbootpro.entry.Person;
import com.maxuan.springbootpro.exception.ResponseResult;
import com.maxuan.springbootpro.service.PersonService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;

@RestController
public class UserController {
    @RequestMapping("/addPerson")
    public ResponseResult addUser(@RequestBody Person person) {
        int ret = personService.addPerson(person);
        System.out.println("add user method is calling");
        return ret == 1 ? ResponseResult.success() : ResponseResult.error("新增用户失败!");
    }
}

POJO类Person.java 代码如下 -

package com.maxuan.springbootpro.entry;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class Person {
    private static final long serialVersionUID = 1L;

    private String id;
    private String name;

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

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public Person() {
    }
}

Spring Boot应用程序类主要的文件App.java 的代码如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

现在,应用程序已在Tomcat端口9001上启动,如下所示 -

image-20201026100116526

打开 POSTMAN 应用程序中的URL,并输入:http://localhost:9001/addUser,如下所示 -
image-20201026100150739

在控制台窗口中,看到在拦截器中添加的System.out.println语句,如下所示 -

image-20201026100215770

十三、Spring Boot Servlet过滤器

过滤器是用于拦截应用程序的HTTP请求和响应的对象,拦截的是servlet。

img

使用场景:

为shiro权限过滤器,编码过滤器,微信接口过滤器,上传文件过滤器等。

通过使用过滤器,可以在两个实例上执行两个操作 -

  • 在将请求发送到控制器之前

  • 在向客户发送响应之前。

    以下代码显示了带有@Component注解的Servlet过滤器实现类的示例代码。

package com.maxuan.springbootpro.component;

import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/

@Component
public class SimpleFilter implements Filter {
    @Override
    public void destroy() {
        System.out.println("Filter destroy");
    }

    @Override
    public void doFilter
            (ServletRequest request, ServletResponse response, FilterChain filterchain)
            throws IOException, ServletException {
        //Filter 先拦截request, doFilter放行, response响应回来,
        // Filter再次拦截 ,执行doFilter()后面的代码
        //arg0.setAttribute("key0", "from Filter doFilter");
        //doFilter之前的代码,用于对请求进行处理
        System.out.println("Filter doFilter start");
        System.out.println("Remote Host:"+request.getRemoteHost());
        System.out.println("Remote Address:"+request.getRemoteAddr());
        filterchain.doFilter(request, response);
        //doFilter之后的代码,用于对请求响应进行处理
        System.out.println("Filter doFilter end");
    }

    @Override
    public void init(FilterConfig filterconfig) throws ServletException {
        //Filter初始化
        // web应用程序启动时,web服务器将创建Filter的实例对象,
        // 并调用其init方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,
        // filter对象只会创建一次,init方法也只会执行一次。
        // 通过init方法的参数,可获得代表当前filter配置信息的FilterConfig对象。
        System.out.println("Filter init");
    }
}

和拦截器的比较:

20180716150604964_1

以下示例显示了在将请求发送到控制器之前从ServletRequest对象读取远程主机和远程地址的代码。

doFilter()方法中,添加了System.out.println()语句来打印远程主机和远程地址。

package com.maxuan.springbootpro.component;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

import org.springframework.stereotype.Component;

@Component
public class SimpleFilter implements Filter {
   @Override
   public void destroy() {}

   @Override
   public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterchain) 
      throws IOException, ServletException {

      System.out.println("Remote Host:"+request.getRemoteHost());
      System.out.println("Remote Address:"+request.getRemoteAddr());
      filterchain.doFilter(request, response);
   }

   @Override
   public void init(FilterConfig filterconfig) throws ServletException {}
}

新建一个servlet测试过滤器有没有生效

package com.maxuan.springbootpro.component;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@WebServlet
public class MyServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----------doGet----------------");
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("-----------doPost----------------");
        resp.getWriter().print("<h1>Hello MyServlet Response return you this</h1>");
//        super.doPost(req, resp);
    }
}

在Spring Boot主应用程序类文件中,添加了返回MyServlet

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
public class App {
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }

   @Bean
   public ServletRegistrationBean myServlet(){
       return new ServletRegistrationBean(new MyServlet(),"/servlet/*");
   }
}

实际案例:过滤非法字符(见视频讲解)

在Tomcat端口9001上看到应用程序已启动。访问:http://localhost:9001/servlet/sdfsdf

image-20201026202113898

十四、Spring Boot Rest模板

jdbctemplate,redistemplate

RestTemple是Spring提供的用于访问Http请求的客户端,RestTemple提供了多种简洁的远程访问服务的方法,省去了很多无用的代码。

相信大家之前都用过apache的HTTPClient类,逻辑繁琐,代码复杂,还要自己编写使用类HttpClientUtil,封装对应的post,get,delete等方法。

RestTemplate的行为可以通过callback回调方法和配置HttpMessageConverter 来定制,用来把对象封装到HTTP请求体,将响应信息放到一个对象中。

RestTemplate提供更高等级的符合HTTP的六种主要方法,可以很简单的调用RESTful服务。

1、创建RestTemplate对象

Rest模板用于创建使用RESTful Web服务的应用程序。使用exchange()方法为所有HTTP方法使用Web服务。 下面给出的代码显示了如何创建Rest模板Bean以自动连接Rest模板对象。

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

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

   @Bean
   public RestTemplate getRestTemplate() {
      return new RestTemplate();
   }
}

当然,也可以通过xml文件配置的方式注入。
默认情况下RestTemplate自动帮我们注册了一组HttpMessageConverter用来处理一些不同的contentType的请求。如果现有的转换器不能满足你的需求,你还可以实现org.springframework.http.converter.HttpMessageConverter接口自己写一个。详情参考官方api
其他相关的配置,也可以在官方文档中查看。当然,对于一个请求来说,超期时间,请求连接时间等都是必不可少的参数,为了更好的适应业务需求,所以可以自己修改restTemplate的配置。

@Bean
    public RestTemplate restTemplate() {
        //生成一个设置了连接超时时间、请求超时时间、异常重试次数3次
        RequestConfig config = RequestConfig.custom().setConnectionRequestTimeout(10000).setConnectTimeout(10000).setSocketTimeout(30000).build();
        HttpClientBuilder builder = HttpClientBuilder.create().setDefaultRequestConfig(config).setRetryHandler(new DefaultHttpRequestRetryHandler(3, false));
        HttpClient httpClient = builder.build();
        ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
        RestTemplate restTemplate = new RestTemplate(requestFactory);
        //做个日志拦截器;从性能上考虑,当前屏蔽该功能
        //restTemplate.setInterceptors(Collections.singletonList(new RestTemplateConsumerLogger()));
        return restTemplate;
    }

2、GET

通过使用RestTemplatepostForObject()或者exchange()方法来使用GET API

execute

1)使用getForObject()方法(演示2种方法调用)

调用代码段:

List forObject = restTemplate.getForObject("http://localhost:9001/user?name={1}", List.class, name);

参数:

//postman里调用
http://localhost:9001/user1?name=maxuan1

2)使用exchange()方法(演示2种方法)

调用代码段:

 HttpHeaders headers = new HttpHeaders();
 headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
 HttpEntity entity = new HttpEntity<>(headers);
 List forObject =restTemplate.exchange("http://localhost:9001/user?name={1}",HttpMethod.GET, entity, List.class,name).getBody();
@RequestMapping(value = "/user1")
    public List<Person> queryUser1(@RequestParam(name = "name", required = false) String name) {
        System.out.println("name======" + name);
        //这里要特别小心,不可以写成<Object,Object>
        HashMap<String, Object> map = new HashMap<>();
        map.put("name", name);
//        List forObject = restTemplate.getForObject("http://localhost:9001/user?name={name}", List.class, map);
        //第二种方式
//      List forObject = restTemplate.getForObject("http://localhost:9001/user?name={1}", List.class, name);
//        System.out.println("list=====" + forObject);
        //第三种方式
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        HttpEntity entity = new HttpEntity<>(headers);
//        List forObject =
//                restTemplate.exchange("http://localhost:9001/user?name={1}", HttpMethod.GET, entity, List.class,
//                        name).getBody();

       //第4种方式
        List forObject =
                restTemplate.exchange("http://localhost:9001/user?name={name}", HttpMethod.GET, entity, List.class,
                        map).getBody();
        return forObject;
    }

2、POST

通过使用RestTemplatepostForObject()或者exchange()方法来使用POST API

1)使用postForObject()方法

调用代码段:

ResponseResult responseResult = restTemplate.postForObject("http://localhost:9001/addPerson",
                person, ResponseResult.class);

参数:

{
    "id":7,
    "name":"maxuan7"
}

响应内容 -

image-20201027143751992

2)使用exchange()方法

调用代码段:

//更通用的execute和exchange方法
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        HttpEntity entity = new HttpEntity<>(person,headers);
        responseResult = restTemplate.exchange("http://localhost:9001/addPerson", HttpMethod.POST, entity, ResponseResult.class).getBody();

参数:

{
    "id":7,
    "name":"maxuan7"
}

响应内容 -

image-20201027143751992

@RestController
public class UserController {
   @Autowired
   RestTemplate restTemplate;

  @RequestMapping("/addPerson1")
    public ResponseResult addUser1(@RequestBody Person person) {
        //post 方法
        ResponseResult responseResult = restTemplate.postForObject("http://localhost:9001/addPerson",
                person, ResponseResult.class);

        //更通用的execute和exchange方法
        HttpHeaders headers = new HttpHeaders();
        headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
        HttpEntity entity = new HttpEntity<>(person,headers);
        responseResult = restTemplate.exchange("http://localhost:9001/addPerson", HttpMethod.POST, entity, ResponseResult.class).getBody();
        System.out.println("responseResult======" + responseResult);
        System.out.println("code======" + responseResult.getCode());
        System.out.println("message======" + responseResult.getMessage());
        return responseResult;
    }
}

3、PUT

put操作跟post操作没啥区别,最主要的特点是put操作没有返回值

@RestController
public class UserController {
   @Autowired
   RestTemplate restTemplate;

  @PutMapping(value = "/user1/{id}")
    public List updateUser1(@PathVariable("id") String id) {
        //第一种
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        Person person=new Person(id,"maxuanupdate");
        HttpEntity entity = new HttpEntity(person, headers);
        System.out.println("id========="+id);
        restTemplate.put("http://localhost:9001/user/{1}", entity, id);
        //第二种
        List list = restTemplate.exchange("http://localhost:9001/user/{1}", HttpMethod.PUT, entity, List.class, id).getBody();
        return list;
    }
}

4、DELETE

delete操作和put一样,没有返回值

@RestController
public class UserController {
   @Autowired
   RestTemplate restTemplate;

   @DeleteMapping("/user1/{id}")
    public List deleteUser1(@PathVariable("id") String id) {
        //第一种
//        System.out.println("id========="+id);
//        restTemplate.delete("http://localhost:9001/user/{1}",id);
        //第二种
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity entity = new HttpEntity(headers);
        List list = restTemplate.exchange("http://localhost:9001/user/{1}", HttpMethod.DELETE, entity, List.class, id).getBody();
        return list;
    }
}

现在,应用程序已在Tomcat端口9001上启动。

启动POSTMAN 演示,可以看到输出,具体演示结果见视频。

十五、Spring Boot文件处理

在本章中,将学习如何使用Web服务上载和下载文件。

1、上传文件

对于上载文件,要将MultipartFile用作请求参数,此API应使用多部分表单数据值。 观察下面给出的代码 -

@RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)

public String fileUpload(@RequestParam("file") MultipartFile file) {
   return null;
}

下面给出了相同的完整代码 -

package com.maxuan.springbootpro.controller;

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class FileController {
    /**
     * 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/upload", method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUpload(@RequestParam("file") MultipartFile file) throws IOException {
        File convertFile = new File("d:/" + file.getOriginalFilename());
        convertFile.createNewFile();
        FileOutputStream fout = new FileOutputStream(convertFile);
        fout.write(file.getBytes());
        fout.close();
        return "File is upload successfully";
    }

2、文件下载

对于文件下载,应该使用InputStreamResource下载文件,需要在Response中设置HttpHeader Content-Disposition,并且需要指定应用程序的响应Media Type

下面给出了相同的完整代码 -

package com.maxuan.springbootpro.controller;

import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class FileController {
   /**
     * 文件下载
     * @return
     * @throws IOException
     */
    @RequestMapping(value = "/download", method = RequestMethod.GET)
    public ResponseEntity<Object> downloadFile(HttpServletResponse response) throws IOException  {
        String filename = "d:/goldeneye.py";
        File file = new File(filename);
        FileSystemResource resource = new FileSystemResource(file);
        HttpHeaders headers = new HttpHeaders();

        headers.add("Content-Disposition", "attachment; filename="+file.getName());
        headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
        headers.add("Pragma", "no-cache");
        headers.add("Expires", "0");
        return ResponseEntity
                .ok()
                .headers(headers)
                .contentLength(resource.contentLength())
                .contentType(MediaType.parseMediaType("application/octet-stream"))
                .body(new InputStreamResource(resource.getInputStream()));
    }
}

主要的Spring Boot应用程序如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

这将在Tomcat端口9002上启动应用程序。
现在点击 POSTMAN 应用程序中的以下URL,可以看到如下所示的输出 -
文件上传 - http://localhost:9002/upload

文件下载 - http://localhost:9002/download

十六、Spring Boot Thymeleaf示例

1、概述

Thymeleaf是一个基于Java的库,用于创建Web应用程序, 它为在Web应用程序中提供XHTML/HTML5提供了很好的支持, 在本章中将详细了解和学习Thymeleaf。

Thymeleaf 是一个跟 Velocity、FreeMarker 类似的模板引擎,它可以完全替代 JSP ,相较与其他的模板引擎,它有如下三个吸引人的特点:

  • Thymeleaf 在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。这是由于它支持 html 原型,然后在 html 标签里增加额外
    的属性来达到模板 + 数据的展示方式。浏览器解释 html 时会忽略未定义的标签属性,所以 thymeleaf 的模板可以静态地运行;当有数据返回到页面时,Thymeleaf 标签会动态地替换掉静态内容,使页面动态显示。
  • Thymeleaf 开箱即用的特性,它提供标准和 Spring 标准两种方言,可以直接套用模板实现 JSTL、 OGNL 表达式效果,避免每天套模板、改 JSTL、改标签的困扰,同时开发人员也可以扩展和创建自定义的方言。
  • Thymeleaf 提供 Spring 标准方言和一个与 SpringMVC 完美集成的可选模块,可以快速的实现表单绑定、属性编辑器、国际化等功能。

2、引入thymeleaf依赖

pom文件中加入以下依赖

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

3、创建thymeleaf模板

在templates下新建index.html,内容如下:

<!doctype html>

<!--注意:引入thymeleaf的名称空间-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
   <p th:text="码炫课堂牛逼">码炫课堂</p>
</body>
</html>

4、启动项目

访问http://localhost:9002 ,展示效果如下:

img

5、通过控制器定位thymeleaf模板

package com.maxuan.springbootpro;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

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

访问http://localhost:9002/home,也能显示index.html的内容。

6、thymeleaf的相关全局配置

SpringBoot支持.properties和.yml两种格式的全局配置,下面给出thymeleaf的yml格式全局配置:

spring:
  thymeleaf:
    enabled: true  #开启thymeleaf视图解析
    encoding: utf-8  #编码
    prefix: classpath:/templates/  #前缀
    cache: false  #是否使用缓存
    mode: HTML  #严格的HTML语法模式
    suffix: .html  #后缀名

properties格式最简配置

spring.thymeleaf.cache=false
spring.thymeleaf.mode = LEGACYHTML5

7、thymeleaf常用标签的使用

用到的User实体如下:

package com.maxuan.springbootpro;

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

public class User {
    String username;
    String password;
    List<String> hobbies;
    Map<String, String> secrets;
    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;
    }

    public List<String> getHobbies() {
        return hobbies;
    }

    public void setHobbies(List<String> hobbies) {
        this.hobbies = hobbies;
    }

    public Map<String, String> getSecrets() {
        return secrets;
    }

    public void setSecrets(Map<String, String> secrets) {
        this.secrets = secrets;
    }
}

具体的属性值为:

package com.maxuan.springbootpro;

import com.hncj.spring.boot.thymeleaf.domain.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

@Controller
public class IndexController {

    @GetMapping("home")
    public String index(Model model) {
        User user = new User();
        user.setUsername("jack");
        user.setPassword("112233");
        user.setHobbies(Arrays.asList(new String[]{"singing", "dancing", "football"}));
        Map<String, String> maps = new HashMap<>();
        maps.put("1", "o");
        maps.put("2", "g");
        maps.put("3", "a");
        maps.put("4", "j");
        user.setSecrets(maps);
        model.addAttribute("user", user);
        return "index";
    }
}

测试界面如下:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <!--字符串输出-->
    <p th:text="'hello SpringBoot'">hello thymeleaf</p>
    <!--数学运算-->
    <p th:text="9 + 10"></p>
    <p th:text="10 * 10"></p>
    <p th:text="1 - 10"></p>
    <p th:text="8 / 3"></p>
    <p th:text="3 % 2"></p>
    <!--操作域对象-->
    <p th:text="${user}"></p>
    <p th:text="${user.username}"></p>

    <!--遍历数组-->
    <table th:each="item, sta:${user.hobbies}">
        <tr>
            <td th:text="${item}"></td>
            <td th:text="${sta.index}"></td>
            <td th:text="${sta.odd}"></td>
            <td th:text="${sta.size}"></td>
        </tr>
    </table>

    <!--遍历map-->
    <div th:each="item:${user.secrets}" th:text="${item.key}"></div>

    <!--遍历数组-->
    <div th:each="item:${user.hobbies}" th:text="${item}"></div>

    <!--设置属性-->
    <input type="text" th:attr="value=${user.username}">
    <input type="text" th:attr="value=${user.username}, title=${user.username}">

    <!--表单数据绑定-->
    <form action="" th:object="${user}">
        <input type="text" th:value="*{username}">
        <input type="password" th:value="*{password}">
        <select>
            <option th:each="item:${user.secrets}" th:text="${item.value}" th:selected="'a' eq ${item.value}"></option>
        </select>
    </form>

    <!--解析html文本内容-->
    <p th:utext="'<span>html</span>'"></p>

    <!--流程控制-->
    <p th:if="${user.username} != ${user.password}">yes</p>
    <div th:switch="${user.username}">
        <p th:case="rose">name is rose</p>
        <p th:case="jack">name is jack</p>
    </div>

    <!--外部引入-->
    <link rel="stylesheet" th:href="@{/css/index.css}">
    <script th:src="@{/js/index.js}"></script>

    <a th:href="@{/home}">home</a>
</body>

</html>

页面显示如下:

image-20201028214502053

十七、Spring Boot使用RESTful Web服务

本章将详细讨论和学习如何使用jQuery AJAX来调用RESTful Web服务。

创建一个简单的Spring Boot Web应用程序并编写一个控制器类文件,用于重定向到HTML文件以使用RESTful Web服务。需要在构建配置文件中添加Spring Boot启动程序Thymeleaf和Web依赖项。

对于Maven用户,请在pom.xml 文件中添加以下依赖项。

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

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

@Controller类文件的代码如下 -

@Controller
public class ProductController {
}

定义请求URI方法以重定向到HTML文件,如下所示 -

@RequestMapping("/view-products")
public String viewProducts() {
   return "view-products";
}

@RequestMapping("/add-products")
public String addProducts() {
   return "add-products";
}

此API http:// localhost:9090 / products响应返回以下JSON,如下所示 -

[
   {
      "id": "1",
      "name": "衣服"
   },
   {
      "id": "2",
      "name": "鞋子"
   },
   {
      "id": "3",
      "name": "帽子"
   }
]

现在,在类路径的templates目录下创建一个view-products.html 文件。在HTML文件中,添加jQuery库并编写了代码以在页面加载时使用RESTful Web服务。

<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

<script>
$(document).ready(function(){
   $.getJSON("http://localhost:9002/products", function(result){
      $.each(result, function(key,value) {
         $("#productsJson").append(value.id+" "+value.name+" ");
      }); 
   });
});
</script>

POST方法和此URL => http:// localhost:9090 / products应包含以下请求正文和响应正文。

请求正文的代码如下 -

{
   "id":"4",
   "name":"车子"
}

响应正文的代码如下 -

新增成功!

现在,在类路径的templates 目录下创建add-products.html 文件。在HTML文件中,添加jQuery库,并在单击按钮时编写了将表单提交到RESTful Web服务的代码。

<html>
<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

    <script>
        $(document).ready(function () {
            $("button").click(function () {
                var newProduct={id:"4",name:"车子"};
                var productstr=JSON.stringify(newProduct);
                $.ajax({
                        type:"POST",
                        url:"http://localhost:9002/products",
                        contentType: "application/json;charset=UTF-8",
                        data:productstr,
                        success:function (data) {
                            alert("新增"+data.name+"成功!");
                        },
                        error:function (data) {
                            alert(data);
                        }
                       }
                      );
            });
        });
    </script>
</head>
<body>
<button>新增商品</button>
</body>
</html>

下面给出的控制器类文件 - ProductController.java 如下 -

package com.maxuan.springbootpro.controller;

import com.maxuan.springbootpro.entry.Product;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Controller
public class ProductController {
    static List<Product> list = new ArrayList<>();

    static {
        list.add(new Product("1", "衣服"));
        list.add(new Product("2", "鞋子"));
        list.add(new Product("3", "帽子"));
    }

    /**
     * 打开查询页面
     * @return
     */
    @RequestMapping("/view-products")
    public String viewProducts() {
        return "view-products";
    }

    /**
     * 打开新增页面
     * @return
     */
    @RequestMapping("/add-products")
    public String addProducts() {
        return "add-products";
    }

    /**
     * 获取商品列表
     * @return
     */
    @GetMapping("/products")
    @ResponseBody
    public List<Product> getProducts(){
        return list;
    }

    /**
     * 新增一个商品
     * @param product
     * @return
     */
    @PostMapping("/products")
    @ResponseBody
    public Product addProduct(@RequestBody Product product){
        list.add(product);
        return product;
    }

}

view-products.html 文件如下 -

<html>
<head>
    <mata charset="UTF-8"/>
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

    <script>
        $(document).ready(function () {
                $.getJSON("http://localhost:9002/products",function (result) {
                    $.each(result,function (key,value) {
                        $("#productsJson").append(value.id+" "+value.name+" ");
                    });
                });
            });
    </script>

</head>
<body>
<div id="productsJson"></div>
</body>
</html>

add-products.html 文件代码如下 -

<html>
<head>
    <meta charset="UTF-8">
    <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.1/jquery.js"></script>

    <script>
        $(document).ready(function () {
            $("button").click(function () {
                var newProduct={id:"4",name:"车子"};
                var productstr=JSON.stringify(newProduct);
                $.ajax({
                        type:"POST",
                        url:"http://localhost:9002/products",
                        contentType: "application/json;charset=UTF-8",
                        data:productstr,
                        success:function (data) {
                            alert("新增"+data.name+"成功!");
                        },
                        error:function (data) {
                            alert(data);
                        }
                       }
                      );
            });
        });
    </script>
</head>
<body>
<button>新增商品</button>
</body>
</html>

主Spring Boot应用程序类文件如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

现在,应用程序已在Tomcat端口9002上启动。

在Web浏览器中访问URL => http://localhost:9002/view-products ,可以看到如下所示的输出 -

image-20201029154819574

访问URL => http://localhost:9002/add-products ,可以看到如下所示的输出 -

image-20201029154957716

现在,单击按钮提交表单,可以看到显示的结果 -

image-20201029154924389

现在,点击查看产品URL => http://localhost:9002/view-products ,查看创建的产品。

image-20201029155023741

十八、Spring Boot国际化

国际化是一个使应用程序适应不同语言和区域而无需对源代码进行工程更改的过程。 用它来说,国际化是对本地化的准备。

在本章中,将详细了解如何在Spring Boot中实现国际化。

2、LocaleResolver

需要确定应用程序的默认Locale。在Spring Boot应用程序中添加LocaleResolver bean。

@Bean
public LocaleResolver localeResolver() {
   SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
   sessionLocaleResolver.setDefaultLocale(Locale.US);
   return sessionLocaleResolver;
}

3、LocaleChangeInterceptor

LocaleChangeInterceptor用于根据添加到请求的语言参数的值更改新的Locale

@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
   LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
   localeChangeInterceptor.setParamName("language");
   return localeChangeInterceptor;
}

为了起到这种作用,需要将LocaleChangeInterceptor添加到应用程序的注册表拦截器中。 配置类应实现WebMvcConfigurer类并覆盖addInterceptors()方法,添加拦截器。

@Override
public void addInterceptors(InterceptorRegistry registry) {
   registry.addInterceptor(localeChangeInterceptor());
}

4、消息源

默认情况下,Spring Boot应用程序从类路径下的src/main/resources文件夹中获取消息源。 缺省语言环境消息文件名应为message.properties,每个语言环境的文件应命名为messages_XX.properties“XX”表示区域代码。

应将所有消息属性设置为键值对。 如果在语言环境中找不到任何属性,则应用程序将使用messages.properties 文件中的默认属性。

默认的messages.properties 如下所示 -

welcome.text=Hi Welcome to Everyone

中文对应的属性文件:message_zh.properties 如下所示 -

welcome.text=大家好

5、HTML文件

在HTML文件中,使用语法#{welcome.text}显示属性文件中的消息。

<h1 th:text = "#{welcome.text}"></h1>

Spring Boot应用程序主类文件如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

控制器类文件如下 -

package com.maxuan.springbootpro.controller;

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

@Controller
public class ProductController {
   @RequestMapping("/locale")
   public String locale() {
      return "locale";
   }
}

配置类支持国际化

package com.maxuan.springbootpro.component;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;

import java.util.Locale;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Configuration
public class InteceptorConfig implements WebMvcConfigurer {

    @Autowired
    LocaleChangeInterceptor localeChangeInterceptor;

    @Autowired
    private UserAccessInterceptor userAccessInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userAccessInterceptor)
                .addPathPatterns("/addPerson")
                .excludePathPatterns("/user/login");

        registry.addInterceptor(localeChangeInterceptor);
    }

    @Bean
    public LocaleResolver localeResolver() {
        SessionLocaleResolver sessionLocaleResolver = new SessionLocaleResolver();
        sessionLocaleResolver.setDefaultLocale(Locale.US);
        return sessionLocaleResolver;
    }
    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("language");
        return localeChangeInterceptor;
    }
}

消息源 - messages.properties 如下所示 -

welcome.text = Welcome everyone

消息源 - messages_cn.properties 如下所示 -

welcome.text = 大家好

HTML文件locale.html 放在类路径的 templates 目录下,如图所示 -

<!DOCTYPE html>
<html>
   <head>
      <meta charset = "UTF-8"/>
      <title>SpringBoot国际化</title>
   </head>
   <body>
      <h1 th:text = "#{welcome.text}"></h1>
   </body>
</html>

应用程序已在Tomcat端口9002上启动。

现在在Web浏览器中访问URL => http://localhost:9002/locale ,可以看到以下输出 -

image-20201030085909487

访问URL => http://localhost:9002/locale?language=cn 将看到如下所示结果 -

image-20201030085928192

十九、Spring Boot启用Swagger3

Swagger3是一个开源项目,用于为RESTful Web服务生成REST API文档。 它提供了一个用户界面,可通过Web浏览器访问RESTful Web服务。

要在Spring Boot应用程序中启用Swagger3,需要在构建配置文件中添加以下依赖项。

<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

现在,在主Spring Boot应用程序中添加@EnableOpenApi注释。 @EnableOpenApi注释用于为Spring Boot应用程序启用Swagger3。

主Spring Boot应用程序的代码如下所示 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import springfox.documentation.oas.annotations.EnableOpenApi;

@SpringBootApplication
@EnableOpenApi
public class App {
   public static void main(String[] args) {
      SpringApplication.run(App.class, args);
   }
}

接下来,创建Docket Bean以为Spring Boot应用程序配置Swagger3。

/**
     * swagger
     * @return
     */
    @Bean
    public Docket createRestApi() {
        return new Docket(DocumentationType.OAS_30)
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("Swagger3接口文档")
                .description("更多请咨询服务开发者smart哥。")
                .contact(new Contact("smart哥。", "http://www.maxuan.cn", "smart_an@gmail.com"))
                .version("1.0")
                .build();
    }

现在,这里显示了在Rest Controller文件中构建两个简单的RESTful Web服务GET和POST的代码 -

package com.maxuan.springbootpro.controller;

import com.maxuan.springbootpro.entry.Person;
import com.maxuan.springbootpro.exception.ResponseResult;
import com.maxuan.springbootpro.service.PersonService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Api(tags = "用户管理")
@RestController
public class UserController {
    /**
     * GET 查询用户
     * @param name
     * @return
     */
    @ApiOperation("查询用户")
    @RequestMapping(value = "/user", method = RequestMethod.GET)
    public List<Person> queryUser(@RequestParam(name = "name", required = false) String name) {
        if (name == null) {
            return list;
        }

        ArrayList<Person> ret = new ArrayList<>();
        for (Person person : list) {
            if (name.equals(person.getName())) {
                ret.add(person);
            }
        }
        return ret;
    }

    /**
     * POST 新增
     * @param person
     * @return
     */
    @ApiOperation("新增用户")
    @PostMapping(value = "/user")
    public List<Person> createUser(@RequestBody Person person) {
//        person.setId("4");
        list.add(person);
        return list;
    }

    /**
     * PUT 修改资源
     * @return
     */
    @ApiOperation("修改用户信息")
    @PutMapping(value = "/user/{id}")
    public List<Person> updateUser(@PathVariable("id") String id, @RequestBody Person person) {
        System.out.println("id from user====" + id);
        System.out.println("person====" + person);
        for (Person everyPerson : list) {
            if (id.equals(everyPerson.getId())) {
//                everyPerson.setName("maxuanupdate");
                list.set(list.indexOf(everyPerson), person);
            }
        }
        System.out.println("list=======" + list);
        return list;
    }

    @ApiOperation("删除用户")
    @DeleteMapping("/user/{id}")
    public List<Person> deleteUser(@PathVariable("id") String id) {
//        for (Person person : list) {
//            if (id != null && person.getId().equals(id)) {
//                list.remove(person);
//            }
//        }

        for (int i=0;i<list.size();i++) {
            if (id != null && list.get(i).getId().equals(id)) {
                list.remove(i);
                i--;
            }
        }
        System.out.println("list========="+list);
        return list;
    }
}

现在,应用程序将在Tomcat端口9002上启动。

现在,在Web浏览器URL => http://localhost:9002/swagger-ui/index.html , 并查看Swagger API功能,结果如下所示:

image-20201030085207305

二十、Spring Boot发送电子邮件

通过使用Spring Boot RESTful Web服务,可以发送包含pop/stml传输层安全性的电子邮件。 在本章中,详细了解如何使用此功能。

首先,需要在构建配置文件中添加Spring Boot Starter Mail依赖项。

Maven用户可以将以下依赖项添加到pom.xml 文件中。

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

设置开启pop3/smtp收发服务

image-20201030151437929

点击【账户】

image-20201030151526361

往下拉,找到如图 POP3/SMTP服务,设置【开启】,开启后会给出一串授权码,需要拷贝保存下来,后面代码里会用到。

image-20201030151613719

如果是发送简单邮件(没有附件),则只需要在properties文件中配置

spring.mail.default-encoding=UTF-8
spring.mail.host=smtp.qq.com
spring.mail.username=184480602@qq.com
##注意这个配置是设置qq邮件接收配置后给的授权码,不是qq密码
spring.mail.password=rdbeetutrrgybgfd
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true

主 Spring Boot 应用程序类文件的代码如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

可以编写一个简单的Rest API,以便在Rest Controller类文件中发送到电子邮件,如图所示。

package com.maxuan.springbootpro.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class EmailController {
   @RequestMapping(value = "/sendemail")
   public String sendEmail() {
      return "Email sent successfully";
   }   
}

编写一个方法来发送带有附件的电子邮件。 定义mail.smtp属性并使用PasswordAuthentication

private void mail() throws Exception{
        //邮件服务器相关配置信息
        Properties properties = new Properties();
        properties.setProperty("mail.host", "smtp.qq.com");
        properties.setProperty("mail.transport.protocol", "smtp");
        properties.setProperty("mail.smtp.auth", "true");
        properties.setProperty("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
        properties.setProperty("mail.smtp.port", "465");
        Session session = Session.getDefaultInstance(properties);
//        session.setDebug(true);

        /**
         * 简单邮件
         */
//        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
//        simpleMailMessage.setFrom("184480602@qq.com");
//        simpleMailMessage.setSubject("简单邮件");
//        simpleMailMessage.setTo("smart_an@163.com");
//        simpleMailMessage.setText("来来来 给你发了一封简单邮件");
//
////        MimeMessage mimeMessage = new MimeMessage();
//        MimeBodyPart bodyPart = new MimeBodyPart();
//
//        javaMailSender.send(simpleMailMessage);

        /**
         * 复杂邮件(带附件)
         */
        Message msg = new MimeMessage(session);
        //设置邮件来源
        msg.setFrom(new InternetAddress("184480602@qq.com", false));
        //设置邮件接收方
        msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse("smart_an@163.com"));
        //设置邮件主题
        msg.setSubject("邀请函");
        //设置发送日期
        msg.setSentDate(new Date());

        //正文部分
        MimeBodyPart messageBodyPart = new MimeBodyPart();
        messageBodyPart.setText("码炫课堂邀请阁下参加技术沙龙。附件:码炫课堂技术交流群12");
        //附件部分
        MimeBodyPart attachPart = new MimeBodyPart();
        attachPart.attachFile("d:/码炫课堂java架构群1群聊二维码.png");

        //消息载体=正文+附件
        Multipart multipart = new MimeMultipart();
        multipart.addBodyPart(messageBodyPart);
        multipart.addBodyPart(attachPart);
        //将消息载体作为整个邮件的内容
        msg.setContent(multipart);

        //以下设置发送
        Transport transport = session.getTransport();
        //连接上发送端
        transport.connect("smtp.qq.com", "184480602@qq.com", "rdbeetutrrgybgfd");
        transport.sendMessage(msg, msg.getAllRecipients());//发送邮件,第二个参数为收件人
        transport.close();
}

现在,从Rest API调用上面的sendmail()方法,如图所示 -

@RequestMapping(value = "/sendemail")
public String sendEmail() throws AddressException, MessagingException, IOException {
   mail();
   return "Email sent successfully";   
}

看到应用程序已在Tomcat端口9002上启动。

现在,从Web浏览器中打开以下URL => http://localhost:9002/sendemail

image-20201030162443437

发送成功后,开登录邮件账户将收到一封电子邮件

image-20201030162527645

二十一、Spring Boot批量服务

批处理服务是在单个任务中执行多个命令的过程, 在本章中,将学习如何在Spring Boot应用程序中创建批处理服务,主要学习spring batch框架。

Spring Batch是处理大量数据操作的一个框架,主要用来读取大量数据,然后进行一定的处理后输出指定的形式。比如我们可以将csv文件中的数据(数据量几百万甚至几千万都是没问题的)批处理插入保存到数据库中,就可以使用该框架。

1、Spring Batch框架的组成部分

  1)JobRepository:用来注册Job容器,设置数据库相关属性。

  2)JobLauncher:用来启动Job的接口

  3)Job:我们要实际执行的任务,包含一个或多个

  4)Step:即步骤,包括:ItemReader->ItemProcessor->ItemWriter

  5)ItemReader:用来读取数据,做实体类与数据字段之间的映射。比如读取csv文件中的人员数据,之后对应实体person的字段做mapper

  6)ItemProcessor:用来处理数据的接口,同时可以做数据校验(设置校验器,使用JSR-303(hibernate-validator)注解),比如将中文性别男/女,转为M/F。同时校验年龄字段是否符合要求等

  7)ItemWriter:用来输出数据的接口,设置数据库源。编写预处理SQL插入语句

2、批处理流程图

具体请看实战部分的代码。

img

3、实战

1)需求:将CSV文件内容保存到mysql中。

要创建批处理服务程序,需要在构建配置文件中添加Spring Boot Starter Batch依赖项和mysql相关依赖项。

Maven用户可以在pom.xml 文件中添加以下依赖项。

 <!--        spring批量处理-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<!-- 实体验证-->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.7.Final</version>
</dependency>
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
<!--连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

2)准备person.csv文件

现在,在资源目录 - src/main/resources 下添加简单的CSV数据文件,并将文件命名为person.csv,如图所示

1,Zhangsan,21,男
2,Lisi,22,女
3,Wangwu,23,男
4,Zhaoliu,24,男
5,Zhouqi,25,女

接下来,为mysql编写一个建表脚本

CREATE TABLE `person` (
  `id` int(11) NOT NULL,
  `name` varchar(10) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `gender` varchar(2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB

3)创建对应的Person类

package com.maxuan.springbootpro.entry;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

import javax.validation.constraints.Size;
import java.io.Serializable;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@ApiModel("用户信息")
public class Person implements Serializable {
    private static final long serialVersionUID = 1L;

    @ApiModelProperty("用户id")
    private String id;

    @Size(min = 2,max = 8)
    @ApiModelProperty("用户名")
    private String name;

    private int age;

    public Person(String id, String name, int age, String gender) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public int getAge() {
        return age;
    }

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

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    private String gender;

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

    public String getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public Person(String id, String name) {
        this.id = id;
        this.name = name;
    }

    public Person() {
    }
}

4)创建处理器

现在,创建一个中间处理器,在从CSV文件读取数据之后和将数据写入SQL之前执行操作。

package com.maxuan.springbootpro.component;

import com.maxuan.springbootpro.entry.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.item.validator.ValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class CvsItemProcessor extends ValidatingItemProcessor<Person> {
    private Logger logger = LoggerFactory.getLogger(CvsItemProcessor.class);

    @Override
    public Person process(Person item) throws ValidationException {
        // 执行super.process()才能调用自定义的校验器
        logger.info("processor start validating...");
        super.process(item);

        // 数据处理,比如将中文性别设置为M/F
        if ("男".equals(item.getGender())) {
            item.setGender("M");
        } else {
            item.setGender("F");
        }
        logger.info("processor end validating...");
        return item;
    }
}

5)配置batch信息

spring:
  batch:
    job:
      # 默认自动执行定义的Job(true),改为false,需要jobLaucher.run执行
      enabled: false
    # spring batch在数据库里面创建默认的数据表,如果不是always则会提示相关表不存在
    initialize-schema: always

6)创建Batch配置文件

从CSV读取数据并写入SQL文件,如下所示。需要在配置类文件中添加@EnableBatchProcessing注释。 @EnableBatchProcessing注释用于启用Spring Boot应用程序的批处理操作。

package com.maxuan.springbootpro.component;

import com.maxuan.springbootpro.entry.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Configuration
@EnableBatchProcessing // 开启批处理的支持
@Import(DruidDBConfig.class) // 注入datasource
public class BatchConfig {
    private Logger logger = LoggerFactory.getLogger(BatchConfig.class);

    /**
     * ItemReader定义:读取文件数据+entirty映射
     * @return
     */
    @Bean
    public ItemReader<Person> reader(){
        // 使用FlatFileItemReader去读cvs文件,一行即一条数据
        FlatFileItemReader<Person> reader = new FlatFileItemReader<>();
        // 设置文件处在路径
        reader.setResource(new ClassPathResource("person.csv"));
        // entity与csv数据做映射
        reader.setLineMapper(new DefaultLineMapper<Person>() {
            {
                setLineTokenizer(new DelimitedLineTokenizer() {
                    {
                        setNames(new String[]{"id", "name", "age", "gender"});
                    }
                });
                setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {
                    {
                        setTargetType(Person.class);
                    }
                });
            }
        });
        return reader;
    }

    /**
     * 注册ItemProcessor: 处理数据+校验数据
     * @return
     */
    @Bean
    public ItemProcessor<Person, Person> processor(){
        CvsItemProcessor itemProcessor = new CvsItemProcessor();
        // 设置校验器
        itemProcessor.setValidator(beanValidator());
        return itemProcessor;
    }

    /**
     * 注册校验器
     * @return
     */
    @Bean
    public BeanValidator beanValidator(){
        return new BeanValidator<Person>();
    }

    /**
     * ItemWriter定义:指定datasource,设置批量插入sql语句,写入数据库
     * @param dataSource
     * @return
     */
    @Bean
    public ItemWriter<Person> writer(DataSource dataSource){
        // 使用jdbcBcatchItemWrite写数据到数据库中
        JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<>();
        // 设置有参数的sql语句
        writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>());
        String sql = "insert into person values(:id,:name,:age,:gender)";
        writer.setSql(sql);
        writer.setDataSource(dataSource);
        return writer;
    }

    /**
     * 注册Job容器,设置数据库相关属性。
     * @param dataSource
     * @param transactionManager
     * @return
     * @throws Exception
     */
    @Bean
    public JobRepository CvsjobRepository(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception{
        JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
        jobRepositoryFactoryBean.setDatabaseType("mysql");
        jobRepositoryFactoryBean.setTransactionManager(transactionManager);
        jobRepositoryFactoryBean.setDataSource(dataSource);
        return jobRepositoryFactoryBean.getObject();
    }

    /**
     * 用来启动Job的接口
     * @param dataSource
     * @param transactionManager
     * @return
     * @throws Exception
     */
    @Bean
    public SimpleJobLauncher CvsjobLauncher(DataSource dataSource, PlatformTransactionManager transactionManager) throws Exception{
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        // 设置jobRepository
        jobLauncher.setJobRepository(CvsjobRepository(dataSource, transactionManager));
        return jobLauncher;
    }

    /**
     * 定义job:实际执行的任务,包含一个或多个
     * @param jobs
     * @param step
     * @return
     */
    @Bean
    public Job importJob(JobBuilderFactory jobs, Step step){
        return jobs.get("importCsvJob")
                .incrementer(new RunIdIncrementer())
                .flow(step)
                .end()
                .listener(jobListener())
                .build();
    }

    /**
     * 注册job监听器
     * @return
     */
    @Bean
    public JobListener jobListener(){
        return new JobListener();
    }

    /**
     * step定义:步骤包括ItemReader->ItemProcessor->ItemWriter 即读取数据->处理校验数据->写入数据
     * @param stepBuilderFactory
     * @param reader
     * @param writer
     * @param processor
     * @return
     */
    @Bean
    public Step step(StepBuilderFactory stepBuilderFactory, ItemReader<Person> reader,
                     ItemWriter<Person> writer, ItemProcessor<Person, Person> processor){
        return stepBuilderFactory
                .get("step")
                // Chunk的机制(即每次读取一条数据,再处理一条数据,累积到一定数量后再一次性交给writer进行写入操作)
                .<Person, Person>chunk(65000)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();

    }
}

reader()方法用于从CSV文件中读取数据,而writer()方法用于将数据写入SQL。

7)创建通知监听器

接下来,将编写一个作业完成通知监听器类 - 用于在作业完成后通知。

package com.maxuan.springbootpro.component;

import com.maxuan.springbootpro.entry.Person;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class JobListener implements JobExecutionListener {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private Logger logger = LoggerFactory.getLogger(JobListener.class);
    private long startTime;
    private long endTime;

    @Override
    public void beforeJob(JobExecution jobExecution) {
        startTime = System.currentTimeMillis();
        logger.info("job process start...");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        endTime = System.currentTimeMillis();
        logger.info("job process end...");
        logger.info("elapsed time: " + (endTime - startTime) + "ms");
        jdbcTemplate.query("SELECT id, name,age,gender FROM person",
                (rs, row) -> new Person(
                        rs.getString(1),
                        rs.getString(2),
                        rs.getInt(3),
                        rs.getString(4))
        ).forEach(person -> logger.info("Found <" + person + "> in the database."));
    }
}

8)配置数据源

##数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.0.108:3306/test?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    password: 123456
    username: root

9)创建数据源配置

package com.maxuan.springbootpro.component;

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Configuration
public class DruidDBConfig {
    private Logger logger = LoggerFactory.getLogger(DruidDBConfig.class);

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Bean
    @Primary  // 被注入的优先级最高
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        logger.info("-------->dataSource[url=" + dbUrl + " ,username=" + username + "]");
        dataSource.setUrl(dbUrl);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        dataSource.setDriverClassName(driverClassName);
        return dataSource;
    }

    @Bean
    public ServletRegistrationBean druidServletRegistrationBean() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean();
        servletRegistrationBean.setServlet(new StatViewServlet());
        servletRegistrationBean.addUrlMappings("/druid/*");
        return servletRegistrationBean;
    }

    /**
     * 注册DruidFilter拦截
     *
     * @return
     */
    @Bean
    public FilterRegistrationBean duridFilterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        Map<String, String> initParams = new HashMap<String, String>();
        //设置忽略请求
        initParams.put("exclusions", "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*");
        filterRegistrationBean.setInitParameters(initParams);
        filterRegistrationBean.addUrlPatterns("/*");
        return filterRegistrationBean;
    }
}

10)定义校验器

定义校验器:使用JSR-303(hibernate-validator)注解,来校验ItemReader读取到的数据是否满足要求。如不满足则不会进行接下来的批处理任务。

package com.maxuan.springbootpro.component;

import org.springframework.batch.item.validator.ValidationException;
import org.springframework.batch.item.validator.Validator;
import org.springframework.beans.factory.InitializingBean;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import java.util.Set;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
public class BeanValidator <T> implements Validator<T>, InitializingBean {

    private javax.validation.Validator validator;

    /**
     * 进行JSR-303的Validator的初始化
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.usingContext().getValidator();
    }

    /**
     * 使用validator方法检验数据
     * @param value
     * @throws ValidationException
     */
    @Override
    public void validate(T value) throws ValidationException {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(value);
        if (constraintViolations.size() > 0) {
            StringBuilder message = new StringBuilder();
            for (ConstraintViolation<T> constraintViolation: constraintViolations) {
                message.append(constraintViolation.getMessage() + "\n");
            }
            throw new ValidationException(message.toString());
        }
    }
}

11)编写JobController类

package com.maxuan.springbootpro.controller;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RestController
public class JobController {

    @Autowired
    SimpleJobLauncher jobLauncher;

    @Autowired
    Job importJob;

    @RequestMapping("/startJob")
    public void startJob() throws Exception{
        // 后置参数:使用JobParameters中绑定参数
        JobParameters jobParameters = new JobParametersBuilder().addLong("time", System.currentTimeMillis())
                .toJobParameters();
        jobLauncher.run(importJob, jobParameters);
    }
}

12)启动访问

访问:http://localhost:9002/startJob 结果如下:

image-20201031200958682

数据已入库,并且查询出来了。

二十二、Spring Boot单元测试用例

单元测试是开发人员为确保单个单元或组件功能正常工作而进行的测试之一。在本教程中,将了解和学习如何在controller层和service层进行单元测试。

1、controller层的单元测试

主要借助于spring-test包的MockMvc来进行模拟浏览器访问的,这在之前演示过一个查询的案例,接下来把增,删,改也演示下。代码如下:

import com.maxuan.springbootpro.App;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class UserControllerTest {
    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @Before
    public void setup(){
        mockMvc= MockMvcBuilders.webAppContextSetup(wac).build();
    }

    @Test
    public void queryUser() throws Exception{
       String ret=mockMvc.perform(MockMvcRequestBuilders
                .get("/user")
                .param("name","maxuan1")
                .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print())
                .andReturn().getResponse().getContentAsString();

        System.out.println("ret===="+ret);
    }
}

2、service层的单元测试

service层的单元测试我们将引入mockito测试框架,Mockito是mocking框架,它让你用简洁的API做测试。而且Mockito简单易学,它可读性强和验证语法简洁。

Mockito 是一个针对 Java 的单元测试模拟框架,它与 EasyMock 和 jMock 很相似,都是为了简化单元测试过程中测试上下文 ( 或者称之为测试驱动函数以及桩函数 ) 的搭建而开发的工具。

要将Mockito Mocks注入Spring Beans,需要在构建配置文件中添加Mockito-core依赖项。
Maven用户可以在pom.xml 文件中添加以下依赖项。

<dependency>
     <groupId>org.mockito</groupId>
     <artifactId>mockito-core</artifactId>
     <version>3.3.3</version>
</dependency>

此处给出了编写Service类的代码,该类包含一个返回String值的方法。

package com.maxuan.springbootpro.service;

import org.springframework.stereotype.Service;
/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Service
public class ProductServiceImpl implements ProductService{
   public String getProductName() {
      return "football";
   }
}

现在,将ProductService类注入另一个Service类文件,如图所示。

package com.maxuan.springbootpro.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/******************************
 *
 * 码炫课堂技术交流Q群:963060292
 * 主讲:smart哥
 *
 ******************************/
@Service
public class OrderServiceImpl implements OrderService{
    @Autowired
    ProductService productService;

    @Override
    public String getProductName() {
        return productService.getProductName();
    }
}

主 Spring Boot应用程序类文件如下 -

package com.maxuan.springbootpro;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

然后,要测试配置应用程序上下文。 就将@Profile("test")注释用于在测试用例运行时配置类。

package com.maxuan.springbootpro.component;

import com.maxuan.springbootpro.service.ProductService;
import org.mockito.Mockito;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Profile;

@Profile("test")
@Configuration
public class ProductServiceTestConfiguration {
   @Bean
   @Primary
   public ProductService productService() {
      return Mockito.mock(ProductService.class);
   }
}

现在编写单元测试用例。

import com.maxuan.springbootpro.App;
import com.maxuan.springbootpro.service.OrderService;
import com.maxuan.springbootpro.service.ProductService;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner;

import static org.mockito.Mockito.when;

@SpringBootTest(classes = App.class)
@ActiveProfiles("test")
@RunWith(SpringRunner.class)
public class MockitoDemoApplicationTest {
   @Autowired
   private OrderService orderService;

   @Autowired
   private ProductService productService;

   @Test
   public void testProductName() {
      when(productService.getProductName()).thenReturn("Mock Product Name");
      String testName = orderService.getProductName();
      Assert.assertEquals("Mock Product Name", testName);
   }
}

得到结果如下:

image-20201110184315978

二十三、Spring Boot快速整合MyBatis (去XML化)

此前,我们主要通过XML来书写SQL和填补对象映射关系。在SpringBoot中我们可以通过注解来快速编写SQL并实现数据访问。(仅需配置:mybatis.configuration.map-underscore-to-camel-case=true)。

先在pom文件中加入mybatis依赖。mysql驱动等等

 <!--添加Mybatis依赖 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.0</version>
</dependency>
<!--mysql驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.20</version>
</dependency>
<!--连接池-->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.21</version>
</dependency>

1、基础注解

MyBatis 主要提供了以下CRUD注解:

  • @Select
  • @Insert
  • @Update
  • @Delete

增删改查占据了绝大部分的业务操作,掌握这些基础注解的使用还是很有必要的,例如下面这段代码无需XML即可完成数据查询:

@Mapper
public interface UserMapper {
    @Select("select * from t_user")
    List<User> list();
}

为什么可以自动映射实体类???

yml文件中开启驼峰模型并添加数据源

##mybatis配置
mybatis:
  configuration:
    ##开启驼峰映射
    map-underscore-to-camel-case: true

##数据源配置
spring: 
 datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.0.108:3306/test?useSSL=false&serverTimezone=UTC&characterEncoding=utf8
    password: 123456
    username: root

2、映射注解

Mybatis主要提供这些映射注解:

  • @Results 用于填写结果集的多个字段的映射关系.
  • @Result 用于填写结果集的单个字段的映射关系.
  • @ResultMap 根据ID关联XML里面.

我们可以在查询SQL的基础上,指定返回的结果集的映射关系,其中property表示实体对象的属性名,column表示对应的数据库字段名。编写数据层代码

@Mapper
public interface UserMapper {
     @Results({
            @Result(property = "userId", column = "USER_ID"),
            @Result(property = "username", column = "USERNAME"),
            @Result(property = "password", column = "PASSWORD"),
            @Result(property = "mobileNum", column = "PHONE_NUM")
    })
    @Select("select * from t_user where 1=1")
    List<User> list();

    @Select("select * from t_user where username like #{username}")
    List<User> findByUsername(String username);

    @Select("select * from t_user where user_id like #{userId}")
    User getOne(String userId);

    @Delete("delete from t_user where user_id like #{userId}")
    int delete(String userId);

    @Update("update person set name=#{name} where id = #{id}")
    int update(String id,String name);
}

经验之谈

为了方便演示和免除手工编写映射关系的烦恼,这里提供了一个快速生成映射结果集的方法,具体内容如下:

/**
     * 1.用于获取结果集的映射关系
     */
    public static String getResultsStr(Class origin) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("@Results({\n");
        for (Field field : origin.getDeclaredFields()) {
            String property = field.getName();
            //映射关系:对象属性(驼峰)->数据库字段(下划线)
            String column = new PropertyNamingStrategy.SnakeCaseStrategy().translate(field.getName()).toUpperCase();
            stringBuilder.append(String.format("@Result(property = \"%s\", column = \"%s\"),\n", property, column));
        }
        stringBuilder.append("})");
        return stringBuilder.toString();
    }

在当前Main方法执行效果如下:然后我们将控制台这段打印信息复制到接口方法上即可。

img

3、高级注解

MyBatis-3 主要提供了以下CRUD的高级注解:

  • @SelectProvider
  • @InsertProvider
  • @UpdateProvider
  • @DeleteProvider

见名知意,这些高级注解主要用于动态SQL,这里以@SelectProvider 为例,主要包含两个注解属性,其中type表示工具类,method 表示工具类的某个方法,用于返回具体的SQL。

@Mapper
public interface UserMapper {
    @SelectProvider(type = UserSqlProvider.class, method = "list222")
    List<User> list2();
}

工具类代码如下:

public class UserSqlProvider {
    public String list222() {
        return "select * from t_user ;
    }
}

4、具体代码

1)引入通用Mapper

<dependency> 
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>1.1.4</version>
</dependency>

2)通用Mapper

在Mapper接口继承通用Mapper并指定泛型,如下:

public interface UserMapper extends Mapper<User> {
}

3)通用Service

BaseService可以根据实际需求来添加常用的CURD接口方法,例如:

public class BaseServiceImpl<T> implements BaseService<T> {

    @Autowired
    private Mapper<T> mapper;//泛型装配

    @Override
    public List<T> list(T entity) {
        return mapper.select(entity);
    }

    @Override
    public T get(T entity) {
        return  mapper.selectOne(entity);
    }

    @Override
    public int update(T entity) {
        return mapper.updateByPrimaryKeySelective(entity);
    }

    @Override
    public int save(T entity) {
        return mapper.insertSelective(entity);
    }

    @Override
    public int delete(T entity) {
        return mapper.deleteByPrimaryKey(entity);
    }
}

4)具体的service类

在Service实现类继承通用Service,如下:

@Service
public class UserServiceImpl extends BaseServiceImpl<User> implements UserService {

}

5)编写restful风格的controller

@RestController
@RequestMapping("/user/*")
public class UserController {

    @Autowired
    UserService userService;

    @RequestMapping("list")
    public List<User> list(User user) {
        return userService.list(user);
    }

    @RequestMapping("get")
    public User get(User user) {
        return userService.get(user);
    }

    @RequestMapping("update")
    public int update(User user) {
        return userService.update(user);
    }

    @RequestMapping("save")
    public int save(User user) {
        return userService.save(user);
    }

    @RequestMapping("delete")
    public int delete(User user) {
        return userService.delete(user);
    }

}

6)具体Mapper

@Mapper
public interface UserMapper {
    /**
     * 方式1:使用注解编写SQL。
     */
    @Select("select * from t_user")
    List<User> list();

    /**
     * 方式2:使用注解指定某个工具类的方法来动态编写SQL.
     */
    @SelectProvider(type = UserSqlProvider.class, method = "listByUsername")
    List<User> listByUsername(String username);

    /**
     * 延伸:上述两种方式都可以附加@Results注解来指定结果集的映射关系.
     *
     * PS:如果符合下划线转驼峰的匹配项可以直接省略不写。
     */
    @Results({
            @Result(property = "userId", column = "USER_ID"),
            @Result(property = "username", column = "USERNAME"),
            @Result(property = "password", column = "PASSWORD"),
            @Result(property = "mobileNum", column = "PHONE_NUM")
    })
    @Select("select * from t_user")
    List<User> listSample();

    @Select("select * from t_user where username like #{username} and password like #{password}")
    User get( String username,  String password);

    @SelectProvider(type = UserSqlProvider.class, method = "getBadUser")
    User getBadUser(@Param("username") String username, @Param("password") String password);

}

7)UserSqlProvider

/**
 * 主要用途:根据复杂的业务需求来动态生成SQL.
 * <p>
 * 目标:使用Java工具类来替代传统的XML文件.(例如:UserSqlProvider.java <-- UserMapper.xml)
 */
public class UserSqlProvider {
    /**
     * 方式1:在工具类的方法里,可以自己手工编写SQL。
     */
    public String listByUsername(String username) {
        return "select * from t_user where username =#{username}";
    }

    /**
     * 方式2:也可以根据官方提供的API来编写动态SQL。
     */
    public String getBadUser( String username,String password) {
        return new SQL() {{
            SELECT("*");
            FROM("t_user");
            if (username != null && password != null) {
                WHERE("username like #{username} and password like #{password}");
            } else {
                WHERE("1=2");
            }
        }}.toString();
    }
}

课后练习:更复杂的映射

实现复杂关系映射之前我们可以在映射文件中通过配置来实现,在使用注解开发时我们需要借助@Results 注解,@Result 注解,@One 注解,@Many 注解。

二十四、Spring Boot保护Web应用程序

如果在类路径上添加了Spring Boot Security依赖项,则Spring Boot应用程序会自动为所有HTTP端点提供基本身份验证。端点“/”“/home”不需要任何身份验证。所有其他端点都需要身份验证。

要将Spring Boot Security添加到Spring Boot应用程序,需要在构建配置文件中添加Spring Boot Starter Security依赖项。

Maven用户可以在pom.xml 文件中添加以下依赖项。

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

/hello----->/login---->login.html----->/hello--->hello.html

/home------>home.html

/------>home.html

保护Web应用程序

首先,使用Thymeleaf模板创建不安全的Web应用程序。
然后,在 src/main/resources/templates 目录下创建一个home.html 文件。

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" 
   xmlns:th = "http://www.thymeleaf.org" 
   xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
   <head>
      <title>Spring Security示例</title>
   </head>
   <body>
      <h1>欢迎您!</h1>
      <p>点击 <a th:href = "@{/hello}">这里</a> 看到问候语.</p>
   </body>
</html>

使用Thymeleaf模板在HTML文件中定义的简单视图/hello。现在,在src/main/resources/templates目录下创建一个文件:hello.html

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" 
   xmlns:th = "http://www.thymeleaf.org" 
   xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
   <head>
      <title>Hello World!</title>
   </head>
   <body>
      <h1>Hello world!</h1>
   </body>
</html>

现在,需要为Home和hello视图设置Spring MVC - View控制器。为此,创建一个扩展WebMvcConfigurer的MVC配置文件。

package com.maxuan.springbootpro.component;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class InteceptorConfig implements WebMvcConfigurer {

    @Override
    /**
     * spring安全配置
     */
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
}

现在,创建一个Web安全配置文件,该文件用于保护应用程序以使用基本身份验证访问HTTP端点。

package com.maxuan.springbootpro.component;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "/home").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login")
                .permitAll()
                .and()
                .logout()
                .permitAll();

    }

    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .passwordEncoder(new BCryptPasswordEncoder())
                .withUser("user")
                .password(new BCryptPasswordEncoder()
                        .encode("123"))
                .roles("USER");
    }
}

现在,在src/main/resources 目录下创建一个login.html 文件,以允许用户通过登录屏幕访问HTTP端点。

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" xmlns:th = "http://www.thymeleaf.org"
   xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">

   <head>
      <title>Spring Security示例</title>
   </head>
   <body>
      <div th:if = "${param.error}">
         无效的用户名和密码.
      </div>
      <div th:if = "${param.logout}">
         你已经注销.
      </div>

      <form th:action = "@{/login}" method = "post">
         <div>
            <label> 用户名 : <input type = "text" name = "username"/> </label>
         </div>
         <div>
            <label> 密码: <input type = "password" name = "password"/> </label>
         </div>
         <div>
            <input type = "submit" value = "登录"/>
         </div>
      </form>
   </body>
</html>

最后,更新hello.html 文件 - 允许用户从应用程序注销并显示当前用户名,如下所示 -

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml" xmlns:th = "http://www.thymeleaf.org" 
   xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">

   <head>
      <title>Hello World!</title>
   </head>
   <body>
      <h1 th:inline = "text">您好,[[${#httpServletRequest.remoteUser}]]!</h1>
      <form th:action = "@{/logout}" method = "post">
         <input type = "submit" value = "注销"/>
      </form>
   </body>

</html>

在Web浏览器中访问URL => http://localhost:9002/ ,将看到如下图所示。

image-20201110183049916

输入用户名和密码(user/password),然后点击登录 -

image-20201110183114813

看到问候语如下:

image-20201110183139627

课程目录:

1、springboot微服务架构演变历程(17:58)

2、springboot框架特性(26:55)

3、springboot自动装配,组件扫描原理(18:58)

4、springboot引导过程&jar启动(17:58)

5、加餐课-手写嵌入式tomcat容器(50:10)

6、idea内和外2种方式实现springboot项目war包部署(19:42)

7、springboot构建系统&代码结构详解(09:55)

8、springboot中bean的3种依赖注入方式详解(20:24)

9、springboot运行器runner详解及实际运用(21:33)

10、springboot程序命令行属性运用详解(14:45)

11、springboot程序文件属性&外部属性运用详解(18:55)

12、springboot程序使用@Value获取属性详解(16:47)

13、springboot程序活动属性&分环境属性配置详解(16:50)

14、图解日志框架在整个系统架构中的地位及作用(17:20)

15、springboot文件日志输出&日志级别详解(17:54)

16、加餐课-logback日志框架&生产环境配置文件详解(24:34)

17、加餐课-模拟生产环境logback日志配置及输出调试(30:07)

18、restful风格的服务设计规范详解(12:53)

19、restful风格服务的get方法实现及代码演示(23:46)

20、restful风格服务的post,put,delete方法实现及代码演示(20:36)

21、springboot中的异常流程&实际案例详解(13:31)

22、加餐课-生产环境中的异常处理模块实现1(16:07)

23、加餐课-生产环境中的异常处理模块实现2(16:53)

24、加餐课-生产环境中的异常处理模块实现3(24:10)

25、springboot的拦截器原理及实现(25:33)

26、springboot的过滤器原理详解(20:34)

27、以栈原理图解过滤器dofilter方法&注解实现(29:40)

28、REST模板在微服务架构中的作用及底层原理(17:40)

29、创建REST模板实例&分布式服务演示(14:44)

30、代码演示4种方式实现REST模板之-get方法(18:26)

31、REST模板实现get,post分布式调用(24:22)

32、REST模板实现put,delete分布式调用(37:37)

33、springboot实现文件上传下载(20:43)

34、springboot使用thymeleaf模板(17:33)

35、thymeleaf前端渲染各类属性演示(29:10)

36、restful服务打通前后端之-查询商品演示(24:35)

37、restful服务打通前后端之-新增商品演示(19:56)

38、springboot国际化(16:00)

39、springboot启用swagger3(28:09)

40、springboot实现邮件发送(30:46)

41、加餐课-springboot批量服务1-流程详解&数据准备(16:55)

42、加餐课-springboot批量服务2-实现读处理器&过程处理器&验证器(23:32)

43、加餐课-springboot批量服务3-实现写入处理器&job容器&job(17:20)

44、加餐课-springboot批量服务4-实现监听器&step&批处理配置(20:54)

45、加餐课-springboot批量服务5-拉通批处理流程并演示批量验证入库(19:28)

46、springboot单元测试1-controller层MockMvc组件详解(21:16)

47、springboot单元测试2-service层Mockito框架详解(26:21)

48、springboot整合mybatis1(完全注解化)详解(38:15)

49、springboot整合mybatis2(复杂动态sql)详解(25:59)

50、springboot整合web保护(引入spring-security)(31:25)