从零入门SpringBoot&整合MyBatis

4 篇文章 0 订阅
2 篇文章 0 订阅

回顾架构现在与未来

三层架构  +  MVC
	架构---> 解耦
开发框架
|
Spring
		IOC:控制反转
			原来我们要一步一步去做,现在直接交给IOC容器去做,我们需要什么拿什么就可以了
		AOP:切面(本质:动态代理)
			为了解决什么?不影响业务本来的情况下,实现动态增加功能,大量应用在日志,业务...等等
	Spring是一个轻量级的Java开源框架,容器
	目的:解决企业开发的复杂性问题
	Spring是"春天",但也是"冬天",配置文件十分复杂;
	
SpringBoot
	SpringBoot并不是新东西,就是Spring的升级版!
	新一代的JavaEE开发标准,开箱即用!-->拿过来就可以用
	它自动帮我们配置了非常多的东西,我们拿来即用!
	特性:约定大于配置!

随着公司体系越来越大,用户越来越多
|
微服务架构---->新架构
	模块化,功能化!
	用户,支付,签到.娱乐,.....;
	人过于多,一套服务器解决不了; 在增加服务器 !    横向
	假设A服务器占用98%资源,B服务器只占用10%    ----负载均衡;
	
	将原来的整体项目,分成模块化,用户就是一个单独的项目,签到也是,项目与项目之间需要通信,如何通信?
	用户非常多,而签到十分少!  给用户多一点服务器,给签到少一点服务器!

微服务架构问题?
	分布式架构会遇到的四个核心问题
	1.这么多服务,客户端该如何去访问?
	2.这么多服务,服务之间如何进行通信?
	3.这么多服务,如何治理呢?
	4.服务挂了,怎么办?
解决方案:
	SpringCloud,是一套生态,就是来解决以上分布式架构的4个问题
	想使用SpringCloud,必须要掌握SpringBoot,因为SpringCloud是基于SpringBoot的;

	1.Spring Cloud NetFlix,出来了一套解决方案! 一站式解决方案!我们都可以直接去这里拿
		API网关: zuul组件
		Feign---->HttpClient---->Http的通信方式,同步并阻塞
		服务注册与发现: Eureka
		熔断机制: Hystrix

		2018年年底,NetFlix宣布无限期停止维护.生态不再维护,就会脱节
	2.Apache Dubbo zookeeper,第二套解决方案
		API: 没有!要么找第三方组件,要么自己实现
		Dubbo是一个高性能的基于Java实现的 RPC 通信框架!
		服务注册与发现: zookeeper-动物园管理者
		熔断机制: 没有!借助了Hystrix
		不完善,dubbo

	3.SpringCloud Alibaba 一站式解决方案!
	
目前,又提出了一种新的方案
	服务网格:下一代微服务标准,Server Mesh
	代表解决方案:istio(未来可能需要掌握)
	
万变不离其宗,一通百通
	1.API网关,服务路由;
	2.HTTP,RPC框架,异步调用
	3.服务注册与发现,高可用
	4.熔断机制,服务降级

为什么要解决这个问题?本质:网络是不可靠的!

SpringBoot

SpringBoot简介

1.回顾Spring
什么是Spring:是一个开源框架,2003年兴起的一个轻量级的Java开发框架;
SpringBoot,目的:解决企业级应用开发复杂性问题,简化开发
2.SpringBoot如何简化开发

  1. 基于POJO的轻量级和低入侵性编程;
  2. 通过IOC,DI和面向接口实现了松耦合;
  3. 基于AOP 进行声明式编程;
  4. 通过Spring,减少了程序员的代码量,很多模板,我们直接它封装好的就可以!

3.什么是SpringBoot

  1. Servlet+Tomcat:步骤繁琐;
  2. SpringMVC:简化了Servlet;
  3. SpringBoot:简化了Spring开发

一个项目或者技术的发展,遵循一条主线:从难到简;
SpringBoot就是一个JavaWeb开发的框架,官方说的:约定大于配置,you can just run.
能够迅速开发web应用,我们只需要几行代码就可以实现Controller;

入门Demo:HelloSpringBoot

准备工作

我们将学习如何快速的创建一个Spring Boot应用,并且实现一个简单的Http请求处理。通过这个例子对Spring Boot有一个初步的了解,并体验其结构简单、开发快速的特性。

环境准备:

java version “1.8.0_181”
Maven-3.6.1
SpringBoot 2.x 最新版

开发工具:IDEA

使用IDEA快速创建项目
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
Tips:如果是第一次使用,可能速度会比较慢,需要耐心等待一切就绪

项目结构
通过上面步骤完成了基础项目的创建。就会自动生成以下文件;

  • 程序的主程序类
  • 一个 application.properties 配置文件
  • 一个测试类
    在这里插入图片描述

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.0.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.csdn</groupId>
    <artifactId>springboot_demo01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springboot_demo01</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>

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

</project>

如上所示,主要有四个部分:

  • 项目元数据信息:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括:groupId、artifactId、version、name、description等
  • parent:继承spring-boot-starter-parent的依赖管理,控制版本与打包等内容
  • dependencies:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了Spring MVC),官网对它的描述是:使用Spring MVC构建Web(包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器。;spring-boot-starter-test用于编写单元测试的依赖包。更多功能模块的使用我们将在后面逐步展开。
  • build:构建配置部分。默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot应用打包成JAR来直接运行。

编写HTTP接口
在主程序的同级目录下,新建一个controller包(一定要在同级目录下,否则识别不到)
在这里插入图片描述

package com.csdn.demo01.controller;

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

//@Controller //如果需要返回字符串就必须在方法上手动加上@ResponseBody
@RestController //自动在方法上加上 @ResponseBody
public class HelloController {

    @RequestMapping("/hello")
    //@ResponseBody
    public String hello(){
        return "HELLO";
    }
}

彩蛋:banner.txt

 /\/\/\                            /  \                   
| \  / |                         /      \                 
|  \/  |                       /          \               
|  /\  |----------------------|     /\     |              
| /  \ |                      |    /  \    |              
|/    \|                      |   /    \   |              
|\    /|                      |  | (  ) |  |              
| \  / |                      |  | (  ) |  |              
|  \/  |                 /\   |  |      |  |   /\         
|  /\  |                /  \  |  |      |  |  /  \        
| /  \ |               |----| |  |      |  | |----|       
|/    \|---------------|    | | /|   .  |\ | |    |       
|\    /|               |    | /  |   .  |  \ |    |       
| \  / |               |    /    |   .  |    \    |       
|  \/  |               |  /      |   .  |      \  |       
|  /\  |---------------|/        |   .  |        \|       
| /  \ |              /   NASA   |   .  |  NASA    \      
|/    \|              (          |      |           )     
|/\/\/\|               |    | |--|      |--| |    |       
------------------------/  \-----/  \/  \-----/  \--------
                        \\//     \\//\\//     \\//        
                         \/       \/  \/       \/      

编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;

  • 控制台输出了我们自定义 的 banner
  • 控制条输出了 Tomcat 访问的端口号!
  • 访问 hello 请求,字符串成功返回!
    在这里插入图片描述
    访问 http://localhost:8080(因为没有指定欢迎页)
    在这里插入图片描述
    访问 http://localhost:8080/hello
    在这里插入图片描述
    将项目达成jar包
    在这里插入图片描述
    打成了jar包后,就可以在任何地方运行了
    在这里插入图片描述
    小结
    简单几步,就完成了一个web接口的开发,SpringBoot就是这么简单。所以我们常用它来建立我们的微服务项目!

SpringBoot运行原理探究

链接
在这里插入图片描述

SpringBoot:配置文件及自动配置原理

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

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

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

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

标记语言

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

yaml配置:

server:
    prot: 8080

xml配置:

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

YAML语法

k:(空格) v   

以此来表示一对键值对(空格不能省略);以空格的缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。

注意 :属性和值的大小写都是十分敏感的。例子:

server:
    port: 8081
    path: /hello

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

k: v

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

“” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思;

比如 : name: “hello \n csdn” 输出 : hello 换行 csdn

‘’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出

比如 : name: ‘hello \n csdn’ 输出 : hello \n csdn

对象、Map(键值对)

k: 
    v1:
    v2:

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

student:
    name: csdn
    age: 9

行内写法

student: {name: csdn,age: 3}

数组( List、set )
用 - 值表示数组中的一个元素,比如:

pets:
 - cat
 - dog
 - pig

行内写法

pets: [cat,dog,pig]

程序实现

1.如果要使用properties配置文件可能导入时存在乱码现象,需要在IDEA中进行调整,我们这里直接使用yml文件,将默认的 application.properties后缀修改为yml
在这里插入图片描述
2.导入配置文件处理器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

3.编写yml 配置文件

person:
  name: 聚梦阿源
  age: 20
  happy: true
  birth: 1999/11/20
  Card: {id: 4405***,name: YJ}
  hobbys:
    - code
    - girl
    - music
  dog:
    name: 小白
    age: 3

4.新建一个pojo的包放入我们的Person类和Dog类

package com.csdn.demo01.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

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

//怎么把配置文件中的值绑定到我们的对象中?
//将配置文件中配置的每一个属性的值,映射到这个组件中
//prefix:在配置文件中的一个对象;
@Component //被Spring托管
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> Card;
    private List<Object> hobbys;
    private Dog dog;
	//set.get.toString太长了 省略
}

package com.csdn.demo01.pojo;

public class Dog {
    private String name;
    private Integer age;
	//set.get.toString太长了 省略
}

5.测试单元中测试

@SpringBootTest
class SpringbootDemo01ApplicationTests {

    @Autowired
    private Person person;
    @Test
    public void test(){
        System.out.println(person);
    }

}

运行后如图
在这里插入图片描述
我们使用的是@configurationProperties的方式,还有一种方式是使用@value

//怎么把配置文件中的值绑定到我们的对象中?
//将配置文件中配置的每一个属性的值,映射到这个组件中
//prefix:在配置文件中的一个对象;
@Component //被Spring托管
//@ConfigurationProperties(prefix = "person")
public class Person {
    @Value("${person.name}") //从配置文件中取值
    private String name;
    @Value("#{11*2}")  //#{SPEL} Spring表达式
    private Integer age;
    @Value("true")  // 字面量
    private Boolean happy;
    private Date birth;
    private Map<String,Object> Card;
    private List<Object> hobbys;
    private Dog dog;
    
    .......
    
 }

运行如图
在这里插入图片描述
这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图
在这里插入图片描述

cp只需要写一次即可 , value则需要每个字段都添加

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

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

复杂类型封装,yml中可以封装对象 , 使用@value就不支持

结论:

  • 配置yml和配置properties都可以获取到值 , 强烈推荐 yml
  • 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value
  • 如果专门编写了一个JavaBean和配置文件进行映射,直接使用@configurationProperties,不要犹豫!

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

@Component //被Spring托管
@ConfigurationProperties(prefix = "person")
//@PropertySource(value="classpath:person.properties")
@Validated //数据校验
public class Person {
    @Email
    private String name;
}

在这里插入图片描述
加载指定配置文件
@PropertySource :加载指定的配置文件;使用@configurationProperties默认从全局配置文件中获取值;

resources目录下新建一个person.properties

 name=properties
@PropertySource(value = "classpath:person.properties")
@Component //注册bean
public class Person {

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

    ......  
}

运行图
在这里插入图片描述
配置文件占位符

${random.value}、${random.int}、${random.long}、${random.int(10)}等等

多环境切换

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

多配置文件

我们在主配置文件编写的时候,文件名可以是 

application-{profile}.properties/yml --->用来指定多个环境版本;

例如:
application-test.properties 代表测试环境配置;
application-dev.properties 代表开发环境配置;

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

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

yml的多文档块

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

---
server:
  port: 8081
#选择要激活那个环境块
spring:
  profiles:
    active: prod

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


---

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

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

配置文件加载位置

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件

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

优先级由高到底,高优先级的配置会覆盖低优先级的配置;

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

我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;

#配置项目的访问路径
server.servlet.context-path=/kuang

SpringBoot:Web开发

静态资源下载 提取码: k6ke

SpringBoot的东西用起来非常简单,因为SpringBoot最大的特点就是自动装配。
使用SpringBoot的步骤:

  • 创建一个SpringBoot应用,选择我们需要的模块,SpringBoot就会默认将我们的需要的模块自动配置好
  • 手动在配置文件中配置部分配置项目就可以运行起来了
  • 专注编写业务代码,不需要考虑以前那样一大堆的配置了。

要熟悉掌握开发,之前学习的自动配置的原理一定要搞明白!
比如SpringBoot到底帮我们配置了什么?我们能不能修改?我们能修改哪些配置?我们能不能扩展?

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

没事就找找类,看看自动装配原理,我们之后来进行一个CRUD的实验测试!

静态资源映射规则
首先,我们搭建一个普通的SpringBoot项目,回顾一下HelloWorld程序!【演示】

那我们要引入我们小实验的测试资源,我们项目中有许多的静态资源,比如,css,js等文件,
这个SpringBoot怎么处理呢?

如果我们是一个web应用,我们的main下会有一个webapp,我们以前都是将所有的页面导在这里面的,对吧!

但是我们现在的pom呢,打包方式是为jar的方式,那么这种方式SpringBoot能不能来给我们写页面呢?当然是可以的,但是SpringBoot对于静态资源放置的位置,是有规定的!

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

SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 这个配置里面,我们可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

比如:addResourceHandlers

public void addResourceHandlers(ResourceHandlerRegistry registry) {

            if (!this.resourceProperties.isAddMappings()) {
                logger.debug("Default resource handling disabled");//默认资源处理已禁用,不推荐使用
            } else {
                Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
                CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
                if (!registry.hasMappingForPattern("/webjars/**")) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{"/webjars/**"}).addResourceLocations(new String[]{"classpath:/META-INF/resources/webjars/"}).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

                String staticPathPattern = this.mvcProperties.getStaticPathPattern();
                if (!registry.hasMappingForPattern(staticPathPattern)) {
                    this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
                }

            }
        }

读一下源代码:
比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源,那什么是webjars呢?
webjars本质就是以jar包的方式引入我们的静态资源 , 我们以前要导入一个静态资源文件,直接导入即可。使用SpringBoot需要使用webjars,我们可以去搜索一下
webjars网站—引入jQuery测试
要使用jQuery,我们只要要引入jQuery对应版本的pom依赖即可!【导入完毕,查看webjars目录结构,并访问Jquery.js文件】

导入依赖:

        <!--webjars -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>jquery</artifactId>
            <version>3.4.1</version>
        </dependency>

在这里插入图片描述

访问:只要是静态资源,SpringBoot就会去对应的路径寻找资源,
我们这里访问 :http://localhost:8081/webjars/jquery/3.4.1/jquery.js
(这里的端口号我自己通过配置文件修改过,根据个人而定)

在这里插入图片描述

那我们项目中要是使用自己的静态资源该怎么导入呢?我们看下一行代码;

我们去找staticPathPattern发现第二种映射规则 : /** , 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下,

	String staticPathPattern = this.mvcProperties.getStaticPathPattern();
	if (!registry.hasMappingForPattern(staticPathPattern)) {
     this.customizeResourceHandlerRegistration(registry.addResourceHandler(new String[]{staticPathPattern}).addResourceLocations(WebMvcAutoConfiguration.getResourceLocations(this.resourceProperties.getStaticLocations())).setCachePeriod(this.getSeconds(cachePeriod)).setCacheControl(cacheControl));
 }
 
//查看源码![在这里插入图片描述](https://img-blog.csdnimg.cn/20191107193632349.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2FZdWFueW8=,size_16,color_FFFFFF,t_70)
this.resourceProperties.getStaticLocations()
|
return this.staticLocations;
|
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = 
new String[]{"classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"};

得出结论

"classpath:/META-INF/resources/", 
"classpath:/resources/",
 "classpath:/static/", 
"classpath:/public/",

我们可以在resources根目录下新建对应的文件夹,都可以存放我们的静态文件;
在这里插入图片描述
我们访问的时候就会去这些文件夹中寻找对应的静态资源文件;
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

继续看源码!可以看到一个欢迎页的映射,就是我们的首页!

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

		private Optional<Resource> getWelcomePage() {
			String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
			return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
		}

		private Resource getIndexHtml(String location) {
			return this.resourceLoader.getResource(location + "index.html");
		}

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

比如我访问 localhost:8080/ ,就会找静态资源文件夹下的 index.html
在这里插入图片描述
推荐放在public文件夹下
Tips: Crtl+F9刷新静态资源文件.刷新访问浏览器即可!如果没有更新,可以清除浏览器缓存重新访问!
在这里插入图片描述

发现走的是static静态资源下的index.html,而不是templates的!

模板引擎Thymeleaf

Thymeleaf官网
Thymeleaf-GitHub
Spring Boot application starters -->找到我们的对应版本
模板引擎
前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。jsp支持非常强大的功能,包括能写Java代码,
但是我们现在的这种情况,SpringBoot这个项目是以jar的方式,不是war,第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。

那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢,SpringBoot推荐你可以来使用模板引擎。

那么这模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有以用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图。
在这里插入图片描述
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。
而这些值,从哪来呢,我们来组装一些数据,我们把这些数据找到。
然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。
只不过不同模板引擎之间,他们语法可能不一样。
现在主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,它是一个高级语言的模板引擎,语法更简单,功能更强大。

如何在SpringBoot中使用Thymeleaf?
1.导入Thymeleaf依赖

        <!--thymeleaf模块.java8 -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-java8time</artifactId>
        </dependency>

使用Thymeleaf
前面呢,我们已经引入了Thymeleaf,那这个要怎么使用呢?
我们首先得按照SpringBoot的自动配置原理看一下我们这个Thymeleaf的自动配置规则,在按照那个规则,我们进行使用。
我们去找一下Thymeleaf的自动配置类;
在这里插入图片描述
在这里插入图片描述

@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

	public static final String DEFAULT_PREFIX = "classpath:/templates/";

	public static final String DEFAULT_SUFFIX = ".html";
	
	private boolean checkTemplate = true;

	private boolean checkTemplateLocation = true;

	private String prefix = DEFAULT_PREFIX;

	private String suffix = DEFAULT_SUFFIX;

	private String mode = "HTML";

	private Charset encoding = DEFAULT_ENCODING;
	.......
}
可知Thymeleaf默认的前后缀
|
DEFAULT_PREFIX = "classpath:/templates/";
DEFAULT_SUFFIX = ".html";


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

测试
在这里插入图片描述

    @RequestMapping("/test")
    public String test(){
		//classpath:/templates/test.html
        return "test";
    }

在这里插入图片描述

Thymeleaf语法

要学习语法,还是参考官网文档最为准确,我们找到对应的版本看一下;
附件:中文文档 ==>提取码: u2s5

练习 : 查出一些数据并在页面中展示

    @GetMapping("/date")
    public String date(Model model){
        model.addAttribute("msg","Hello Thymeleaf!");
        return "date";
    }

我们要使用thymeleaf,需要在html文件中导入命名空间的约束方便提示
并且在配置文件中关闭thymeleaf的缓存

xmlns:th="http://www.thymeleaf.org"
#关闭thymeleaf缓存
spring.thymeleaf.cache=false

前端页面

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf date</title>
</head>
<body>
	<!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
    <div th:text="${msg}">hello</div>
</body>
</html>

在这里插入图片描述

我们可以使用任意的 th:attr 来替换Html中原生属性的值
全部属性可以参考官网文档#10 : th语法
在这里插入图片描述
我们能写那些表达式呢?我们可以看到官方文档 #4

Simple expressions:(表达式语法)

Variable Expressions: ${...}:获取变量值;OGNL;
    1)、获取对象的属性、调用方法
    2)、使用内置的基本对象: #18
         #ctx : the context object.
         #vars: the context variables.
         #locale : the context locale.
         #request : (only in Web Contexts) the HttpServletRequest object.
         #response : (only in Web Contexts) the HttpServletResponse object.
         #session : (only in Web Contexts) the HttpSession object.
         #servletContext : (only in Web Contexts) the ServletContext object.
                
                ${session.foo}
      3)、内置的一些工具对象:
      #execInfo : information about the template being processed.
      #messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax.
      #uris : methods for escaping parts of URLs/URIs
      #conversions : methods for executing the configured conversion service (if any).
      #dates : methods for java.util.Date objects: formatting, component extraction, etc.
      #calendars : analogous to #dates , but for java.util.Calendar objects.
      #numbers : methods for formatting numeric objects.
      #strings : methods for String objects: contains, startsWith, prepending/appending, etc.
      #objects : methods for objects in general.
      #bools : methods for boolean evaluation.
      #arrays : methods for arrays.
      #lists : methods for lists.
      #sets : methods for sets.
      #maps : methods for maps.
      #aggregates : methods for creating aggregates on arrays or collections.
      #ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration).

==============================================================================================
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
   补充:配合 th:object="${session.user}:

   <div th:object="${session.user}">
    <p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
    <p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
    <p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
    </div>
    
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
            @{/order/process(execId=${execId},execType='FAST')}

Fragment Expressions: ~{...}:片段引用表达式
            <div th:insert="~{commons :: main}">...</div>
            
Literals(字面量)
      Text literals: 'one text' , 'Another one!' ,…
      Number literals: 0 , 34 , 3.0 , 12.3 ,…
      Boolean literals: true , false
      Null literal: null
      Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
    String concatenation: +
    Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
    Binary operators: + , - , * , / , %
    Minus sign (unary operator): -
Boolean operations:(布尔运算)
    Binary operators: and , or
    Boolean negation (unary operator): ! , not
Comparisons and equality:(比较运算)
    Comparators: > , < , >= , <= ( gt , lt , ge , le )
    Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
    If-then: (if) ? (then)
    If-then-else: (if) ? (then) : (else)
    Default: (value) ?: (defaultvalue)
Special tokens:
    No-Operation: _

测试

    @GetMapping("/date")
    public String date(Model model){
        model.addAttribute("msg","<h2>Hello Thymeleaf</h2>");
        model.addAttribute("users", Arrays.asList("zhangsan","Lisi"));
        return "date";
    }

前端

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf date</title>
</head>
<body>
    <!--th:text就是将div中的内容设置为它指定的值,和之前学习的Vue一样-->
    <!-- th:text 不解析html标签-->
    <div th:text="${msg}">hello</div>
    <!-- th:utext 解析html标签 在页面显示样式-->
    <div th:utext="${msg}">hello</div>

    <!--对象,遍历
    for(user : users){
    }
    -->
    <h3 th:each="user:${users}" th:text="${user}"></h3>
    <hr>
    <!--行内写法-->
    <h3 th:each="user:${users}">[[${user}]]</h3>
</body>
</html>

很多样式,我们即使现在学习了,也会忘记,所以我们在学习过程中,需要使用什么,根据官方文档来查询,才是最重要的,要熟练使用官方文档!

SpringMVC自动配置

Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:

Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
Support for serving static resources, including support for WebJars (covered later in this document)).
Automatic registration of Converter, GenericConverter, and Formatter beans.
Support for HttpMessageConverters (covered later in this document).
Automatic registration of MessageCodesResolver (covered later in this document).
Static index.html support.
Custom Favicon support (covered later in this document).
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration
(interceptors, formatters, view controllers, and other features), you can add your own
@Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
If you wish to provide custom instances of RequestMappingHandlerMapping,
RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver,
you can declare a WebMvcRegistrationsAdapter instance to provide such components.

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with
@EnableWebMvc.

三毛钱翻译
|
|
Spring MVC Auto-configuration
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.

The auto-configuration adds the following features on top of Spring’s defaults:

//包含视图解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

//支持静态资源文件夹的路径,以及webjars
Support for serving static resources, including support for WebJars (covered later in this document)).

//自动注册了Converter-->转换器,这就是我们网页提交数据到后台自动封装成为对象的东西
(比如把18字符串自动转换为int类型)

//Formatter:-->格式化器
(比如页面给我们了一个2019-8-10,它会给我们自动格式化为Date对象)
Automatic registration of Converter, GenericConverter, and Formatter beans.

//HttpMessageConverters:SpringMVC用来转换Http请求和响应的的,
(比如我们要把一个User对象转换为JSON字符串,可以去看官网文档解释;)
Support for HttpMessageConverters (covered later in this document).

//定义错误代码生成规则的
Automatic registration of MessageCodesResolver (covered later in this document).

//首页定制
Static index.html support.

//图标定制
Custom Favicon support (covered later in this document).

//初始化数据绑定器:帮我们把请求数据绑定到JavaBean中!
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
....

我们来仔细对照看它怎么实现的,它告诉我们SpringBoot已经帮我们自动配置好了SpringMVC,然后自动配置了哪些东西呢?

ContentNegotiatingViewResolver

		@Bean
		@ConditionalOnBean(ViewResolver.class)
		@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
		public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
			ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
			resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
			// ContentNegotiatingViewResolver uses all the other view resolvers to locate
			// a view so it should have a high precedence
			resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
			return resolver;
		}

点进ContentNegotiatingViewResolver.class这类,找到对应的解析视图的代码

@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
        RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
        Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
        List<MediaType> requestedMediaTypes = this.getMediaTypes(((ServletRequestAttributes)attrs).getRequest());
        if (requestedMediaTypes != null) {
            List<View> candidateViews = this.getCandidateViews(viewName, locale, requestedMediaTypes);
            View bestView = this.getBestView(candidateViews, requestedMediaTypes, attrs);
            if (bestView != null) {
                return bestView;
            }
        }

        String mediaTypeInfo = this.logger.isDebugEnabled() && requestedMediaTypes != null ? " given " + requestedMediaTypes.toString() : "";
        if (this.useNotAcceptableStatusCode) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
            }

            return NOT_ACCEPTABLE_VIEW;
        } else {
            this.logger.debug("View remains unresolved" + mediaTypeInfo);
            return null;
        }
    }

可以看到getCandidateViews()方法:获取候选视图的信息

//getCandidateViews中看到他是把所有的视图解析器拿来,进行while循环,挨个解析
Iterator var5 = this.viewResolvers.iterator();
ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的 

属性viewResolvers,看看它是在哪里进行赋值的!

protected void initServletContext(ServletContext servletContext) {
	//这里它是从beanFactory工具中获取容器中的所有视图解析器,ViewRescolver.class , 把所有的视图解析器来组合的
        Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
        ViewResolver viewResolver;
        if (this.viewResolvers == null) {
            this.viewResolvers = new ArrayList(matchingBeans.size());
            Iterator var3 = matchingBeans.iterator();

            while(var3.hasNext()) {
                viewResolver = (ViewResolver)var3.next();
                if (this != viewResolver) {
                    this.viewResolvers.add(viewResolver);
                }
            }
        } else {
            for(int i = 0; i < this.viewResolvers.size(); ++i) {
                viewResolver = (ViewResolver)this.viewResolvers.get(i);
                if (!matchingBeans.contains(viewResolver)) {
                    String name = viewResolver.getClass().getName() + i;
                    this.obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolver, name);
                }
            }
        }

        AnnotationAwareOrderComparator.sort(this.viewResolvers);
        this.cnmFactoryBean.setServletContext(servletContext);
    }

既然它是在容器中去找视图解析器,我们是否可以猜想,我们就可以去实现定制了呢?

我们可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;我们去实现一下

我们在我们的主程序中去写一个视图解析器来试试;

    @Bean //放到bean中
    public ViewResolver MyViewResolver(){
        return new MyViewResolver();
    }

    //我们写一个静态内部类,视图解析器就需要实现ViewResolver接口
    private static class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String s, Locale locale) throws Exception {
            return null;
        }
    }

怎么看我们自己写的视图解析器有没有起作用呢?我们给dispatcherServlet中的 doDispatch方法加个断点进行调试一下,因为所有的请求都会走到这个方法中
在这里插入图片描述
我们启动我们的项目,然后随便访问一个页面,看一下Debug信息;
可以看到我们确实走到了doDispatch()方法;
在这里插入图片描述
找到this
在这里插入图片描述
找到视图解析器,我们看到我们自己定义的就在这里了
在这里插入图片描述
所以说,我们如果想要使用自己定制化的东西,我们只需要给容器中添加这个组件就好了!剩下的事情SpringBoot就会帮我们做了

转换器和格式化器

		@Bean
		@Override
		public FormattingConversionService mvcConversionService() {
		 	//拿到配置文件中的格式化规则
			WebConversionService conversionService = 
			new WebConversionService(this.mvcProperties.getDateFormat());
			addFormatters(conversionService);
			return conversionService;
		}
	public String getDateFormat() {
		return this.dateFormat;
	}
	
	public void setDateFormat(String dateFormat) {
		this.dateFormat = dateFormat;
	}

可以看到在我们的Properties文件中,我们可以进行自动配置它!如果注册了自己的格式化方式,就会注册到Bean中,否则不会注册

我们可以在配置文件中配置日期格式化的规则:

spring.mvc.date-format=

修改SpringBoot的默认配置

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

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

SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),
如果有就用用户配置的,如果没有就用自动配置的;
如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!

扩展使用SpringMVC(推荐的)

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

五毛钱翻译(参考)
|
|
如果您希望保留Spring Boot MVC功能,并且希望添加其他MVC配置(拦截器、格式化程序、视图控制器和其他功能),
则可以添加自己的@configuration类,类型为webmvcconfiguer,但不添加@EnableWebMvc.
如果希望提供RequestMappingHandlerMapping、RequestMappingHandlerAdapter或ExceptionHandlerExceptionResolver
的自定义实例,则可以声明WebMVCregistrationAdapter实例来提供此类组件。

如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。

新包叫config,写一个类MyMvcConfig

package com.csdn.web.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

//应为类型要求为WebMvcConfigurer,所以我们实现其接口
//可以使用自定义类扩展MVC的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

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

访问
在这里插入图片描述
跳转成功!所以我们要扩展SpringMVC,官方就推荐我们这么去使用,既保SpringBoot留所有的自动配置,也能用我们扩展的配置!

原理分析

  • WebMvcAutoConfiguration 是 SpringMVC的自动配置类,里面有个类WebMvcAutoConfigurationAdapter
  • 这个类上有一个注解@Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class})
  • 我们点进EnableWebMvcConfiguration这个类,它继承了一个父类DelegatingWebMvcConfiguration,这个父类中有这样一段代码
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
	 //从容器中获取所有的webmvcConfigurer
    @Autowired(required = false)
    public void setConfigurers(List<WebMvcConfigurer> configurers) {
        if (!CollectionUtils.isEmpty(configurers)) {
            this.configurers.addWebMvcConfigurers(configurers);
        }

    }

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

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

点进去

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

        while(var2.hasNext()) {
            WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
            delegate.addViewControllers(registry);
        }

    }

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

全面接管SpringMVC

If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

五毛钱翻译
|
如果您想完全控制Spring MVC,可以添加自己的@Configuration,并用@EnableWebMvc进行注释。

全面接管即:SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己去配置!只需在我们的配置类中要加一个@EnableWebMvc.

我们看下如果我们全面接管了SpringMVC了,我们之前SpringBoot给我们配置的静态资源映射一定会无效,我们可以去测试一下;

不加注解之前,访问首页
在这里插入图片描述
给配置类加上注解:@EnableWebMvc
在这里插入图片描述
我们发现所有的SpringMVC自动配置都失效了!回归到了最初的样子;

当然,我们开发中,不推荐使用全面接管SpringMVC
思考问题?为什么加了一个注解,自动配置就失效了!我们看下源码:

这里发现它是导入了一个类,我们可以继续进去看


@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}

它继承了一个父类

public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
}

我们返回去看Webmvc自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })

//这个注解的意思就是:容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
		...
}

总结:@EnableWebMvc将WebMvcConfigurationSupport组件导入进来了;而导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能!

在SpringBoot中会有非常多的Configurer帮助我们进行扩展配置,只要看见了这个,我们就应该多留心注意*

RestfulCRUD

新建springboot项目勾选web以及thymeleaf

pojo实体类

package com.csdn.myproject.pojo;

public class Department {
    private Integer id;
    private String departmentName;

    public Department() {
    }

    public Department(Integer id, String departmentName) {
        this.id = id;
        this.departmentName = departmentName;
    }
	//get/set,toString省略
}

package com.csdn.myproject.pojo;

import java.util.Date;

public class Employee {
    private Integer id;
    private String lastName;
    private String email;
    private Integer gender; //1 male  0 female
    private Department department;
    private Date birth;

    public Employee() {
    }

    public Employee(Integer id, String lastName, String email, Integer gender, Department department, Date birth) {
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = gender;
        this.department = department;
        this.birth = birth;
    }
    //get/set,toString省略
}

dao层

package com.csdn.myproject.dao;

import com.csdn.myproject.pojo.Department;

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

@Repository
public class DepartmentDao {
    //准备数据
    private static Map<Integer, Department> departments=null;

    static {
        departments=new HashMap<>();
        departments.put(101,new Department(101,"教学部"));
        departments.put(102,new Department(102,"教研部"));
        departments.put(103,new Department(103,"市场部"));
        departments.put(104,new Department(104,"人事部"));
        departments.put(105,new Department(105,"后勤部"));
    }
    //获得所有的部门
    public Collection<Department> getDepartment(){
        return  departments.values();
    }
    //根据ID获取部门
    public Department getDepartmentById(int id){
        return departments.get(id);
    }
}

package com.csdn.myproject.dao;

import com.csdn.myproject.pojo.Department;
import com.csdn.myproject.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Repository
public class EmployeeDao {
    //准备数据
    private static Map<Integer, Employee> employees=null;
    @Autowired
    private DepartmentDao departmentDao;
    //默认数据,需要Department
    static{
        employees=new HashMap<>();
        employees.put(1001,new Employee(1001,"小明","xm@qq.com",1,new Department(101,"教学部")));
        employees.put(1002,new Employee(1002,"小张","xz@qq.com",1,new Department(102,"教研部")));
        employees.put(1003,new Employee(1003,"小美","xm0@qq.com",0,new Department(103,"市场部")));
        employees.put(1004,new Employee(1004,"小丽","xl@qq.com",0,new Department(104,"人事部")));
        employees.put(1005,new Employee(1005,"小胖","xp@qq.com",1,new Department(105,"后勤部")));
    }

    //保存员工
    private static Integer initID=1006;
    public void save(Employee employee){
        if(employee.getId()==null){
            employee.setId(initID++);
        }
        employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
        employees.put(employee.getId(),employee);
    }
    //查询所有员工
    public Collection<Employee> getAll(){
        return  employees.values();
    }
    //根据ID查询员工
    public Employee getById(int id){
        return employees.get(id);
    }
    //删除一个员工
    public void delete(int id){
        employees.remove(id);
    }
}

首页映射
方式一:编写一个Controller实现(不推荐,略)
方式二:编写MVC的拓展配置(推荐使用)

package com.csdn.myproject.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

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

导入完毕这些之后,我们还需要导入我们的前端页面,及静态资源文件!

  • css,js等放在static文件夹下
  • html放在templates文件夹下

为了保证资源导入稳定,我们建议在所有资源导入时候使用 th:去替换原有的资源路径!

<link href="asserts/css/bootstrap.min.css" th:href="@{/asserts/css/bootstrap.min.css}" 
rel="stylesheet">

这样,无论我们项目发布名称如何变化,它都可以自动的寻找到

准备工作OK!

国际化

1.编写国际化配置文件,抽取页面需要显示的国际化页面消息 (以登录页面为例)
先在IDEA中统一设置properties的编码问题!
在这里插入图片描述
resources资源文件下新建一个i18n目录,建立一个login.propetries文件以及login_zh_CN.properties,IDEA会自动识别了我们要做国际化操作;文件夹发生变化
在这里插入图片描述
在这里插入图片描述
快捷方便
并且IDEA有Resource Bundle可以同事编辑三个不同配置文件
在这里插入图片描述
2.看一下SpringBoot对国际化的自动配置
MessageSourceAutoConfiguration ,里面有一个方法,这里发现SpringBoot已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;

public class MessageSourceAutoConfiguration {
    private static final Resource[] NO_RESOURCES = new Resource[0];

    public MessageSourceAutoConfiguration() {
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.messages") //我们的配置文件可以直接放在类路径下叫: messages.properties, 就可以进行国际化操作了
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        if (StringUtils.hasText(properties.getBasename())) {
        //设置国际化文件的基础名(去掉语言国家代码的)
            messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }

        if (properties.getEncoding() != null) {
            messageSource.setDefaultEncoding(properties.getEncoding().name());
        }

        messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
        Duration cacheDuration = properties.getCacheDuration();
        if (cacheDuration != null) {
            messageSource.setCacheMillis(cacheDuration.toMillis());
        }

        messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
        messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
        return messageSource;
    }
}

我们真实 的情况是放在了i18n目录下,所以我们要去配置这个messages的路径;

spring.messages.basename=i18n.login

3.去页面获取国际化的值
查看Thymeleaf的文档,找到message取值操作为: #{…}
静态页面修改如下
在这里插入图片描述
标签内写IDEA会有提示,行内写法没有提示(看个人习惯)
重启项目访问发现已经默认转换成中文
在这里插入图片描述
下面实现可以根据按钮自动切换中文英文

去webmvc自动配置文件中寻找LocaleResolver,可以看到SpringBoot默认配置了

@Bean
		@ConditionalOnMissingBean
		@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
		public LocaleResolver localeResolver() {
			//容器中没有就自己配,有的话就用用户配置的
			if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
				return new FixedLocaleResolver(this.mvcProperties.getLocale());
			}
			AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
			localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
			return localeResolver;
		}

AcceptHeaderLocaleResolver 这个类中有一个方法

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

那假如我们现在想点击链接让我们的国际化资源生效,就需要让我们自己的locale生效!

我们去自己写一个自己的LocaleResolver,可以在链接上携带区域信息!

修改一下前端页面的跳转连接;

 <!--themeleaf参数传递问题, 使用括号() -->
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

我们去写一个处理的组件类

package com.csdn.myproject.component;

import org.springframework.web.servlet.LocaleResolver;
import org.thymeleaf.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

//处理国际化问题
//可以在链接上携带区域信息
public class MyLocaleResolver implements LocaleResolver {

    //返回一个国际化信息
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //http://localhost:8080/index.html?l=en_US
        //获取请求的参数  en_US
        String language = request.getParameter("l");
        Locale locale=Locale.getDefault();//获得默认的信息
        //判断,如果请求的链接不为空
        if(!StringUtils.isEmpty(language)){
            //拿到请求的参数,分割请求参数 [en,US]
            String[] s = language.split("_");
            locale=new Locale(s[0],s[1]);

        }
        return locale;
    }

    //不用写
    @Override
    public void setLocale(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {

    }
}

为了让我们的区域化信息能够生效,我们需要在我们自己的MvcConofig下添加bean;

    @Bean
    public LocaleResolver LocaleResolver(){
        return new MyLocaleResolver();
    }

重启项目测试~

登陆功能实现

在前端相应的地方修改如下
在这里插入图片描述
在这里插入图片描述
Controller类

@Controller
public class LoginController {

    @PostMapping("/user/login")
    public String login(@RequestParam("username") String username,
                        @RequestParam("password")String password,
                        Model model){
        //具体业务
        if(!StringUtils.isEmpty(username) && "123456".equals(password)){
            return "redirect:/main.html";
        }else{
            model.addAttribute("msg","用户名或者密码错误");
            return "index";
        }
    }
}

在web扩展配置文件中添加试图控制

registry.addViewController("/main.html").setViewName("dashboard");

问题:这个时候我们就会发现不管等不登陆 都可以直接通过main.html访问到后台

登陆拦截器

拦截器我们通过用户Session来判定是否有登陆,所以我们需要修改一下我们之前的LoginController

@Controller
public class LoginController {

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

配置拦截器

package com.csdn.myproject.config;


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

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

public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登陆成功之后,应该有用户的session;
        Object loginUser = request.getSession().getAttribute("loginUser");
        if(loginUser==null){ //没有登陆
            request.setAttribute("msg","没有权限,请先登录!");
            request.getRequestDispatcher("/index.html").forward(request,response);
            return  false;
        }else{
            return true;
        }
    }

}

在web扩展配置文件中重写addInterceptors方法

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerInterceptor())
                //拦截所有请求
                .addPathPatterns("/**")
                //排除登录页 首页
                .excludePathPatterns("/index.html","/","/user/login")
                //排除静态资源
                .excludePathPatterns("/css/**","/img/**","/js/**");
    }
选择项:可以将 dashboard.html 中的 company name 改为  [[${session.loginUser}]]
这样登陆跳转左上角就会显示用户名

增删改查

员工列表展示
1.提取公共页面
新建一个commons目录并新建一个commons.html

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

<!-- 头部导航栏-->
<nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="nav">
    <a class="navbar-brand col-sm-3 col-md-2 mr-0" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">[[${session.loginUser}]]</a>
    <input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
    <ul class="navbar-nav px-3">
        <li class="nav-item text-nowrap">
            <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">注销</a>
        </li>
    </ul>
</nav>
<!--侧边栏 -->
<nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar">
    <div class="sidebar-sticky">
        <ul class="nav flex-column">
            <li class="nav-item">
            	<!-- ${active=='main.html'?'nav-link active':'nav-link'} 三元运算 判断标签高亮-->
                <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
                        <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
                        <polyline points="9 22 9 12 15 12 15 22"></polyline>
                    </svg>
                    首页 <span class="sr-only">(current)</span>
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file">
                        <path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"></path>
                        <polyline points="13 2 13 9 20 9"></polyline>
                    </svg>
                    Orders
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-shopping-cart">
                        <circle cx="9" cy="21" r="1"></circle>
                        <circle cx="20" cy="21" r="1"></circle>
                        <path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"></path>
                    </svg>
                    Products
                </a>
            </li>
            <li class="nav-item">
                <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-users">
                        <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                        <circle cx="9" cy="7" r="4"></circle>
                        <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                        <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
                    </svg>
                    员工管理
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-bar-chart-2">
                        <line x1="18" y1="20" x2="18" y2="10"></line>
                        <line x1="12" y1="20" x2="12" y2="4"></line>
                        <line x1="6" y1="20" x2="6" y2="14"></line>
                    </svg>
                    Reports
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
                        <polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
                        <polyline points="2 17 12 22 22 17"></polyline>
                        <polyline points="2 12 12 17 22 12"></polyline>
                    </svg>
                    Integrations
                </a>
            </li>
        </ul>

        <h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
            <span>Saved reports</span>
            <a class="d-flex align-items-center text-muted" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus-circle"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="16"></line><line x1="8" y1="12" x2="16" y2="12"></line></svg>
            </a>
        </h6>
        <ul class="nav flex-column mb-2">
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Current month
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Last quarter
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Social engagement
                </a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="http://getbootstrap.com/docs/4.0/examples/dashboard/#">
                    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
                        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
                        <polyline points="14 2 14 8 20 8"></polyline>
                        <line x1="16" y1="13" x2="8" y2="13"></line>
                        <line x1="16" y1="17" x2="8" y2="17"></line>
                        <polyline points="10 9 9 9 8 9"></polyline>
                    </svg>
                    Year-end sale
                </a>
            </li>
        </ul>
    </div>
</nav>
</html>

在list.html和dashboard.html中分别插入导航栏和侧边栏以及传入参数

dashboard.html
<div th:replace="~{commons/commons::nav}"></div>
<div th:replace="~{commons/commons::sidebar(active='main.html')}"></div>
list.html
<div th:replace="~{commons/commons::nav}"></div>
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>

将list.html进行进一步的优化
更新table标签里的内容

<table class="table table-striped table-sm">
                        <thead>
                        <tr>
                            <th>id</th>
                            <th>名称</th>
                            <th>性别</th>
                            <th>所在部门</th>
                            <th>邮箱</th>
                            <th>出生日期</th>
                            <th>操作</th>
                        </tr>
                        </thead>
                        <tbody>
                        <tr th:each="emp:${emps}">
                            <td th:text="${emp.getId()}"></td>
                            <td th:text="${emp.getLastName()}"></td>
                            <td th:text="${emp.getGender()==0?'':''}"></td>
                            <td th:text="${emp.department.getDepartmentName()}"></td>
                            <td>[[${emp.getEmail()}]]</td>
                            <td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
                            <td>
                                <button class="btn btn-sm btn-primary">编辑</button>
                                <button class="btn btn-sm btn-danger">删除</button>
                            </td>
                        </tr>
                        </tbody>
</table>

Crtl+F9 Build一下 访问测试
在这里插入图片描述
添加员工
在list页面个人习惯找一个地方加上添加员工的链接

<a class="nav-link" th:href="@{/emp}">添加员工</a>

编写对应的controller

	//跳转到添加员工表单页面
    @GetMapping("/emp")
    public String toAdd(Model model){
        //查出所有的部门 便于下选框遍历内容也不用去前端写硬代码
        Collection<Department> departments = departmentDao.getDepartment();
        model.addAttribute("departments",departments);
        return "emp/add";
    }

添加前端页面;复制list页面,修改mian标签里内容即可
也可以自己去bootstrap官网自己寻找喜欢的样式!

<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                    <form th:action="@{/emp}" method="post">
                        <div class="form-group">
                            <label>名称</label>
                            <input type="text" name="lastName" class="form-control" placeholder="请输入你的名称">
                        </div>
                        <div class="form-group">
                            <label>性别</label><br/>
                            <div class="form-check form-check-inline">
                                <input class="form-check-input" type="radio" name="gender"  value="1">
                                <label class="form-check-label"></label>
                            </div>
                            <div class="form-check form-check-inline">
                                <input class="form-check-input" type="radio" name="gender"  value="0">
                                <label class="form-check-label"></label>
                            </div>
                        </div>
                        <div class="form-group">
                            <label>出生日期</label>
                            <input type="text" name="birth" class="form-control" placeholder="yyyy-MM--dd HH:mm:ss">
                        </div>
                        <div class="form-group">
                            <label>邮箱</label>
                            <input type="email" name="email" class="form-control" placeholder="请输入你的邮箱">
                        </div>
                        <div class="form-group">
                            <label>所在部门</label>
                            <!--我们在controller接受的是一个Employee,所以我们需要提交的是其中的一个属性! -->
                            <select class="form-control" name="department.id">
                                <option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
                            </select>
                        </div>
                        <button type="submit" class="btn btn-primary">添加</button>
                    </form>
				</main>

编写提交信息添加员工controller

    @PostMapping("/emp")
    public String add(Employee employee){
        System.out.println("save==>"+employee);//调试用
        //添加的操作
        employeeDao.save(employee); //调用底层业务方法保存员工信息;
        return "redirect:/emps";
    }

回忆:重定向和转发 以及 /的问题?

原理探究 : ThymeleafViewResolver

	public static final String REDIRECT_URL_PREFIX = "redirect:";
    public static final String FORWARD_URL_PREFIX = "forward:";
	
	//.......more
	
    protected View createView(String viewName, Locale locale) throws Exception {
        if (!this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
            vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
            return null;
        } else {
            String forwardUrl;
            if (viewName.startsWith("redirect:")) {
                vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
                forwardUrl = viewName.substring("redirect:".length(), viewName.length());
                RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
                return (View)this.getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
            } else if (viewName.startsWith("forward:")) {
                vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
                forwardUrl = viewName.substring("forward:".length(), viewName.length());
                return new InternalResourceView(forwardUrl);
            } else if (this.alwaysProcessRedirectAndForward && !this.canHandle(viewName, locale)) {
                vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
                return null;
            } else {
                vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a {} instance will be created for it", viewName, this.getViewClass().getSimpleName());
                return this.loadView(viewName, locale);
            }
        }
    }

在这里插入图片描述
我们能不能修改这个默认的格式呢?
webmvc的自动配置文件;找到一个日期格式化的方法,我们可以看一下

		@Bean
		@Override
		public FormattingConversionService mvcConversionService() {
			WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
			addFormatters(conversionService);
			return conversionService;
		}

查看getDateFormat()

public String getDateFormat() {
		return this.dateFormat;
	}

如果有下载源码的话就可以看见官方注释写的默认格式

/**
	 * Date format to use. For instance, `dd/MM/yyyy`.
	 */
	private String dateFormat;

因此,如果我们想要更改日期格式的话,可以在配置文件中设置

spring.mvc.date-format=yy-MM-dd

修改员工
我们要实现员工修改功能,需要实现两步

1点击修改按钮,去到编辑页面,我们可以直接使用添加员工的页面实现

2.显示原数据,修改完毕后跳回列表页面!

修改跳转链接的位置

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

对应的Controller

    //去员工的修改页面
    @GetMapping("/emp/{id}")
    public String toUpdateEmp(@PathVariable("id") Integer id, Model model){
        //查出原来的数据
        Employee employee = employeeDao.getById(id);
        model.addAttribute("emp",employee);
        //查出所有的部门
        Collection<Department> departments = departmentDao.getDepartment();
        model.addAttribute("departments",departments);
        return "emp/update";
    }

我们需要在这里将add页面复制一份,改为update页面;需要修改页面,将我们后台查询数据回显

				<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
                    <form th:action="@{/updateEmp}" method="post">
                        <div class="form-group">
                            <label>名称</label>
                            <input type="text" th:value="${emp.getLastName()}" name="lastName" class="form-control" placeholder="请输入你的名称">
                        </div>
                        <div class="form-group">
                            <label>性别</label><br/>
                            <div class="form-check form-check-inline">
                                <input  th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender"  value="1">
                                <label class="form-check-label"></label>
                            </div>
                            <div class="form-check form-check-inline">
                                <input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender"  value="0">
                                <label class="form-check-label"></label>
                            </div>
                        </div>
                        <div class="form-group">
                            <label>出生日期</label>
                            <input type="text" th:value="${emp.birth}" name="birth" class="form-control" placeholder="yyyy-MM--dd HH:mm:ss">
                        </div>
                        <div class="form-group">
                            <label>邮箱</label>
                            <input type="email" th:value="${emp.getEmail()}" name="email" class="form-control" placeholder="请输入你的邮箱">
                        </div>
                        <div class="form-group">
                            <label>所在部门</label>
                            <!--我们在controller接受的是一个Employee,所以我们需要提交的是其中的一个属性! -->
                            <select class="form-control" name="department.id">
                                <option th:selected="${emp.getDepartment().getId()==dept.getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
                            </select>
                        </div>
                        <button type="submit" class="btn btn-primary">修改</button>
                    </form>
				</main>

访问发现日期没有格式化,优化

<input name="birth" type="text" class="form-control" th:value="${#dates.format(emp.birth,'yyyy-MM-dd HH:mm:ss')}">

对应修改信息提交Controller

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

测试发现每次修改都是直接重新添加一个.发现问题,优化!

//将id传进去.
<input type="hidden" name="id" th:value="${emp.getId()}">

删除员工
修改跳转链接的位置

<a class="btn btn-sm btn-danger" th:href="@{/deleteEmp/}+${emp.id}" >删除</a>

Controller

    //删除员工
    @GetMapping("/deleteEmp/{id}")
    public String deleteEmp(@PathVariable("id") Integer id){
        employeeDao.delete(id);
        return "redirect:/emps";
    }

用户注销功能
在commons/commons.html中的导航栏中修改如下

<a class="nav-link" th:href="@{/logout}">注销</a>

Controller

    @GetMapping("/logout")
    public String logout(HttpSession session){
        session.removeAttribute("loginUser");
        return "redirect:/index.html";
    }

定制错误页面404

我们只需要在模板目录下添加一个error文件夹
文件夹中存放我们相应的错误页面,比如  404.html  或者 4xx.html ...more
SpringBoot就会帮我们自动使用了!

Mybatis + Druid 数据访问

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

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

Sping Data 官网:https://spring.io/projects/spring-data

数据库相关的启动器 : 可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.1.7.RELEASE/reference/htmlsingle/#using-boot-starter

JDBC
新建一个项目测试:springboot_demo_data ; 引入相应的模块!基础模块
在这里插入图片描述
由于我的本地mysql是5.X版本

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.47</version>  //自己添加的
        </dependency>

我们先连接上数据库 , 直接使用IDEA连接即可【右边栏的DataBase】

SpringBoot中,我们只需要简单的配置就可以实现数据库的连接了;

我们使用yml的配置文件进行操作!

spring:
  datasource:
    username: root
    password: root
    # 8.X版本需要添加时区 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver

配置完,我们就可以直接去使用了,因为SpringBoot已经帮我们进行了自动配置;

@SpringBootTest
class Springboot04DataApplicationTests {

    @Autowired
    DataSource dataSource;
    @Test
    void contextLoads() throws SQLException {
        //查看默认数据源:  com.zaxxer.hikari.HikariDataSource
        System.out.println(dataSource.getClass());
        //获得数据库链接
        Connection connection = dataSource.getConnection();

        //HikariProxyConnection@580965610 wrapping com.mysql.jdbc.JDBC4Connection@49ede9c7
        System.out.println(connection);

        //关闭
        connection.close();
    }

}

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

我们来全局搜索一下,找到数据源的所有自动配置都在 :DataSourceProperties 文件下
我们可以来探究下这里自动配置的原理以及能配置哪些属性

HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
在yml文件中按住Crtl键点username 发现跳到 DataSourceProperties
从之前看源码我们可以大致知道有Properties 就必有AutoConfiguration
关于数据源我们并不做介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。

CRUD操作

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

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

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

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

5、JdbcTemplate  的自动配置原理是依赖 org.springframework.boot.autoconfigure.jdbc 包下的
 org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration 类

JdbcTemplate主要提供以下几类方法

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

测试

@RestController
public class JdbcController {

    @Autowired
    JdbcTemplate jdbcTemplate;

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

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

    @GetMapping("/updateUser/{id}")
    public String updateUser(@PathVariable("id") int id){
        String sql="update mybatis.user set name =?,pwd=? where id="+id;
        Object[] objects = new Object[2];
        objects[0]="聚梦阿源哟";
        objects[1]="666666";
        jdbcTemplate.update(sql,objects);
        return "updateUser-ok";
    }

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


页面访问测试,OK!

原理探究 :

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

SpringBoot默认支持以下数据源:

com.zaxxer.hikari.HikariDataSource (Spring Boot 2.0 以上,默认使用此数据源)

org.apache.tomcat.jdbc.pool.DataSource

org.apache.commons.dbcp2.BasicDataSource

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

/**
	 * Generic DataSource configuration.
	 */
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingBean(DataSource.class)
	@ConditionalOnProperty(name = "spring.datasource.type")
	static class Generic {

		@Bean
		DataSource dataSource(DataSourceProperties properties) {
			return properties.initializeDataSourceBuilder().build();
		}

	}

自定义数据源 DruidDataSource

RUID 简介

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

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

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

引入数据源

一步需要在应用的 pom.xml 文件中添加上 Druid 数据源依赖,
而这个依赖可以从 Maven 仓库官网 Maven Repository 中获取
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.21</version>
        </dependency>

现在我们去切换数据源;

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

spring:
  datasource:
    username: root
    password: root
    # 8.X版本需要添加时区 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    # 指定数据源
    type: com.alibaba.druid.pool.DruidDataSource

数据源切换之后,同理可以注入 DataSource,然后获取到它,输出一看便知是否成功切换;
在这里插入图片描述
切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;

我们可以配置一些参数来测试一下

spring:
  datasource:
    username: root
    password: root
    # 8.X版本需要添加时区 serverTimezone=UTC
    url: jdbc:mysql://localhost:3306/mybatis?useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.jdbc.Driver
    # 指定数据源
    type: com.alibaba.druid.pool.DruidDataSource
    #Spring Boot 默认是不注入这些属性值的,需要自己绑定
    #druid 数据源专有配置
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    poolPreparedStatements: true

    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果允许时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4j
    filters: stat,wall,log4j
    maxPoolPreparedStatementPerConnectionSize: 20
    useGlobalDataSourceStat: true
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500

log4j日志依赖

<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

配置 Druid 数据源监控

Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。

所以第一步需要设置 Druid 的后台管理页面,比如 登录账号、密码 等;配置后台管理;

@Configuration
public class DruidConfig {

    @ConfigurationProperties(prefix = "spring.datasource")
    @Bean
    public DataSource druidDataSource(){
        return  new DruidDataSource();
    }

    //后台监控:web.xml,ServletRegistrationBean
    //因为SpringBoot 内置了Servlet容器, 所以没有web.xml  替代方法: ServletRegistrationBean
    @Bean
    public ServletRegistrationBean StatViewServlet(){
        ServletRegistrationBean<StatViewServlet> bean=
                new ServletRegistrationBean<>(new StatViewServlet(),"/druid/*");
        //后台需要有人登陆,账号密码配置
        Map<String,String> initParameters = new HashMap<>();
        //增加配置
        //登陆的key是固定的,  loginUsername loginPassword
        initParameters.put("loginUsername","admin"); //后台管理界面的登录账号
        initParameters.put("loginPassword","123456");//后台管理界面的登录密码

        //允许谁可以访问
        //initParameters.put("allow", "localhost"):表示只有本机可以访问
        //initParameters.put("allow", ""):为空或者为null时,表示允许所有访问
        initParameters.put("allow","");
        //禁止谁能访问
        //initParameters.put("csdn","192.168.11.123");表示禁止此ip访问

        bean.setInitParameters(initParameters); //设置初始化参数
        return bean;
        //这些参数可以在 com.alibaba.druid.support.http.StatViewServlet
        //的父类 com.alibaba.druid.support.http.ResourceServlet 中找到
    }
}

配置完毕后,我们可以选择访问 : http://localhost:8080/druid
在这里插入图片描述
登陆
在这里插入图片描述

配置 Druid web 监控 filter

    //filter
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean bean = new FilterRegistrationBean();

        bean.setFilter(new WebStatFilter());
        //可以过滤哪些请求?
        HashMap<String,String> initParameters = new HashMap<>();

        //exclusions: 设置哪些请求过滤排除掉,从而不进行统计
        initParameters.put("exclusions","*.js,*.css,/druid/*");
        bean.setInitParameters(initParameters);
        //"/*" 表示过滤所有请求
        bean.setUrlPatterns(Arrays.asList("/*"));
        return  bean;
    }

SpringBoot整合MyBatis

导入mybatis所需要的依赖

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

配置数据库连接信息

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

测试一下连接是否成功

@SpringBootTest
class Springboot05MybatisApplicationTests {

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

}

输出成功,配置OK!

创建实体类

package com.csdn.mybatis.pojo;

public class User {
    private Integer id;
    private String name;
    private String pwd;

    public User() {
    }

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

    public Integer getId() {
        return id;
    }

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

    public String getName() {
        return name;
    }

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

    public String getPwd() {
        return pwd;
    }

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

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

配置Mapper接口类

package com.csdn.mybatis.mapper;

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

import java.util.List;

//这个注解表示了这是一个 mybatis 的 Mapper 类;  Dao
@Mapper
@Repository
public interface UserMapper {
    //查询全部用户
    List<User> queryUserList();
    //根据ID查询用户
    User queryUserById(int id);
    //添加用户
    int addUser(User user);
    //更新用户
    int updateUser(User user);
    //删除用户
    int deleteUser(int id);
}


Mapper映射文件

在resources下创建一个文件夹mapper,Mapper映射文件都写在这个mapper文件夹里面
<?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.csdn.mybatis.mapper.UserMapper">

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

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

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

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

    <delete id="deleteUser" parameterType="integer">
        delete from mybatis.user where id=#{id}
    </delete>
</mapper>

SpringBoot 整合

以前 MyBatis 没有跟 spring 整合时,
配置数据源、事务、连接数据库的账号、密码等都是在 myBatis 核心配置文件中进行的

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

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

#别名
mybatis.type-aliases-package=com.csdn.mybatis.pojo
#mapper映射文件
mybatis.mapper-locations=classpath:mapper/*.xml

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

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

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

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

	//....
}	

因为是测试,这里就不写Service层了,直接上Controller层

package com.csdn.mybatis.controller;


import com.csdn.mybatis.mapper.UserMapper;
import com.csdn.mybatis.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    UserMapper userMapper;
    //查询全部用户
    @GetMapping("/queryUserList")
    public  List<User> queryUserList(){
        List<User> users = userMapper.queryUserList();
        for (User user : users) {
            System.out.println(user);
        }
        return users;
    }
    //根据ID查询用户
    @GetMapping("/queryUserById/{id}")
    public User queryUserById(@PathVariable("id") int id){
        return userMapper.queryUserById(id);
    }
    //添加一个用户
    @GetMapping("/addUser")
    public String addUser(){
        userMapper.addUser(new User(5,"阿毛","456789"));
        return "ok";
    }
    //修改一个用户
    @GetMapping("/updateUser")
    public String updateUser(){
        userMapper.updateUser(new User(5,"阿毛","421319"));
        return "ok";
    }
    //根据id删除用户
    @GetMapping("/deleteUser/{id}")
    public String deleteUser(@PathVariable("id") int id){
        userMapper.deleteUser(id);
        return "ok";
    }
}

运行测试OK!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值