Spring Boot

1. springboot的引言

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

springboot(微框架) = springmvc(控制器) + spring core(项目管理)

SSM Spring springmvc mybatis <—— SSM Spring struts2|struts1 mybatis <—- SSH Spring Struts Hibernate


微服务

  1. 微服务是一种用于构建应用的架构方案。微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作(和出现故障)时不会相互影响。
  2. 微服务就是应用的各项核心功能,而且这些服务均可独立运行。
  3. 但是,微服务架构不只是应用核心功能间的这种松散耦合,它还涉及重组开发团队、涉及如何进行服务间通信以应对不可避免的故障、满足未来的可扩展性并实现新的功能集成。

微服务架构的优势

  1. 微服务可通过分布式部署,大幅提升您的团队和日常工作效率。您还可以并行开发多个微服务。这意味着更多开发人员可以同时开发同一个应用,进而缩短开发所需的时间。
  1. 加速做好面市准备:由于开发周期缩短,微服务架构有助于实现更加敏捷的部署和更新。
  2. 高度可扩展:随着某些服务的不断扩展,您可以跨多个服务器和基础架构进行部署,充分满足自身需求。
  3. 出色的弹性:只要确保正确构建,这些独立的服务就不会彼此影响。这意味着,一个服务出现故障不会导致整个应用下线,这一点与单体式应用模型不同。
  4. 易于部署:相对于传统的单体式应用,基于微服务的应用更加模块化且小巧,所以您无需为它们的部署操心。虽然对部署时的协作要求更高,但之后能获得巨大回报。
  5. 易于访问:由于大型应用被拆分成了多个小型服务,所以开发人员能够更加轻松地理解、更新和增强这些服务,从而缩短开发周期(尤其是在搭配使用敏捷的开发方法时)。
  6. 更加开放:由于使用了多语言 API,所以开发人员可以根据需要实现的功能,自由选用最适合的语言和技术。

    分布式与微服务的区别

  • 微服务是架构设计方式,分布式是系统部署方式,两者概念不同;
  • 微服务是指很小的服务,可以小到只完成一个功能,这个服务可以单独部署运行,不同服务之间通过rpc调用;
  • 分布式是指服务部署在不同的机器上,一个服务可以提供一个或多个功能,服务之间也是通过rpc来交互或者是webservice来交互的;

    1. 两者的关系是,系统应用部署在超过一台服务器或虚拟机上,且各分开部署的部分彼此通过各种通讯协议交互信息,就可算作分布式部署,生产环境下的微服务肯定是分布式部署的,分布式部署的应用不一定是微服务架构的,比如集群部署,它是把相同应用复制到不同服务器上,但是逻辑功能上还是单体应用。

2. springboot的特点

  1. 创建独立的Spring应用程序
  2. 嵌入的Tomcat,无需部署WAR文件
  3. 简化Maven配置
  4. 自动配置Spring,没有XML配置

3. springboot 的约定大于配置

项目目录结构:

SpringBoot(三阶段) - 图3

  • springboot 项目中必须在src/main/resources中放入application.yml(.properties)核心配置文件 名字必须为:application
  • springboot 项目中必须在src/main/java中所有子包之外构建全局入口类型,xxApplication,入口类一个springboot项目只能有一个

4. springboot的环境搭建

环境要求:

  1. # 1.System Requirements
  2. JDK18.+
  3. MAVEN3.3 or Gradle 5.x and 6.x (4.10 is also supported but in a deprecated form)
  4. Spring Framework 5.2.4.RELEASE
  5. # 2.ServletContainers:
  6. Tomcat 9.0
  7. Jetty 9.4
  8. Undertow 2.0
  9. # 3.开发工具
  10. IDEA 2018版本
  11. Eclipse 版本 17版本之后

4.1 项目中引入依赖
  1. <!--继承springboot的父项目-->
  2. <parent>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-parent</artifactId>
  5. <version>2.2.5.RELEASE</version>
  6. </parent>
  7. <dependencies>
  8. <!--引入springboot的web支持-->
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-web</artifactId>
  12. </dependency>
  13. </dependencies>

4.2 引入配置文件
    `项目中src/main/resources/application.yml`

4.3 建包并创建控制器
//在项目中创建指定的包结构
/*
     com
        +| wsjy
                +| controller */ 
                    @Controller
                    @RequestMapping("/hello")
                    public class HelloController {
                        @RequestMapping("/hello")
                        @ResponseBody
                        public String hello(){
                            System.out.println("======hello world=======");
                            return "hello";
                        }
                    }

4.4 编写入口类
//在项目中如下的包结构中创建入口类 Application
/*
    com
        +| wsjy                  */
            @SpringBootApplication
            public class Application {
                public static void main(String[] args) {
                    SpringApplication.run(Application.class,args);
                }
            }

4.5 运行main启动项目
o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8989 (http)
com.wsjy.Application : Started Application in 2.152 seconds (JVM running for 2.611)

//说明:  出现以上日志说明启动成功

4.6 访问项目
//注意: springboot的项目默认没有项目名
//访问路径:  http://localhost:8080/hello/hello

5.springboot入门案例分析

    虽然没有进行任何配置,但入门案例依旧可正常运行。除controller之外的内容,都是项目创建时自动生成的:pom.xml,spring boot主程序类。因此,需要分析和了解的就是这两个文件。

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.2.5.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wsjy</groupId>
    <artifactId>springboot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot</name>
    <description>Demo 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>
    <!-- spring boot打包插件:可以将项目打包成一个可执行jar包 -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
    在pom.xml中主要有两个部分需要着重关注:<parent>和<dependencies>。

版本仲裁
    当前项目的父项目是spring-boot-starter-parent。
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
    从spring-boot-starter-parent再向上找一层,可以看到其父项目spring-boot-dependencies。
<parent>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-dependencies</artifactId>
   <version>2.2.5.RELEASE</version>
</parent>
    进入spring-boot-dependencies后,观察<properties>可知,spring boot通过spring-boot-dependencies来进行版本仲裁。

启动器
<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>
    <dependencies>用于导入项目运行所需要的组件。

    spring-boot-starter-web、spring-boot-starter-test是spring boot的场景启动器。

    spring boot将所有应用场景抽取出来一个个的启动器,在启动器中封装了对应场景运行时所需的全部依赖,项目中需要什么场景,导入对应的启动器即可。

主程序类

//@SpringBootApplication:用来标注一个主程序类,代表当前应用是一个spring boot
@SpringBootApplication
public class SpringbootApplication {
    public static void main(String[] args) {
        //运行spring boot应用
        SpringApplication.run(SpringbootApplication.class, args);
    }
}
    @SpringBootApplication是个组合注解,该注解内部有两个很重要的注解:@SpringBootConfiguration和@EnableAutoConfiguration。
......
@SpringBootConfiguration
@EnableAutoConfiguration
......
public @interface SpringBootApplication {
    ......
}

@SpringBootConfiguration
    表示被注解的类就是一个spring boot的配置类。
......
@Configuration
public @interface SpringBootConfiguration {
    ......
}
    该注解也是组合注解,内部被@Configuration所注解,说明被@SpringBootConfiguration注解的就是一个spring的配置类。

@EnableAutoConfiguration
    该注解用于告诉spring boot开启**自动配置**。
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    ......
}
    @AutoConfigurationPackage:自动配置包,**将主程序类所在的包及子包中所有组件都扫描进spring容器。**
......
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
    ......
}
    @Import({Registrar.class}):spring的底层注解,用于向容器中导入Registrar.class组件。
public abstract class AutoConfigurationPackages {
    ......
//注册元信息
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
/*
new AutoConfigurationPackages.PackageImports(metadata))
.getPackageNames().toArray(new String[0])
计算该值可得到被@SpringBootApplication注解的类所在的包名
(debug状态下,可以选中右键执行计算操作)
*/
      AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
  }
......
}
    @Import({AutoConfigurationImportSelector.class}):导入自动配置组件选择器AutoConfigurationImportSelector。
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    ......
    //导入组件选择器,根据注解的元信息进行导入    
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        ......
            //调用getAutoConfigurationEntry(annotationMetadata)进入导入
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
        ......
    }
    ......
    //返回所有需要导入的自动配置类组件的完全限定名,将这些组件添加到spring容器中
    //有了这些自动配置类,免去了程序员手动配置和注入的工作。
    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        ......
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            //调用getCandidateConfigurations(annotationMetadata, attributes)实现
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        ......
        }
    }
    ......
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        //使用类加载器根据EnableAutoConfiguration的设置得到所有自动配置类的完全限定名
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), 
                              this.getBeanClassLoader());
        ......
    }
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }
    ......
}
public final class SpringFactoriesLoader {
    ......

    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    }

    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        ......
        //使用类加载器加载类路径下的META-INF/spring.factories文件
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
        ......
    }
    ......
}

SpringBoot(三阶段) - 图4

    spring boot在进行自动配置之前,会在容器中查看是否存在用户配置:
  • 如果不存在,使用自动配置;
  • 如果存在:spring boot会判断该配置是否只允许配置一个。只允许配置一个时会使用用户配置,可以配置多个时,就会将用户配置和自动配置组合到一起。

6. 启动tomcat端口占用问题

server:
  port: 8989                 #用来指定内嵌服务器端口号
  context-path: /springboot  #用来指定项目的访问路径

7. springboot相关注解说明

# Spring boot通常有一个名为 xxxApplication(项目名+Application)的类,入口类中有一个main方法, 在main方法中使用SpringApplication.run(xxxApplication.class,args)启动springboot应用的项目。

# @SpringBootApplication 注解等价于: 
    @SpringBootConfiguration            标识注解,标识这是一个springboot的配置类
    @EnableAutoConfiguration            自动与项目中集成的第三方技术进行集成
    @ComponentScan                         扫描入口类所在子包以及子包后代包中注解

8. springboot中配置文件的拆分

#说明: 在实际开发过程中生产环境和测试环境有可能是不一样的 因此将生产中的配置和测试中的配置拆分开,是非常必要的在springboot中也提供了配置文件拆分的方式. 这里以生产和开发测试端口号不一致为例:

#生产中端口:9999
#开发测试中端口为: 8888


#拆分如下:
spring:
  profiles:
    active: prod
server:
  servlet:
    context-path: /user
---
#开发测试
server:
  port: 8888
spring:
  profiles: dev
---
#生产
server:
  port: 9999
spring:
  profiles: prod

YAML

    YAML是一个可读性高,用来表达数据序列化的格式。
    YAML参考了其他多种语言,包括:C语言、Python、Perl,并从XML、电子邮件的数据格式(RFC 2822)中获得灵感。Clark Evans在2001年首次发表了这种语言,另外Ingy döt Net与Oren Ben-Kiki也是这语言的共同设计者。当前已经有数种编程语言或脚本语言支持(或者说解析)这种语言。
    YAML是"YAML Ain't a Markup Language"(YAML不是一种标记语言)的递归缩写。
    在开发的这种语言时,_YAML_ 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言),但为了强调这种语言以数据做为中心,而不是以标记语言为重点,而用反向缩略语重命名。

YAML语法

多行缩进
    数据结构可以用类似大纲的缩排方式呈现,结构通过缩进来表示,连续的项目通过减号“-”来表示,map结构里面的key-value对用冒号“:”来分隔。
    需求:描述学生信息
student:
    name: tom
    age: 20
    subjects:
        - mysql
        - java
        - html
    address:
        number: 1024
        street: 和平街
        district: 朝阳区
        city: 北京市
        country: 中国

注意:

  1. 字符串不一定要用双引号标识,如果要用,注意双引号和单引号的区别:单引号会转译特殊字符,去除其特殊含义,将其变为一个普通字符串;
  2. 在缩排中空白字符的数目并不是非常重要,只要相同阶层的元素左侧对齐就可以了(不能使用TAB字符);
  3. 允许在文件中加入选择性的空行,以增加可读性;
  4. 在一个档案中,可同时包含多个文件,并用“——”分隔;
  5. 选择性的符号“…”可以用来表示档案结尾(在利用串流的通讯中,这非常有用,可以在不关闭串流的情况下,发送结束讯号)。
    单行缩写
     YAML也有用来描述好几行相同结构的数据的缩写语法,数组用'[]'包括起来,hash用'{}'来包括。
     以上案例可使用单行缩写:
    
student: {name: tom,age: 20,subjects: [mysql,java,html],address: {number: 1024,street: 和平街,district: 朝阳区,city: 北京市,country: 中国}}

不同数据类型语法格式

value数据格式 value数据表示的是字面量数据,即string、number、boolean,以上数据可直接赋值,无特殊书写格式。 object/hash格式 有多行缩进和单行缩写两种语法。

#多行缩进
    student:
        name: tom
        age: 25
#单行缩写
    student: {name: tom,age: 25}

array/list/set格式 有多行缩进和单行缩写两种语法。

#多行缩进
    subjects:
        - mysql
        - java
        - html
#单行缩写
    subjects: [mysql,java,html]

获取配置文件的值

@ConfigurationProperties

YAML
    案例:获取application.yml文件内容。

步骤1:准备内容,

spring:
    profiles:
      active: prod

server:
    servlet:
      context-path: /user

student:
   name: tom
   age: 20
   parents: [jhon,kate]
   scores: {mysql: 80,java: 90,html: 100}
   sisters:
    - rose
    - anne
  dog: {name: 小白,age: 2}

---
server:
   port: 8888
spring:
   profiles: dev

---
server:
   port: 9999
spring:
   profiles: prod

步骤2:准备实体类对应配置文件中的内容(Student,Dog) Student.java

//将Student注册到spring容器
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
//@ConfigurationProperties用于将实体类属性与配置文件属性绑定
//当读取到配置文件内容,将获取的值注入Student对象属性
//prefix表示获取的内容从配置文件的哪个位置开始
@ConfigurationProperties(prefix = "student")
public class Student {
    private String name;
    private Integer age;
    private String[] parents;
    private Map<String,Integer> scores;
    private List<String> sisters;
    private Dog dog;
}

@ConfigurationProperties注解依赖于spring-boot-configuration-processor,需要在项目中导入该依赖,导入该依赖后,在配置文件中配置对应实体类的属性时,会有自动提示。

<!--导入配置文件处理器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

Dog.java

@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Dog{
    private String name;
    private Integer age;
}

步骤3:测试。重新启动spring boot,运行单元测试。

@Controller
@RequestMapping("/user")
public class UserController {
    @Autowired
    private Student student;
    @GetMapping("/hello")
    @ResponseBody
    public String hello(){
        System.out.println("hello springboot!!");
        System.out.println(student);
        return "hello springboot!!";
    }
}
    localhost:9999/user/user/hello,查看控制台,可看到配置文件中的值都被注入到实体类对象中。

9.springboot中创建自定义简单对象

9.1 管理单个对象

在springboot中可以管理自定义的简单组件对象的创建可以直接使用注解形式创建。

# 1.使用 @Repository  @Service @Controller 以及@Component管理不同简单对象
    如: 比如要通过工厂创建自定义User对象:
@Component
@Data//lombok注解:等价于类中的setter/getter和无参构造,要使用需导入lombok依赖
public class User {
  private String id;
  private String name;
  ......
}
    <!--lombok依赖-->
    <dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <version>1.18.12</version>
   <scope>provided</scope>
 </dependency>
# 2.通过工厂创建之后可以在使用处任意注入该对象
    如:在控制器中使用自定义简单对象创建
@Controller
@RequestMapping("hello")
public class HelloController {
    @Autowired
    private User user;
      ......
}

9.2 管理多个对象

在springboot中如果要管理复杂对象必须使用@Configuration + @Bean注解进行管理

# 1.管理复杂对象的创建
@Configuration(推荐)|@Component(不推荐)
public class Beans {
    @Bean
    public Calendar getCalendar(){
        return Calendar.getInstance();
    }
}
# 2.使用复杂对象
@Controller
@RequestMapping("hello")
public class HelloController {
    @Autowired
    private Calendar calendar;
    ......
}
# 注意: 
              1.@Configuration 配置注解主要用来生产多个组件交给工厂管理  (注册形式)
              2.@Component     用来管理单个组件                      (包扫描形式)

10.springboot中注入

springboot中提供了三种注入方式: 注入基本属性,对象注入

10.1 基本属性注入
# 1.@Value 属性注入   [重点]
@Controller
@RequestMapping("hello")
public class HelloController {
    @Value("${name}")
    private String name;
}
# 2.在配置文件中注入
name: xiaohei

10.2 对象方式注入
# 1. @ConfigurationProperties(prefix="前缀")
@Component
@Data
@ConfigurationProperties(prefix = "user")
public class User {
    private String id;
    private String name;
    private Integer age;
    private String  bir;
    .....
}
# 2. 编写配置文件
user:
  id: 24
  name: xiaohei
  age: 23
  bir: 2012/12/12
# 3. 引入依赖构建自定义注入元数据
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

11. springboot中两种模板配置

11.1 集成jsp模板

11.1.1 引入jsp的集成jar包
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>

<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
</dependency>

11.1.2 引入jsp运行插件
<build>
    <finalName>springboot_day1</finalName>
    <!--引入jsp运行插件-->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

11.1.3 配置视图解析器
#在配置文件中引入视图解析器
spring:
  mvc:
    view:
      prefix: /       # /代表访问项目中webapp中页面
      suffix: .jsp

11.1.4 第一种方式使用插件启动SpringBoot(三阶段) - 图5

11.1.5 第二种方式使用idea中指定工作目录启动 [推荐]SpringBoot(三阶段) - 图6

11.1.6 启动访问jsp页面
http://localhost:8989/cmfz/index.jsp

11.1.7 修改jsp无须重启应用
server.servlet.jsp.init-parameters.development=true

11.2 集成thymelaf模板

Thymeleaf是一个用于web和独立环境的现代服务器端Java模板引擎。 —摘自https://www.thymeleaf.org/

Thymeleaf是跟Velocity、FreeMarker类似的模板引擎,它可以完全替代JSP,相较与其他的模板引擎相比, Thymeleaf在有网络和无网络的环境下皆可运行,即它可以让美工在浏览器查看页面的静态效果,也可以让程序员在服务器查看带数据的动态页面效果。

注意: 1、thymeleaf和jsp模板只能同时使用其中之一,不要混用。 2、thymeleaf模板默认只能通过视图跳转,如果希望直接访问页面,需要进行设置。

11.2.1 引入依赖
    <!--使用thymelaf-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

11.2.2 编写配置
spring:
  profiles:
    active: dev
  thymeleaf:
    prefix: classpath:/templates/                    #使用模板目录
    suffix: .html                                    #使用模板后缀
    encoding: utf-8                                  #使用模板编码
    enabled: true                                    #启用thymelaf模板
    servlet:
      content-type: text/html                        #使用模板响应类型
  resources:
    static-locations: classpath:/static/             #指定静态资源目录

注:以上的thymeleaf配置全为默认配置,可以不写。

11.2.3 编写控制器测试
@Controller    //一定要是@Controller 不能再使用@RestController注解
@RequestMapping("user")
public class HelloController {
    @GetMapping("index")
    public String index(){
        System.out.println("测试与 thymeleaf 的集成");
        return "index";
    }
}

11.2.4 在templates目录中定义模板
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>首页</title>
    </head>
    <body>
        <h1 th:text="这是首页"/>
    </body>
</html>

11.2.5 测试访问
http://localhost:9999/user/index

11.2.6 查看结果

11.2.7 开启直接访问html页面
#在static-locations后加入classpath:/templates/即可,多个路径之间使用逗号隔开

11.2.8 测试
http://localhost:9999/user/index.html

直接访问静态页面时,由于没有经过thymeleaf模板转化,静态页面展现会有问题,比如中文乱码、静态资源无法引入等,此时需要进行对应配置才可解决对应问题。比如出现中文乱码时可配置国际化来解决。 一般来说,不建议直接访问静态页面,也就是说,不应该将templates目录设置静态资源目录。如果网站需要给用户访问时提供首页,可以在控制器中拦截index.html请求,通过视图跳转到index.hmtl。

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

11.3 Thymeleaf基本使用

使用时必须在页面中加入thymeleaf如下命名空间:

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

11.3.1 展示单个数据

a. 设置数据
model.addAttribute("name","张三"); 或 request.setAttribute("name","小黑");

b. 获取数据
<span th:text="${name}"/>  --->获取数据

c. 获取并解析含有html标签数据
model.addAttribute("name","<a href=''>张三</a>");
model.addAttribute("username","小陈");
  • 直接获取原样输出
<span th:text="${name}"/>
  • 获取并解析
 <span th:utext="${name}"/>
  • 将数据赋值给表单元素
    <input type="text" th:value="${username}"/>
    
# 总结
    1.使用 th:text="${属性名}"  获取对应数据,获取数据时会将对应标签中数据清空,因此最好是空标签
    2.使用 th:utext="${属性名}" 获取对应的数据,可以将数据中html先解析在渲染到页面
    3.使用 th:value="${属性名}" 获取数据直接作为表单元素value属性

11.3.2 展示对象数据
 model.addAttribute("user",new User("21","xiaochen",23,new Date()));
id:<span th:text="${user.id}"></span>
name:<span th:text="${user.name}"></span>
age:<span th:text="${user.age}"></span>
bir: <span th:text="${user.bir}"></span>  ====  <span th:text="${#dates.format(user.bir, 'yyyy-MM-dd HH:mm')}"></span> 日期格式化

11.3.3 条件展示数据
 model.addAttribute("user",new User("21","xiaochen",23,new Date()));
<span th:if="${user.age} eq 23">
  青年
</span>
# 运算符
    gt:great than(大于)>
    ge:great equal(大于等于)>=
    eq:equal(等于)==
    lt:less than(小于)<
    le:less equal(小于等于)<=
    ne:not equal(不等于)!=

11.3.4 展示多条数据
  • 直接遍历集合
 <ul th:each="user:${users}">
   <li th:text="${user.id}"></li>
   <li th:text="${user.name}"></li>
   <li th:text="${user.age}"></li>
   <li th:text="${#dates.format(user.bir,'yyyy-MM-dd')}"></li>
</ul>
  • 遍历时获取遍历状态
 <ul th:each="user,userStat:${users}">
   <li><span th:text="${userStat.count}"/>-<span th:text="${user.id}"/></li>   获取遍历次数  count 从1开始 index 从0开始
   <li><span th:text="${userStat.odd}"/>-<span th:text="${user.name}"/></li>   获取当前遍历是否是奇数行
   <li><span th:text="${userStat.even}"/>-<span th:text="${user.age}"/></li>   获取当前遍历是否是偶数行
   <li><span th:text="${userStat.size}"/>-<span th:text="${user.bir}"/></li>   获取当前集合的总条数
</ul>

11.3.5 引入静态资源

使用thymeleaf模板项目中静态资源默认放在resources路径下static目录中

  • 项目中放入对应静态资源
  • 页面中引入
    <link rel="stylesheet" th:href="@{/css/index.css}">
    <script th:src="@{/js/jquery-3.3.1.js}"></script>
    

12. springboot集成mybatis

12.1 引入依赖
<!--整合mybatis-->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.3</version>
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.1.12</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.38</version>
</dependency>

>说明:由于springboot整合mybatis版本中默认依赖mybatis 因此不需要额外引入mybati版本,否则会出现冲突

12.2 配置文件
spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///mydb
    username: root
    password: root
  thymeleaf:
    servlet:
      content-type: text/html
    encoding: UTF-8
    prefix: classpath:/templates/
    suffix: .html
    enabled: true

11.3 加入mybatis配置
#配置文件中加入如下配置:

mybatis:
  mapper-locations: classpath:com/wsjy/mapper/*.xml  #指定mapper配置文件位置
  type-aliases-package: com.wsjy.entity              #指定别名的包

logging:
  level:
    root: info #根日志
    com.wsjy.dao: debug #子日志:指定记录某个包的日志,打印SQL
//入口类中加入如下配置:
@SpringBootApplication
@MapperScan("com.wsjy.dao")   //必须在入口类中加入这个配置
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

12.4 建表
CREATE TABLE `t_user` (
  `uid` int(4) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(50) NOT NULL,
  PRIMARY KEY (`uid`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8

12.5 开发实体类
@Component
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User implements Serializable {
    private Integer uid;
    private String userName;
    private String password;
}

12.6 开发DAO接口以及Mapper(resources/com/wsjy/mapper/userMapper.xml)
public interface UserDao {
    List<User> findAll();
    User findByUid(Integer uid);
}
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wsjy.dao.UserDao">
    <select id="findAll" resultType="User">
        select * from t_user
    </select>

    <select id="findByUid" resultType="User" parameterType="int">
        select * from t_user where uid=#{uid}
    </select>
</mapper>

12.7 开发Service以及实现
public interface UserService {
    List<User> getAllUsers();
    User getUserByUid(Integer uid);
}
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;
    @Override
    public List<User> getAllUsers() {
        return userDao.findAll();
    }

    @Override
    public User getUserByUid(Integer uid) {
        return userDao.findByUid(uid);
    }
}

12.8 开发controller
@Controller
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("findByUid/{uid}")
    @ResponseBody
    public User findByUid(@PathVariable("uid") Integer uid, Model model){
        User user = userService.getUserByUid(uid);
        return user;
    }
    @GetMapping("findAll")
    @ResponseBody
    public List<User> findAll(Model model){
        List<User> allUsers = userService.getAllUsers();
        return allUsers;
    }

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

}

12.9 编写前端:

resouces/static/js/jquery-3.3.1.js

resources/css/index.css

div{
    width: 800px;
    height: 800px;
    border: 1px solid black;
}

resources/templates/index.html

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>首页</title>
    <link rel="stylesheet" th:href="@{/css/index.css}"/>
    <script type="text/javascript" th:src="@{/js/jquery-3.3.1.js}"></script>
</head>
<body>

    <a id="byId" href="javascript:void(0)">查询用户编号为1的用户信息</a>
    <a id="all" href="javascript:void(0)">查询所有用户信息</a>
    <div id="content"></div>
    <script>
        $(function () {
            $("#byId").click(function () {
                $.getJSON("findByUid/1",function (data) {
                    // var user=JSON.parse(data)
                    $("#content").append("<ul><li>用户编号:"+data.uid+"</li>" +
                        "<li>用户名称:"+data.userName+"</li>" +
                        "<li>用户密码:"+data.password+"</li></ul>")
                })
            })
            $("#all").click(function () {
                $.getJSON("findAll",function (data) {
                    for (var i = 0; i <data.length ; i++) {
                        var ul=$("<ul><li>用户编号:"+data[i].uid+"</li>" +
                            "<li>用户名称:"+data[i].userName+"</li>" +
                            "<li>用户密码:"+data[i].password+"</li></ul>")
                        $("#content").append(ul)
                    }
                })
            })
        })
    </script>
</body>
</html>

13.springboot集成redis

redis : NoSQL 作用: 1、缓存常用数据 2、 进行快速读/写 ( 商品抢购、抢红包…… ) 是否缓存到redis的判断依据: 1、业务数据是否常用?命中率高不高? 常用,命中高—>redis 2、业务数据读操作多还是写操作多?读操作多—>redis 3、业务数据的大小?综合考虑

13.1 引入依赖
    <dependencies>
        <!--spring-data-redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--thymeleaf-->
        <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>

        <!--mybatis-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.3</version>
        </dependency>

        <!--mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.17</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--druid-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.22</version>
        </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>

13.2 配置文件
server:
  servlet:
    context-path: /subject
  port: 8888

spring:
  datasource:     #配置数据源
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///taotao
    username: root
    password: root
  thymeleaf:      #配置thymeleaf模板
    servlet:
      content-type: text/html
    encoding: UTF-8
    prefix: classpath:/templates/
    suffix: .html
    enabled: true
  redis:          #配置redis
    database: 0
    host: localhost
    port: 6379
    password:

mybatis:          #mybatis配置
  type-aliases-package: com.wsjy.domain
  mapper-locations: classpath:/com/wsjy/mapper/*.xml

注意:启动类上要加@MapperScan(“dao包全限定名”)

13.3 开发实体类

Subject.java

//注意:要使用redis缓存实体类对象,必须让实体类实现序列化接口,否则在redis序列化过程中会报错
@Data
@Accessors(chain = true)
public class Subject implements Serializable {
    private Integer sub_no;
    private String sub_name;
}

13.4 开发DAO接口以及Mapper(resources/com/wsjy/mapper/subjectMapper.xml)

SubjectDao.java

public interface SubjectDao {
    List<Subject> findAll();
}

subjectMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wsjy.dao.SubjectDao">
    <select id="findAll" resultType="Subject">
        select * from t_subject
    </select>
</mapper>

13.5 开发service及实现
public interface SubjectService {
    List<Subject> getAllSubjects();
}
@Service
public class SubjectServiceImpl implements SubjectService {
    @Autowired
    private SubjectDao subjectDao;

    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Override
    public List<Subject> getAllSubjects() {        
        List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
        if (null == subs) {//从数据库中获取            
            System.out.println("mysql");
            subs=subjectDao.findAll();
            redisTemplate.opsForValue().set("subs",subs);            
        }else{//从redis中获取
            System.out.println("redis");
        }
        return subs;
    }
}

13.6 开发controller
@Controller
public class SubjectController {
    @Autowired
    private SubjectService subjectService;

    @GetMapping("/getAll")
    public String getAll(Model model){
        List<Subject> subs = subjectService.getAllSubjects();
        model.addAttribute("subs",subs);
        return "index";
    }
}

13.7 编写前端:

index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <table border="1" align="center">
        <tr th:each="sub:${subs}">
            <td th:text="${sub?.sub_no}"></td>
            <td th:text="${sub?.sub_name}"></td>
        </tr>
    </table>
</body>
</html>

13.8 测试
- http://localhost:8888/subject/getAll

13.9 修改序列化器

测试访问后,使用keys *命令查看,可见redis的0号库中已经存在了一个key:”\xAC\xED\x00\x05t\x00\x04subs”,不便于阅读,这是由于redis默认使用了JdkSerializationRedisSerializer序列化器,如果希望看到key为subs,需要更改redis序列化器为StringRedisSerializer。

@Service
public class SubjectServiceImpl implements SubjectService {
    @Autowired
    private SubjectDao subjectDao;
    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Override
    public List<Subject> getAllSubjects() {
        //创建序列化器
        RedisSerializer serializer = new StringRedisSerializer();
        //设置redis序列化策略:StringRedisSerializer
        redisTemplate.setKeySerializer(serializer);
        List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
        if (null == subs) {//从数据库中获取            
            System.out.println("mysql");
            subs=subjectDao.findAll();
            redisTemplate.opsForValue().set("subs",subs);            
        }else{//从redis中获取
            System.out.println("redis");
        }
        return subs;
    }
}

13.10 高并发访问时,如何保证只查询一次数据库?

当系统出现高并发访问时,以上代码无法保证只查询一次数据库,其余请求到redis中查询。

@Controller
public class SubjectController {
    @Autowired
    private SubjectService subjectService;

    @GetMapping("/getAll")
    public String getAll(Model model){
//        List<Subject> subs = subjectService.getAllSubjects();
//        model.addAttribute("subs",subs);
        //模拟10000个线程同时发送请求
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        for (int i = 0; i <10000; i++) {
            executorService.submit(()->subjectService.getAllSubjects());
        }
        return "index";
    }
}
    执行测试,可看到控制台中输出了大量的mysql,证明高并发下,确实是无法保证只查询数据库一次的。要确保redis中subs为空也只有一个线程去查询数据库,其他的线程全从redis中获取数据,可使用线程同步来解决。
@Service
public class SubjectServiceImpl implements SubjectService {
    @Autowired
    private SubjectDao subjectDao;
    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;

    @Override
    public List<Subject> getAllSubjects() {
        List<Subject> subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
        //使用双重检测锁来保证多线程并发时,只有一个线程去数据库中查询,其他的线程都去redis缓存中获取
        if (null == subs) {
            synchronized (this){
                subs = (List<Subject>) redisTemplate.opsForValue().get("subs");
                if (null == subs) {
                    System.out.println("mysql");
                    subs=subjectDao.findAll();
                    redisTemplate.opsForValue().set("subs",subs);
                }else{
                    System.out.println("redis");
                }
            }
        }else{
            System.out.println("redis");
        }
        return subs;
    }
}

13.11 其它redis操作
/*新增*/
//controller
@GetMapping("/addSubject/{sub_no}/{sub_name}")
    public String addSubject(@PathVariable("sub_no")Integer sub_no,
                             @PathVariable("sub_name")String sub_name,
                             Model model){
        subjectService.addSubject(sub_no,sub_name);
        return getAll(model);
    }
//service
public void addSubject(Integer sub_no,String sub_name){
        RedisSerializer serializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(serializer);
        List<Subject> subs = getAllSubjects();
        subs.add(new Subject().setSub_no(sub_no).setSub_name(sub_name));
        redisTemplate.opsForValue().set("subs",subs);
        subjectDao.addSubject(sub_no,sub_name);
    }
//dao
void addSubject(@RequestParam("sub_no") Integer sub_no,
                @RequestParam("sub_name") String sub_name);
//xml映射文件
    <insert id="addSubject">
        insert into t_subject(sub_no,sub_name) values(#{sub_no},#{sub_name})
    </insert>

/*修改*/
//controller
@GetMapping("/updateSubject/{sub_no}/{sub_name}")
    public String updateSubject(@PathVariable("sub_no")Integer sub_no,
                                @PathVariable("sub_name")String sub_name,
                                Model model){
        subjectService.updateSubject(sub_no,sub_name);
        return getAll(model);
}
//service
public void updateSubject(Integer sub_no,String sub_name){
        List<Subject> subs = getAllSubjects();
        for (Subject sub : subs) {
            if (sub_no.equals(sub.getSub_no())) {
                sub.setSub_name(sub_name);
                break;
            }
        }
        redisTemplate.opsForValue().set("subs",subs);
        subjectDao.updateSubjectBySub_no(sub_no,sub_name);
    }
//dao
void updateSubjectBySub_no(@RequestParam("sub_no") Integer sub_no,
                           @RequestParam("sub_name") String sub_name);
//xml映射文件
    <update id="updateSubjectBySub_no">
        update t_subject set sub_name=#{sub_name} where sub_no=#{sub_no}
    </update>      

/*删除*/
//controller
@GetMapping("/delete/{key}")
    public String delete(@PathVariable("key") String key){
        subjectService.deleteRedisContent(key);
        return "index";
    }
//service
public void deleteRedisContent(String key){
        RedisSerializer serializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(serializer);
        redisTemplate.delete(key);
    }
注意:如果缓存到redis中时设置了序列化策略,则在删除时也必须设置相同的序列化策略,否则redisTemplate会使用默认的JdkSerializationRedisSerializer,这会导致程序执行成功,但无法删除redis键的情况出现。

14.开启jsp页面热部署

14.1 引言

在springboot中默认对jsp运行为生产模式,不允许修改内容保存后立即生效,因此在开发过程需要调试jsp页面每次需要重新启动服务器这样极大影响了我们的效率,为此springboot中提供了可以将默认的生产模式修改为调试模式,改为调试模式后就可以保存立即生效,如何配置为测试模式需要在配置文件中加入如下配置即可修改为开发模式。

14.2 配置开启测试模式
server:
  port: 8989
  jsp-servlet:
    init-parameters:
      development: true  #开启jsp页面的调试模式

15.springboot中devtools热部署

15.1 引言
`为了进一步提高开发效率,springboot为我们提供了全局项目热部署,日后在开发过程中修改了部分代码以及相关配置文件后,不需要每次重启使修改生效,在项目中开启了springboot全局热部署之后只需要在修改之后等待几秒即可使修改生效。`

15.2 开启热部署

15.2.1 项目中引入依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-devtools</artifactId>
   <optional>true</optional>
</dependency>

15.2.2 设置idea中支持自动编译
# 1.开启自动编译

    Preferences | Build, Execution, Deployment | Compiler -> 勾选上 Build project automatically 这个选项

# 2.开启允许在运行过程中修改文件
    ctrl + alt + shift + / ---->选择1.Registry ---> 勾选 compiler.automake.allow.when.app.running 这个选项

15.2.3 启动项目检测热部署是否生效
# 1.启动出现如下日志代表生效
2019-07-17 21:23:17.566  INFO 4496 --- [  restartedMain] com.wsjy.InitApplication               : Starting InitApplication on chenyannandeMacBook-Pro.local with PID 4496 (/Users/chenyannan/IdeaProjects/ideacode/springboot_day1/target/classes started by chenyannan in /Users/chenyannan/IdeaProjects/ideacode/springboot_day1)
2019-07-17 21:23:17.567  INFO 4496 --- [  restartedMain] com.wsjy.InitApplication               : The following profiles are active: dev
2019-07-17 21:23:17.612  INFO 4496 --- [  restartedMain] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@66d799c5: startup date [Wed Jul 17 21:23:17 CST 2019]; root of context hierarchy
2019-07-17 21:23:18.782  INFO 4496 --- [  restartedMain] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8989 (http)
2019-07-17 21:23:18.796  INFO 4496 --- [  restartedMain] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2019-07-17 21:23:18.797  INFO 4496 --- [  restartedMain] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.20

注意:日志出现restartedMain代表已经生效,在使用热部署时如果遇到修改之后不能生效,请重试重启项目在试

16. logback日志的集成

16.1 logback简介

Logback是由log4j创始人设计的又一个开源日志组件。目前,logback分为三个模块:logback-core,logback-classic和logback-access。是对log4j日志展示进一步改进

16.2 日志的级别
> DEBUG < INFO < WARN < ERROR < OFF
>
> 日志级别由低到高:  `日志级别越高输出的日志信息越少`

16.3 项目中日志分类
> 日志分为两类
>
>  一种是rootLogger :     用来监听项目中所有的运行日志 包括引入依赖jar中的日志 
>
>  一种是logger :         用来监听项目中指定包中的日志信息

16.4 java项目中使用

16.4.1 logback配置文件
    logback的配置文件必须放在项目根目录中 且名字必须为logback.xml。
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
    <!--定义项目中日志输出位置-->
    <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
        <!--定义项目的日志输出格式-->
        <!--定义项目的日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern> [%p] %d{yyyy-MM-dd HH:mm:ss} %m %n</pattern>
        </layout>
    </appender>

    <!--项目中root日志控制-->
    <root level="INFO">
        <appender-ref ref="stdout"/>
    </root>
    <!--项目中指定包日志控制-->
    <logger name="com.wsjy.dao" level="DEBUG"/>

</configuration>

16.4.2 具体类中使用日志

创建日志对象

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Controller
@RequestMapping("/hello")
public class HelloController {
    //声明日志成员
    private static Logger logger = LoggerFactory.getLogger(HelloController.class);
    @RequestMapping("/hello")
    @ResponseBody
    public String hello(){
        System.out.println("======hello world=======");
        logger.debug("DEBUG");
        logger.info("INFO");
        logger.warn("WARN");
        logger.error("ERROR");
        return "hello";
    }
}

使用lombok

@Controller
@RequestMapping("/hello")
@Slf4j //使用该注解后,可在类中直接使用log对象记录日志
public class HelloController {
    @RequestMapping("/hello")
    @ResponseBody
    public String hello(){
        System.out.println("======hello world=======");
        log.debug("DEBUG");
        log.info("INFO");
        log.warn("WARN");
        log.error("ERROR");
        return "hello";
    }
}

16.4.3 使用默认日志配置
    如果只需要记录程序运行日志(即程序中不使用logger对象主动记录日志),则无需配置logback.xml,只需要在springboot配置文件中进行以下配置即可。
logging:
  level:
    root: info #根日志
    com.wsjy.dao: debug #子日志:指定记录某个包的日志
  file:
    path: E:/logs #指定日志文件所在目录,默认日志文件名为spring.log

17. 切面编程

17.1 引言

springboot是对原有项目中spring框架和springmvc的进一步封装,因此在springboot中同样支持spring框架中AOP切面编程,不过在springboot中为了快速开发仅仅提供了注解方式的切面编程.

17.2 使用

17.2.1 引入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

17.2.2 相关注解
/**
    @Aspect 用来类上,代表这个类是一个切面
    @Before 用在方法上代表这个方法是一个前置通知方法 
    @After 用在方法上代表这个方法是一个后置通知方法
    @Around 用在方法上代表这个方法是一个环绕的方法
**/

17.2.3 前置切面
@Aspect
@Component
public class MyAspect {
    @Before("execution(* com.wsjy.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("前置通知");
        joinPoint.getTarget();//目标对象
        joinPoint.getSignature();//方法签名
        joinPoint.getArgs();//方法参数
    }
}

17.2.4 后置切面
@Aspect
@Component
public class MyAspect {
    @After("execution(* com.wsjy.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        System.out.println("后置通知");
        joinPoint.getTarget();//目标对象
        joinPoint.getSignature();//方法签名
        joinPoint.getArgs();//方法参数
    }
}
> **注意: 前置通知和后置通知都没有返回值,方法参数都为joinpoint**

17.2.5 环绕切面
@Aspect
@Component
public class MyAspect {
    @Around("execution(* com.wsjy.service.*.*(..))")
    public Object before(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        System.out.println("进入环绕通知");
        proceedingJoinPoint.getTarget();//目标对象
        proceedingJoinPoint.getSignature();//方法签名
        proceedingJoinPoint.getArgs();//方法参数
        Object proceed = proceedingJoinPoint.proceed();//放行执行目标方法
        System.out.println("目标方法执行之后回到环绕通知");
        return proceed;//返回目标方法返回值
    }
}

注意: 环绕通知存在返回值,参数为ProceedingJoinPoint,如果不执行放行,不会执行目标方法,一旦放行必须将目标方法的返回值返回,否则调用者无法接受返回数据


18. 文件上传下载

18.1 文件上传

定义:用户访问当前系统,将自己本地计算机中文件通过浏览器上传到当前系统所在的服务器过程中称之为文件的上传

18.1.1 准备上传页面

upload.html

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>文件上传</title>
</head>
<body>
    <form th:action="@{upload}" method="post" enctype="multipart/form-data">
        <input type="file" name="upload"/>
        <input type="submit" value="文件上传"/>
    </form>
</body>
</html>
<!--
    1. 表单提交方式必须是post
    2. 表单的enctype属性必须为multipart/form-data
    3. 后台接受变量名字要与文件选择name属性一致
-->

uploadSuccess.html

<html>
<head>
    <title>上传成功</title>
</head>
<body>
    上传成功!!!
</body>
</html>

18.1.2 编写控制器
    @GetMapping("upload.html")
    public String upload(){
        return "upload";
    }

    //MultipartFile upload:该变量名必须和<input type="file" name="upload"/>的name属性保持一致
    @PostMapping("upload")
    public String excuteUpload(MultipartFile upload, HttpServletRequest request) throws IOException {
        //获取上传文件的真实文件名
        String realName = upload.getOriginalFilename();
        //设置上传文件存放的目录:target/classes/static/files
        String realPath = ResourceUtils.getURL("classpath:").getPath() + "static/files";
        System.out.println(realPath);
        //按日期存放上传文件的日期目录
        String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
        //构建上传文件真实存放目录
        File uploadDir = new File(realPath,dateDir);
        if (!uploadDir.exists()) {//目录不存在时,创建
            uploadDir.mkdirs();
        }
        //设置上传文件前缀:时间戳+UUID
        String uploadFileNamePrefix=new SimpleDateFormat("yyyyMMddhhmmssSSS").format(new Date())+UUID.randomUUID().toString().replace("-","");
        //得到上传文件后缀
        String uploadFileNameSuffix = FilenameUtils.getExtension(realName);
        //构建上传到服务器的文件名
        String uploadFileName=uploadFileNamePrefix+"."+uploadFileNameSuffix;
        //将文件上传到服务器
        upload.transferTo(new File(uploadDir,uploadFileName));
        return "uploadSuccess";
    }

18.1.3 修改文件上传大小
#上传时出现如下异常:  上传文件的大小超出默认配置  默认10M
nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException: the request was rejected because its size (38443713) exceeds the configured maximum (10485760)
#修改上传文件大小:
spring:
  profiles:
    active: dev
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql:///taotao
    username: root
    password: root
  thymeleaf:
    servlet:
      content-type: text/html
    encoding: UTF-8
    prefix: classpath:/templates/
    suffix: .html
    enabled: true
  servlet: 
    multipart: #设置上传文件大小
      max-file-size: 500MB
      max-request-size: 500MB

18.2 文件下载
    在resources/static/files目录中存放spring.log文件。

18.2.1 提供下载文件链接
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>文件下载</title>
</head>
<body>
    <a th:href="@{download(fileName=spring.log)}">spring.log</a>
</body>
</html>

18.2.2 开发控制器
    @GetMapping("download.html")
    public String download(){
        return "download";
    }

    //注意,文件下载无需跳转,因此不能设置方法返回值
    @GetMapping("download")
    public void executeDownload(String fileName/*下载文件名称,前端传入*/, HttpServletResponse response) throws IOException {
        //获取下载文件路径
        String realPath = ResourceUtils.getURL("classpath:").getPath() + "static/files";
        //构建下载文件
        File downloadFile = new File(realPath,fileName);
        //得到文件输入流
        FileInputStream is = new FileInputStream(downloadFile);
        //设置响应头,指定以附件形式下载
        response.setHeader("content-disposition","attachment;fileName="+fileName);
        //得到响应输出流
        ServletOutputStream os = response.getOutputStream();
        //文件复制
        IOUtils.copy(is,os);
        //释放资源
        IOUtils.closeQuietly(is);
        IOUtils.closeQuietly(os);
    }

19.验证码

19.1 准备验证码页面

verifycode.html

<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <title>验证码</title>
    <script type="text/javascript" th:src="@{/js/jquery-3.3.1.js}"></script>
</head>
<body>
<input id="verifyCodeInput"  name="verifyInput" placeholder="请输入验证码">
<img id="verifyCodeImg" th:src="@{getVerifyCode}">
<script>
    $(function () {
        $("#verifyCodeImg").click(function () {
            var src = "getVerifyCode?"+new Date().getTime(); //加时间戳,防止浏览器利用缓存
            $(this).attr("src",src);
        })

        $("#verifyCodeInput").change(function () {
            var val=$(this).val();
            console.log(val)
            $.getJSON("checkVerifyCode/"+val,function (data) {
                if (data) {
                    alert("验证成功")
                }else{
                    alert("验证失败")
                }
            })
        })
    })
</script>
</body>
</html>

19.2 编写工具类

VerifyCode.java

package com.wsjy.utils;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

public class VerifyCode {
    public static  String drawRandomText(int width, int height, BufferedImage verifyImg) {
        Graphics2D graphics = (Graphics2D)verifyImg.getGraphics();
        graphics.setColor(Color.WHITE);//设置画笔颜色-验证码背景色
        graphics.fillRect(0, 0, width, height);//填充背景
        graphics.setFont(new Font("微软雅黑", Font.BOLD, 40));
        //数字和字母的组合
        String baseNumLetter= "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ";
        StringBuffer sBuffer = new StringBuffer();
        int x = 10;  //旋转原点的 x 坐标
        String ch = "";
        Random random = new Random();
        for(int i = 0;i < 4;i++){
            graphics.setColor(getRandomColor());
            //设置字体旋转角度
            int degree = random.nextInt() % 30;  //角度小于30度
            int dot = random.nextInt(baseNumLetter.length());
            ch = baseNumLetter.charAt(dot) + "";
            sBuffer.append(ch);
            //正向旋转
            graphics.rotate(degree * Math.PI / 180, x, 45);
            graphics.drawString(ch, x, 45);
            //反向旋转
            graphics.rotate(-degree * Math.PI / 180, x, 45);
            x += 48;
        }
        //画干扰线
        for (int i = 0; i <6; i++) {
            // 设置随机颜色
            graphics.setColor(getRandomColor());
            // 随机画线
            graphics.drawLine(random.nextInt(width), random.nextInt(height),
                              random.nextInt(width), random.nextInt(height));
        }
        //添加噪点
        for(int i=0;i<30;i++){
            int x1 = random.nextInt(width);
            int y1 = random.nextInt(height);
            graphics.setColor(getRandomColor());
            graphics.fillRect(x1, y1, 2,2);
        }
        return sBuffer.toString();
    }
    /**
     * 随机取色
     */
    private static Color getRandomColor() {
        Random ran = new Random();
        Color color = new Color(ran.nextInt(256),
                ran.nextInt(256), ran.nextInt(256));
        return color;
    }
}

19.3 编写控制器
    @GetMapping("verifycode.html")
    public String verifyCode(){
        return "verifycode";
    }
    //生成验证码
    @RequestMapping("getVerifyCode")
    public void getVerificationCode(HttpServletResponse response,HttpServletRequest request) {
        try {
            int width=200;
            int height=69;
            BufferedImage verifyImg=new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
            //生成对应宽高的初始图片
            String randomText = VerifyCode.drawRandomText(width,height,verifyImg);
            //将生成的验证存入session,以备验证时使用
            request.getSession().setAttribute("verifyCode", randomText);
            response.setContentType("image/png");//必须设置响应内容类型为图片,否则前台不识别
            OutputStream os = response.getOutputStream(); //获取文件输出流
            ImageIO.write(verifyImg,"png",os);//输出图片流
            os.flush();
            os.close();//关闭流
        } catch (IOException e) {
            log.error(e.getMessage());
            e.printStackTrace();
        }
    }
    //验证
    @GetMapping("checkVerifyCode/{verifyCode}")
    @ResponseBody
    public boolean checkVerifyCode(@PathVariable("verifyCode") String verifyCode,HttpServletRequest request){
        String sessionVerifyCode = (String) request.getSession().getAttribute("verifyCode");
        if (sessionVerifyCode != null) {
            if (sessionVerifyCode.equalsIgnoreCase(verifyCode)) {
                return true;
            }else{
                return false;
            }
        }
        return false;
    }

20. 拦截器

    使用拦截器完成登录验证。

    拦截器由springMVC提供,只需要将拦截器注册到springboot的拦截器数组中即可正常使用。

20.1开发登录业务

UserDao.java

int findUserByUserNameAndPassword(String username, String password);

userMapper.xml

<select id="findUserByUserNameAndPassword" parameterType="string" resultType="int">
    select count(*) from t_user where username=#{username} and password=#{password}
</select>

UserService.java

boolean login(String username, String password);

UserServiceImpl.java

    public boolean login(String username, String password) {
        int count=userDao.findUserByUserNameAndPassword(username,password);
        if (count > 0) {
            return true;
        }
        return false;
    }

UserController.java

    @GetMapping("login.html")
    public String toLogin(){
        return "login";
    }
    @GetMapping("login")
    public String login(String username, String password, HttpSession session){
        boolean flag = userService.login(username, password);
        if (flag) {
            session.setAttribute("user",username);
            return "redirect:/index.html";
        }
        return "redirect:/login.html";
    }

20.2 开发拦截器
package com.wsjy.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //进行登录验证
        String user = (String) request.getSession().getAttribute("user");
        if (user != null) {
            return true;
        }
        //request.getRequestDispatcher("login.html").forward(request,response);
        response.sendRedirect("login.html");
        return false;
    }
}

20.3 注册拦截器
package com.wsjy.config;

import com.wsjy.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //注册拦截器
        registry.addInterceptor(new MyInterceptor())
        //添加拦截的请求路径
            .addPathPatterns("/**")
        //排除不拦截的请求路径
              .excludePathPatterns("/login","/login.html","/js/**","/css/**","/files/**");
    }

    //自定义拦截器后,如果出现静态资源无法访问(404)的情况,可以进行以下配置来解决
    /* 
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/templates/");
    }
    */
}

21. war包部署

21.1 设置打包方式为war
**war**

21.2 在插件中指定入口类
<build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <!--使用热部署出现中文乱码解决方案-->
        <configuration>
          <fork>true</fork>
          <!--增加jvm参数-->
          <jvmArguments>-Dfile.encoding=UTF-8</jvmArguments>
          <!--指定入口类-->
          <mainClass>com.wsjy.Application</mainClass>
        </configuration>
      </plugin>
    </plugins>
</build>

21.3 排除内嵌的tomcat
    springboot开发时使用的是内嵌tomcat,因此,使用war包进行部署时,必须排除内嵌tomcat,否则在部署到tomcat时,会出现web容器嵌套,导致错误。
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-tomcat</artifactId>
  <scope>provided</scope>   <!--去掉内嵌tomcat-->
</dependency>

<dependency>
  <groupId>org.apache.tomcat.embed</groupId>
  <artifactId>tomcat-embed-jasper</artifactId>
  <scope>provided</scope>  <!--去掉使用内嵌tomcat解析jsp-->
</dependency>

21.4 配置入口类
//1.继承SpringBootServletInitializer
//2.覆盖configure方法
public class Application extends SpringBootServletInitializer{
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(Application.class);
    }
}

21.5 打包测试
/* 一旦使用war包部署注意:
    1. application.yml 中配置port context-path 失效
    2. 访问时使用打成war包的名字和外部tomcat端口号进行访问项目
*/

22. jar部署

    springboot推荐jar包部署,即项目无需进行任何修改,打包成jar包后,可通过以下命令运行项目:
java -jar jar包完整路径
    需要注意的是,springboot的jar包部署方式可以应对绝大部分场景,唯独在项目中有文件上传时,若使用之前的代码会报错,因为在项目运行时,无法将文件上传到jar包中。

    要解决这个问题,可将文件上传目录指定映射到系统中某个目录,当进行文件上传操作时,使用对应目录来保存文件。

22.1 配置上传映射路径
spring:
  resources:
    static-locations: file:${upload.dir} #配置静态文件路径
upload:
  dir: E:/logs #指定文件上传目录

22.2 修改文件上传代码

UserController.java

    @Value("${upload.dir}")//属性注入:使用配置文件中的值
    private String uploadStr;

    @PostMapping("upload")
    public String executeUpload(MultipartFile upload, HttpServletRequest request) throws IOException {

        //获取上传文件的真实文件名
        String realName = upload.getOriginalFilename();
        //设置上传文件存放的目录:resources/static/files
//        String realPath = ResourceUtils.getURL("classpath:").getPath() + "static/files";
        String realPath = uploadStr+"/files";//使用系统目录映射上传文件存放路径
        //按日期存放上传文件的日期目录
        String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
        //构建上传文件真实存放目录
        File uploadDir = new File(realPath,dateDir);
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }
        //设置上传文件前缀:时间戳+UUID
        String uploadFileNamePrefix=new SimpleDateFormat("yyyyMMddhhmmssSSS").format(new Date())+
                UUID.randomUUID().toString().replace("-","");
        //得到上传文件后缀
        String uploadFileNameSuffix = FilenameUtils.getExtension(realName);
        //构建上传到服务器的文件名
        String uploadFileName=uploadFileNamePrefix+"."+uploadFileNameSuffix;
        //将文件上传到服务器
        upload.transferTo(new File(uploadDir,uploadFileName));
        return "uploadSuccess";
    }

InterceptorConfig.java

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Value("${upload.dir}")//属性注入:使用配置文件中的值
    private String uploadStr;

    ......

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/templates/")
                .addResourceLocations("file:"+uploadStr);
    }
}

22.3 测试
    使用maven重新打包后,前往项目target目录,使用java -jar 运行对应项目,执行文件上传,正常运行,在对应系统目录中可查看上传的文件。