SpingBoot
前言
导学
资料!版权
本文章为二创 作品!原创 在这里:狂神说笔记——SpringBoot 快速入门 20 - subeiLY - 博客园 (cnblogs.com)
参考视频为:https://www.bilibili.com/video/BV1PE411i7CV/
正文
SpringBoot 快速入门
1.什么是 SpringBoot
回顾什么是 Spring?
Spring 是一个开源框架,2003 年兴起的一个轻量级的 Java 开发框架,作者:Rod Johnson 。
Spring 是为了解决企业级应用开发的复杂性而创建的,简化开发 。
Spring 是如何简化 Java 开发的?
为了降低 Java 开发的复杂性,Spring 采用了以下 4 种关键策略:
1、基于 POJO 的轻量级和最小侵入性编程,所有东西都是 bean;
2、通过 IOC,依赖注入(DI)和面向接口实现松耦合;
3、基于切面(AOP)和惯例进行声明式编程;
4、通过切面和模版减少样式代码,RedisTemplate,xxxTemplate;
什么是 SpringBoot?
学过 javaweb 的同学就知道,开发一个 web 应用,从最初开始接触 Servlet 结合 Tomcat, 跑出一个 Hello Wolrld 程序,是要经历特别多的步骤;后来就用了框架 Struts,再后来是 SpringMVC,到了现在的 SpringBoot,过一两年又会有其他 web 框架出现;你们有经历过框架不断的演进,然后自己开发项目所有的技术也在不断的变化、改造吗?建议都可以去经历一遍;
言归正传,什么是 SpringBoot 呢,就是一个 javaweb 的开发框架,和 SpringMVC 类似,对比其他 javaweb 框架的好处,官方说是简化开发,约定大于配置, you can “just run”,能迅速的开发 web 应用,几行代码开发一个 http 接口。
所有的技术框架的发展似乎都遵循了一条主线规律:从一个复杂应用场景 衍生 一种规范框架,人们只需要进行各种配置而不需要自己去实现它,这时候强大的配置功能成了优点;发展到一定程度之后,人们根据实际生产应用情况,选取其中实用功能和设计精华,重构出一些轻量级的框架;之后为了提高开发效率,嫌弃原先的各类配置过于麻烦,于是开始提倡“约定大于配置”,进而衍生出一些一站式的解决方案。
是的这就是 Java 企业级应用->J2EE->spring->springboot 的过程。
随着 Spring 不断的发展,涉及的领域越来越多,项目整合开发需要配合各种各样的文件,慢慢变得不那么易用简单,违背了最初的理念,甚至人称配置地狱。Spring Boot 正是在这样的一个背景下被抽象出来的开发框架,目的为了让大家更容易的使用 Spring 、更容易的集成各种常用的中间件、开源软件;
Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序。也就是说,它并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合用于提升 Spring 开发者体验的工具。Spring Boot 以约定大于配置的核心思想 ,默认帮我们进行了很多设置,多数 Spring Boot 应用只需要很少的 Spring 配置。同时它集成了大量常用的第三方库配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),Spring Boot 应用中这些第三方库几乎可以零配置的开箱即用。
简单来说就是 SpringBoot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 maven 整合了所有的 jar 包,spring boot 整合了所有的框架 。
Spring Boot 出生名门,从一开始就站在一个比较高的起点,又经过这几年的发展,生态足够完善,Spring Boot 已经当之无愧成为 Java 领域最热门的技术。
Spring Boot 的主要优点 :
为所有 Spring 开发者更快的入门;
开箱即用 ,提供各种默认配置来简化项目配置;
内嵌式容器简化 Web 项目;
没有冗余代码生成和 XML 配置的要求;
2.什么是微服务架构
什么是微服务?
微服务是一种架构风格,它要求我们在开发一个应用时,将单个应用程序开发为一组小服务的方法,每个小服务都在自己的进程中运行,并与轻量级机制(通常是 HTTP 资源 API)进行通信。简单说:每个功能元素最终都是一个可独立替换和独立升级的软件单元。
单体应用架构
所谓单体应用架构( all in one)是指,我们将一个应用的中的所有应用服务都封装在一个应用中。
无论是 ERP、CRM 或是其他什么系统,你都把数据库访问,web 访问,等等各个功能放到一个 war 包内。
这样做的好处是,易于开发和测试;也十分方便部罢;当需要扩展时,只需要将 war 复制多份,然后放到多个服务器上,再做个负载均衡就可以了。
单体应用架构的缺点是,哪怕我要修改一个非常小的地方,我都需要停掉整个服务,重新打包、部署这个应用 war 包。特别是对于一个大型应用,我们不可能吧所有内容都放在一个应用里面,我们如何维护、如何分工合作都是问题。
例如:Apache Dubbo
微服务架构
all in one 的架构方式,我们把所有的功能单元放在一个应用里面。然后我们把整个应用部罢到服务器上。如果负载能力不行,我们将整个应用进行水平复制,进行扩展,然后在负载均衡。
所谓微服务架构,就是打破之前 all in one 的架构方式,把每个功能元素独立出来,肥独立出来的功能元素的动态组合,需要的功能元素才去拿来组合,需要多一些时可以整合多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。
这样做的好处是:
节省了调用资源。
每个功能元素的服都是一个可替换的、可独立升级的软件代码。
图 1:单体和微服务
微服务技术栈有那些?
微服务技术条目
落地技术
服务开发
SpringBoot、Spring、SpringMVC 等
服务配置与管理
Netfix 公司的 Archaius、阿里的 Diamond 等
服务注册与发现
Eureka、Consul、Zookeeper 等
服务调用
Rest、PRC、gRPC
服务熔断器
Hystrix、Envoy 等
负载均衡
Ribbon、Nginx 等
服务接口调用(客户端调用服务的简化工具)
Fegin 等
消息队列
Kafka、RabbitMQ、ActiveMQ 等
服务配置中心管理
SpringCloudConfig、Chef 等
服务路由(API 网关)
Zuul 等
服务监控
Zabbix、Nagios、Metrics、Specatator 等
全链路追踪
Zipkin、Brave、Dapper 等
数据流操作开发包
SpringCloud Stream(封装与 Redis,Rabbit,Kafka 等发送接收消息)
时间消息总栈
SpringCloud Bus
服务部署
Docker、OpenStack、Kubernetes 等
如何构建微服务?
一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过 http 相互请求调用。比如一个电商系统,查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务共同构建了个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
但是这种庞大的系统架构给部罢和运维带来很大的难度。于是, spring 为我们带来了构建大型分布式微服务的全套、全程产品:
构建一个个功能独立的微服务应用单元,可以使用 Spring boot,可以帮我们快速构建一个应用;
大型分布式网络服务的调用,这部分由 Spring cloud 来完成,实现分布式;
在分布式中间,进行流式数据计算、批处理,我们有 Spring cloud data flow。
Spring 为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案。
3.第一个 springboot 程序
基本环境
创建基础项目说明
项目创建方式一 :使用 Spring Initializr 的 Web 页面创建项目
打开 https://start.spring.io/
填写项目信息
点击”Generate Project“按钮生成项目;下载此项目
解压项目包,并用 IDEA 以 Maven 项目导入 ,(并不是打开)一路下一步即可,直到项目导入完毕。
如果是第一次使用,可能速度会比较慢,包比较多、需要耐心等待一切就绪。
报错 bug:Plugin ‘org.springframework.boot:spring-boot-maven-plugin:‘ not found
项目创建方式二: 使用 IDEA 直接创建项目(常用)
创建一个新项目
选择 spring initalizr , 可以看到默认就是去官网的快速构建工具那里实现
填写项目信息
选择初始化的组件(初学勾选 Web 即可 )
填写项目路径
等待项目构建成功
报错 bug:Cannot download ‘https://start.spring.io ’: Connection reset
项目结构分析 :
通过上面步骤完成了基础项目的创建。就会自动生成以下文件:
程序的主启动类
一个 application.properties 配置文件
一个 测试类
一个 pom.xml
pom.xml 分析
打开 pom.xml,看看 Spring Boot 项目的依赖:
<?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.5.5</version >
<relativePath />
</parent >
<groupId > com.github</groupId >
<artifactId > demo</artifactId >
<version > 0.0.1-SNAPSHOT</version >
<name > demo</name >
<description > demo</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 >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-autoconfigure</artifactId >
<version > 2.1.0.RELEASE</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-autoconfigure</artifactId >
<version > 2.5.5</version >
</dependency >
</dependencies >
<build >
<plugins >
<plugin >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-maven-plugin</artifactId >
</plugin >
</plugins >
</build >
</project >
其中关于剔除依赖的说明
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-autoconfigure</artifactId >
<version > 2.1.0.RELEASE</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-autoconfigure</artifactId >
<version > 2.5.5</version >
</dependency >
这两个依赖是用来在 Spring Boot 项目中自动配置应用程序的。具体来说,spring-boot-autoconfigure
这个依赖包含了 Spring Boot 的核心自动配置功能,它会根据你的项目中的类路径和注解来自动配置你的应用程序。
第一个依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.1.0.RELEASE</version> </dependency>
是 Spring Boot 的默认版本,它包含了大部分常用的自动配置。
第二个依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure</artifactId> <version>2.5.5</version> </dependency>
是 Spring Boot 的更新版本,它包含了一些新的自动配置特性。如果你的项目需要使用这些新的特性,那么你需要添加这个依赖。
Spring Boot 的核心自动配置功能是指 Spring Boot 会根据你的项目中的类路径和注解来自动配置你的应用程序。具体来说,Spring Boot 会在启动时扫描你的项目中的所有类,然后根据这些类上的注解来决定是否需要进行自动配置。例如,如果你在项目中使用了@EnableAutoConfiguration
注解,那么 Spring Boot 就会自动配置一些常用的组件,如数据源、缓存等等。这样可以大大简化你的开发过程,让你更加专注于业务逻辑的实现。
编写 HTTP 接口
在主程序的同级目录下,新建一个 controller 包,一定要在同级目录下,否则识别不到;
在包中新建一个 HelloController 类
package com.github.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello () {
return "hello,SpringBoot!" ;
}
}
是的,@RestController
注解会告诉 Spring Boot 将该类中所有的方法返回值都视为 RESTful 风格的响应体,而不是视图名称。这意味着,当你访问/hello
路径时,返回的字符串将直接作为 HTTP 响应体返回给客户端,而不是转发到对应的 HTML 页面。
如果你想让返回值转发到对应的 HTML 页面,可以使用@Controller
或@ResponseBody
注解来代替@RestController
注解。例如:
@Controller
public class HelloController {
@RequestMapping("/hello")
public String hello () {
return "hello" ;
}
}
或者:
@RestController
public class HelloController {
@RequestMapping("/hello")
@ResponseBody
public String hello () {
return "<h1>hello</h1>" ;
}
}
编写完毕后,从主程序启动项目,浏览器发起请求,看页面返回;控制台输出了 Tomcat 访问的端口号!
至此,就完成了一个 web 接口的开发,第一个 Spring Boot 完成。
将项目打成 jar 包,点击 maven 的 package
运行jar包命令:java -jar xxx.jar
附注
4.Springboot 自动装配原理
对于 Maven 项目,我们一般从 pom.xml 文件探究起。
1.Pom.xml
父依赖——主要是管理项目的资源过滤及插件!
<parent >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-parent</artifactId >
<version > 2.5.5</version >
<relativePath />
</parent >
<parent >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-dependencies</artifactId >
<version > 2.5.5</version >
</parent >
这里才是真正管理 SpringBoot 应用里面所有依赖版本的地方,SpringBoot 的版本控制中心;
spring-boot-dependencies:核心依赖在父工程中!所以,我们在写或引入一些
SpringBoot 依赖时,不需要指定版本,因为本身就有这些版本仓库。
启动器
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter</artifactId >
</dependency >
springboot-boot-starter-xxx:就是 spring-boot 的场景启动器;
比如 spring-boot-starter-web:帮我们导入了 web 模块正常运行所依赖的组件;
SpringBoot 将所有的功能场景都抽取出来,变成一个个的 starter (启动器),只需要在项目中引入这些 starter 即可,所有相关的依赖都会导入进来 , 我们要用什么功能就导入什么样的场景启动器即可;也可以自己自定义 starter。
2.主启动类
默认的主启动类——@SpringBootApplication
@SpringBootApplication
public class DemoApplication {
public static void main (String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@ComponentScan
这个注解在 Spring 中对应 XML 配置中的元素。
作用:自动扫描并加载符合条件的组件或者 bean , 将这个 bean 定义加载到 IOC 容器中。
依次按照如下层级点进去:
@SpringBootConfiguration:spring boot 的配置;
@Configuration:spring 配置类,对应 Spring 的 xml 配置文件;
@Component:说明这是一个 Spring 组件,负责启动应用!
@EnableAutoConfiguration:开启自动配置功能;
@AutoConfigurationPackage:自动配置包;
@Import({Registrar.class}):自动配置包注册;
@Import({AutoConfigurationImportSelector.class}):自动导入组件;
List<String> configurations = this .getCandidateConfigurations(annotationMetadata, attributes);
protected List<String> getCandidateConfigurations (AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this .getSpringFactoriesLoaderFactoryClass(), this .getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct." );
return configurations;
}
这个方法又调用了 SpringFactoriesLoader 类的静态方法!进入 SpringFactoriesLoader 类 loadFactoryNames() 方法
public static List<String> loadFactoryNames (Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null ) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
继续点击查看 loadSpringFactories 方法
private static Map<String, List<String>> loadSpringFactories (ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null ) {
return result;
} else {
HashMap result = new HashMap ();
try {
Enumeration urls = classLoader.getResources("META-INF/spring.factories" );
while (urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource (url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while (var6.hasNext()) {
Entry<?, ?> entry = (Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for (int var12 = 0 ; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList ();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
throw new IllegalArgumentException ("Unable to load factories from location [META-INF/spring.factories]" , var14);
}
}
}
发现一个多次出现的文件: spring.factories
,全局搜索 Ctrl+N 它。
WebMvcAutoConfiguration——在上面的自动配置类随便找一个打开看看,比如 : WebMvcAutoConfiguration
可以看到这些一个个的都是 JavaConfig 配置类,而且都注入了一些 Bean,可以找一些自己认识的类,看着熟悉一下!
所以,自动配置真正实现是从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件 ,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射实例化为对应标注了 @Configuration 的 JavaConfig 形式的 IOC 容器配置类 , 然后将这些都汇总成为一个实例并加载到 IOC 容器中。
结论:
SpringBoot 在启动的时候从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值;
将这些值作为自动配置类导入容器,自动配置类就生效,帮我们进行自动配置工作;
整个 J2EE 的整体解决方案和自动配置都在 springboot-autoconfigure 的 jar 包中;
它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器;
容器中也会存在非常多的 xxxAuto Configuration 的文件(@Bean),就是这些类给容器中导入了这个场景需要的所有组件并自动配置,@Configuration, JavaConfig;
有了自动配置类,免去了我们手动编写配置注入功能组件等的工作!
3.SpringApplication
@SpringBootApplication
public class DemoApplication {
public static void main (String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
该方法主要分两部分,一部分是 SpringApplication 的实例化,二是 run 方法的执行。
这个类主要做了以下四件事情:
推断应用的类型是普通的项目还是 Web 项目;
查找并加载所有可用初始化器 , 设置到 initializers 属性中;
找出所有的应用程序监听器,设置到 listeners 属性中;
推断并设置 main 方法的定义类,找到运行的主类。
查看构造器 :
run 方法 :
5.Yaml 语法
配置文件
SpringBoot 使用一个全局的配置文件 , 配置文件名称是固定的。
application.properties
application.yml
配置文件的作用 :修改 SpringBoot 自动配置的默认值,因为 SpringBoot 在底层都给我们自动配置好了;
比如我们可以在配置文件中修改 Tomcat 默认启动的端口号!测试一下!
yaml 概述
YAML 是 “YAML Ain’t a Markup Language” (YAML 不是一种标记语言)的递归缩写。
在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)
这种语言以数据做为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用 xml 来配置;比如一个简单的端口配置,我们来对比下 yaml 和 xml
传统 xml 配置:
<server >
<port > 8081</port >
</server >
yml 基础语法
1、空格不能省略
2、以缩进来控制层级关系,只要是左边对齐的一列数据都是同一个层级的。
3、属性和值的大小写都是十分敏感的。
字面量:普通的值 [ 数字,布尔值,字符串 ]
字面量直接写在后面就可以 , 字符串默认不用加上双引号或者单引号;
注意:
“ ” 双引号,不会转义字符串里面的特殊字符 , 特殊字符会作为本身想表示的意思; 比如 : name: “github\n subei” 输出 : github 换行 subei
‘’ 单引号,会转义特殊字符 , 特殊字符最终会变成和普通字符一样输出 比如 : name: ‘github\n subei’ 输出 : github\n subei
name: github
student:
name: github
age: 18
student2: { name: github , age: 18 }
pets:
- cat
- dog
- pig
pets2: [cat , dog , pig ]
6.给属性赋值的几种方式
yaml 文件在于:可以直接给实体类直接注入匹配值!
Yaml 注入配置文件
在 springboot 项目中的 resources 目录下新建一个文件 application.yaml
编写一个实体类 Dog;
getset 方法,无参有参——快捷键:alt+insert
package com.github.pojo;
import org.springframework.stereotype.Component;
@Component
public class Dog {
private String name;
private Integer age;
}
思考:我们原来是如何给 bean 注入属性值的! @Value,给宠物类测试一下:
@Component
public class Dog {
@Value("旺财")
private String name;
@Value("3")
private Integer age;
}
在 SpringBoot 的测试类下注入宠物类输出一下;
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
Dog dog;
@Test
void contextLoads () {
System.out.println(dog);
}
}
输出成功,@Value 注入成功,这是之前的实现方式。
再编写一个复杂一点的实体类:Person 类
@Component
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> list;
private Dog dog;
}
使用 yaml 配置的方式进行注入,注意区别和优势,编写一个 yaml 配置。
person:
name: subeiLY
age: 18
happy: true
birth: 2002 /03/04
maps: { k1: v1 , k2: v2 }
lists:
- code
- book
- music
dog:
name: 来福
age: 2
将 person 类注入到类中!
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> list;
private Dog dog;
}
IDEA 提示,springboot 配置注解处理器没有找到,让我们看文档,我们可以查看文档,找到一个依赖!但它打不开,不影响。导入如下配置即可。
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-configuration-processor</artifactId >
<optional > true</optional >
</dependency >
确认以上配置都 OK 之后,去测试类中测试一下:
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
Person person;
@Test
void contextLoads () {
System.out.println(person);
}
}
yaml 配置注入到实体类完全 OK!
加载指定配置文件
@PropertySource :加载指定的配置文件;
@configurationProperties:默认从全局配置文件中获取值;
在 resources 目录下新建一个 person.properties 文件。
在我们的代码中指定加载 person.properties 文件。
@Component
@PropertySource(value = "classpath:application.properties")
public class Person {
@Value("${name}")
private String name;
......
}
再次输出测试一下:指定配置文件绑定成功!
配置文件占位符
person:
name: subeiLY${random.uuid}
age: ${random.int}
happy: true
birth: 2002 /03/04
maps: { k1: v1 , k2: v2 }
lists:
- code
- book
- music
dog:
name: ${person.hello:other}_来福
age: 2
回顾 properties 配置
上面采用的 yaml 方法都是最简单的方式,开发中最常用的;也是 springboot 所推荐的!
对于其他的实现方式,道理都是相同的;写还是那样写;配置文件除了 yml 还有我们之前常用的 properties。
【注意】properties 配置文件在写中文的时候,会有乱码 , 我们需要去 IDEA 中设置编码格式为 UTF-8;
settings–>FileEncodings 中配置;
测试步骤 :
新建一个实体类 User
@Component
public class User {
private String name;
private int age;
private String sex;
}
编辑配置文件 application.properties
user1.name =subei
user1.age =16
user1.sex =男
在 User 类上使用@Value 来进行注入!
@Component
@PropertySource(value = "classpath:application.properties")
public class User {
@Value("${user1.name}")
private String name;
@Value("#{8*2}")
private int age;
@Value("男")
private String sex;
}
Springboot 测试
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
User user;
@Test
void contextLoads () {
System.out.println(user);
}
}
对比小结
@Value 这个使用起来并不友好!我们需要为每个属性单独注解赋值,比较麻烦;我们来看个功能对比图:
@ConfigurationProperties 只需要写一次即可 , @Value 则需要每个字段都添加;
松散绑定:这个什么意思呢? 比如我的 yml 中写的 last-name,这个和 lastName 是一样的, - 后面跟着的字母默认是大写的。这就是松散绑定。可以测试一下!
JSR303 数据校验,这个就是我们可以在字段是增加一层过滤器验证,可以保证数据的合法性;
复杂类型封装,yml 中可以封装对象,使用 value 就不支持。
结论 :
配置 yml 和配置 properties 都可以获取到值 , 强烈推荐 yml;
如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下 @value;
如果说,我们专门编写了一个 JavaBean 来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!
7.JSR303 数据校验
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-validation</artifactId >
<version > 2.5.6</version >
</dependency >
Springboot 中可以用@validated 来校验数据,如果数据异常则会统一抛出异常,方便异常中心统一处理。先写个注解让 name 只能支持 Email 格式;
@Component
@ConfigurationProperties(prefix = "person")
@Validated
public class Person {
@Email(message="邮箱格式错误")
private String name;
......
}
运行结果:default message [name];
使用数据校验,可以保证数据的正确性; 下面列出一些常见的使用:
@NotNull(message ="名字不能为空")
private String userName;
@Max(value =120,message="年龄最大不能查过120")
private int age;
@Email(message ="邮箱格式错误")
// 空检查
@Null 验证对象是否为null
@NotNull 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty 检查约束元素是否为NULL或者是EMPTY.
// Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true
@AssertFalse 验证 Boolean 对象是否为 false
// 长度检查
@Size(min =, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内
@Length(min =, max=) string is between min and max included.
// 日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前
@Future 验证 Date 和 Calendar 对象是否在当前时间之后
@Pattern 验证 String 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
8.多环境配置及配置文件位置
profile 是 Spring 对不同环境提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;
多配置文件
在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml , 用来指定多个环境版本;
例如:
application-test.properties 代表测试环境配置;
application-dev.properties 代表开发环境配置;
但是 Springboot 并不会直接启动这些配置文件,它默认使用 application.properties 主配置文件; 我们需要通过一个配置来选择需要激活的环境:
spring.profiles.active =dev
yml 的多文档块
和 properties 配置文件中一样,但是使用 yml 去实现不需要创建多个配置文件,更方便 !
server:
port: 8081
spring:
profiles:
active: dev
---
server:
port: 8082
spring:
profiles: dev
---
server:
port: 8083
spring:
profiles: test
注意:如果 yml 和 properties 同时都配置了端口,并且没有激活其他环境 , 默认会使用 properties 配置文件的!
配置文件加载位置
官方外部配置文档参考
springboot 启动会扫描以下位置的 application.properties 或者 application.yml 文件作为 Spring boot 的 默认配置文件。
优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件
SpringBoot 会从这四个位置全部加载主配置文件;互补配置;
我们在最低级的配置文件中设置一个项目访问路径的配置来测试互补问题;
server.servlet.context-path =/github
指定位置加载配置文件
可以通过 spring.config.location 来改变默认的配置文件位置;
项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件优先级最高。
java -jar spring-boot-config.jar --
spring.config.location =F:/application.properties
9.自动配置原理再理解
分析自动配置原理
以 HttpEncodingAutoConfiguration(Http 编码自动配置)为例解释自动配置原理。
@Configuration
@EnableConfigurationProperties({HttpProperties.class})
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
private final Encoding properties;
public HttpEncodingAutoConfiguration (HttpProperties properties) {
this .properties = properties.getEncoding();
}
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter () {
CharacterEncodingFilter filter = new
OrderedCharacterEncodingFilter ();
filter.setEncoding(this .properties.getCharset().name());
filter.setForceRequestEncoding(this .properties.shouldForce(org.springframew
ork.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this .properties.shouldForce(org.springframe
work.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
}
总结 : 根据当前不同的条件判断,决定这个配置类是否生效!
一但这个配置类生效;这个配置类就会给容器中添加各种组件;
这些组件的属性是从对应的 properties 类中获取的,这些类里面的每一个属性又是和配置文件绑定的;
所有在配置文件中能配置的属性都是在 xxxxProperties 类中封装着;
配置文件能配置什么就可以参照某个功能对应的这个属性类
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
}
要点:
SpringBoot 启动会加载大量的自动配置类。
我们看我们需要的功能有没有在 SpringBoot 默认写好的自动配置类当中;
我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)。
给容器中自动配置类添加组件的时候,会从 properties 类中获取某些属性。我们只需要在配置文件中 指定这些属性的值即可;
xxxxAutoConfigurartion:自动配置类;给容器中添加组件;
xxxxProperties:封装配置文件中相关属性。
了解:@Conditional
关注一个细节问题:自动配置类必须在一定的条件下才能生效。
@Conditional 派生注解(Spring 注解版原生的@Conditional 作用)
作用:必须是@Conditional 指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,加载了这么多的配置类,但不是所有的都生效了。
如何知道哪些自动配置类生效?
可以通过启用 debug=true 属性;来让控制台打印自动配置报告,这样就可以很方便的知道哪些自动配置类生效。
Positive matches:(自动配置类启用的:正匹配)
Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)
Unconditional classes: (没有条件的类)
10.自定义 starter
说明
启动器模块是一个 空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库。
命名归约:
官方命名:
前缀: spring-boot-starter-xxx
比如:spring-boot-starter-web…
自定义命名:
xxx-spring-boot-starter
比如:mybatis-spring-boot-starter
编写启动器
在 IDEA 中新建一个空项目 spring-boot-starter-diy
新建一个普通 Maven 模块:spring-boot-starter
新建一个 Springboot 模块:spring-boot-starter-autoconfigure
在 starter 中 导入 autoconfigure 的依赖!spring-boot-starter 无需编写什么代码,只需让该工程引入 hello-spring-boot-starter-autoconfigure 依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns ="http://maven.apache.org/POM/4.0.0"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" >
<modelVersion > 4.0.0</modelVersion >
<groupId > org.example</groupId >
<artifactId > spring-boot-starter</artifactId >
<version > 1.0-SNAPSHOT</version >
<properties >
<maven.compiler.source > 8</maven.compiler.source >
<maven.compiler.target > 8</maven.compiler.target >
</properties >
<dependencies >
<dependency >
<groupId > com.github</groupId >
<artifactId > spring-boot-starter-autoconfigure</artifactId >
<version > 0.0.1-SNAPSHOT</version >
</dependency >
</dependencies >
</project >
spring-boot-starter-autoconfigure
的 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.5.6</version >
<relativePath />
</parent >
<groupId > com.github</groupId >
<artifactId > spring-boot-starter-autoconfigure</artifactId >
<version > 0.0.1-SNAPSHOT</version >
<name > spring-boot-starter-autoconfigure</name >
<description > spring-boot-starter-autoconfigure</description >
<properties >
<java.version > 1.8</java.version >
</properties >
<dependencies >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter</artifactId >
</dependency >
</dependencies >
</project >
将 autoconfigure 项目下多余的文件都删掉,Pom 中只留下一个 starter,这是所有的启动器基本配置。
编写一个个人的服务
package com.github;
public class HelloService {
HelloProperties helloProperties;
public HelloProperties getHelloProperties () {
return helloProperties;
}
public void setHelloProperties (HelloProperties helloProperties) {
this .helloProperties = helloProperties;
}
public String sayHello (String name) {
return helloProperties.getPrefix() + name +
helloProperties.getSuffix();
}
}
编写 HelloProperties 配置类
package com.github;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties(prefix = "github.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix () {
return prefix;
}
public void setPrefix (String prefix) {
this .prefix = prefix;
}
public String getSuffix () {
return suffix;
}
public void setSuffix (String suffix) {
this .suffix = suffix;
}
}
编写自动配置类并注入 bean,测试!
package com.github;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
public HelloService helloService () {
HelloService service = new HelloService ();
service.setHelloProperties(helloProperties);
return service;
}
}
在 resources 编写一个自己的 META-INF\spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration =\
com.github.HelloServiceAutoConfiguration
安装到 maven 仓库中!
新建项目测试我们自己的写的启动器
新建一个 SpringBoot 项目。
导入我们自己写的启动器。
<dependency >
<groupId > com.github</groupId >
<artifactId > spring-boot-starter-autoconfigure</artifactId >
<version > 0.0.1-SNAPSHOT</version >
</dependency >
编写一个 HelloController 进行测试自己的写的接口!
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String hello () {
return helloService.sayHello("whh" );
}
}
编写配置文件 application.properties
github.hello.prefix ="qqqq"
github.hello.suffix ="pppp"
启动项目进行测试 !
Spring Boot 开发单体应用(1)
1.SpringBoot Web 开发
使用 SpringBoot 的步骤 :
创建一个 SpringBoot 应用,选择我们需要的模块,SpringBoot 就会默认将我们的需要的模块自动配置好;
手动在配置文件中配置部分配置项目就可以运行起来了。
专注编写业务代码,不需要考虑以前那样一大堆的配置了。
自动装配
spring boot 到底帮我们配置了什么?我们能不能进行修改?能修改哪些东西?能不能扩展?
xxxxAutoConfiguraion… 向容器中自动配置组件;
xxxxProperties:自动配置类,装配配置文件中自定义的一些内容!
2.静态资源处理
静态资源映射规则
搭建一个普通的 SpringBoot 项目。
写请求非常简单,那我们要引入我们前端资源,我们项目中有许多的静态资源,比如 css,js 等文件,这 个 SpringBoot 怎么处理呢?
如果我们是一个 web 应用,我们的 main 下会有一个 webapp,我们以前都是将所有的页面导在这里面 的,对吧!但是我们现在的 pom 呢,打包方式是为 jar 的方式,那么这种方式 SpringBoot 能不能来给我们 写页面呢?当然是可以的,但是 SpringBoot 对于静态资源放置的位置,是有规定的!
SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面;有一个方法: 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,我们可以去搜索一下:
网站:https://www.webjars.org 【网站带看,并引入 jQuery 测试】
要使用 jQuery,我们只要要引入 jQuery 对应版本的 pom 依赖即可!
<dependency >
<groupId > org.webjars</groupId >
<artifactId > jquery</artifactId >
<version > 3.4.1</version >
</dependency >
导入完毕,查看 webjars 目录结构,并访问 Jquery.js 文件!
第二种静态资源映射规则
当项目中要是使用自己的静态资源该怎么导入呢?看下一行代码;
去找 staticPathPattern 发现第二种映射规则:/**, 访问当前的项目任意资源,它会去找 resourceProperties 这个类,我们可以点进去看一下分析:
public class ResourceProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = new String []{"classpath:/META-INF/resources/" , "classpath:/resources/" , "classpath:/static/" , "classpath:/public/" };
private String[] staticLocations;
private boolean addMappings;
private final ResourceProperties.Chain chain;
private final ResourceProperties.Cache cache;
public ResourceProperties () {
this .staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
this .addMappings = true ;
this .chain = new ResourceProperties .Chain();
this .cache = new ResourceProperties .Cache();
}
public String[] getStaticLocations() {
return this .staticLocations;
}
......
}
ResourceProperties 可以设置和我们静态资源有关的参数;这里面指向了它会去寻找资源的文件夹,即上面数组的内容。 所以得出结论,以下四个目录存放的静态资源可以被我们识别:
"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"
自定义静态资源路径
也可以自己通过配置文件来指定一下,哪些文件夹是需要我们放静态资源文件的,在 application.properties 中配置;
spring.web.resources.static-locations =classpath:/coding/,classpath:/github/
一旦自己定义了静态文件夹的路径,原来的自动配置就都会失效了!
3.首页和图标定制
继续向下看源码!可以看到一个欢迎页的映射,就是我们的首页!
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping (ApplicationContext applicationContext, FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping (new TemplateAvailabilityProviders (applicationContext), applicationContext, this .getWelcomePage(), this .mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(this .getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(this .getCorsConfigurations());
return welcomePageHandlerMapping;
}
private Optional<Resource> getWelcomePage () {
String[] locations = WebMvcAutoConfiguration.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" );
}
关于网站图标说明:
与其他静态资源一样,Spring Boot 在配置的静态内容位置中查找 favicon.ico。如果存在这样的文件,它将自动用作应用程序的 favicon。
关闭 SpringBoot 默认图标!
spring.mvc.favicon.enabled =false
自己放一个图标在静态资源目录下,我放在 resources 目录下,图标的命名必须是favicon.ico
!
清除浏览器缓存!刷新网页,发现图标已经变成自己的了!
4.Thymeleaf 模板引擎及语法
模板引擎
前端交给我们的页面,是 html 页面。如果是我们以前开发,我们需要把他们转成 jsp 页面,jsp 好处就是当我们查出一些数据转发到 JSP 页面以后,我们可以用 jsp 轻松实现数据的显示,及交互等。
jsp 支持非常强大的功能,包括能写 Java 代码,但是呢,我们现在的这种情况,SpringBoot 这个项目首先是以 jar 的方式,不是 war,像第二,我们用的还是嵌入式的 Tomcat,所以呢,他现在默认是不支持 jsp 的。
那不支持 jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?
SpringBoot 推荐你可以来使用模板引擎:
模板引擎,我们其实大家听到很多,其实 jsp 就是一个模板引擎,还有以用的比较多的 freemarker,包括 SpringBoot 给我们推荐的 Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样 的,什么样一个思想呢我们来看一下这张图:
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些 值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们 想要的内容给我们写出去,这就是我们这个模板引擎,不管是 jsp 还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下 SpringBoot 给我们推荐的 Thymeleaf 模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他 的这个语法更简单。而且呢,功能更强大。
首先,我们来看 SpringBoot 里边怎么用。
引入 Thymeleaf
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-thymeleaf</artifactId >
</dependency >
Maven 会自动下载 jar 包,我们可以去看下下载的东西;
thymeleaf 分析
前面已经引入了 Thymeleaf,那这个要怎么使用呢?
我们首先得按照 SpringBoot 的自动配置原理看一下我们这个 Thymeleaf 的自动配置规则,在按照那个规则,我们进行使用。
先去找一下 Thymeleaf 的自动配置类:ThymeleafProperties
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/" ;
public static final String DEFAULT_SUFFIX = ".html" ;
private boolean checkTemplate = true ;
private boolean checkTemplateLocation = true ;
private String prefix = "classpath:/templates/" ;
private String suffix = ".html" ;
private String mode = "HTML" ;
private Charset encoding;
private boolean cache;
private Integer templateResolverOrder;
private String[] viewNames;
private String[] excludedViewNames;
private boolean enableSpringElCompiler;
private boolean renderHiddenMarkersBeforeCheckboxes;
private boolean enabled;
private final ThymeleafProperties.Servlet servlet;
private final ThymeleafProperties.Reactive reactive;
}
可以在其中看到默认的前缀和后缀!
我们只需要把我们的 html 页面放在类路径下的 templates 下,thymeleaf 就可以帮我们自动渲染了。
使用 thymeleaf 什么都不需要配置,只需要将他放在指定的文件夹下即可!
测试 :
编写一个 TestController
@Controller
public class TestController {
@RequestMapping("/t1")
public String test () {
return "test" ;
}
}
编写一个测试页面 test.html 放在 templates 目录下
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" />
<title > 测试</title >
</head >
<body >
<h1 > 测试页</h1 >
</body >
</html >
启动项目请求测试
Thymeleaf 语法学习
语法学习,参考官网:Thymeleaf
做个最简单的练习:我们需要查出一些数据,在页面中展示。
修改测试请求,增加数据传输;
@Controller
public class TestController {
@RequestMapping("/t1")
public String test (Model model) {
model.addAttribute("msg" ,"hello,Thymeleaf" );
return "test" ;
}
}
要使用 thymeleaf,需要在 html 文件中导入命名空间的约束,方便提示。可以去官方文档的#3 中看一下命名空间拿来过来:
<html xmlns:th="http://www.thymeleaf.org" >
编写前端页面
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<title > 测试</title >
</head >
<body >
<h1 > 测试页</h1 >
<div th:text ="${msg}" > </div >
</body >
</html >
OK,入门搞定,我们来认真研习一下 Thymeleaf 的使用语法!
可以使用任意的 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.
3 )、内置的一些工具对象:
#execInfo : information about the template being processed.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion
service (if any) .
#dates : methods for java.util.Date objects: formatting, component
extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar
objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith,
prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or
collections.
============================================================================
======
Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样;
Message Expressions: #{...}:获取国际化内容
Link URL Expressions: @{...}:定义URL;
Fragment Expressions: ~{...}:片段引用表达式
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(数学运算)
Binary operators: + , - , * , / , %
Minus sign (unary operator) : -
Boolean operations:(布尔运算)
Binary operators: and , or
Boolean negation (unary operator) : ! , not
Comparisons and equality:(比较运算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators:条件运算(三元运算符)
If-then: (if ) ? (then)
If-then-else : (if ) ? (then) : (else )
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
练习测试 :
编写一个 Controller,放一些数据。
@RequestMapping("/t2")
public String test2 (Map<String, Object> map) {
map.put("msg" ,"<h1>MSD</h1>" );
map.put("users" , Arrays.asList("subei" ,"github" ));
return "test" ;
}
编写前端页面
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<title > 测试</title >
</head >
<body >
<h1 > 测试页</h1 >
<div th:text ="${msg}" > </div >
<div th:utext ="${msg}" > </div >
<h3 th:each ="user :${users}" th:text ="${user}" > </h3 >
</body >
</html >
启动测试!
根据官方文档来查询,才是最重要的,要熟练使用官方文档!
5.Spring MVC 配置原理
阅读官网
在进行项目编写前,还需要知道一个东西,就是 SpringBoot 对我们的 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
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 .
仔细对照,看一下它怎么实现的,它告诉我们 SpringBoot 已经帮我们自动配置好了 SpringMVC,然后自动配置了哪些东西呢?
ContentNegotiatingViewResolver 内容协商视图解析器 。
自动配置了 ViewResolver,就是之前学习的 SpringMVC 的视图解析器;
即根据方法的返回值取得视图对象(View),然后由视图对象决定如何渲染(转发,重定向)。
去看看这里的源码:我们找到 WebMvcAutoConfiguration , 然后搜索 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));
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
@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 中看到他是把所有的视图解析器拿来,进行 while 循环,挨个解析!
Iterator var5 = this .viewResolvers.iterator();
所以得出结论:ContentNegotiatingViewResolver 这个视图解析器就是用来组合所有的视图解析器的
再去研究下他的组合逻辑,看到有个属性 viewResolvers,看看它是在哪里进行赋值的!
protected void initServletContext (ServletContext servletContext) {
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this .obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this .viewResolvers == null ) {
this .viewResolvers = new ArrayList (matchingBeans.size());
}
}
既然它是在容器中去找视图解析器,是否可以猜想,可以去实现一个视图解析器了呢?
可以自己给容器中去添加一个视图解析器;这个类就会帮我们自动的将它组合进来;
在主程序中去写一个视图解析器来试试;
@Bean
public ViewResolver myViewResolver () {
return new MyViewResolver ();
}
private static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName (String s, Locale locale) throws Exception {
return null ;
}
}
看我们自己写的视图解析器有没有起作用呢? 我们给 DispatcherServlet 中的 doDispatch 方法加个断点进行调试一下,因为所有的请求都会走到这个方法中。
启动我们的项目,然后在浏览器随便访问一个页面
,看一下 Debug 信息;
所以说,如果想要使用自己定制化的东西,只需要给容器中添加这个组件就好了!剩下的事情 SpringBoot 就会帮我们做了!
转换器和格式化器
@Bean
public FormattingConversionService mvcConversionService () {
WebConversionService conversionService = new WebConversionService (this .mvcProperties.getDateFormat());
this .addFormatters(conversionService);
return conversionService;
}
public String getDateFormat () {
return this .dateFormat;
}
public void setDateFormat (String dateFormat) {
this .dateFormat = dateFormat;
}
private String dateFormat;
可以看到在 Properties 文件中,我们可以进行自动配置它!
修改 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.
要做的就是编写一个@Configuration 注解类,并且类型要为 WebMvcConfigurer,还不能标注@EnableWebMvc 注解;我们去自己写一个;我们新建一个包叫 config,写一个类 MyMvcConfig;
@Configuration
public class MyMVCConfig implements WebMvcConfigurer {
@Override
public void addViewControllers (ViewControllerRegistry registry) {
registry.addViewController("/test" ).setViewName("test" );
}
}
确实也跳转过来了!所以说,要扩展 SpringMVC,官方推荐我们这么去使用,既保留 SpringBoot 所有的自动配置,也能用我们扩展的配置!
具体可以去分析一下原理:
WebMvcAutoConfiguration 是 SpringMVC 的自动配置类,里面有一个类 WebMvcAutoConfigurationAdapter
这个类上有一个注解,在做其他自动配置时会导入: @Import(EnableWebMvcConfiguration.class)
当点进 EnableWebMvcConfiguration 这个类看一下,它继承了一个父类: DelegatingWebMvcConfiguration
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite ();
public DelegatingWebMvcConfiguration () {
}
@Autowired(
required = false
)
public void setConfigurers (List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this .configurers.addWebMvcConfigurers(configurers);
}
}
}
可以在这个类中去寻找一个上面刚才设置的 viewController 当做参考,发现它调用了一个。
public void addWebMvcConfigurers (List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this .delegates.addAll(configurers);
}
}
点进去:
public void addFormatters (FormatterRegistry registry) {
Iterator var2 = this .delegates.iterator();
while (var2.hasNext()) {
WebMvcConfigurer delegate = (WebMvcConfigurer)var2.next();
delegate.addFormatters(registry);
}
}
所以得出结论:所有的 WebMvcConfiguration 都会被作用,不止 Spring 自己的配置类,我们自己的配置类当然也会被调用;
全面接管 SpringMVC
If you want to take complete control of Spring MVC
you can add your own @Configuration annotated with @EnableWebMvc.
全面接管即:SpringBoot 对 SpringMVC 的自动配置不需要了,所有都是我们自己去配置!
只需在我们的配置类中要加一个@EnableWebMvc。
看下如果我们全面接管了 SpringMVC 了,我们之前 SpringBoot 给我们配置的静态资源映射一定会无效,我们可以去测试一下;
不加注解之前,访问首页:
发现所有的 SpringMVC 自动配置都失效了!回归到了最初的样子;
所以,开发中不推荐使用全面接管 SpringMVC。
为什么加了一个注解,自动配置就失效了!看下源码:
可以发现它是导入了一个类,可以继续进去看:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DelegatingWebMvcConfiguration.class})
public @interface EnableWebMvc {
}
它继承了一个父类WebMvcConfigurationSupport
:
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite ();
.......
}
来回顾一下 Webmvc 自动配置类:
@Configuration
@ConditionalOnWebApplication(
type = Type.SERVLET
)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
public static final String DEFAULT_PREFIX = "" ;
public static final String DEFAULT_SUFFIX = "" ;
private static final String[] SERVLET_LOCATIONS = new String []{"/" };
public WebMvcAutoConfiguration () {
}
......
}
总结一句话:@EnableWebMvc 将 WebMvcConfigurationSupport 组件导入进来了;而导入的 WebMvcConfigurationSupport 只是 SpringMVC 最基本的功能!
这就是在 SpringMVC 上加了一层封装!
Spring Boot 开发单体应用 2
6.配置环境及首页
新建 spring boot 项目,导入依赖包。
<?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.1.7.RELEASE</version >
<relativePath />
</parent >
<groupId > com.github</groupId >
<artifactId > springboot-04-demo</artifactId >
<version > 0.0.1-SNAPSHOT</version >
<name > springboot-04-demo</name >
<description > springboot-04-demo</description >
<properties >
<java.version > 1.8</java.version >
<project.build.sourceEncoding > UTF-8</project.build.sourceEncoding >
<project.reporting.outputEncoding > UTF-8</project.reporting.outputEncoding >
</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-thymeleaf</artifactId >
</dependency >
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
</dependency >
<dependency >
<groupId > org.mybatis.spring.boot</groupId >
<artifactId > mybatis-spring-boot-starter</artifactId >
<version > 2.1.4</version >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<scope > runtime</scope >
</dependency >
<dependency >
<groupId > org.webjars</groupId >
<artifactId > jquery</artifactId >
<version > 3.6.0</version >
</dependency >
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-test</artifactId >
<scope > test</scope >
</dependency >
</dependencies >
<build >
<plugins >
<plugin >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-maven-plugin</artifactId >
</plugin >
</plugins >
</build >
</project >
导入实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender;
private Department department;
private Date birth;
}
配置 dao 层
package com.github.dao;
import com.github.pojo.Department;
import org.springframework.stereotype.Repository;
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 <Integer, Department>();
departments.put(101 ,new Department (101 ,"运营部" ));
departments.put(102 ,new Department (102 ,"策划部" ));
departments.put(103 ,new Department (103 ,"法务部" ));
departments.put(104 ,new Department (104 ,"开发部" ));
departments.put(105 ,new Department (105 ,"宣传部" ));
}
public Collection<Department> getDepartments () {
return departments.values();
}
public Department getDepartment (Integer id) {
return departments.get(id);
}
}
package com.github.dao;
import com.github.pojo.Department;
import com.github.pojo.Employee;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
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;
static {
employees = new HashMap <>();
employees.put(1001 ,new Employee (1001 ,"Quary" ,"A2835467@qq.com" ,1 ,new Department (1001 ,"运营部" ),new Date ()));
employees.put(1002 ,new Employee (1002 ,"Quary" ,"B2835467@qq.com" ,0 ,new Department (1002 ,"策划部" ),new Date ()));
employees.put(1003 ,new Employee (1003 ,"Quary" ,"C2835467@qq.com" ,1 ,new Department (1003 ,"法务部" ),new Date ()));
employees.put(1004 ,new Employee (1004 ,"Quary" ,"D2835467@qq.com" ,0 ,new Department (1004 ,"开发部" ),new Date ()));
employees.put(1005 ,new Employee (1005 ,"Quary" ,"F2835467@qq.com" ,1 ,new Department (1005 ,"宣传部" ),new Date ()));
}
private static Integer initid=1006 ;
public void save (Employee employee) {
if (employee.getId()==null ){
employee.setId(initid);
}
employee.setDepartment(departmentDao.getDepartment(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
public Collection<Employee> getAll () {
return employees.values();
}
public Employee getEmployee (Integer id) {
return employees.get(id);
}
public void delete (Integer id) {
employees.remove(id);
}
}
导入静态资源
css,js 等放在 static 文件夹下
html 放在 templates 文件夹下
启动类由于是未连接数据库,需要修改为如下:
package com.github;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class Springboot04DemoApplication {
public static void main (String[] args) {
SpringApplication.run(Springboot04DemoApplication.class, args);
}
}
报错 bug:java: 程序包 org.junit.jupiter.api 不存在
<dependency >
<groupId > org.junit.jupiter</groupId >
<artifactId > junit-jupiter-api</artifactId >
<version > 5.5.0</version >
<scope > test</scope >
</dependency >
IDEA springboot 启动报错:APPLICATION FAILED TO START : Failed to configure a DataSource
原因:基于 IDEA 自带的 spring Initializr 来创建 springboot 项目的,选取了 MySQL Driver 的依赖包配置,但却没有在配置文件(.yml/.properties/.yaml)中配置过数据源等相关信息,因此就会报错,无法找到数据源 Datasource 的路径。这里由于我是为了掩饰 nacos 的某些功能,因此暂时不需要用到数据库。
解决方法:
在配置文件里面,配置数据源
如果你不需要使用到数据库的话,可以直接在启动类的注解上修改即可:
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
首页实现
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class IndexController {
@RequestMapping({"/","/index.html"})
public String index () {
return "index" ;
}
}
package com.github.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Controller
public class IndexController implements WebMvcConfigurer {
@Override
public void addViewControllers (ViewControllerRegistry registry) {
registry.addViewController("/" ).setViewName("index" );
registry.addViewController("/index.html" ).setViewName("index" );
}
}
解决了首页问题,还需要解决一个资源导入的问题;为了保证资源导入稳定,建议在所有资源导入时候使用 th:去替换原有的资源路径!这也是模板规范。
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<link th:href ="@{/asserts/css/bootstrap.min.css}" rel ="stylesheet" />
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta http-equiv ="Content-Type" content ="text/html; charset=UTF-8" />
<meta
name ="viewport"
content ="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<meta name ="description" content ="" />
<meta name ="author" content ="" />
<title > Signin Template for Bootstrap</title >
<link th:href ="@{/css/bootstrap.min.css}" rel ="stylesheet" />
<link th:href ="@{/css/signin.css}" rel ="stylesheet" />
</head >
<body class ="text-center" >
<form class ="form-signin" action ="dashboard.html" >
<img
class ="mb-4"
th:src ="@{/img/bootstrap-solid.svg}"
alt =""
width ="72"
height ="72"
/>
<h1 class ="h3 mb-3 font-weight-normal" > Please sign in</h1 >
<label class ="sr-only" > Username</label >
<input
type ="text"
class ="form-control"
placeholder ="Username"
required =""
autofocus =""
/>
<label class ="sr-only" > Password</label >
<input
type ="password"
class ="form-control"
placeholder ="Password"
required =""
/>
<div class ="checkbox mb-3" >
<label >
<input type ="checkbox" value ="remember-me" /> Remember me
</label >
</div >
<button class ="btn btn-lg btn-primary btn-block" type ="submit" >
Sign in
</button >
<p class ="mt-5 mb-3 text-muted" > © 2020-2021</p >
<a class ="btn btn-sm" > 中文</a >
<a class ="btn btn-sm" > English</a >
</form >
</body >
</html >
</html >
7.页面国际化
有的时候,我们的网站会去涉及中英文甚至多语言的切换,这时候我们就需要学习国际化了!
准备工作
先在 IDEA 中统一设置 properties 的编码问题!
编写国际化配置文件,抽取页面需要显示的国际化页面消息。
配置文件编写
在 resources 资源文件下新建一个 i18n 目录,存放国际化配置文件
建立一个 login.properties 文件,还有一个 login_zh_CN.properties;发现 IDEA 自动识别了我们要做 国际化操作;文件夹变了!
在这上面去新建一个文件;
编写配置,依次添加其他页面内容即可!
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名
login.btn=Sign in
login.password=Password
login.remember=Remember me
login.tip=Please sign in
login.username=Username
login.btn=登录
login.password=密码
login.remember=记住我
login.tip=请登录
login.username=用户名
配置文件生效探究
去看一下 SpringBoot 对国际化的自动配置!这里又涉及到一个类: 里面有一个方法,这里发现 SpringBoot 已经自动配置好了管理我们国际化资源文件的组件 ResourceBundleMessageSource;MessageSourceAutoConfiguration
@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
配置页面国际化值
去页面获取国际化的值,查看 Thymeleaf 的文档,找到 message 取值操作为: #{…}。我们去页面测试下:
可以去启动项目,访问一下,发现已经自动识别为中文的了!
配置国际化解析
在 Spring 中有一个国际化的 Locale (区域信息对象);里面有一个叫做 LocaleResolver (获取区域信息 对象)的解析器!去 webmvc 自动配置文件找一下!看到 SpringBoot 默认配置:
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(
prefix = "spring.mvc",
name = {"locale"}
)
public LocaleResolver localeResolver () {
if (this .mvcProperties.getLocaleResolver() == org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver (this .mvcProperties.getLocale());
} else {
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver ();
localeResolver.setDefaultLocale(this .mvcProperties.getLocale());
return localeResolver;
}
}
AcceptHeaderLocaleResolver 这个类中有一个方法。
public Locale resolveLocale (HttpServletRequest request) {
Locale defaultLocale = this .getDefaultLocale();
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,可以在链接上携带区域信息!
先修改一下前端页面的跳转:
<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.github.component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
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) {
String language = request.getParameter("l" );
Locale locale = Locale.getDefault();
if (!StringUtils.isEmpty(language)) {
String[] split = language.split("_" );
locale = new Locale (split[0 ], split[1 ]);
}
return locale;
}
@Override
public void setLocale (HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Locale locale) {
}
}
为了让区域化信息能够生效,需要再配置一下这个组件!在个人的 MvcConofig 下添加 bean;
@Bean
public LocaleResolver localeResolver () {
return new MyLocaleResolver ();
}
重启项目,访问一下,发现点击按钮可以实现成功切换!
8.登录功能实现
禁用模板缓存
spring.thymeleaf.cache =false
模板引擎修改后,想要实时生效!页面修改完毕后, + 重新编译!即可生效!Ctrl+F9
登录
把登录页面的表单提交地址写一个 controller!
<form class="form-signin" th:action="@{/user/login}" method="post" >
</form>
编写对应的 controller
@Controller
public class LoginController {
@PostMapping("/user/login")
public String login (@RequestParam("username") String username,
@RequestParam("password") String password,
Model model, HttpSession session) {
if (!StringUtils.isEmpty(username) && "123456" .equals(password)){
session.setAttribute("loginUser" ,username);
return "dashboard" ;
}else {
model.addAttribute("msg" ,"用户名密码错误" );
return "index" ;
}
}
}
测试登录,默认用户名:admin,密码:123456
登录失败的话,需要将后台信息输出到前台,可以在首页标题下面加上判断!
<p style ="color: red" th:text ="${msg}" th:if ="${not #strings.isEmpty(msg)}" > </p >
优化,登录成功后,由于是转发,链接不变,可以重定向到首页!
再添加一个视图控制映射,在自己的 MyMvcConfig 中:
registry.addViewController("/main.html" ).setViewName("dashboard" );
将 Controller 的代码改为重定向;
return "redirect:/main.html" ;
登录拦截器
先自定义一个拦截器:
package com.github.controller;
import org.springframework.web.servlet.HandlerInterceptor;
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 {
Object user = request.getSession().getAttribute("loginUser" );
if (user==null ){
request.setAttribute("msg" ,"没有权限,请登录账户" );
request.getRequestDispatcher("/index.html" ).forward(request,response);
return false ;
}else {
return true ;
}
}
}
然后将拦截器注册到 SpringMVC 配置类当中——!MyMVCConfig.java
@Override
public void addInterceptors (InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor ())
.addPathPatterns("/**" )
.excludePathPatterns("/index.html" ,"/" ,"/user/login" ,"/static/**" );
}
然后在后台主页,获取用户登录的信息——。dashboard.html
报错:No mapping for GET /css/bootstrap.min.css
在 MyMvcConfig.java 内加上这两个函数
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/" , "classpath:/resources/" ,
"classpath:/static/" , "classpath:/public/" };
@Override
public void addResourceHandlers (ResourceHandlerRegistry registry) {
if (!registry.hasMappingForPattern("/webjars/**" )) {
registry.addResourceHandler("/webjars/**" ).addResourceLocations(
"classpath:/META-INF/resources/webjars/" );
}
if (!registry.hasMappingForPattern("/**" )) {
registry.addResourceHandler("/**" ).addResourceLocations(
CLASSPATH_RESOURCE_LOCATIONS);
}
}
9.员工列表实现
RestFul 风格
要求: 需要使用 Restful 风格实现 CRUD 操作!
普通 CRUD(uri 来区分操作)
RestfulCRUD
查询
getEmp
emp–GET
添加
addEmp?xxx
emp–POST
修改
updateEmp?id=xxx&xxx=xx
emp/{id}–PUT
删除
deleteEmp?id=1
emp/{id}—DELETE
实验功能
请求 URI
请求方式
查询所有员工
emps
GET
查询某个员工(来到修改页面)
emp/1
GET
来到添加页面
emp
GET
添加员工
emp
POST
来到修改页面(查出员工进行信息回显)
emp/1
GET
修改员工
emp
PUT
删除员工
emp/1
DELETE
员工列表的跳转
修改首页侧边栏的 Customers 为员工管理。
a 链接添加请求
<li class ="nav-item" >
<a class ="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 >
将 list 文件放到 emp 文件夹下
编写 Controller
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list (Model model) {
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps" ,employees);
return "emp/list" ;
}
}
启动测试。
Thymeleaf 公共页面元素抽取
步骤:
抽取公共片段 th:fragment 定义模板名;
引入公共片段 th:insert 插入模板名;
实现:
使用 list 列表做演示!要抽取头部 nav 标签,在 dashboard 中将 nav 部分定义一个模板名;
在目录下新建一个包,其中新建用来放置公共页面代码。templates``commons``commons.html
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<nav
class ="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0"
th:fragment ="topbar"
>
<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/#"
>Sign out</a
>
</li >
</ul >
</nav >
<nav
class ="col-md-2 d-none d-md-block bg-light sidebar"
th:fragment ="siderbar"
>
<div class ="sidebar-sticky" >
<ul class ="nav flex-column" >
<li class ="nav-item" >
<a
class ="nav-link active"
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-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 >
Dashboard <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 class ="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 >
删除和中顶部导航栏和侧边栏的代码。dashboard.html``list.html
分别在和删除的部分插入提取出来的公共部分和。dashboard.html``list.html``topbar``sidebar
<div th:replace ="~{commons/commons::topbar}" > </div >
<div class ="container-fluid" >
<div class ="row" >
<div th:replace ="~{commons/commons::siderbar}" > </div >
</div >
</div >
点亮高亮
在页面中,使高亮的代码是属性。class="nav-link active"
可以通过传递参数判断点击了哪个标签实现相应的高亮,首先在的侧边栏标签传递参数为。dashboard.html``active``dashboard.html
<div th:replace ="~{commons/commons::siderbar(active='dashboard.html')}" > </div >
同样在的侧边栏标签传递参数为。list.html``active``list.html
<div th:replace ="~{commons/commons::siderbar(active='list.html')}" > </div >
在公共页面相应标签部分利用 thymeleaf 接收参数,利用三元运算符判断决定是否高亮。commons.html``active
显示员工信息
修改性别的显示和 date 的显示,并添加和两个标签。编辑``删除
<table class ="table table-striped table-sm" >
<thead >
<tr >
<th > id</th >
<th > lastName</th >
<th > email</th >
<th > gender</th >
<th > department</th >
<th > birth</th >
<th > 操作</th >
</tr >
</thead >
<tbody >
<tr th:each ="emp:${emps}" >
<td th:text ="${emp.getId()}" > </td >
<td th:text ="${emp.getLastName()}" > </td >
<td th:text ="${emp.getEmail()}" > </td >
<td th:text ="${emp.getGender()==0?'女':'男'}" > </td >
<td th:text ="${emp.getDepartment().getDepartmentName()}" > </td >
<td th:text ="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm')}" > </td >
<td >
<a class ="btn btn-sm btn-primary" > 编辑</a >
<a class ="btn btn-sm btn-danger" > 删除</a >
</td >
</tr >
</tbody >
</table >
10.增加员工实现
在页面增添一个按钮,点击该按钮时发起一个请求。list.html``增加员工``/add
编写对应的 controller。
@GetMapping("/add")
public String add (Model model) {
return "emp/add" ;
}
创建添加员工页面 add
<form >
<div class ="form-group" >
<label > LastName</label >
<input type ="text" class ="form-control" placeholder ="quary" />
</div >
<div class ="form-group" >
<label > Email</label >
<input type ="email" class ="form-control" placeholder ="1524368@qq.com" />
</div >
<div class ="form-group" >
<label > Gender</label > <br />
<div class ="form-check form-check-inline" >
<input class ="form-check-input" type ="radio" name ="gender" value ="1" />
<label class ="form-check-label" > 男</label >
</div >
<div 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 > department</label >
<select class ="form-control" >
<option > 1</option >
<option > 2</option >
<option > 3</option >
<option > 4</option >
<option > 5</option >
</select >
</div >
<div class ="form-group" >
<label > Birth</label >
<input type ="text" class ="form-control" placeholder ="quary" />
</div >
<button type ="submit" class ="btn btn-primary" > 添加</button >
</form >
修改一下前端和后端,处理点击的请求。通过方式提交请求,在中添加一个方法用来处理页面点击提交按钮的操作,返回到添加员工页面。
@Autowired
DepartmentDao departmentDao;
@GetMapping("/add")
public String add (Model model) {
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments" ,departments);
return "emp/add" ;
}
<main role ="main" class ="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4" >
<form >
<div class ="form-group" >
<label > LastName</label >
<input
type ="text"
name ="lastName"
class ="form-control"
placeholder ="quary"
/>
</div >
<div class ="form-group" >
<label > Email</label >
<input
type ="email"
name ="email"
class ="form-control"
placeholder ="15243685@qq.com"
/>
</div >
<div class ="form-group" >
<label > Gender</label > <br />
<div class ="form-check form-check-inline" >
<input class ="form-check-input" type ="radio" name ="gender" value ="1" />
<label class ="form-check-label" > 男</label >
</div >
<div 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 > department</label >
<select class ="form-control" name ="department.id" >
<option
th:each ="department:${departments}"
th:text ="${department.getDepartmentName()}"
th:value ="${department.getId()}"
></option >
</select >
</div >
<div class ="form-group" >
<label > Birth</label >
<input
type ="text"
name ="birth"
class ="form-control"
placeholder ="birth:yyyy/MM/dd"
/>
</div >
<button type ="submit" class ="btn btn-primary" > 添加</button >
</form >
</main >
重启主程序,点击添加员工,成功跳转到页面。add.html
完整增加员工功能
由于在页面,当我们填写完信息,点击按钮,应该完成添加返回到页面,展示新的员工信息;因此在点击按钮的一瞬间,我们同样发起一个请求,与上述发出的请求路径一样,但这里发出的是请求。add.html``添加``list``add.html``添加``/add``提交按钮``post
修改 add 页面 form 表单提交地址和方式
<form th:action ="@{/add}" method ="post" > </form >
编写对应的 controller,在中添加一个方法用来处理点击的操作。EmployeeController``addEmp``添加按钮
@PostMapping("/add")
public String addEmp (Employee employee) {
return "redirect:/emps" ;
}
原理探究 : ThymeleafViewResolver
public static final String REDIRECT_URL_PREFIX = "redirect:" ;
public static final String FORWARD_URL_PREFIX = "forward:" ;
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);
}
}
}
编写 controller 接收调试打印。
@PostMapping("/add")
public String addEmp (Employee employee) {
System.out.println(employee);
employeeDao.save(employee);
return "redirect:/emps" ;
}
重启主程序,进行测试,进入添加页面,填写相关信息,注意日期格式默认为。yyyy/MM/dd
提交发现页面出现了 400 错误!
生日是我们提交的是一个日期, 第一次使用的 / 正常提交成功了,后面使用 - 就错误了,所以这里面应该存在一个日期格式化的问题; SpringMVC 会将页面提交的值转换为指定的类型,默认日期是按照 / 的方式提交 ; 比如将 2021/11/09 转换为一个 date 对象。 那思考一个问题?那能不能修改这个默认的格式呢? 先去看 webmvc 的自动配置文件;找到一个日期格式化的方法:
@Bean
public FormattingConversionService mvcConversionService () {
WebConversionService conversionService = new WebConversionService (this .mvcProperties.getDateFormat());
this .addFormatters(conversionService);
return conversionService;
}
public String getDateFormat () {
return this .dateFormat;
}
所以可以自定义的去修改这个时间格式化问题,在配置文件中修改一下;
spring.mvc.date-format =yyyy-MM-dd
11.修改员工信息
list 页面编辑按钮增添请求。
当用户点击标签时,应该跳转到编辑页面(开始创建)进行编辑。将页面的编辑标签添加属性,实现点击请求到编辑页面。编辑``update.html``list.html``href``/edit/id号
<a class ="btn btn-sm btn-primary" th:href ="@{/edit/{id}(id=${emp.getId()})}"
>编辑</a
>
编写对应的 controller
@RequestMapping("/edit/{id}")
public String toUpdateAll (@PathVariable("id") int id, Model model) {
Employee employee = employeeDao.getEmployee(id);
model.addAttribute("emp" , employee);
Collection<Department> departments = departmentDao.getDepartments();
model.addAttribute("departments" , departments);
return "/emp/update" ;
}
将 add 页面复制一份,改为 update 页面;需要修改页面,将后台查询数据回显。
<form th:action ="@{/edit}" method ="post" >
<div class ="form-group" >
<label > LastName</label >
<input
th:value ="${emp.getLastName()}"
type ="text"
name ="lastName"
class ="form-control"
placeholder ="lastname:zsr"
/>
</div >
<div class ="form-group" >
<label > Email</label >
<input
th:value ="${emp.getEmail()}"
type ="email"
name ="email"
class ="form-control"
placeholder ="email:xxxxx@qq.com"
/>
</div >
<div class ="form-group" >
<label > Gender</label > <br />
<div class ="form-check form-check-inline" >
<input
th:checked ="${emp.getGender()==1}"
class ="form-check-input"
type ="radio"
name ="gender"
value ="1"
/>
<label class ="form-check-label" > 男</label >
</div >
<div 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 > department</label >
<select class ="form-control" name ="department.id" >
<option
th:selected ="${department.getId()==emp.department.getId()}"
th:each ="department:${departments}"
th:text ="${department.getDepartmentName()}"
th:value ="${department.getId()}"
></option >
</select >
</div >
<div class ="form-group" >
<label > Birth</label >
<input
th:value ="${emp.getBirth()}"
type ="text"
name ="birth"
class ="form-control"
placeholder ="birth:yyyy/MM/dd"
/>
</div >
<button type ="submit" class ="btn btn-primary" > 修改</button >
</form >
<input
th:value ="${#dates.format(emp.getBirth(),'yyyy-MM-dd')}"
type ="text"
name ="date"
class ="form-control"
placeholder ="birth:yy/MM/dd"
/>
修改表单提交的地址:
<form th:action ="@{/updateEmp}" method ="post" > </form >
编写对应的 controller
@PostMapping("/updateEmp")
public String updateEmp (Employee employee) {
employeeDao.save(employee);
return "redirect:/emps" ;
}
指定修改人的 id。
12.删除员工
list 页面,编写提交地址。点击标签时,应该发起一个请求,删除指定的用户,然后重新返回到页面显示员工数据。删除``list
<a class ="btn btn-sm btn-danger" th:href ="@{/delete/{id}(id=${emp.getId()})}"
>删除</a
>
编写 Controller
@GetMapping("/delete/{id}")
public String delete (@PathVariable("id") Integer id) {
employeeDao.delete(id);
return "redirect:/emps" ;
}
404 页面
在模板目录下添加一个 error 文件夹,文件夹中存放我们相应的错误页面;比如 404.html 或者 4xx.html 等等,SpringBoot 就会帮我们自动使用了!
13.注销页面
注销请求,在提取出来的公共页面,顶部导航栏处中的标签添加属性,实现点击发起请求。commons``href``/user/logout
<a class ="nav-link" href ="#" th:href ="@{/user/loginOut}" > Sign out</a >
编写对应的 controller,处理点击标签的请求,在中编写对应的方法,清除 session,并重定向到首页。注销``LoginController
@RequestMapping("/user/loginOut")
public String logout (HttpSession session) {
session.invalidate();
return "redirect:/index.html" ;
}
14.定制错误数据
SpringBoot 默认的错误处理机制
浏览器访问的默认的错误处理效果:
如果是其他客户端,默认响应一个 json 数据;
错误处理原理分析:
我们看到自动配置类:ErrorMvcAutoConfiguration 错误处理的自动配置类;
这里面注入了几个很重要的 bean;
DefaultErrorAttributes
BasicErrorController
ErrorPageCustomizer
DefaultErrorViewResolver
错误处理步骤 :
一旦系统出现了 4xx 或者 5xx 之类的错误,ErrorPageCustomizer 就会生效(定制错误的响应规则)
@Bean
public ErrorPageCustomizer errorPageCustomizer (DispatcherServletPath
dispatcherServletPath) {
return new ErrorPageCustomizer (this .serverProperties,
dispatcherServletPath);
}
发现一个方法 registerErrorPages 注册错误页面:
@Override
public void registerErrorPages (ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage (
this .dispatcherServletPath.getRelativePath(this .properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
public String getPath () {
return this .path;
}
@Value("${error.path:/error}")
private String path = "/error" ;
系统一旦出现错误之后就会来到 /error 请求进行处理;这个请求会被 BasicErrorController 处理:
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml (HttpServletRequest request,
HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status,model);
return (modelAndView != null ) ? modelAndView : new ModelAndView ("error" ,model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error (HttpServletRequest request)
{
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity <>(status);
}
Map<String, Object> body = getErrorAttributes(request,
isIncludeStackTrace(request, MediaType.ALL));
return new ResponseEntity <>(body, status);
}
来看看 resolveErrorView 这个方法:
protected ModelAndView resolveErrorView (HttpServletRequest request,
HttpServletResponse response,
HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this .errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request,
status, model);
if (modelAndView != null ) {
return modelAndView;
}
}
return null ;
}
在之前看到有这样一个 bean DefaultErrorViewResolver 默认的错误视图解析器 :
public class DefaultErrorViewResolver implements ErrorViewResolver , Ordered
{
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap <>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx" );
views.put(Series.SERVER_ERROR, "5xx" );
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
public ModelAndView resolveErrorView (HttpServletRequest request,
HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()),
model);
if (modelAndView == null &&
SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()),
model);
}
return modelAndView;
}
private ModelAndView resolve (String viewName, Map<String, Object> model)
{
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider =
this .templateAvailabilityProviders.getProvider(errorViewName,
this .applicationContext);
if (provider != null ) {
return new ModelAndView (errorViewName, model);
}
return resolveResource(errorViewName, model);
}
}
所以说:定制错误页面,我们可以建立一个 error 目录,然后放入对应的错误码 html 文件! 比如:404.html 500.html 4xx.html 5xx.html
这些页面的信息数据在哪里呢?
我们找到 DefaultErrorAttributes 这个 bean 对象;里面有很多的 addxx 方法,就是添加不同的信息;
至此,用 SpringBoot 开发一个简单的单体应用对我们来说就没什么太大的问题了!
SpringBoot 操作数据库(1)
1.整合 JDBC
SpringData 简介
整合 JDBC
导入测试数据库
CREATE DATABASE `springboot`
;
USE `springboot`;
DROP TABLE IF EXISTS `department`;
CREATE TABLE `department` (
`id` int (3 ) NOT NULL AUTO_INCREMENT COMMENT '部门id' ,
`department_name` varchar (20 ) NOT NULL COMMENT '部门名字' ,
PRIMARY KEY (`id`)
) ENGINE= InnoDB AUTO_INCREMENT= 106 DEFAULT CHARSET= utf8;
insert into `department`(`id`,`department_name`) values (101 ,'技术部' ),
(102 ,'销售部' ),(103 ,'售后部' ),(104 ,'后勤部' ),(105 ,'运营部' );
DROP TABLE IF EXISTS `employee`;
CREATE TABLE `employee` (
`id` int (5 ) NOT NULL AUTO_INCREMENT COMMENT '雇员id' ,
`last_name` varchar (100 ) NOT NULL COMMENT '名字' ,
`email` varchar (100 ) NOT NULL COMMENT '邮箱' ,
`gender` int (2 ) NOT NULL COMMENT '性别1 男, 0 女' ,
`department` int (3 ) NOT NULL COMMENT '部门id' ,
`birth` datetime NOT NULL COMMENT '生日' ,
PRIMARY KEY (`id`)
) ENGINE= InnoDB AUTO_INCREMENT= 1006 DEFAULT CHARSET= utf8;
insert into
`employee`(`id`,`last_name`,`email`,`gender`,`department`,`birth`) values
(1001 ,'张三' ,'243357594@qq.com' ,1 ,101 ,'2021-03-06 15:04:33' ),(1002 ,'李
四' ,'243357594@qq.com' ,1 ,102 ,'2021-03-06 15:04:36' ),(1003 ,'王
五' ,'243357594@qq.com' ,0 ,103 ,'2021-03-06 15:04:37' ),(1004 ,'赵
六' ,'243357594@qq.com' ,1 ,104 ,'2021-03-06 15:04:39' ),(1005 ,'孙
七' ,'243357594@qq.com' ,0 ,105 ,'2021-03-06 15:04:45' );
新建一个项目测试:springboot-data-jdbc; 引入相应的模块!基础模块
项目建好之后,发现自动帮我们导入了如下的启动器:
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-jdbc</artifactId >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<scope > runtime</scope >
</dependency >
编写 yaml 配置文件连接数据库;
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
配置完这一些东西后,就可以直接去使用了!因为 SpringBoot 已经默认帮我们进行了自动配置;去测试类测试一下:
@SpringBootTest
class Springboot05JdbcApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads () throws SQLException {
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println(connection);
connection.close();
}
}
结果:可以看到他默认给我们配置的数据源为: class com.zaxxer.hikari.HikariDataSource,我们并没有手动配置。
来全局搜索一下,找到数据源的所有自动配置都在:DataSourceAutoConfiguration 文件:
@Import({Hikari.class, Tomcat.class, Dbcp2.class, OracleUcp.class, Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
protected PooledDataSourceConfiguration () {
}
}
这里导入的类都在 DataSourceConfiguration 配置类下,可以看出 Spring Boot 2.6.3 默认使用 HikariDataSource 数据源,而以前版本,如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源;
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;
可以使用 spring.datasource.type 指定自定义的数据源类型,值为要使用的连接池实现的完全限定名 。
关于数据源不做过多介绍,有了数据库连接,显然就可以 CRUD 操作数据库了。但是仍需要先了解一个对象——。JdbcTemplate
JdbcTemplate
有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接 (java.sql.Connection),有了连接,就可以使用原生的 JDBC 语句来操作数据库;
即使不使用第三方第数据库操作框架,如 MyBatis 等,Spring 本身也对原生的 JDBC 做了轻量级的封装,即。JdbcTemplate
数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。
JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类。
JdbcTemplate 主要提供以下几类方法 :
execute 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句;
update 方法及 batchUpdate 方法:update 方法用于执行新增、修改、删除等语句;batchUpdate 方法用于执行批处理相关语句;
query 方法及 queryForXXX 方法:用于执行查询相关语句;
call 方法:用于执行存储过程、函数相关语句。
测试案例
编写一个 Controller,注入 jdbcTemplate,编写测试方法进行访问测试;
package com.github.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/jdbc")
public class JdbcController {
@Autowired
JdbcTemplate jdbcTemplate;
@GetMapping("/list")
public List<Map<String, Object>> userList () {
String sql = "select * from employee" ;
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
return maps;
}
@GetMapping("/add")
public String addUser () {
String sql = "insert into employee(last_name, email,gender,department,birth)" +
" values ('夸克','243357594@qq.com',1,101,'" + new Date ().toLocaleString() +"')" ;
jdbcTemplate.update(sql);
return "addOk" ;
}
@GetMapping("/update/{id}")
public String updateUser (@PathVariable("id") int id) {
String sql = "update employee set last_name=?,email=? where id=" +id;
Object[] objects = new Object [2 ];
objects[0 ] = "subei" ;
objects[1 ] = "243357594@163.com" ;
jdbcTemplate.update(sql,objects);
return "updateOk" ;
}
@GetMapping("/delete/{id}")
public String delUser (@PathVariable("id") int id) {
String sql = "delete from employee where id=?" ;
jdbcTemplate.update(sql,id);
return "deleteOk" ;
}
}
至此,CURD 的基本操作,使用 JDBC 就搞定了。
2.整合 Druid
Druid 简介
Java 程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
Github 地址:https://github.com/alibaba/druid
com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
配置
缺省值
说明
name
配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this).
url
连接数据库的 url,不同数据库不一样。例如: mysql: jdbc:mysql://10.20.153.104:3306/druid2 oracle: jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username
连接数据库的用户名
password
连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用 ConfigFilter。
driverClassName
根据 url 自动识别
这一项可配可不配,如果不配置 druid 会根据 url 自动识别 dbType,然后选择相应的 driverClassName
initalSize
0
初始化时建立物理连接的个数。初始化发生在显示调用 init 方法,或者第一次 getConnection 时
maxActive
8
最大连接池数量
maxIdle
8
已经不再使用,配置了也没效果
minIdle
最小连接池数量
maxWait
获取连接时最大等待时间,单位毫秒。配置了 maxWait 之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置 useUnfairLock 属性为 true 使用非公平锁。
poolPreparedStatements
false
是否缓存 preparedStatement,也就是 PSCache。PSCache 对支持游标的数据库性能提升巨大,比如说 oracle。在 mysql 下建议关闭。
maxOpenPreparedStatements
-1
要启用 PSCache,必须配置大于 0,当大于 0 时,poolPreparedStatements 自动触发修改为 true。在 Druid 中,不会存在 Oracle 下 PSCache 占用内存过多的问题,可以把这个数值配置大一些,比如说 100
validationQuery
用来检测连接是否有效的 sql,要求是一个查询语句。如果 validationQuery 为 null,testOnBorrow、testOnReturn、testWhileIdle 都不会其作用。
validationQueryTimeout
单位:秒,检测连接是否有效的超时时间。底层调用 jdbc Statement 对象的 void setQueryTimeout(int seconds)方法。
testOnBorrow
true
申请连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能。
testOnReturn
false
归还连接时执行 validationQuery 检测连接是否有效,做了这个配置会降低性能
testWhileIdle
false
建议配置为 true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于 timeBetweenEvictionRunsMillis,执行 validationQuery 检测连接是否有效。
timeBetweenEvictionRunsMillis
1 分钟 (1.0.14)
有两个含义: 1) Destroy 线程会检测连接的间隔时间,如果连接空闲时间大于等于 minEvictableIdleTimeMillis 则关闭物理连接 2) testWhileIdle 的判断依据,详细看 testWhileIdle 属性的说明。
numTestsPerEvictionRun
不再使用,一个 DruidDataSource 只支持一个 EvictionRun
numTestsPerEvictionRun
30 分钟 (1.0.14)
连接保持空闲而不被驱逐的最长时间
connectionInitSqls
物理连接初始化的时候执行的 sql
exceptionSorter
根据 dbType 自动识别
当数据库抛出一些不可恢复的异常时,抛弃连接
filters
属性类型是字符串,通过别名的方式配置扩展插件,常用 的插件有:监控统计用的 filter:stat 日志用的 filter:log4j 防御 sql 注入的 filter:wall
proxyFilters
类型是 List,如果同时配置了 filters 和 proxyFilters,是组合关系,并非替换关系
配置数据源
添加上 Druid 数据源依赖。
<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
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
数据源切换之后,在测试类中注入 DataSource,然后获取到它,输出一看便知是否成功切换;
切换成功!既然切换成功,就可以设置数据源连接初始化大小、最大连接数、等待时间、最小连接数 等设置项;可以查看源码。
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
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,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 >
为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了;需要自己添加 DruidDataSource 组件到容器中,并绑定属性;
package com.github.controller;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class DruidConfig {
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource () {
return new DruidDataSource ();
}
}
去测试类中测试一下;看是否成功!
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
@Autowired
DataSource dataSource;
@Test
public void contextLoads () throws SQLException {
System.out.println(dataSource.getClass());
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize());
connection.close();
}
}
配置 Druid 数据源监控
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装路由器时,人家也提 供了一个默认的 web 页面。
所以第一步需要设置 Druid 的后台管理页面,比如登录账号、密码等;配置后台管理;
@Bean
public ServletRegistrationBean statViewServlet () {
ServletRegistrationBean bean = new ServletRegistrationBean (new StatViewServlet (), "/druid/*" );
Map<String, String> initParams = new HashMap <>();
initParams.put("loginUsername" , "admin" );
initParams.put("loginPassword" , "123456" );
initParams.put("allow" , "" );
bean.setInitParameters(initParams);
return bean;
}
配置 Druid web 监控 filter 过滤器
@Bean
public FilterRegistrationBean webStatFilter () {
FilterRegistrationBean bean = new FilterRegistrationBean ();
bean.setFilter(new WebStatFilter ());
Map<String, String> initParams = new HashMap <>();
initParams.put("exclusions" , "*.js,*.css,/druid/*,/jdbc/*" );
bean.setInitParameters(initParams);
bean.setUrlPatterns(Arrays.asList("/*" ));
return bean;
}
平时在工作中,按需求进行配置即可,主要用作监控!
3.整合 MyBatis
整合测试
导入 MyBatis 所需要的依赖
<dependency >
<groupId > org.mybatis.spring.boot</groupId >
<artifactId > mybatis-spring-boot-starter</artifactId >
<version > 2.2.1</version >
</dependency >
配置数据库连接信息
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
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,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
测试数据库是否连接成功!
@SpringBootTest
class Springboot05MybatisApplicationTests {
@Autowired
DataSource dataSource;
@Test
void contextLoads () throws SQLException {
System.out.println(dataSource.getClass());
System.out.println(dataSource.getConnection());
}
}
创建实体类,导入!Lombok
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
创建 mapper 目录以及对应的 Mapper 接口——!DepartmentMapper.java
@Mapper
@Repository
public interface DepartmentMapper {
List<Department> getDepartments () ;
Department getDepartment (Integer id) ;
}
对应的 Mapper 映射文件——DepartmentMapper.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.github.mapper.DepartmentMapper" >
<select id ="getDepartments" resultType ="Department" >
select * from department;
</select >
<select id ="getDepartment" resultType ="Department" parameterType ="int" >
select * from department where id = #{id};
</select >
</mapper >
maven 配置资源过滤问题
<resources >
<resource >
<directory > src/main/java</directory >
<includes >
<include > **/*.xml</include >
</includes >
<filtering > true</filtering >
</resource >
</resources >
既然已经提供了 MyBatis 的映射配置文件,自然要告诉 spring boot 这些文件的位置。
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
mybatis.type-aliases-package=com.github.pojo
编写部门的 DepartmentController 进行测试!
@RestController
public class DepartmentController {
@Autowired
DepartmentMapper departmentMapper;
@GetMapping("/getDepartments")
public List<Department> getDepartments () {
return departmentMapper.getDepartments();
}
@GetMapping("/getDepartment/{id}")
public Department getDepartment (@PathVariable("id") Integer id) {
return departmentMapper.getDepartment(id);
}
}
增加一个员工类再测试下,为之后做准备。
新建一个 pojo 类 Employee;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender;
private Integer department;
private Date birth;
private Department eDepartment;
}
新建一个 EmployeeMapper 接口
@Mapper
@Repository
public interface EmployeeMapper {
List<Employee> getEmployees () ;
int save (Employee employee) ;
Employee get (Integer id) ;
int delete (Integer id) ;
}
编写 EmployeeMapper.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.github.mapper.EmployeeMapper" >
<resultMap id ="EmployeeMap" type ="Employee" >
<id property ="id" column ="eid" />
<result property ="lastName" column ="last_name" />
<result property ="email" column ="email" />
<result property ="gender" column ="gender" />
<result property ="birth" column ="birth" />
<association property ="eDepartment" javaType ="Department" >
<id property ="id" column ="did" />
<result property ="departmentName" column ="dname" />
</association >
</resultMap >
<select id ="getEmployees" resultMap ="EmployeeMap" >
select e.id as eid,last_name,email,gender,birth,d.id as did,d.department_name as dname
from department d,employee e
where d.id = e.department
</select >
<insert id ="save" parameterType ="Employee" >
insert into employee (last_name,email,gender,department,birth)
values (#{lastName},#{email},#{gender},#{department},#{birth});
</insert >
<select id ="get" resultType ="Employee" >
select * from employee where id = #{id}
</select >
<delete id ="delete" parameterType ="int" >
delete from employee where id = #{id}
</delete >
</mapper >
编写 EmployeeController 类进行测试。
@RestController
public class EmployeeController {
@Autowired
EmployeeMapper employeeMapper;
@GetMapping("/getEmployees")
public List<Employee> getEmployees () {
return employeeMapper.getEmployees();
}
@GetMapping("/save")
public int save () {
Employee employee = new Employee ();
employee.setLastName("kuangshen" );
employee.setEmail("qinjiang@qq.com" );
employee.setGender(1 );
employee.setDepartment(101 );
employee.setBirth(new Date ());
return employeeMapper.save(employee);
}
@GetMapping("/get/{id}")
public Employee get (@PathVariable("id") Integer id) {
return employeeMapper.get(id);
}
@GetMapping("/delete/{id}")
public int delete (@PathVariable("id") Integer id) {
return employeeMapper.delete(id);
}
}
测试结果完成!!!
SpringBoot 操作数据库(2)
4.SpringSecurity 权限控制
1.安全简介
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
市面上存在比较有名的:Shiro,Spring Security !
这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么 Spring Security 框架的出现是为了解决什么问题呢?
首先我们看下它的官网介绍:Spring Security 官网地址
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于 spring 的应用程序的标准。
Spring Security 是一个框架,侧重于为 Java 应用程序提供身份验证和授权。与所有 Spring 项目一样,Spring 安全性的真正强大之处在于它可以轻松地扩展以满足定制需求。
从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。
怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而 Spring Scecurity 就是其中的一种。
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
2.实战测试
1.实验环境搭建
新建一个初始的 springboot 项目 web 模块,thymeleaf 模块
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-thymeleaf</artifactId >
</dependency >
导入静态资源
welcome.html
|views
|level1
1.html
2.html
3.html
|level2
1.html
2.html
3.html
|level3
1.html
2.html
3.html
Login.html
controller 跳转!
package com.github.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class RouterController {
@RequestMapping({"/","/index"})
public String index () {
return "index" ;
}
@RequestMapping("/toLogin")
public String toLogin () {
return "views/login" ;
}
@RequestMapping("/level1/{id}")
public String level1 (@PathVariable("id") int id) {
return "views/level1/" +id;
}
@RequestMapping("/level2/{id}")
public String level2 (@PathVariable("id") int id) {
return "views/level2/" +id;
}
@RequestMapping("/level3/{id}")
public String level3 (@PathVariable("id") int id) {
return "views/level3/" +id;
}
}
测试实验环境是否 OK!
2.认识 SpringSecurity
Spring Security 是针对 Spring 项目的安全框架,也是 Spring Boot 底层安全模块默认的技术选型,他可以实现强大的 Web 安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
WebSecurityConfigurerAdapter:自定义 Security 策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启 WebSecurity 模式
Spring Security 的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户 ID 和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在 Spring Security 中存在。
3.认证和授权
目前,我们的测试环境,是谁都可以访问的,只要使用 Spring Security 增加上认证和授权的功能。
引入 Spring Security 模块
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-security</artifactId >
</dependency >
编写 Spring Security 配置类
编写基础配置类
package com.github.config;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity http) throws Exception {
}
}
定制请求的授权规则
@Override
protected void configure (HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/" ).permitAll()
.antMatchers("/level1/**" ).hasRole("vip1" )
.antMatchers("/level2/**" ).hasRole("vip2" )
.antMatchers("/level3/**" ).hasRole("vip3" );
}
测试一下:发现除了首页都进不去了!因为目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!
在 configure()方法中加入以下配置,开启自动配置的登录功能!
测试一下:发现,没有权限的时候,会跳转到登录的页面!
查看刚才登录页的注释信息;
可以定义认证规则,重写 configure(AuthenticationManagerBuilder auth)方法。
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("subei" ).password("123456" ).roles("vip2" ,"vip3" )
.and()
.withUser("root" ).password("123456" ).roles("vip1" ,"vip2" ,"vip3" )
.and()
.withUser("guest" ).password("123456" ).roles("vip1" ,"vip2" );
}
测试,使用这些账号登录进行测试!发现会报错!
原因:要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码。
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder ())
.withUser("subei" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip2" ,"vip3" )
.and()
.withUser("root" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip1" ,"vip2" ,"vip3" )
.and()
.withUser("guest" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip1" ,"vip2" );
}
测试,发现,登录成功,并且每个角色只能访问自己认证下的规则!
4.权限控制和注销
图标库网站:https://semantic-ui.com/
开启自动配置的注销的功能
@Override
protected void configure (HttpSecurity http) throws Exception {
http.formLogin();
http.logout();
}
在前端,增加一个注销的按钮,index.html 导航栏中。
<a class ="item" th:href ="@{/logout}" >
<i class ="address card icon" > </i > 注销
</a >
去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!
但是,当我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
http.logout().logoutSuccessUrl("/" );
测试,注销完毕后,发现跳转到首页,OK!
现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如 subei 这个用户,它只有 vip2,vip3 功能,那么登录则只显示这两个功能,而 vip1 的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
需要结合 thymeleaf 中的一些功能
sec:authorize=“isAuthenticated()”:是否认证登录!来显示不同的页面。
Maven 依赖:
<dependency >
<groupId > org.thymeleaf.extras</groupId >
<artifactId > thymeleaf-extras-springsecurity5</artifactId >
<version > 3.0.4.RELEASE</version >
</dependency >
修改前端页面
<dependency >
<groupId > org.thymeleaf.extras</groupId >
<artifactId > thymeleaf-extras-springsecurity5</artifactId >
<version > 3.0.4.RELEASE</version >
</dependency >
<div class ="right menu" >
<div sec:authorize ="!isAuthenticated()" >
<a class ="item" th:href ="@{/login}" >
<i class ="address card icon" > </i > 登录
</a >
</div >
<div sec:authorize ="isAuthenticated()" >
<a class ="item" >
<i class ="address card icon" > </i >
用户名:<span sec:authentication ="principal.username" > </span > 角色:<span
sec:authentication ="principal.authorities"
></span >
</a >
</div >
<div sec:authorize ="isAuthenticated()" >
<a class ="item" th:href ="@{/logout}" >
<i class ="address card icon" > </i > 注销
</a >
</div >
</div >
重启测试,可以登录试试看,登录成功后确实,显示了我们想要的页面;
如果注销 404 了,就是因为它默认防止 csrf 跨站请求伪造,因为会产生安全问题,可以将请求改为 post 表单提交,或者在 spring security 中关闭 csrf 功能;还可以试试:在配置中增加 http.csrf().disable();
http.csrf().disable();
http.logout().logoutSuccessUrl("/" );
继续将下面的角色功能块认证完成!
<div class ="column" sec:authorize ="hasRole('vip1')" >
<div class ="ui raised segment" >
<div class ="ui" >
<div class ="content" >
<h5 class ="content" > Level 1</h5 >
<hr />
<div >
<a th:href ="@{/level1/1}" > <i class ="bullhorn icon" > </i > Level-1-1</a >
</div >
<div >
<a th:href ="@{/level1/2}" > <i class ="bullhorn icon" > </i > Level-1-2</a >
</div >
<div >
<a th:href ="@{/level1/3}" > <i class ="bullhorn icon" > </i > Level-1-3</a >
</div >
</div >
</div >
</div >
</div >
<div class ="column" sec:authorize ="hasRole('vip2')" >
<div class ="ui raised segment" >
<div class ="ui" >
<div class ="content" >
<h5 class ="content" > Level 2</h5 >
<hr />
<div >
<a th:href ="@{/level2/1}" > <i class ="bullhorn icon" > </i > Level-2-1</a >
</div >
<div >
<a th:href ="@{/level2/2}" > <i class ="bullhorn icon" > </i > Level-2-2</a >
</div >
<div >
<a th:href ="@{/level2/3}" > <i class ="bullhorn icon" > </i > Level-2-3</a >
</div >
</div >
</div >
</div >
</div >
<div class ="column" sec:authorize ="hasRole('vip3')" >
<div class ="ui raised segment" >
<div class ="ui" >
<div class ="content" >
<h5 class ="content" > Level 3</h5 >
<hr />
<div >
<a th:href ="@{/level3/1}" > <i class ="bullhorn icon" > </i > Level-3-1</a >
</div >
<div >
<a th:href ="@{/level3/2}" > <i class ="bullhorn icon" > </i > Level-3-2</a >
</div >
<div >
<a th:href ="@{/level3/3}" > <i class ="bullhorn icon" > </i > Level-3-3</a >
</div >
</div >
</div >
</div >
</div >
测试一下!
权限控制和注销搞定!
5.记住我
现在的情况:只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
开启记住我功能
@Override
protected void configure (HttpSecurity http) throws Exception {
http.rememberMe();
}
再次启动项目测试一下,发现登录页多了一个记住我功能,登录之后关闭浏览器,然后重新打开浏览器访问,发现用户依旧存在!
思考:如何实现的呢?其实非常简单
点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie。
结论:登录成功后,将 cookie 发送给浏览器保存,以后登录带上这个 cookie,只要通过检查就可以免登录了。如果点击注销,则会删除这个 cookie,具体的原理参考 JavaWeb 阶段!
6.定制登录页
现在这个登录页面都是 spring security 默认的,怎么样可以使用自己写的 Login 界面呢?
在刚才的登录页配置后面指定 loginpage
http.formLogin().loginPage("/toLogin" );
然后前端也需要指向我们自己定义的 login 请求
<a class ="item" th:href ="@{/toLogin}" >
<i class ="address card icon" > </i > 登录
</a >
要登录,则需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为 post:
<form th:action ="@{/login}" method ="post" >
<div class ="field" >
<label > Username</label >
<div class ="ui left icon input" >
<input type ="text" placeholder ="Username" name ="username" />
<i class ="user icon" > </i >
</div >
</div >
<div class ="field" >
<label > Password</label >
<div class ="ui left icon input" >
<input type ="password" name ="password" />
<i class ="lock icon" > </i >
</div >
</div >
<input type ="submit" class ="ui blue submit button" />
</form >
4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看 formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!
http.formLogin()
.usernameParameter("username" )
.passwordParameter("password" )
.loginPage("/toLogin" )
.loginProcessingUrl("/login" );
5、在登录页增加记住我的多选框
<input type ="checkbox" name ="remember" /> 记住我
6、后端验证处理!
http.rememberMe().rememberMeParameter("remember" );
测试,OK
完整配置代码
package com.github.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/" ).permitAll()
.antMatchers("/level1/**" ).hasRole("vip1" )
.antMatchers("/level2/**" ).hasRole("vip2" )
.antMatchers("/level3/**" ).hasRole("vip3" );
http.formLogin()
.usernameParameter("username" )
.passwordParameter("password" )
.loginPage("/toLogin" )
.loginProcessingUrl("/login" );
http.csrf().disable();
http.logout().logoutSuccessUrl("/" );
http.rememberMe().rememberMeParameter("remember" );
}
@Override
protected void configure (AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder ())
.withUser("subei" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip2" ,"vip3" )
.and()
.withUser("root" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip1" ,"vip2" ,"vip3" )
.and()
.withUser("guest" ).password(new BCryptPasswordEncoder ().encode("123456" )).roles("vip1" ,"vip2" );
}
}
SpringBoot 操作数据库(3)
5.整合四郎 Shiro
1.四郎简介
1.什么是四郎?
Apache Shiro 是一个 Java 的安全(权限)框架。
Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在 JavaSE 环境,也可以用在 Java EE 环
境。
Shiro 可以完成,认证,授权,加密,会话管理,Web 集成,缓存等。
下载地址:http://shiro.apache.org/
2.有哪些功能?
Authentication:身份认证、登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的 JavaSE 环境,也可以是 Web 环境;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
网站支持:Web 支持,可以非常容易的集成到 Web 环境;
Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
Concurrency:Shiro 支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
测试:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
记住我:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
3.白架构(外部)
从外部来看 Shiro,即从应用程序角度来观察如何使用 shiro 完成工作:
subject: 应用代码直接交互的对象是 Subject,也就是说 Shiro 的对外 API 核心就是 Subject, Subject 代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等,与 Subject 的所有交互都会委托给 SecurityManager;Subject 其 实是一个门面,SecurityManageer 才是实际的执行者
SecurityManager:安全管理器,即所有与安全有关的操作都会与 SercurityManager 交互,并且它 管理着所有的 Subject,可以看出它是 Shiro 的核心,它负责与 Shiro 的其他组件进行交互,它相当于 SpringMVC 的 DispatcherServlet 的角色
Realm:Shiro 从 Realm 获取安全数据(如用户,角色,权限),就是说 SecurityManager 要验证 用户身份,那么它需要从 Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从 Realm 得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把 Realm 看成 DataSource;
4.Shiro 架构(内部)
主题:任何可以与应用交互的 '用户';
Security Manager:相当于 SpringMVC 中的 DispatcherServlet;是 Shiro 的心脏,所有具体的交互 都通过 Security Manager 进行控制,它管理者所有的 Subject,且负责进行认证,授权,会话,及缓存的管理。
Authenticator:负责 Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的那些功能;
Realm:可以有一个或者多个的 realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用 JDBC 实现,也可以是内存实现等等,由用户提供;所以一般在应用中都需要实现自己的 realm。
SessionManager:管理 Session 生命周期的组件,而 Shiro 并不仅仅可以用在 Web 环境,也可以用在普通的 JavaSE 环境中。
CacheManager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密,解密等。
2.Hello,Shiro
1.快速实践
创建一个,用于学习 Shiro,删掉不必要的东西。maven父工程
创建一个普通的 Maven 子工程:shiro-01-helloworld。
根据官方文档,导入 Shiro 的依赖。
<dependencies >
<dependency >
<groupId > org.apache.shiro</groupId >
<artifactId > shiro-core</artifactId >
<version > 1.8.0</version >
</dependency >
<dependency >
<groupId > org.slf4j</groupId >
<artifactId > jcl-over-slf4j</artifactId >
<version > 2.0.0-alpha1</version >
</dependency >
<dependency >
<groupId > org.slf4j</groupId >
<artifactId > slf4j-log4j12</artifactId >
<version > 2.0.0-alpha1</version >
</dependency >
<dependency >
<groupId > org.apache.logging.log4j</groupId >
<artifactId > log4j-core</artifactId >
<version > 2.17.1</version >
</dependency >
</dependencies >
编写 Shiro 配置——log4j.properties。
log4j.rootLogger =INFO, stdout
log4j.appender.stdout =org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout =org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern =%d %p [%c] - %m %n
# General Apache libraries
log4j.logger.org.apache =WARN
# Spring
log4j.logger.org.springframework =WARN
# Default Shiro logging
log4j.logger.org.apache.shiro =INFO
# Disable verbose logging
log4j.logger.org.apache.shiro.util.ThreadContext =WARN
log4j.logger.org.apache.shiro.cache.ehcache.EhCache =WARN
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345 , president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5
编写自己的 QuickStrat
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Quickstart {
private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class);
public static void main (String[] args) {
Factory<SecurityManager> factory = new IniSecurityManagerFactory ("classpath:shiro.ini" );
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();
Session session = currentUser.getSession();
session.setAttribute("someKey" , "aValue" );
String value = (String) session.getAttribute("someKey" );
if (value.equals("aValue" )) {
log.info("Retrieved the correct value! [" + value + "]" );
}
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken ("lonestarr" , "vespa" );
token.setRememberMe(true );
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!" );
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it." );
}
catch (AuthenticationException ae) {
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully." );
if (currentUser.hasRole("schwartz" )) {
log.info("May the Schwartz be with you!" );
} else {
log.info("Hello, mere mortal." );
}
if (currentUser.isPermitted("lightsaber:wield" )) {
log.info("You may use a lightsaber ring. Use it wisely." );
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only." );
}
if (currentUser.isPermitted("winnebago:drive:eagle5" )) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!" );
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!" );
}
currentUser.logout();
System.exit(0 );
}
}
测试运行一下。
发现,执行完毕什么都没有,可能是 maven 依赖中的作用域问题,我们需要将 scope 作用域删掉,
默认是在 test,然后重启,那么我们的 quickstart 就结束了,默认的日志消息!
[main] INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler...
[main] INFO [Quickstart] - Retrieved the correct value! [aValue]
[main] INFO [Quickstart] - User [lonestarr] logged in successfully.
[main] INFO [Quickstart] - May the Schwartz be with you!
[main] INFO [Quickstart] - You may use a lightsaber ring. Use it wisely.
[main] INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
OK!
2.代码解释
导入了一堆包!
类的描述
通过工厂模式创建 SecurityManager 的实例对象。
Factory<SecurityManager> factory = new
IniSecurityManagerFactory ("classpath:shiro.ini" );
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
do :
获取当前的 Subject
Subject currentUser = SecurityUtils.getSubject();
session 的操作
Session session = currentUser.getSession();
session.setAttribute("someKey" , "aValue" );
String value = (String) session.getAttribute("someKey" );
if (value.equals("aValue" )) {
log.info("==Retrieved the correct value! [" + value + "]" );
}
用户认证功能
permissions:
if (!currentUser.isAuthenticated()) {
UsernamePasswordToken token = new UsernamePasswordToken ("lonestarr" , "vespa" );
token.setRememberMe(true );
try {
currentUser.login(token);
} catch (UnknownAccountException uae) {
UnknownAccountException异常
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!" );
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it." );
}
your application?
catch (AuthenticationException ae) {
}
}
log.info("User [" + currentUser.getPrincipal() + "] logged in successfully." );
角色检查
if (currentUser.hasRole("schwartz" )) {
log.info("May the Schwartz be with you!" );
} else {
log.info("Hello, mere mortal." );
}
权限检查,粗粒度
if (currentUser.isPermitted("lightsaber:wield" )) {
log.info("You may use a lightsaber ring. Use it wisely." );
} else {
log.info("Sorry, lightsaber rings are for schwartz masters only." );
}
权限检查,细粒度
if (currentUser.isPermitted("winnebago:drive:eagle5" )) {
log.info("You are permitted to 'drive' the winnebago with license
plate (id) 'eagle5'. " +
"Here are the keys - have fun!" );
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5'
winnebago!" );
}
注销操作
退出系统System.exit(0)
;
OK,一个简单的 Shiro 程序体验结束!!!
3.集成 shiro
1.准备工作
搭建一个 SpringBoot 项目、选中 web 模块即可!
导入 Maven 依赖thymeleaf
<dependency >
<groupId > org.thymeleaf</groupId >
<artifactId > thymeleaf-spring5</artifactId >
</dependency >
<dependency >
<groupId > org.thymeleaf.extras</groupId >
<artifactId > thymeleaf-extras-java8time</artifactId >
</dependency >
编写一个页面 index.html——templates
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<title > 首页</title >
</head >
<body >
<h1 > 首页哦!</h1 >
<p th:text ="${msg}" > </p >
</body >
</html >
编写 controller 进行访问测试。
package com.github.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex (Model model) {
model.addAttribute("msg" ,"Hello,Shiro!" );
return "index" ;
}
}
访问测试!
2.整合 Shiro
回顾核心 API:
Subject:用户主体
SecurityManager:安全管理器
Realm:Shiro 连接数据
步骤 :
导入 Shiro 和 spring 整合的依赖。
<dependency >
<groupId > org.apache.shiro</groupId >
<artifactId > shiro-spring</artifactId >
<version > 1.4.1</version >
</dependency >
编写 Shiro 配置类——config包
package com.github.config;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
}
创建一个 realm 对象,需要自定义一个 realm 的类,用来编写一些查询的方法,或者认证与授权的逻辑。
package com.github.config;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
public class UserRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection principalCollection) {
System.out.println("执行 => 授权逻辑PrincipalCollection" );
return null ;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了 => 认证逻辑AuthenticationToken" );
return null ;
}
}
将这个类注册到我们的 Bean 中!——ShiroConfig
@Configuration
public class ShiroConfig {
@Bean
public UserRealm userRealm () {
return new UserRealm ();
}
}
创建DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager
getDefaultWebSecurityManager (@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager ();
securityManager.setRealm(userRealm);
return securityManager;
}
创建ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean
getShiroFilterFactoryBean (@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new
ShiroFilterFactoryBean ();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
完整的配置:
package com.github.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean
getShiroFilterFactoryBean (@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new
ShiroFilterFactoryBean ();
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
@Bean(name = "securityManager")
public DefaultWebSecurityManager
getDefaultWebSecurityManager (@Qualifier("userRealm") UserRealm userRealm) {
DefaultWebSecurityManager securityManager = new
DefaultWebSecurityManager ();
securityManager.setRealm(userRealm);
return securityManager;
}
@Bean
public UserRealm userRealm () {
return new UserRealm ();
}
}
3.页面拦截实现
编写两个页面、在 templates 目录下新建一个 user 目录add.html、update.html
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" />
<title > ADD</title >
</head >
<body >
<h1 > ADD</h1 >
</body >
</html >
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" />
<title > update</title >
</head >
<body >
<h1 > update</h1 >
</body >
</html >
编写跳转到页面的 controller
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex (Model model) {
model.addAttribute("msg" ,"Hello,Shiro!" );
return "index" ;
}
@RequestMapping("/user/add")
public String toAdd () {
return "user/add" ;
}
@RequestMapping("/user/update")
public String toUpdate () {
return "user/update" ;
}
}
在 index 页面上,增加跳转链接
<body >
<h1 > 首页哦!</h1 >
<p th:text ="${msg}" > </p >
<hr />
<a th:href ="@{/user/add}" > add</a > | <a th:href ="@{/user/update}" > update</a >
</body >
测试页面跳转是否 OK
准备添加 Shiro 的内置过滤器
@Bean
public ShiroFilterFactoryBean
getShiroFilterFactoryBean (@Qualifier("securityManager") DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new
ShiroFilterFactoryBean ();
shiroFilterFactoryBean.setSecurityManager(securityManager);
Map<String,String> filterMap = new LinkedHashMap <String, String>();
filterMap.put("/user/add" ,"authc" );
filterMap.put("/user/update" ,"authc" );
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap);
return shiroFilterFactoryBean;
}
再起启动测试,访问链接进行测试!拦截 OK!但是发现,点击后会跳转到一个 Login.jsp 页面,这 个不是我们想要的效果,我们需要自己定义一个 Login 页面!
编写一个个人的 Login.html 页面
<!DOCTYPE html >
<html lang ="en" >
<head >
<meta charset ="UTF-8" />
<title > 登录</title >
</head >
<body >
<h1 > 登录</h1 >
<hr />
<form action ="" >
<p > 用户名: <input type ="text" name ="username" /> </p >
<p > 密码: <input type ="text" name ="password" /> </p >
<p >
<input type ="submit" />
</p >
</form >
</body >
</html >
编写跳转的 controller
@RequestMapping("/toLogin")
public String toLogin () {
return "login" ;
}
在 shiro 中配置一下! ShiroFilterFactoryBean() 方法下面
再次测试,成功的跳转到了我们指定的 Login 页面!
优化一下,使用通配符来操作!
测试一下!
4.登录认证操作
编写登录的 controller
@RequestMapping("/login")
public String login (String username,String password,Model model) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken (username,
password);
try {
subject.login(token);
return "index" ;
} catch (UnknownAccountException e) {
model.addAttribute("msg" ,"用户名不存在" );
return "login" ;
} catch (IncorrectCredentialsException e) {
model.addAttribute("msg" ,"密码错误" );
return "login" ;
}
}
在前端修改对应的信息输出或者请求!登录页面增加一个 msg 提示:
<p style ="color:red;" th:text ="${msg}" > </p >
<form th:action ="@{/login}" >
<p > 用户名: <input type ="text" name ="username" /> </p >
<p > 密码: <input type ="text" name ="password" /> </p >
<p >
<input type ="submit" />
</p >
</form >
测试一下:
在 UserRealm 中编写用户认证的判断逻辑:
@Override
protected AuthenticationInfo doGetAuthenticationInfo (AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了 => 认证逻辑AuthenticationToken" );
String name = "root" ;
String password = "root" ;
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
if (!userToken.getUsername().equals(name)){
return null ;
}
return new SimpleAuthenticationInfo ("" , password, "" );
}
测试一下!
5.整合数据库
CREATE DATABASE `mybatis`;
USE `mybatis`;
CREATE TABLE `user `(
`id` INT (20 ) NOT NULL PRIMARY KEY,
`name` VARCHAR (30 ) DEFAULT NULL ,
`pwd` VARCHAR (30 ) DEFAULT NULL
)ENGINE= INNODB DEFAULT CHARSET= utf8;
INSERT INTO `user `(`id`,`name`,`pwd`) VALUES
(1 ,'subei' ,'123456' ),
(2 ,'张三' ,'123456' ),
(3 ,'李四' ,'123456' );
导入 Mybatis 相关依赖
<dependency >
<groupId > org.mybatis.spring.boot</groupId >
<artifactId > mybatis-spring-boot-starter</artifactId >
<version > 2.1.0</version >
</dependency >
<dependency >
<groupId > mysql</groupId >
<artifactId > mysql-connector-java</artifactId >
<scope > runtime</scope >
</dependency >
<dependency >
<groupId > log4j</groupId >
<artifactId > log4j</artifactId >
<version > 1.2.17</version >
</dependency >
<dependency >
<groupId > com.alibaba</groupId >
<artifactId > druid</artifactId >
<version > 1.1.12</version >
</dependency >
编写配置文件-连接配置——application.yml
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=false
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
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,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
编写 mybatis 的配置——application.properties
mybatis.type-aliases-package =com.github.pojo
mybatis.mapper-locations =classpath:mapper/*.xml
编写实体类,引入 Lombok
<dependency >
<groupId > org.projectlombok</groupId >
<artifactId > lombok</artifactId >
<version > 1.18.22</version >
</dependency >
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
编写 Mapper 接口
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName (String name) ;
}
编写 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.github.mapper.UserMapper" >
<select id ="queryUserByName" parameterType ="String"
resultType ="User" >
select * from user where name = #{name}
</select >
</mapper >
编写 UserService 层
public interface UserService {
public User queryUserByName (String name) ;
}
@Service
public class UserServiceImpl implements UserService {
@Autowired
UserMapper mapper;
@Override
public User queryUserByName (String name) {
return mapper.queryUserByName(name);
}
}
测试一下,保证能够从数据库中查询出来。
@SpringBootTest
class SpringbootShiroApplicationTests {
@Autowired
UserServiceImpl userService;
@Test
void contextLoads () {
System.out.println(userService.queryUserByName("root" ));
}
}
改造 UserRealm,连接到数据库进行真实的操作!
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userService;
@Override
protected AuthorizationInfo
doGetAuthorizationInfo (PrincipalCollection principals) {
System.out.println("执行了=>授权逻辑PrincipalCollection" );
return null ;
}
@Override
protected AuthenticationInfo
doGetAuthenticationInfo (AuthenticationToken token) throws
AuthenticationException {
System.out.println("执行了=>认证逻辑AuthenticationToken" );
UsernamePasswordToken userToken = (UsernamePasswordToken)token;
User user =
userService.queryUserByName(userToken.getUsername());
if (user==null ){
return null ;
}
return new SimpleAuthenticationInfo ("" , user.getPwd(), "" );
}
}
思考:密码比对原理探究
这个 Shiro,是怎么帮我们实现密码自动比对的呢?
去 realm 的父类 的父类 中找一个方法;AuthorizingRealm``AuthenticatingRealm
核心: () 翻译过来:获取证书匹配器;getCredentialsMatcher
去看这个接口 有很多的实现类,MD5 盐值加密;CredentialsMatcher
密码一般都不能使用明文保存?
如何把一个字符串加密为 MD5;
替换当前的 Realm 的 CredentialsMatcher 属性,直接使用 对象, 并设置加密算法;Md5CredentialsMatcher
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher ();
hashedCredentialsMatcher.setHashAlgorithmName("MD5" );
hashedCredentialsMatcher.isStoredCredentialsHexEncoded();
SimpleHash simpleHash = new SimpleHash ("MD5" ,user.getPwd() );
String s = simpleHash.toHex();
return new SimpleAuthenticationInfo ("" ,s,"" );
6.用户授权操作
使用 shiro 的过滤器来拦截请求即可!
在中添加一个过滤器:ShiroFilterFactoryBean
filterMap.put("/user/add" ,"perms[user:add]" );
再次启动测试一下,访问 add,发现以下错误!未授权错误!
注意:当我们实现权限拦截后,shiro 会自动跳转到未授权的页面,但没有这个页面,所有 401 了;
配置一个未授权的提示的页面,增加一个 controller 提示;
@RequestMapping("/noauth")
@ResponseBody
public String noAuth () {
return "未经授权不能访问此页面" ;
}
然后再 shiroFilterFactoryBean 中配置一个未授权的请求页面!
shiroFilterFactoryBean.setUnauthorizedUrl("/noauth" );
测试,现在没有授权,可以跳转到我们指定的位置了!
7.用户授权操作
在 UserRealm 中添加授权的逻辑,增加授权的字符串!
@Override
protected AuthorizationInfo doGetAuthorizationInfo (PrincipalCollection
principals) {
System.out.println("执行了=>授权逻辑PrincipalCollection" );
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo ();
info.addStringPermission("user:add" );
return info;
}
再次登录测试,发现登录的用户是可以进行访问 add 页面了!授权成功!
问题,我们现在完全是硬编码,无论是谁登录上来,都可以实现授权通过,但是真实的业务情况应该 是,每个用户拥有自己的一些权限,从而进行操作,所以说,权限,应该在用户的数据库中,正常的情 况下,应该数据库中是由一个权限表的,我们需要联表查询,但是这里为了大家操作理解方便一些,我 们直接在数据库表中增加一个字段来进行操作!
修改实体类,增加一个字段
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
在自定义的授权认证中,获取登录的用户,从而实现动态认证授权操作!
在用户登录授权的时候,将用户放在 Principal 中,改造下之前的代码
return new SimpleAuthenticationInfo (user, user.getPwd(), "" );
@Override
protected AuthorizationInfo
doGetAuthorizationInfo (PrincipalCollection principals) {
System.out.println("执行了=>授权逻辑PrincipalCollection" );
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo ();
Subject subject = SecurityUtils.getSubject();
User currentUser = (User) subject.getPrincipal();
info.addStringPermission(currentUser.getPerms());
return info;
}
给数据库中的用户增加一些权限
在过滤器中,将 update 请求也进行权限拦截下
启动测试,登录不同的账户,进行测试一下!测试完美通过 OK!
8.整合 Thymeleaf
根据权限展示不同的前端页面
添加 Maven 的依赖;
<dependency >
<groupId > com.github.theborakompanioni</groupId >
<artifactId > thymeleaf-extras-shiro</artifactId >
<version > 2.0.0</version >
</dependency >
配置一个 shiro 的 Dialect ,在 shiro 的配置中增加一个 Bean
@Bean
public ShiroDialect getShiroDialect () {
return new ShiroDialect ();
}
修改前端的配置
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<title > 首页</title >
</head >
<body >
<h1 > 首页哦!</h1 >
<p th:text ="${msg}" > </p >
<hr />
<div shiro:hasPermission ="user:add" >
<a th:href ="@{/user/add}" > add</a >
</div >
<div shiro:hasPermission ="user:update" >
<a th:href ="@{/user/update}" > update</a >
</div >
</body >
</html >
测试一下,可以发现,现在首页什么都没有了,因为我们没有登录,我们可以尝试登录下,来判断这个 Shiro 的效果!登录后,可以看到不同的用户,有不同的效果,现在就已经接近完美了~!但还有问题!
在用户登录后应该把信息放到 Session 中,我们完善下!在执行认证逻辑时候,加入 session。
Subject subject = SecurityUtils.getSubject();
subject.getSession().setAttribute("loginUser" ,user);
前端从 session 中获取,然后用来判断是否显示登录。
<p th:if ="${session.loginUser==null}" >
<a th:href ="@{/toLogin}" > 登录</a >
</p >
测试一下!
SpringBoot 操作数据库(4)
6.集成 Swagger
官网:API Documentation & Design Tools for Teams | Swagger
1.Swagger 简介
前后端分离
前端 -> 前端控制层、视图层;
后端 -> 后端控制层、服务层、数据访问层;
前后端通过 API 进行交互;
前后端相对独立且松耦合。
产生的问题
前后端集成,前端或者后端无法做到“及时协商,尽早解决”,最终导致问题集中爆发。
解决方案
首先定义 schema[计划的提纲],并实时跟踪最新的 API,降低集成风险。
早些年:指定 word 计划文档;
前后端分离:
前端测试后端接口:postman
后端提供接口,需要实时更新最新的消息及改动!
Swagger
号称世界上最流行的 API 框架;
Restful API 文档在线自动生成器 => API 文档 与 API 定义同步更新 。
直接运行,在线测试 API;
支持多种语言(如:Java,PHP 等);
官网:https://swagger.io/
2.SpringBoot 集成 Swagger
SpringBoot 集成 Swagger => springfox ,两个 jar 包
Springfox -> swagger2 ;
swagger -> springmvc。
使用 Swagger
要求:JDK1.8 + 否则 swagger2 无法运行。
步骤:
新建一个 SpringBoot-web 项目
添加 Maven 依赖
<dependency >
<groupId > io.springfox</groupId >
<artifactId > springfox-swagger2</artifactId >
<version > 2.9.2</version >
</dependency >
<dependency >
<groupId > io.springfox</groupId >
<artifactId > springfox-swagger-ui</artifactId >
<version > 2.9.2</version >
</dependency >
编写 HelloController,测试确保运行成功!
@RestController
public class HelloController {
@RequestMapping(value = "/hello")
public String hello () {
return "Hello" ;
}
}
要使用 Swagger,我们需要编写一个配置类-SwaggerConfig 来配置 Swagger
@Configuration
@EnableSwagger2
public class SwaggerConfig {
}
由于 spring boot 版本问题,如果启动项目报错空指针异常,将 spring boot 降级为 2.5.5 版本即可。访问测试:http://localhost:8080/swagger-ui.html,可以看到 swagger 的界面。
注:3.0的jar访问不了,降级2.9.2就可以了
。
3.配置 Swagger
Swagger 实例 Bean 是 Docket,所以通过配置 Docket 实例来配置 Swaggger。
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket docket () {
return new Docket (DocumentationType.SWAGGER_2);
}
}
可以通过 apiInfo()属性配置文档信息;
private ApiInfo apiInfo () {
Contact contact = new Contact ("联系人名字" , "http://xxx.xxx.com/联系人访问链接" , "联系人邮箱" );
return new ApiInfo (
"Swagger学习" ,
"学习演示如何配置Swagger" ,
"v1.0" ,
"http://terms.service.url/组织链接" ,
contact,
"Apach 2.0 许可" ,
"许可链接" ,
new ArrayList <>()
);
}
Docket 实例关联上 apiInfo()
@Bean
public Docket docket () {
return new Docket (DocumentationType.SWAGGER_2).apiInfo(apiInfo());
}
重启项目,访问测试 http://localhost:8080/swagger-ui.html 效果如下:
4.配置扫描端口
构建 Docket 时通过 select()方法配置怎么扫描接口。
@Bean
public Docket docket () {
return new Docket (DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.github.controller" ))
.build();
}
重启项目测试,由于我们配置根据包的路径扫描接口,所以只能看到一个类!!!
除了通过包路径配置扫描接口外,还可以通过配置其他方式扫描接口,这里注释一下所有的配置方式:
any() // 扫描所有,项目中的所有接口都会被扫描到
none() // 不扫描接口
// 通过方法上的注解扫描,如withMethodAnnotation(GetMapping.class)只扫描get请求
withMethodAnnotation(final Class<? extends Annotation> annotation)
// 通过类上的注解扫描,如.withClassAnnotation(Controller.class)只扫描有controller注解的类中的接口
withClassAnnotation(final Class<? extends Annotation> annotation)
basePackage(final String basePackage) // 根据包路径扫描接口
除此之外,还可以配置接口扫描过滤:
@Bean
public Docket docket () {
return new Docket (DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.github.controller" ))
.paths(PathSelectors.ant("/github/**" ))
.build();
}
可选值还有:
regex(final String pathRegex) // 通过正则表达式控制
ant(final String antPattern) // 通过ant()控制
5.配置 Swagger 开关
通过 enable()方法配置是否启用 swagger,如果是 false,swagger 将不能在浏览器中访问了。
@Bean
public Docket docket () {
return new Docket (DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(false )
.select()
.apis(RequestHandlerSelectors.basePackage("com.github.controller" ))
.paths(PathSelectors.ant("/github/**" ))
.build();
}
如何动态配置当项目处于 test、dev 环境时显示 swagger,处于 prod 时不显示?
在配置文件配置 swaggerFlag 的值,在 dev、prod、stag 的配置文件中配置,然后在 swaggerConfig 里面获取这个值,这样不管环境怎么变都不影响代码!
@Bean
public Docket docket (Environment environment) {
Profiles of = Profiles.of("dev" , "test" );
boolean b = environment.acceptsProfiles(of);
return new Docket (DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(b)
.select()
.apis(RequestHandlerSelectors.basePackage("com.github.controller" ))
.paths(PathSelectors.ant("/github/**" ))
.build();
}
在项目中增加一个 dev 的配置文件查看效果!
6.配置 API 分组
如果没有配置分组,默认是 default。通过 groupName()方法即可配置分组:
@Bean
public Docket docket (Environment environment) {
return new Docket (DocumentationType.SWAGGER_2).apiInfo(apiInfo())
.groupName("hello" );
}
重启项目查看分组
如何配置多个分组?配置多个分组只需要配置多个 docket 即可:
@Bean
public Docket docket1 () {
return new Docket (DocumentationType.SWAGGER_2).groupName("group1" );
}
@Bean
public Docket docket2 () {
return new Docket (DocumentationType.SWAGGER_2).groupName("group2" );
}
@Bean
public Docket docket3 () {
return new Docket (DocumentationType.SWAGGER_2).groupName("group3" );
}
重启项目查看。
7.实体配置
新建一个实体类
@ApiModel("用户实体")
public class User {
@ApiModelProperty("用户名")
public String username;
@ApiModelProperty("密码")
public String password;
}
只要这个实体在请求接口的返回值上(即使是泛型),都能映射到实体项中:
@RequestMapping("/getUser")
public User getUser () {
return new User ();
}
重启查看测试:
注:并不是因为@ApiModel 这个注解让实体显示在这里了,而是只要出现在接口方法的返回值上的实体都会显示在这里,而@ApiModel 和@ApiModelProperty 这两个注解只是为实体添加注释的。
@ApiModel 为类添加注释;
@ApiModelProperty 为类属性添加注释;
8.常用注解
Swagger 的所有注解定义在 io.swagger.annotations 包下!
下面列一些经常用到的,未列举出来的可以另行查阅说明:
Swagger 注解
简单说明
@Api(tags = “xxx 模块说明”)
作用在模块类上
@ApiOperation(“xxx 接口说明”)
作用在接口方法上
@ApiModel(“xxxPOJO 说明”)
作用在模型类上:如 VO、BO
@ApiModelProperty(value = “xxx 属性说明”,hidden = true)
作用在类方法和属性上,hidden 设置为 true 可以隐藏该属性
@ApiParam(“xxx 参数说明”)
作用在参数、方法和字段上,类似@ApiModelProperty
@ApiOperation("Github控制类")
@GetMapping(value = "/github")
@ResponseBody
public String github (@ApiParam("用户名") String username) {
return "Hay" + username;
}
@ApiOperation("Post测试类")
@PostMapping(value = "/postt")
@ResponseBody
public User postt (@ApiParam("用户名") User user) {
return user;
}
这样的话,可以给一些比较难理解的属性或者接口,增加一些配置信息,让人更容易阅读!
相较于传统的 Postman 或 Curl 方式测试接口,使用 swagger 简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击 Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
Swagger 是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出 Word 接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭 Swagger,一来出于安全考虑二来也可以节省运行时内存。
9.其他皮肤
可以导入不同的包实现不同的皮肤定义:
默认的 ,访问 http://localhost:8080/swagger-ui.html
<dependency >
<groupId > io.springfox</groupId >
<artifactId > springfox-swagger-ui</artifactId >
<version > 2.9.2</version >
</dependency >
bootstrap-ui 访问 http://localhost:8080/doc.html
<dependency >
<groupId > com.github.xiaoymin</groupId >
<artifactId > swagger-bootstrap-ui</artifactId >
<version > 1.9.1</version >
</dependency >
Layui-ui 访问 http://localhost:8080/docs.html
<dependency >
<groupId > com.github.caspar-chen</groupId >
<artifactId > swagger-ui-layer</artifactId >
<version > 1.1.3</version >
</dependency >
mg-ui 访问 http://localhost:8080/document.html
<dependency >
<groupId > com.zyplayer</groupId >
<artifactId > swagger-mg-ui</artifactId >
<version > 1.0.6</version >
</dependency >
7.异步、定时、邮件任务
1.异步任务
新建一个空 spring boot 项目,创建一个 service 包
创建一个类 AsyncService
异步处理还是非常常用的,比如我们在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功,所以我们一般会采用多线程的方式去处理这些任务。
编写方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况;
@Service
public class AsyncService {
public void hello () {
try {
Thread.sleep(3000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中...." );
}
}
编写 controller 包
编写 AsyncController 类,去写一个 Controller 测试一下
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello () {
asyncService.hello();
return "success" ;
}
}
访问:http://localhost:8080/hello 进行测试,3 秒后出现 success,这是同步等待的情况。
问题:我们如果想让用户直接得到消息,就在后台使用多线程的方式进行处理即可,但是每次都需要自己手动去编写多线程的实现的话,太麻烦了,我们只需要用一个简单的办法,在我们的方法上加一个简单的注解即可,如下:
给 hello 方法添加@Async 注解;
@Async
public void hello () {
try {
Thread.sleep(3000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("业务进行中...." );
}
SpringBoot 就会自己开一个线程池,进行调用!但是要让这个注解生效,还需要在主程序上添加一个注解@EnableAsync,开启异步注解功能;
@EnableAsync
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main (String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
重启测试,网页瞬间响应,后台代码依旧执行!
2.邮件任务
邮件发送,在日常开发中,使用非常的多,Springboot 也帮我们做了支持!!!
邮件发送需要引入 spring-boot-start-mail
SpringBoot 自动配置 MailSenderAutoConfiguration
定义 MailProperties 内容,配置在 application.yml 中
自动装配 JavaMailSender
测试邮件发送
测试:
引入 pom 依赖
<dependency >
<groupId > org.springframework.boot</groupId >
<artifactId > spring-boot-starter-mail</artifactId >
</dependency >
看它引入的依赖,可以看到 jakarta.mail
<dependency >
<groupId > com.sun.mail</groupId >
<artifactId > jakarta.mail</artifactId >
<version > 1.6.4</version >
<scope > compile</scope >
</dependency >
查看自动配置类:MailSenderAutoConfiguration
这个类中存在 bean,JavaMailSenderImpl。
@ConfigurationProperties(
prefix = "spring.mail"
)
public class MailProperties {
private static final Charset DEFAULT_CHARSET;
private String host;
private Integer port;
private String username;
private String password;
private String protocol = "smtp" ;
private Charset defaultEncoding;
private Map<String, String> properties;
private String jndiName;
}
配置文件:
spring.mail.username=2943357594@qq.com
spring.mail.password=你的qq授权码
spring.mail.host=smtp.qq.com
spring.mail.properties.mail.smtp.ssl.enable=true
获取授权码:在 QQ 邮箱中的设置->账户->开启 pop3 和 smtp 服务。
Spring 单元测试
@SpringBootTest
class Springboot09TestApplicationTests {
@Autowired
JavaMailSenderImpl mailSender;
@Test
public void contextLoads () {
SimpleMailMessage message = new SimpleMailMessage ();
message.setSubject("通知-明天学习Linux" );
message.setText("今晚7:30复习" );
message.setTo("2943357594@qq.com" );
message.setFrom("2943357594@qq.com" );
mailSender.send(message);
}
@Test
public void contextLoads2 () throws MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper (mimeMessage, true );
helper.setSubject("通知-明天学习Linux" );
helper.setText("<b style='color:red'>今天 7:30复习</b>" ,true );
helper.addAttachment("1.jpg" ,new File ("图片路径" ));
helper.addAttachment("2.jpg" ,new File ("图片链接" ));
helper.setTo("2943357594@qq.com" );
helper.setFrom("2943357594@qq.com" );
mailSender.send(mimeMessage);
}
}
在后期开发中,只需要使用 Thymeleaf 进行前后端结合即可开发自己网站邮件收发功能了!
3.定时任务
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息,Spring 为我们提供了异步执行任务调度的方式,提供了两个接口。
TaskExecutor 接口
TaskScheduler 接口
两个注解:
@EnableScheduling
@Scheduled
cron 表达式:
字段
允许值
允许的特殊字符
秒
0-59
,-*/
分
0-59
,-*/
小时
0-23
,-*/
日期
1-31
,-*?/L W C
月份
1-12
,-*/
星期
0-7 或 SUN-SAT 0,7 是 SUN
,-*?/L C #
特殊字符
代表含义
,
枚举
-
区间
*
任意
/
步长
?
日/星期冲突匹配
L
最后
W
工作日
C
和 calendar 联系后计算过的值
#
星期,4#2,第二个星期三
测试步骤:
创建一个 ScheduledService
里面存在一个 hello 方法,他需要定时执行,怎么处理呢?
@Service
public class ScheduledService {
@Scheduled(cron = "0 * * * * 0-7")
public void hello () {
System.out.println("hello……" );
}
@Scheduled(cron = "0/2 * * * * ?")
public void hello2 () {
System.out.println("hello2……" );
}
}
这里写完定时任务之后,需要在主程序上增加@EnableScheduling开启定时任务功能
。
@EnableAsync
@EnableScheduling
@SpringBootApplication
public class Springboot09TestApplication {
public static void main (String[] args) {
SpringApplication.run(Springboot09TestApplication.class, args);
}
}
详细了解下 cron 表达式:http://www.bejson.com/othertools/cron/
常用的表达式:
(1 )0 /2 * * * * ? 表示每2 秒 执行任务
(1 )0 0 /2 * * * ? 表示每2 分钟 执行任务
(1 )0 0 2 1 * ? 表示在每月的1 日的凌晨2 点调整任务
(2 )0 15 10 ? * MON-FRI 表示周一到周五每天上午10 :15 执行作业
(3 )0 15 10 ? 6L 2002 -2006 表示2002 -2006 年的每个月的最后一个星期五上午10 :15 执行作
(4 )0 0 10 ,14 ,16 * * ? 每天上午10 点,下午2 点,4 点
(5 )0 0 /30 9 -17 * * ? 朝九晚五工作时间内每半小时
(6 )0 0 12 ? * WED 表示每个星期三中午12 点
(7 )0 0 12 * * ? 每天中午12 点触发
(8 )0 15 10 ? * * 每天上午10 :15 触发
(9 )0 15 10 * * ? 每天上午10 :15 触发
(10 )0 15 10 * * ? 每天上午10 :15 触发
(11 )0 15 10 * * ? 2005 2005 年的每天上午10 :15 触发
(12 )0 * 14 * * ? 在每天下午2 点到下午2 :59 期间的每1 分钟触发
(13 )0 0 /5 14 * * ? 在每天下午2 点到下午2 :55 期间的每5 分钟触发
(14 )0 0 /5 14 ,18 * * ? 在每天下午2 点到2 :55 期间和下午6 点到6 :55 期间的每5 分钟触发
(15 )0 0 -5 14 * * ? 在每天下午2 点到下午2 :05 期间的每1 分钟触发
(16 )0 10 ,44 14 ? 3 WED 每年三月的星期三的下午2 :10 和2 :44 触发
(17 )0 15 10 ? * MON-FRI 周一至周五的上午10 :15 触发
(18 )0 15 10 15 * ? 每月15 日上午10 :15 触发
(19 )0 15 10 L * ? 每月最后一日的上午10 :15 触发
(20 )0 15 10 ? * 6L 每月的最后一个星期五上午10 :15 触发
(21 )0 15 10 ? * 6L 2002 -2005 2002 年至2005 年的每月的最后一个星期五上午10 :15 触发
(22 )0 15 10 ? * 6 #3 每月的第三个星期五上午10 :15 触发
SpringBoot 操作数据库(5)
8.富文本编辑器
1.简介
思考:我们平时在博客园,或者 CSDN 等平台进行写作的时候,有同学思考过他们的编辑器是怎么实现的吗?
在博客园后台的选项设置中,可以看到一个文本编辑器的选项:
其实这个就是富文本编辑器,市面上有许多非常成熟的富文本编辑器,比如:
Editor.md ——功能非常丰富的编辑器,左端编辑,右端预览,非常方便,完全免费
wangEditor ——基于 javascript 和 css 开发的 Web 富文本编辑器, 轻量、简洁、界面美观、易用、开源免费。
TinyMCE——TinyMCE 是一个轻量级的基于浏览器的所见即所得编辑器,由 JavaScript 写成。它对 IE6+和 Firefox1.5+都有着非常良好的支持。功能齐全,界面美观,就是文档是英文的,对开发人员英文水平有一定要求。
百度 ueditor——UEditor 是由百度 web 前端研发部开发所见即所得富文本 web 编辑器,具有轻量,功能齐全,可定制,注重用户体验等特点,开源基于 MIT 协议,允许自由使用和修改代码,缺点是已经没有更新了
kindeditor ——界面经典。
Textbox——Textbox 是一款极简但功能强大的在线文本编辑器,支持桌面设备和移动设备。主要功能包含内置的图像处理和存储、文件拖放、拼写检查和自动更正。此外,该工具还实现了屏幕阅读器等辅助技术,并符合 WAI-ARIA 可访问性标准。
CKEditor ——国外的,界面美观。
Quill ——功能强大,还可以编辑公式等
simditor ——界面美观,功能较全。
summernote ——UI 好看,精美
jodit ——功能齐全
froala Editor ——界面非常好看,功能非常强大,非常好用(非免费)
总之,目前可用的富文本编辑器有很多…这只是其中的一部分。
2.Editor.md
这里使用的就是,作为一个资深码农,Mardown 必然是我们程序猿最喜欢的格式,看下面,就爱上了!Editor.md
3.基础工程搭建
数据库设计
article:文章表
字段
备注
id
int
文章的唯一 ID
author
varchar
作者
title
varchar
标题
content
longtext
文章的内容
建表 SQL:
use springboot;
CREATE TABLE `article` (
`id` int (10 ) NOT NULL AUTO_INCREMENT COMMENT 'int文章的唯一ID' ,
`author` varchar (50 ) NOT NULL COMMENT '作者' ,
`title` varchar (100 ) NOT NULL COMMENT '标题' ,
`content` longtext NOT NULL COMMENT '文章的内容' ,
PRIMARY KEY (`id`)
) ENGINE= InnoDB DEFAULT CHARSET= utf8;
基础项目搭建
建一个 SpringBoot 项目配置。
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Article implements Serializable {
private int id;
private String author;
private String title;
private String content;
}
3、mapper 接口:
@Mapper
@Repository
public interface ArticleMapper {
List<Article> queryArticles () ;
int addArticle (Article article) ;
Article getArticleById (int id) ;
int deleteArticleById (int id) ;
}
<?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.github.mapper.ArticleMapper" >
<select id="queryArticles" resultType="Article" >
select * from article
</select>
<select id="getArticleById" resultType="Article" >
select * from article where id = #{id}
</select>
<insert id="addArticle" parameterType="Article" >
insert into article (author,title,content) values (#{author},#{title},#{content});
</insert>
<delete id="deleteArticleById" parameterType="int" >
delete from article where id = #{id}
</delete>
</mapper>
既然已经提供了 myBatis 的映射配置文件,自然要告诉 spring boot 这些文件的位置
# 指定myBatis的核心配置文件与Mapper映射文件
mybatis. mapper-locations=classpath:com/github/mapper/*.xml
# 注意:对应实体类的路径
mybatis. type-aliases-package=com. github. pojo
编写一个 Controller 测试下,是否 ok;
@RestController
public class ArticleController {
@Autowired
ArticleMapper articleMapper;
@GetMapping("/queryArticles")
public List<Article> queryArticles () {
return articleMapper.queryArticles();
}
}
4.文章编辑整合
导入 editor.md 资源 ,删除多余文件。
编辑文章页面 editor.html、需要引入 jQuery;
<!DOCTYPE html >
<html class ="x-admin-sm" lang ="zh" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<title > 哇哈哈'Blog</title >
<meta name ="renderer" content ="webkit" />
<meta http-equiv ="X-UA-Compatible" content ="IE=edge,chrome=1" />
<meta
name ="viewport"
content ="width=device-width,user-scalable=yes, minimum-scale=0.4, initial-scale=0.8,target-densitydpi=low-dpi"
/>
<link rel ="stylesheet" th:href ="@{/css/editormd.css}" />
<link
rel ="shortcut icon"
href ="https://pandao.github.io/editor.md/favicon.ico"
type ="image/x-icon"
/>
</head >
<body >
<div class ="layui-fluid" >
<div class ="layui-row layui-col-space15" >
<div class ="layui-col-md12" >
<form name ="mdEditorForm" >
<div > 标题:<input type ="text" name ="title" /> </div >
<div > 作者:<input type ="text" name ="author" /> </div >
<div id ="article-content" >
<textarea name ="content" id ="content" style ="display:none;" >
</textarea >
</div >
</form >
</div >
</div >
</div >
</body >
<script th:src ="@{/lib/jquery.min.js}" > </script >
<script th:src ="@{/editormd.js}" > </script >
<script type ="text/javascript" >
var testEditor;
$(function ( ) {
testEditor = editormd ('article-content' , {
width : '95%' ,
height : 400 ,
syncScrolling : 'single' ,
path : '../lib/' ,
saveHTMLToTextarea : true ,
emoji : true ,
theme : 'dark' ,
previewTheme : 'dark' ,
editorTheme : 'pastel-on-dark' ,
tex : true ,
flowChart : true ,
sequenceDiagram : true ,
imageUpload : true ,
imageFormats : ['jpg' , 'jpeg' , 'gif' , 'png' , 'bmp' , 'webp' ],
imageUploadURL : '/article/file/upload' ,
onload : function ( ) {
console .log ('onload' , this );
},
toolbarIcons : function ( ) {
return [
'undo' ,
'redo' ,
'|' ,
'bold' ,
'del' ,
'italic' ,
'quote' ,
'ucwords' ,
'uppercase' ,
'lowercase' ,
'|' ,
'h1' ,
'h2' ,
'h3' ,
'h4' ,
'h5' ,
'h6' ,
'|' ,
'list-ul' ,
'list-ol' ,
'hr' ,
'|' ,
'link' ,
'reference-link' ,
'image' ,
'code' ,
'preformatted-text' ,
'code-block' ,
'table' ,
'datetime' ,
'emoji' ,
'html-entities' ,
'pagebreak' ,
'|' ,
'goto-line' ,
'watch' ,
'preview' ,
'fullscreen' ,
'clear' ,
'search' ,
'|' ,
'help' ,
'info' ,
'releaseIcon' ,
'index' ,
];
},
toolbarIconTexts : {
releaseIcon : '<span bgcolor="gray">发布</span>' ,
index : '<span bgcolor="red">返回首页</span>' ,
},
toolbarHandlers : {
releaseIcon : function (cm, icon, cursor, selection ) {
mdEditorForm.method = 'post' ;
mdEditorForm.action = '/article/addArticle' ;
mdEditorForm.submit ();
},
index : function ( ) {
window .location .href = '/' ;
},
},
});
});
</script >
</html >
编写 Controller,进行跳转,以及保存文章
@RestController
@RequestMapping("/article")
public class ArticleController {
@Autowired
ArticleMapper articleMapper;
@GetMapping("/queryArticles")
public List<Article> queryArticles () {
return articleMapper.queryArticles();
}
@GetMapping("/toEditor")
public String toEditor () {
return "editor" ;
}
@PostMapping("/addArticle")
public String addArticle (Article article) {
articleMapper.addArticle(article);
return "editor" ;
}
}
图片上传问题
前端 js 中添加配置
imageUpload : true ,
imageFormats : ["jpg" , "jpeg" , "gif" , "png" , "bmp" , "webp" ],
imageUploadURL : "/article/file/upload" ,
后端请求,接收保存这个图片, 需要导入 FastJson 的依赖!
@RequestMapping("/file/upload")
@ResponseBody
public JSONObject fileUpload (@RequestParam(value = "editormd-image-file", required = true) MultipartFile file, HttpServletRequest request) throws IOException {
String path = System.getProperty("user.dir" )+"/upload/" ;
Calendar instance = Calendar.getInstance();
String month = (instance.get(Calendar.MONTH) + 1 )+"月" ;
path = path+month;
File realPath = new File (path);
if (!realPath.exists()){
realPath.mkdir();
}
System.out.println("上传文件保存地址:" +realPath);
String filename = "ks-" +UUID.randomUUID().toString().replaceAll("-" , "" );
file.transferTo(new File (realPath +"/" + filename));
JSONObject res = new JSONObject ();
res.put("url" ,"/upload/" +month+"/" + filename);
res.put("success" , 1 );
res.put("message" , "upload success!" );
return res;
}
3、解决文件回显显示的问题,设置虚拟目录映射!在我们自己拓展的 MvcConfig 中进行配置即可!
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers (ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**" )
.addResourceLocations("file:" +System.getProperty("user.dir" )+"/upload/" );
}
}
表情包问题
自己手动下载,emoji 表情包,放到图片路径下:
editormd.emoji = {
path : "../editormd/plugins/emoji-dialog/emoji/" ,
ext : ".png"
};
5.文章展示
Controller 中增加方法
@GetMapping("/{id}")
public String show (@PathVariable("id") int id,Model model) {
Article article = articleMapper.getArticleById(id);
model.addAttribute("article" ,article);
return "article" ;
}
编写页面 article.html
<!DOCTYPE html >
<html lang ="en" xmlns:th ="http://www.thymeleaf.org" >
<head >
<meta charset ="UTF-8" />
<meta
name ="viewport"
content ="width=device-width, initial-scale=1, maximum-scale=1"
/>
<title th:text ="${article.title}" > </title >
</head >
<body >
<div >
<h2 style ="margin: auto 0" th:text ="${article.title}" > </h2 >
作者:<span style ="float: left" th:text ="${article.author}" > </span >
<div id ="doc-content" >
<textarea
style ="display:none;"
placeholder ="markdown"
th:text ="${article.content}"
></textarea >
</div >
</div >
<link rel ="stylesheet" th:href ="@{/editormd/css/editormd.preview.css}" />
<script th:src ="@{/lib/jquery.min.js}" > </script >
<script th:src ="@{/lib/marked.min.js}" > </script >
<script th:src ="@{/lib/prettify.min.js}" > </script >
<script th:src ="@{/lib/raphael.min.js}" > </script >
<script th:src ="@{/lib/underscore.min.js}" > </script >
<script th:src ="@{/lib/sequence-diagram.min.js}" > </script >
<script th:src ="@{/lib/flowchart.min.js}" > </script >
<script th:src ="@{/lib/jquery.flowchart.min.js}" > </script >
<script th:src ="@{/editormd.js}" > </script >
<script type ="text/javascript" >
var testEditor;
$(function ( ) {
testEditor = editormd.markdownToHTML ('doc-content' , {
htmlDecode : 'style,script,iframe' ,
emoji : true ,
taskList : true ,
tocm : true ,
tex : true ,
flowChart : true ,
sequenceDiagram : true ,
codeFold : true ,
});
});
</script >
</body >
</html >
9.整合 Dubbo+Zookeeper
1.分布式理论
什么是分布式系统?
在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据 。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的 CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。
因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
Dubbo 文档
随着互联网的飞速发展,Web 应用的规模不断扩大,最终我们发现传统的垂直架构(单体)已经无法应对。分布式服务架构和流计算架构势在必行,迫切需要一个治理体系来保证架构的有序演进。
单体架构
当流量很低时,只有一个应用,所有的特性都部署在一起,减少部署节点和成本。此时,数据访问框架(ORM)是简化 CRUD 工作量的关键。
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
缺点:
1、性能扩展比较难
2、协同开发问题
3、不利于升级维护
垂直架构
当流量变大时,添加单体应用实例并不能很好地加速访问,提高效率的一种方法是将单体应用拆分成离散的应用程序。此时,用于加速前端页面开发的 Web 框架(MVC)是关键。
通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展
也更方便,更有针对性。
缺点: 公用模块无法重复利用,开发性的浪费
分布式服务架构
当垂直应用越来越多时,应用之间的交互是不可避免的,一些核心业务被提取出来,作为独立的服务,逐渐形成一个稳定的服务中心,这样前端应用就可以更好地响应多变的市场需求。迅速地。此时,用于业务重用和集成的分布式服务框架(RPC)是关键。
流计算架构
当服务越来越多时,容量评估变得困难,而且小规模的服务也经常造成资源浪费。为了解决这些问题,需要增加调度中心,根据流量对集群容量进行管理,提高集群的利用率。这时,用来提高机器利用率的资源调度和治理中心(SOA)是关键。
2.什么是 RPC?
RPC【Remote Procedure Call】是指远程过程调用,像调用本地方法一样调用远程方法,是一种进程间通信方式,他是一种技术的思想,而不是规范。
它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说两台服务器 A,B,一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
为什么要用 RPC 呢?
就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC 就是要像调用本地的函数一样去调远程函数;
RPC 基本原理
服务消费方(client)调用以本地调用方式调用服务;
client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
client stub 找到服务地址,并将消息发送到服务端;
server stub 收到消息后进行解码;
server stub 根据解码结果调用本地的服务;
本地服务执行并将结果返回给 server stub;
server stub 将返回结果打包成消息并发送至消费方;
client stub 接收到消息,并进行解码;
服务消费方得到最终结果。
具体参考:简书 ;官网说法 。
时序步骤解析
RPC 两个核心模块:通讯,序列化。
3.什么是 dubbo?
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
dubbo 官网
了解 Dubbo 的特性;
查看官方文档
Dubbo 架构
服务提供者(Provider) :暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者(Consumer) : 调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
注册中心(Registry) :注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
监控中心(Monitor) :服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
调用关系说明
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
Dubbo 的基本要求
在大规模服务出现之前,应用程序可能只是通过 RMI 或 Hessian 暴露或引用远程服务,通过配置服务 URL 进行调用,通过 F5 等硬件完成负载均衡。
当服务越来越多,配置服务 URL 变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 此时,需要一个服务注册中心来动态注册和发现服务,使服务的位置透明化。通过在消费者端获取服务提供者地址列表,可以实现软负载均衡和 Failover,减少对 F5 硬件负载均衡器的依赖和部分成本。
当事情进一步发展时,服务依赖变得如此复杂,以至于它甚至无法告诉之前启动哪些应用程序,甚至架构师也无法完全描述应用程序架构关系 。这时就需要自动绘制应用程序的依赖关系图,帮助架构师理清关系。
然后,流量变得更重,服务的容量问题暴露出来,需要多少台机器来支持这个服务?什么时候应该加机器? 要解决这些问题,首先要将每天的服务调用量和响应时间量作为容量规划的参考。二、动态调整权重,增加一台在线机器的权重,并记录响应时间的变化,直到达到阈值,记录此时的访问次数,然后将此访问次数乘以总机器数计算反过来的能力。
更多参考:网页 。
4.Dubbo 环境搭建
window 下安装 zookeeper
下载 zookeeper:地址 ① ,地址 ② ,下载 3.7.0!解压 zookeeper;
运行/bin/zkServer.cmd ,初次运行会报错,没有 zoo.cfg 配置文件;
可能遇到问题:闪退!
解决方案:编辑 zkServer.cmd 文件末尾添加。这样运行出错就不会退出,会提示错误信息,方便找到原因。pause
报错:错误: 找不到或无法加载主类 org.apache.zookeeper.server.quorum.QuorumPeerMain
将 conf 目录下的 zoo_sample.cfg 文件,复制一份,重命名为 zoo.cfg:
在安装目录下面新建一个空的 data 文件夹和 log 文件夹:
修改 zoo.cfg 配置文件,将 dataDir=/tmp/zookeeper 修改成 zookeeper 安装目录所在的 data 文件夹,再添加一条添加数据日志的配置(需要根据自己的安装路径修改)。
修改完成后再次双击 zkServer.cmd 启动程序:
Zookeeper启动失败:Unexpected exception, exiting abnormally java.net.BindException: Address alr
这是 2181 端口被占用导致的,需要结束占用 2181 端口的进程。
进去命令提示符之后,输入“netstat -aon | findstr 2181”命令,按回车键,如下图所示:
打开任务管理器,显示进程的 pid 之后,找到 pid 为 19236 的进程,点击结束进程。
测试,双击 zkCli.cmd 启动客户端。
ls /
:列出 zookeeper 根下保存的所有节点。
create –e /subei 123
:创建一个 subei 节点,值为 123。
get /subei
:获取/subei 节点的值。
5.window 下安装 dubbo-admin
dubbo 本身并不是一个服务软件。它其实就是一个 jar 包,能够帮你的 java 程序连接到 zookeeper,并利用 zookeeper 消费、提供服务。
但是为了让用户更好的管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin,不过这个监控即使不装也不影响使用。
下载 dubbo-admin
解压进入目录
修改 dubbo-admin-master-0.2.0\dubbo-admin\src\main\resources\application.properties 指定 zookeeper 地址
server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default .vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 注册中心的地址
dubbo.registry.address=zookeeper:
在项目目录下打包 dubbo-admin
mvn clean package -Dmaven.test.skip=true
执行 dubbo-admin\target 下的 dubbo-admin-0.0.1-SNAPSHOT.jar
java -jar dubbo-admin-0.0 .1 -SNAPSHOT.jar
安装完成!
新版实现
下载代码: git clone https://github.com/apache/dubbo-admin.git
在 中指定注册中心地址dubbo-admin-server/src/main/resources/application.properties
构建
mvn clean package -Dmaven.test.skip=true
启动
mvn --projects dubbo-admin-server spring-boot:run
或者
cd dubbo-admin-distribution/target; java -jar dubbo-admin-0.1.jar
访问 http://localhost:8080
不太稳定,启动失败!!!
6.框架搭建
启动 zookeeper!
IDEA 创建一个空项目;
创建一个模块,实现服务提供者:provider-server,选择 web 依赖即可;
项目创建完毕,写一个服务,比如卖票的服务;
编写接口:
package com.github.service;
public interface TicketService {
public String getTicket () ;
}
编写实现类:
package com.github.service;
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket () {
return "《subeiLY》" ;
}
}
创建一个模块,实现服务消费者:consumer-server,选择 web 依赖即可
项目创建完毕,写一个服务,比如用户的服务; 编写 service
package com.github.service;
public class UserService {
}
7.服务提供者
将服务提供者注册到注册中心,需要整合 Dubbo 和 zookeeper,所以需要导包。
<dependency >
<groupId > org.apache.dubbo</groupId >
<artifactId > dubbo-spring-boot-starter</artifactId >
<version > 2.7.8</version >
</dependency >
<dependency >
<groupId > com.github.sgroschupf</groupId >
<artifactId > zkclient</artifactId >
<version > 0.1</version >
</dependency >
zookeeper 及其依赖包,解决日志冲突,还需要剔除日志依赖;
<dependency >
<groupId > org.apache.curator</groupId >
<artifactId > curator-framework</artifactId >
<version > 5.2.0</version >
</dependency >
<dependency >
<groupId > org.apache.curator</groupId >
<artifactId > curator-recipes</artifactId >
<version > 5.2.0</version >
</dependency >
<dependency >
<groupId > org.apache.zookeeper</groupId >
<artifactId > zookeeper</artifactId >
<version > 3.7.0</version >
<exclusions >
<exclusion >
<groupId > org.slf4j</groupId >
<artifactId > slf4j-log4j12</artifactId >
</exclusion >
</exclusions >
</dependency >
在 springboot 配置文件中配置 dubbo 相关属性!
dubbo.application.name =provider-server
dubbo.registry.address =zookeeper://127.0.0.1:2181
dubbo.scan.base-packages =com.github.service
在 service 的实现类中配置服务注解,发布服务!注意导包问题。
package com.github.service;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;
@Service
@Component
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket () {
return "《subeiLY》" ;
}
}
逻辑理解:应用启动起来,dubbo 就会扫描指定的包下带有@component 注解的服务,将它发布在指定的注册中心中!
8.消费者
导入依赖,和之前的依赖一样;
<dependency >
<groupId > org.apache.dubbo</groupId >
<artifactId > dubbo-spring-boot-starter</artifactId >
<version > 2.7.8</version >
</dependency >
<dependency >
<groupId > com.github.sgroschupf</groupId >
<artifactId > zkclient</artifactId >
<version > 0.1</version >
</dependency >
<dependency >
<groupId > org.apache.curator</groupId >
<artifactId > curator-framework</artifactId >
<version > 5.2.0</version >
</dependency >
<dependency >
<groupId > org.apache.curator</groupId >
<artifactId > curator-recipes</artifactId >
<version > 5.2.0</version >
</dependency >
<dependency >
<groupId > org.apache.zookeeper</groupId >
<artifactId > zookeeper</artifactId >
<version > 3.7.0</version >
<exclusions >
<exclusion >
<groupId > org.slf4j</groupId >
<artifactId > slf4j-log4j12</artifactId >
</exclusion >
</exclusions >
</dependency >
配置参数
dubbo.application.name =consumer-server
dubbo.registry.address =zookeeper://127.0.0.1:2181
本来正常步骤是需要将服务提供者的接口打包,然后用 pom 文件导入,这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
完善消费者的服务类
package com.github.provider.seriver;
import com.github.consumer.seriver.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Reference
TicketService ticketService;
public void bugTicket () {
String ticket = ticketService.getTicket();
System.out.println("在注册中心买到:" +ticket);
}
}
测试类
import com.github.provider.seriver.UserService;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads () {
userService.bugTicket();
}
}
启动测试
开启 zookeeper
打开 dubbo-admin 实现监控【可以不用做】
开启服务者
SpingBoot + dubbo + zookeeper 实现分布式开发的应用,其实就是一个服务拆分的思想!!!
Spring Boot 系列结束
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)