根据以下学习视频,个人整理的笔记
https://www.bilibili.com/video/BV1PE411i7CV?vd_source=7a8946d22777450e46486d5fd60d8d4d
真正学会一个框架
学会自己看官方文档!结合官方文档看源码!
微服务阶段
回顾:
- javase:OOP
- mysql:持久化
- html+css+js+jQuery+Vue框架:视图
- javaweb:独立开发MVC三层架构的网站
- ssm:企业级框架,简化了我们的开发流程,配置也开始较为复杂
war:tomcat运行
spring再简化:SpringBoot - jar:内嵌tomcat;微服务架构!
服务越来越多:SpringCloud
程序员和码农的区别
程序猿:程序 = 数据结构 + 算法;
码 农:程序 = 面向对象 + 框架;
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配置的要求
真的很爽,我们快速去体验开发个接口的感觉吧!
微服务
什么是微服务?
微服务是一种架构风格,他要求我们在开发一个应用的时候,这个应用必须建成一系列小服务组合,可以通过http方式进行通信。要说微服务架构,先得说说过去我们的单体应用架构。
单体应用架构
所谓单体应用架构(all in one)是指,我们将一个应用的中的所有应用服务都封装在一个应用中。
无论是ERP、CRM或是其他什么系统,你都把数据库访问,web访问, 等等各个功能放到一个war包内。(目前我们写的项目就是如此,将一个系统整体打包成war包)
- 这样做的好处是,易于开发和测试;也十分方便部署;当需要扩展时,只需要将war复制多份,然后放到多个服务器上,再做个负载均衡就可以了。
- 单体应用架构的缺点是,哪怕我要修改一个非常小的地方,我都需要停掉整个服务,重新打包、部署这个应用war包。特别是对于一个大型应用,我们不可能把所有内容都放在一个应用里面,我们如何维护、如何分工合作都是问题。
微服务架构
all in one的架构方式,我们把所有的功能单元放在一个应用里面。然后我们把整个应用部署到服务器上。如果负载能力不行,我们将整个应用进行水平复制,进行扩展,然后在负载均衡。
所谓微服务架构,就是打破之前all in one的架构方式,把每个功能元素独立出来。把独立出来的功能元素的动态组合,需要的功能元素才去拿来组合,需要多一些时,可以整合多个功能元素。所以微服务架构是对功能元素进行复制,而没有对整个应用进行复制。
下这样做的好处是:
- 节省了调用资源
- 每个功能元素的服务都是一个可替换的、可独立升级的软件代码
Martin Flower于2014年3月25日写的《Microservices》 ,详细的阐述了什么是微服务。
- 原文地址: http://martinfowler.com/articles/microservices.html
- 翻译: https://www.cnblogs.com/liuning8023/p/4493156.html
如何构建微服务
一个大型系统的微服务架构,就像一个复杂交织的神经网络,每一个神经元就是一个功能元素,它们各自完成自己的功能,然后通过http相互请求调用。比如一个电商系统,查缓存、连数据库、浏览页面、结账、支付等服务都是一个个独立的功能服务,都被微化了,它们作为一个个微服务共同构建了一个庞大的系统。如果修改其中的一个功能,只需要更新升级其中一个功能服务单元即可。
但是这种庞大的系统架构给部署和运维带来很大的难度。于是,spring为我们带来了构建大型分布式微服务的全套、全程产品:
- 构建一个个功能独立的微服务应用单元,可以使用springboot, 可以帮我们快速构建一个应用;
- 大型分布式网络服务的调用,这部分由spring cloud来完成,实现分布式;
- 在分布式中间,进行流式数据计算、批处理,我们有spring cloud data flow
- spring为我们想清楚了整个从开始构建应用到大型分布式应用全流程方案。
第一个SpringBoot程序
基本环境:
- JDK1.8
- maven3.6.1
- springboot最新版
- IDEA
官方提供了一个快速生成的网站!IDEA集成了这个网站
- 我们可以在官网直接下载后,导入IDEA开发
- 也可以直接使用IDEA创建一个SpringBoot项目(我们一般用这个)
使用网站生成的例子
然后直接用IDEA去打开这个生成的项目即可
看一下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.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.kuang</groupId>
<artifactId>helloworld</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>helloworld</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!--web依赖:tomcat、dispatcherServlet、xml...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--所有的SpringBoot依赖都是使用spring-boot-starter作为前缀的
没有版本号是因为可以继承父依赖的
-->
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<!--打jar包的插件-->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
如上所示,主要有四个部分:
- 项目元数据信息:创建时候输入的Project Metadata部分,也就是Maven项目的基本元素,包括: groupld、 artifactld、 version、 name、 description等
- parent:继承spring- boot -starter-parent的依赖管理,控制版本与打包等内容
- dependencies:项目具体依赖,这里包含了spring-boot-starter-web用于实现HTTP接口(该依赖中包含了SpringMVC),官网对它的描述是:使用SpringMVC构建Web (包括RESTful)应用程序的入门者,使用Tomcat作为默认嵌入式容器。spring-boot-starter-test用于编写单元测试的依赖包。更多功能模块的使用我们将在后面逐步展开。
- build: 构建配置部分。默认使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot应用打包成JAR包来直接运行。
打包成JAR包来直接运行
使用IDEA生成的例子
使用vpn才能访问外网,保证项目访问spring网站流畅
- 更改端口号
在核心配置文件 application.properties 中添加如下代码
#更改项目的端口号
server.port=9090
- SpringBoot自定义启动Banner
在resources目录下新建一个banner.txt,然后把我们自定义的内容写入banner.txt,即可。SpringBoot会自动识别
原理初步探索【重点】
我们之前写的HelloSpringBoot,到底是怎么运行的呢,Maven项目,我们一般从pom.xml文件探究起;
其中有一个父依赖,这个父依赖主要是管理项目的资源过滤及插件!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
点进去,发现还有一个父依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.7.0</version>
</parent>
这个父依赖才是真正管理SpringBoot应用里面所有依赖版本的地方,SpringBoot的版本控制中心;
以后我们导入依赖默认是不需要写版本(因为有这些版本仓库);但是如果导入的包没有在依赖中管理着就需要手动配置版本了;
pom.xml中还有启动器
<!--启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
启动器:说白了就是SpringBoot的启动场景。
比如spring-boot-starter-web,它就会帮我们自动导入web环境所有的依赖。
SpringBoot会将所有的功能场景,都变成一个个的启动器
我们要使用什么功能,就只需要找到对应的启动器就可以了
主程序
package com.kuang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//@SpringBootApplication:标注这个类是一个SpringBoot的应用,启动类下的所有资源被导入
@SpringBootApplication
public class Springboot01HelloworldApplication {
public static void main(String[] args) {
//将SpringBoot应用启动
SpringApplication.run(Springboot01HelloworldApplication.class, args);
}
}
注解剖析:
@SpringBootApplication
@SpringBootConfiguration:SpringBoot的配置
@Configuration:spring配置类
@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 = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
META-INF/spring.factories这是自动配置的核心文件,所有的自动配置类都在这里了
初步总结:SpringBoot所有的自动配置都是在启动的时候扫描并加载spring.factories(所有的自动配置类都在这里面,但不是所有的自动配置类都生效,要判断条件是否成立,只要导入了对应的start,就有对应的启动器了,有了启动器,我们自动配置类就会生效,然后就配置成功了)
- SpringBoot在启动的时候,从类路径下 META-INF/spring.factories 获取指定的值
- 将这些自动配置的类导入容器,自动配置就会生效,帮我们进行自动配置
- 以前我们需要自动配置的东西,现在SpringBoot帮我们做了
- 整个JavaEE,解决方案和自动配置的东西都在 spring-boot-autoconfigure 这个jar包下
- 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器中
- 容器中也会存在非常多的xxxAutoConfiguration的类,就是这些类给容器中导入了这个场景需要的所有组件;并自动配置
- 有了自动配置类,就免去了我们手动编写的配置文件的工作
SpringApplication.run(Springboot01HelloworldApplication.class, args);
这个SpringApplication主要做了以下四件事情
- 推断应用的类型是普通的项目还是Web项目
- 查找并加载所有可用初始化器 ,设置到initializers属性中
- 找出所有的应用程序监听器,设置到listeners属性中
- 推断并设置main方法的定义类,找到运行的主类
SpringBoot配置
SpringBoot这个配置文件中到底可以配置哪些东西呢?
官方的配置太多了
我们了解原理:一通百通
application.properties这个文件可以删掉,官方不推荐使用 properties 配置文件。推荐使用 yaml 配置文件
SpringBoot使用一个全局的配置文件 ,配置文件名称是固定的
- application.properties
- 语法结构:key=value
- application.yaml
- 语法结构:key:空格value
**配置文件的作用 :**可以修改SpringBoot自动配置的默认值,因为SpringBoot在底层都给我们自动配置好了;
YAML是 “YAML Ain’t a Markup Language” (YAML不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)
这种语言以数据作为中心,而不是以标记语言为重点!
以前的配置文件,大多数都是使用xml来配置;比如一个简单的端口配置,我们来对比下xml和yaml
传统xml配置:
<server>
<port>9090<port>
</server>
yaml配置:
server:
port: 9090
很明显yaml看起来更加轻巧一点
yaml和properties对比
yaml
# 对空格的要求十分高!
# 可以注入到我们的配置类中!
# 普通的key-value
name: qinjiang
# 对象
student1:
name: qinjiang
age: 3
# 行内写法
student2: {name: qinjiang,age: 3}
# 数组
pets1:
- cat
- dog
- pig
# 数组
pets2: [cat,dog,pig]
properties
# properties 只能保存键值对
name=qinjiang
student.name=qinjiang
student.age=3
yaml给实体类赋值
yaml可以直接给实体类赋值
例子:
package com.kuang.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Dog {
private String name;
private Integer age;
public Dog() {
}
public Dog(String name, Integer age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
package com.kuang.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;
import java.util.Map;
/*
@ConfigurationProperties作用:
将配置文件中配置的每一个属性的值,映射到这个组件中;
告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定
参数 prefix = “person” : 将配置文件中的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> lists;
private Dog dog;
public Person() {
}
public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Dog dog) {
this.name = name;
this.age = age;
this.happy = happy;
this.birth = birth;
this.maps = maps;
this.lists = lists;
this.dog = dog;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getHappy() {
return happy;
}
public void setHappy(Boolean happy) {
this.happy = happy;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Map<String, Object> getMaps() {
return maps;
}
public void setMaps(Map<String, Object> maps) {
this.maps = maps;
}
public List<Object> getLists() {
return lists;
}
public void setLists(List<Object> lists) {
this.lists = lists;
}
public Dog getDog() {
return dog;
}
public void setDog(Dog dog) {
this.dog = dog;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", happy=" + happy +
", birth=" + birth +
", maps=" + maps +
", lists=" + lists +
", dog=" + dog +
'}';
}
}
person:
name: qinjiang
age: 3
happy: false
birth: 2019/11/02
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: 旺财
age: 3
这是测试类,最后测试一下
package com.kuang;
import com.kuang.pojo.Dog;
import com.kuang.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person);
//Person{name='qinjiang', age=3, happy=false, birth=Sat Nov 02 00:00:00 CST 2019, maps={k1=v1, k2=v2}, lists=[code, music, girl], dog=Dog{name='旺财', age=3}}
}
}
yaml配置文件功能很强大,还能这么写。SpringBoot推荐我们使用yaml。所以我们一般用yaml配置文件,不用properties
person:
name: qinjiang${random.uuid} # 随机uuid
age: ${random.int} # 随机int
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: ${person.hello:other}_旺财 # 如果有person.hello就用person.hello的值,如果没有就默认为other
age: 1
yaml支持松散绑定
松散绑定:这个什么意思呢? 比如我的yaml中写的 first-name,这个和 firstName 是一样的,后面跟着的字母 默认是大写的。这就是松散绑定。可以测试一下 (yaml文件中写first-name,实体类中写firstName,是可以对应起来的)
例子:
package com.kuang.pojo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "dog")
public class Dog {
private String firstName;
private Integer age;
public Dog() {
}
public Dog(String firstName, Integer age) {
this.firstName = firstName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"firstName='" + firstName + '\'' +
", age=" + age +
'}';
}
}
dog:
first-name: 阿黄
age: 3
package com.kuang;
import com.kuang.pojo.Dog;
import com.kuang.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog);
//Dog{firstName='阿黄', age=3}
}
}
小结:
- 配置yaml和配置properties都可以获取到值,强烈推荐 yaml;
- 如果我们在某个业务中,只需要获取配置文件中的某个值,可以使用一下@value**(这是properties配置文件方式)**
- 如果说,我们专门编写了一个JavaBean来和配置文件进行一一映射,就直接@configurationProperties,不要犹豫!(这是yaml配置文件方式)
yaml支持JSR303数据校验
JSR303数据校验,这个就是我们可以在字段上增加一层过滤器验证,可以保证数据的合法性
常见的参数
@NotNull(message="名字不能为空")
private String userName;
@Max(value=120,message="年龄最大不能查过120")
private int age;
@Email(message="邮箱格式错误")
private String email;
空检查
@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 对象是否符合正则表达式的规则
.......等等
除此以外,我们还可以自定义一些数据校验规则
例子:
可能需要添加这个依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
package com.kuang.pojo;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.Email;
import java.util.Date;
import java.util.List;
import java.util.Map;
@Component
@ConfigurationProperties(prefix = "person")//如果不配置使用了这个注解:就会提示报红,但不影响运行
@Validated //数据校验
public class Person {
@Email()//表明这个name必须是电子邮箱数据格式
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
public Person() {
}
public Person(String name, Integer age, Boolean happy, Date birth, Map<String, Object> maps, List<Object> lists, Dog dog) {
this.name = name;
this.age = age;
this.happy = happy;
this.birth = birth;
this.maps = maps;
this.lists = lists;
this.dog = dog;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getHappy() {
return happy;
}
public void setHappy(Boolean happy) {
this.happy = happy;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Map<String, Object> getMaps() {
return maps;
}
public void setMaps(Map<String, Object> maps) {
this.maps = maps;
}
public List<Object> getLists() {
return lists;
}
public void setLists(List<Object> lists) {
this.lists = lists;
}
public Dog getDog() {
return dog;
}
public void setDog(Dog dog) {
this.dog = dog;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", happy=" + happy +
", birth=" + birth +
", maps=" + maps +
", lists=" + lists +
", dog=" + dog +
'}';
}
}
person:
name: qinjiang # 这个值必须为电子邮箱数据格式,否则测试运行会报错
age: 3
happy: false
birth: 2019/11/02
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: 旺财
age: 3
package com.kuang;
import com.kuang.pojo.Dog;
import com.kuang.pojo.Person;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
多环境配置及配置文件位置
配置文件yaml可以存在的位置以及它们的优先级
多环境配置
server:
port: 8081
# 默认启动8081端口
# 我们也可以选择激活某个端口
spring:
profiles:
active: dev # 这里选择激活了下面8082端口
# --- 这个代表分割线
---
server:
port: 8082
spring:
profiles: dev
---
server:
port: 8083
spring:
profiles: test
自动装配原理【重点】
精髓
-
SpringBoot启动会加载大量的自动配置类(xxxxAutoConfiguration,这些都在spring.factories核心文件中),每一个自动配置类中都有对应的 xxxxProperties,这些xxxxProperties中有着相关的属性与默认值,这些属性又是和配置文件(application.yaml)绑定的(通过这个注解@ConfigurationProperties绑定),所以我们可以通过配置文件来修改SpringBoot自动配置的默认值;
-
如果我们想写一个自动配置类,首先看我们需要的功能有没有在SpringBoot默认写好的自动配置类当中;
-
我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件存在在其中,我们就不需要再手动配置了)
-
给容器中自动配置类(xxxxAutoConfiguration)添加组件的时候,会从properties类中获取某些属性。我们只需要在配置文件(application.yaml)中指定这些属性的值即可;
xxxxAutoConfiguration:自动配置类;给容器中添加组件
xxxxProperties:封装配置文件中相关属性
了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;
@Conditional派生注解(Spring注解版原生的@Conditional作用)
作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置里面的所有内容才生效;
那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的自动配置类,但不是所有的都生效了。
我们怎么知道哪些自动配置类生效?
我们可以通过启用debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效了
#开启springboot的调试类
debug: true
输出的日志中大致分为一下几类:
Positive matches:已经启用了,并且生效的
Negative matches:没有启动,没有匹配成功的自动配置类
Unconditional classes:没有条件的类
掌握吸收原理,以不变应万变
SpringBoot Web开发
需要思考:SpringBoot到底帮我们配置了什么?我们能不能进行修改?能修改哪些东西?能不能扩展?
静态资源导入
通过webjars方式导入静态资源的例子:
导入依赖
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
直接在目录下导入静态资源的例子:
这三个存放静态资源的路径都是默认的,如果你在SpringBoot核心配置文件里自己修改了存放静态资源默认路径的值,则存放静态资源的路径以修改的路径的值为准**(所以一般我们用默认的这三个路径存放静态资源,不要自己去修改存放静态资源路径)**
首页和图标定制
首页例子:
图标定制(了解即可):不同版本的SpringBoot实现方式不一样!!!
Thymeleaf模板引擎
-
前端交给我们的页面,是html页面。如果是我们以前开发,我们需要把他们转成jsp页面,jsp好处就是当我们查出一些数据转发到JSP页面以后,我们可以用jsp轻松实现数据的显示,及交互等。
-
jsp支持非常强大的功能,包括能写Java代码,但是呢,我们现在的这种情况,SpringBoot这个项目首先是以jar的方式,不是war,第二,我们用的还是嵌入式的Tomcat,所以呢,他现在默认是不支持jsp的。
-
那不支持jsp,如果我们直接用纯静态页面的方式,那给我们开发会带来非常大的麻烦,那怎么办呢?
所以SpringBoot推荐你可以来使用模板引擎:
模板引擎,我们其实大家听到很多,其实jsp就是一个模板引擎,还有用的比较多的freemarker,包括SpringBoot给我们推荐的Thymeleaf,模板引擎有非常多,但再多的模板引擎,他们的思想都是一样的,什么样一个思想呢我们来看一下这张图:
模板引擎的作用就是我们来写一个页面模板,比如有些值呢,是动态的,我们写一些表达式。而这些值,从哪来呢,就是我们在后台封装一些数据。然后把这个模板和这个数据交给我们模板引擎,模板引擎按照我们这个数据帮你把这表达式解析、填充到我们指定的位置,然后把这个数据最终生成一个我们想要的内容给我们写出去,这就是我们这个模板引擎,不管是jsp还是其他模板引擎,都是这个思想。只不过呢,就是说不同模板引擎之间,他们可能这个语法有点不一样。其他的我就不介绍了,我主要来介绍一下SpringBoot给我们推荐的Thymeleaf模板引擎,这模板引擎呢,是一个高级语言的模板引擎,他的这个语法更简单。而且呢,功能更强大。
我们呢,就来看一下这个模板引擎,那既然要看这个模板引擎。首先,我们来看SpringBoot里边怎么用。
引入Thymeleaf
怎么引入呢,对于springboot来说,什么事情不都是一个start的事情嘛,我们去在项目中引入一下。
给大家三个网址:
- Thymeleaf 官网:https://www.thymeleaf.org/
- Thymeleaf 在Github 的主页:https://github.com/thymeleaf/thymeleaf
- Spring官方文档:找到我们对应的版本https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
找到对应的pom依赖:可以适当点进源码看下本来的包!
<dependency>
<groupId>org.thymeleaf</groupId>
<artifactId>thymeleaf-spring5</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-java8time</artifactId>
</dependency>
**导入依赖即可!**注意版本问题。
Thymeleaf所有的模板引擎都写在templates目录下
例子:
test.html
<!DOCTYPE html>
<!--命名空间的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--所有的html元素都可以被thymeleaf接管: th:元素名 -->
<!--必须要加下面的注释否则会报错-->
<!--/*@thymesVar id="msg" type=""*/-->
<div th:text="${msg}"></div>
</body>
</html>
IndexController.java
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
//在templates目录下的所有页面,只能通过controller来跳转
//这个需要模板引擎的支持! 需要导入thymeleaf依赖
@Controller
public class IndexController {
@RequestMapping("/test")
public String test(Model model){
model.addAttribute("msg","hello,springboot");
return "test";
}
}
Thymeleaf语法
Thymeleaf语法具体更多的语法自行去官方文档了解
先展示一两个语法的例子,更多的语法后面都会用到
例子:
test.html
<!DOCTYPE html>
<!--命名空间的约束-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<!--所有的html元素都可以被thymeleaf接管:th:元素名-->
<!--必须要加下面的注释否则会报错-->
<!--/*@thymesVar id="msg" type=""*/-->
<div th:text="${msg}"></div>
<!--utext不会转义msg传过来的html标签元素-->
<div th:utext="${msg}"></div>
<hr>
<!--/*@thymesVar id="users" type=""*/-->
<h3 th:each="user:${users}" th:text="${user}"></h3>
<!--第二种写法-->
<!--<h3 th:each="user:${users}">[[ ${user} ]]</h3>-->
</body>
</html>
IndexController.java
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Arrays;
//在templates目录下的所有页面,只能通过controller来跳转
//这个需要模板引擎的支持! 需要导入thymeleaf依赖
@Controller
public class IndexController {
@RequestMapping("/test")
public String test(Model model){
model.addAttribute("msg","<h1>hello,springboot</h1>");
//Arrays.asList()把数组转换成集合
model.addAttribute("users", Arrays.asList("qinjiang","kuangshen"));
return "test";
}
}
SpringMVC自动配置原理
-
在进行项目编写前,我们还需要知道一个东西,就是SpringBoot对我们的SpringMVC还做了哪些配置,包括如何扩展,如何定制。途径一:源码分析,途径二:官方文档!
-
只有把这些都搞清楚了,我们在之后使用才会更加得心应手。
-
地址 :https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration
页面搜索:Spring MVC Auto-configuration,学会阅读官方文档!
扩展SpringMVC
扩展 SpringMVC 的功能,其中有很多功能
**看官方文档,结合例子理解,**这个例子主要是自定义视图解析器:
package com.kuang.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Locale;
//如果想自定义一些定制化的功能,只要写这个组件,然后将它交给SpringBoot,SpringBoot就会帮我们自动装配
//扩展 SpringMVC 的功能,其中有很多功能,这个例子主要是自定义视图解析器
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//public interface ViewResolver 实现了视图解析器接口的类,我们就可以把它看做视图解析器
@Bean
public ViewResolver myViewResolver(){
return new MyViewResolver();
}
//自定义了一个视图解析器MyViewResolver
public static class MyViewResolver implements ViewResolver{
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
return null;
}
}
}
有些扩展功能的具体实现方法因SpringBoot版本而异,所以学会结合官方文档剖析源码很重要
这么多的自动配置,原理都是一样的,通过这个WebMVC的自动配置原理分析,我们要学会一种学习方式,通过源码探究,得出结论;这个结论一定是属于自己的,而且一通百通。
SpringBoot的底层,大量用到了这些设计细节思想,所以,没事需要多阅读源码!得出结论;
SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(如果用户自己配置@bean),如果有就用用户配置的,如果没有就用自动配置的;
如果有些组件可以存在多个,比如我们的视图解析器,就将用户配置的和自己默认的组合起来!
例子:
package com.kuang.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
//如果我们要扩展SpringMVC,官方建议我们这样去做!
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
//视图跳转
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/kuang").setViewName("test");
//http://localhost:8080/kuang 能够跳到templates目录下的test.html页面
}
}
在SpringBoot源码中会有非常多的 xxxxConfiguration 扩展配置(注意不是xxxxAutoConfiguration),只要看见了这个@Configuration,我们就应该多留心注意!!!
员工管理系统
这个系统不是完整的,它只是为了看一下,使用SpringBoot开发,我们要进行哪些操作!
- 首页配置
- 所有页面的静态资源都需要使用thymeleaf接管!
- 具体实现的方法,看thymeleaf语法。
- url用 @{…}
比如:
<!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>
<!-- Bootstrap core CSS -->
<!--加载静态资源,添加样式-->
<!--thymeleaf模板引擎的语法,url用 @{...} -->
<!--这样就算项目访问路径改变了,也照样能读取对应的静态资源,路径不需要写死-->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<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">© 2017-2018</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
-
页面国际化
- 我们需要配置 i18n(internationalization,头尾字母i和n,中间18个字母) 文件
- 如果需要在项目中进行按钮自动切换,我们需要自定义一个组件 LocaleResolver
- 记得将自己写的组件配置到 spring 容器中 @Bean
- 国际化用:#{…}
-
登录和拦截器
- 注意:有些资源加载不出来,需要用vpn才能加载,因为有些资源是国外服务器的
-
员工列表展示
-
提取公共页面
-
th:fragment="topbar" <!--抽取顶部导航栏的代码-->
-
th:replace="~{commons/commons::topbar}" <!--实现顶部导航栏代码的复用-->
-
如果要传递参数可以直接使用 () 传参,其它地方接收判断(一般用三元运算符判断)即可
-
-
列表循环展示
-
-
员工的增删改查
-
404
具体代码,查看项目文件,这个项目是没有用到数据库的
整合JDBC使用
SpringData简介
对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),SpringBoot 底层都是采用 SpringData 的方式进行统一处理。
SpringBoot 底层都是采用 SpringData 的方式进行统一处理各种数据库,SpringData 也是 Spring 中与 SpringBoot、SpringCloud 等齐名的知名项目。
Sping Data 官网:https://spring.io/projects/spring-data
数据库相关的启动器 :可以参考官方文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
例子
创建一个SpringBoot项目
添加 JDBC API、MySQL Driver 两个依赖,后面有需要用到web依赖自行去pom.xml里面导依赖
新建 application.yaml
spring:
datasource:
username: root
password: 123456
# 假如时区报错了,就增加一个时区的配置就ok了,serverTimezone=UTC
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
# com.mysql.jdbc.Driver 这个已经过时
driver-class-name: com.mysql.cj.jdbc.Driver
Springboot04DataApplicationTests.java
package com.kuang;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
@SpringBootTest
class Springboot04DataApplicationTests {
//不再推荐使用@Autowired,请使用@Resource来标记,实现bean的自动装配
@Resource
DataSource dataSource;
@Test
void contextLoads() throws SQLException {
//查看一下默认的数据源:class com.zaxxer.hikari.HikariDataSource 曾经接触过的数据源有 dbcp、c3p0、Tomcat jdbc
//HikariDataSource 号称javaweb 当前速度最快的数据源,相比于传统的连接池更加优秀
System.out.println(dataSource.getClass());
//获得数据库连接
Connection connection = dataSource.getConnection();
System.out.println(connection);
//在SpringBoot里面有很多 xxxxtemplate ,这是SpringBoot已经配置好的模板bean,拿来就能用
//关闭
connection.close();
}
}
新建 JDBCController.java
package com.kuang.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.RestController;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
public class JDBCController {
@Resource
JdbcTemplate jdbcTemplate;
//查询数据库的所有信息
//没有实体类,数据库中的东西,怎么获取? 万能的Map
@GetMapping("/userList")
public List<Map<String,Object>> userList(){
String sql = "select * from user";
List<Map<String, Object>> list_maps = jdbcTemplate.queryForList(sql);
return list_maps;
}
//增加用户
@GetMapping("/addUser")
public String addUser(){
String sql = "insert into mybatis.user(id,name,pwd) values (4,'小明','123456')";
jdbcTemplate.update(sql);
return "add-ok!";
}
//修改用户
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id") int id){
//预编译传参,自己回顾原生的jdbc代码
String sql = "update mybatis.user set name=?,pwd=? where id=" + id;
//封装数据
Object[] objects = new Object[2];
objects[0] = "小明2";
objects[1] = "zzzxxx";
jdbcTemplate.update(sql,objects);//预编译传参,自己回顾原生的jdbc代码
return "update-ok!";
}
//删除用户
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id")int id){
String sql = "delete from mybatis.user where id=?";
jdbcTemplate.update(sql,id);
return "delete-ok!";
}
}
去Springboot04DataApplication主程序入口启动一下,查询数据库的所有信息 结果如下
其它三个 增加、修改、删除 请求也是能够实现的!
整合Druid数据源
Druid简介
Java程序很大一部分要操作数据库,为了提高性能,操作数据库的时候,又不得不使用数据库连接池。
Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP、PROXOOL 等 DB(数据库) 池的优点,同时加入了日志监控。
Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。
Druid 已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源
我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控
Github地址:https://github.com/alibaba/druid/
- com.alibaba.druid.pool.DruidDataSource 基本配置参数如下:
例子
添加 Druid 数据源依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
添加log4j依赖,因为我们配置中用到了
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
application.yaml
spring:
datasource:
username: root
password: 123456
# 假如时区报错了,就增加一个时区的配置就ok了,serverTimezone=UTC
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
# com.mysql.jdbc.Driver 这个已经过时
driver-class-name: com.mysql.cj.jdbc.Driver
# 可以配置自己想使用的数据源,指定了之后,SpringBoot默认的数据源就不会用了
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters, stat:监控统计、 log4j:日志记录、 wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
JDBCController.java 中的代码照样能跑
Druid强大的地方在于它能够自定义一些配置
新建DruidConfig.java
package com.kuang.config;
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.annotation.WebFilter;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DruidConfig {
//@ConfigurationProperties 绑定 application.yaml 中的 spring.datasource
@ConfigurationProperties(prefix = "spring.datasource")
@Bean
public DataSource druidDataSource(){
return new DruidDataSource();
}
//后台监控功能
//这个 ServletRegistrationBean 相当于 web.xml
//因为SpringBoot内置了Servlet容器,所以没有web.xml,替代方法:把 ServletRegistrationBean 注册到Spring中
@Bean
public ServletRegistrationBean statViewServlet(){
ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
//后台需要有人登录,账号密码配置
Map<String, String> initParameters = new HashMap<>();
//增加配置
initParameters.put("loginUsername","admin");//登录的key:loginUsername是固定参数
initParameters.put("loginPassword","123456");//登录的key:loginPassword是固定参数
//允许谁可以访问
initParameters.put("allow","");//这是允许所有人都可以访问
//禁止谁能访问
//initParameters.put("kuangshen","192.168.11.123");//禁止这个ip:192.168.11.123 访问
bean.setInitParameters(initParameters); //初始化参数
return bean;
}
//filter 过滤器
@Bean
public FilterRegistrationBean webStatFilter(){
FilterRegistrationBean bean = new FilterRegistrationBean();
bean.setFilter(new WebStatFilter());
//可以过滤哪些请求呢?
Map<String, String> initParameters = new HashMap<>();
//这些请求不进行过滤
//exclusions:排除在外
initParameters.put("exclusions","*.js,*.css,/druid/*");
bean.setInitParameters(initParameters);
return bean;
}
}
启动SpringBoot看看效果!
整合MyBatis框架
整合包
mybatis-spring-boot-starter
导入这个依赖才能整合
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
创建一个新项目,添加 SpringWeb、MySQLDriver、JDBC API 依赖,导入整合mybatis的依赖
项目的整体目录结构
application.properties
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 整合mybatis
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
User.java
package com.kuang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
UserMapper.java
package com.kuang.controller;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class UserController {
@Resource
private UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList(){
List<User> userList = userMapper.queryUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
@GetMapping("/addUser")
public String addUser(){
userMapper.addUser(new User(4,"kkkxxx","123123"));
return "add--ok";
}
@GetMapping("/updateUser")
public String updateUser(){
userMapper.updateUser(new User(4,"xxxppp","321www"));
return "update--ok";
}
@GetMapping("/deleteUser")
public String deleteUser(){
userMapper.deleteUser(4);
return "delete--ok";
}
}
UserMapper.xml
<?xml version="1.0" encoding="utf8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.mapper.UserMapper">
<select id="queryUserList" resultType="User">
select * from user;
</select>
<select id="queryUserById" resultType="User">
select * from user where id = #{id};
</select>
<insert id="addUser" parameterType="User">
insert into user (id, name, pwd) values (#{id},#{name},#{pwd});
</insert>
<update id="updateUser" parameterType="User">
update user set name=#{name},pwd=#{pwd} where id = #{id};
</update>
<delete id="deleteUser" parameterType="int">
delete from user where id = #{id};
</delete>
</mapper>
UserController.java
package com.kuang.controller;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class UserController {
@Resource
private UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList(){
List<User> userList = userMapper.queryUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
}
启动SpringBoot服务测试一下!
SpringSecurity
安全简介
**在 Web 开发中,安全一直是非常重要的一个方面。**安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
如果我们使用过滤器、拦截器实现web安全,我们会用到大量的原生代码,繁琐冗余!所以我们用框架实现web安全!
市面上存在比较有名的框架:Shiro,Spring Security !
这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?
- 首先我们看下它的官网介绍:Spring Security官网地址 https://spring.io/projects/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),可以对应用中的领域对象进行细粒度的控制。
环境搭建
新建SpringBoot项目,导入web依赖,导入thymeleaf依赖
导入静态资源!具体查看项目
application.properties
# 关闭模板引擎thymeleaf缓存,方便我们调试
spring.thymeleaf.cache=false
RouterController.java
package com.kuang.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;
}
}
启动SpringBoot服务,看下效果
认识SpringSecurity
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,他可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式,(@Enablexxxx 开启某个功能)
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
参考官网:https://spring.io/projects/spring-security
查看我们自己项目中的版本,找到对应的帮助文档:https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5
用户认证和授权
导入SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
SecurityConfig.java
package com.kuang.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;
//它能够自动将代码横切进去的,AOP思想
@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();//它这里面会有一个/login请求
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//一般情况下都是从数据库中进行认证
//auth.jdbcAuthentication()
//我们现在没有连数据库,所以从内存中去认证
//inMemoryAuthentication 从内存里面去认证
//BCryptPasswordEncoder() 密码加密的一种方式 java有原生的加密方式,md5
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").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");
}
}
注销及权限控制
导入security-thymeleaf整合包依赖
<!--security-thymeleaf整合包,我们可以在thymeleaf里面写一些关于SpringSecurity的操作-->
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity4</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
index.html
<!DOCTYPE html>
<!--需要先导入相关的命名空间 -->
<html lang="en" xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>首页</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
<link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
<div class="ui secondary menu">
<a class="item" th:href="@{/index}">首页</a>
<!--登录注销-->
<div class="right menu">
<!--需要先导入相关的命名空间,并且可能需要降低SpringBoot的版本 2.0.9.RELEASE -->
<div sec:authorize="!isAuthenticated()"><!--如果未登录,显示登录按钮-->
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<div sec:authorize="isAuthenticated()"><!--如果已经登录,显示用户名-->
<a class="item">
用户名:<span sec:authentication="name"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()"><!--如果已经登录,显示注销按钮-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注销
</a>
</div>
</div>
</div>
</div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study by 秦疆</h3>
</div>
<div>
<br>
<div class="ui three column stackable grid">
<!--菜单根据用户的角色动态实现-->
<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>
</div>
</div>
</div>
<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>
</body>
</html>
可能需要降低SpringBoot的版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<!--<version>2.7.0</version>-->
<!--有些功能需要降低版本-->
<version>2.0.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
SecurityConfig.java
package com.kuang.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;
//它能够自动将代码横切进去的,AOP思想
@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();//它这里面会有一个/login请求
//注销,这样就开启了注销功能。注销后一般会跳到首页
http.logout().logoutSuccessUrl("/");
//如果注销失败可能需要关闭 跨站域请求伪造(csrf) 功能
http.csrf().disable();
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//一般情况下都是从数据库中进行认证
//auth.jdbcAuthentication()
//我们现在没有连数据库,所以从内存中去认证
//inMemoryAuthentication 从内存里面去认证
//BCryptPasswordEncoder() 密码加密的一种方式 java有原生的加密方式,md5
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").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");
}
}
记住我及首页定制
SecurityConfig.java
package com.kuang.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;
//它能够自动将代码横切进去的,AOP思想
@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().loginPage("/toLogin")//定制登录页面
.usernameParameter("user")//自定义接收前端的参数
.passwordParameter("pwd")//自定义接收前端的参数
.loginProcessingUrl("/login");//和前端的登录请求一样
//注销,这样就开启了注销功能。注销后一般会跳到首页
http.logout().logoutSuccessUrl("/");
//如果注销失败可能需要关闭 跨站域请求伪造(csrf) 功能
http.csrf().disable();
//开启记住我功能 本质就是cookie 默认保存14天
http.rememberMe().rememberMeParameter("remember");//自定义接收前端的参数
}
//认证
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//一般情况下都是从数据库中进行认证
//auth.jdbcAuthentication()
//我们现在没有连数据库,所以从内存中去认证
//inMemoryAuthentication 从内存里面去认证
//BCryptPasswordEncoder() 密码加密的一种方式 java有原生的加密方式,md5
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").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");
}
}
login.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>登录</title>
<!--semantic-ui-->
<link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
</head>
<body>
<!--主容器-->
<div class="ui container">
<div class="ui segment">
<div style="text-align: center">
<h1 class="header">登录</h1>
</div>
<div class="ui placeholder segment">
<div class="ui column very relaxed stackable grid">
<div class="column">
<div class="ui form">
<form th:action="@{/login}" method="post">
<div class="field">
<label>Username</label>
<div class="ui left icon input">
<input type="text" placeholder="Username" name="user">
<i class="user icon"></i>
</div>
</div>
<div class="field">
<label>Password</label>
<div class="ui left icon input">
<input type="password" name="pwd">
<i class="lock icon"></i>
</div>
</div>
<div class="field">
<input type="checkbox" name="remember"> 记住我
</div>
<input type="submit" class="ui blue submit button"/>
</form>
</div>
</div>
</div>
</div>
<div style="text-align: center">
<div class="ui label">
</i>注册
</div>
<br><br>
<small>blog.kuangstudy.com</small>
</div>
<div class="ui segment" style="text-align: center">
<h3>Spring Security Study by 秦疆</h3>
</div>
</div>
</div>
<script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
<script th:src="@{/qinjiang/js/semantic.min.js}"></script>
</body>
</html>
Shiro
简介
- Apache Shiro是一个强大且易用的Java安全框架
- 可以完成身份验证、授权、加密、会话管理、web集成、缓存等
- Shiro 不仅可以用在 JavaSE 环境中,也可以用在 JavaEE 环境中
官网: http://shiro.apache.org/
GitHub:apache/shiro: Apache Shiro (github.com) 可以从GitHub上下载
有哪些功能
Authentication:身份认证/登录,验证用户是不是拥有相应的身份;
Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Manager:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support:Web支持,可以非常容易的集成到Web环境;
Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency:shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing:提供测试支持;
Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
Shiro架构(外部)
从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作
应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
- Subject:主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与Subject的所有交互都会委托给SecurityManager;可以把Subject认为是一个门面;SecurityManager才是实际的执行者;
- SecurityManager:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
- Realm:域,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全数据源。
也就是说对于我们而言,最简单的一个Shiro应用:
- 应用代码通过Subject来进行认证和授权,而Subject又委托给SecurityManager;
- 我们需要给Shiro的SecurityManager注入Realm,从而让SecurityManager能得到合法的用户及其权限进行判断。
从以上也可以看出,Shiro不提供维护用户/权限,而是通过Realm让开发人员自己注入
Shiro架构(内部)
-
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
-
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
- Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
- SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所以,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
- SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached(内存)中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的
-
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
快速体验
创建一个普通的maven项目
在普通的maven项目下新建一个module
-
导入依赖
pom.xml
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>springboot-08-shiro</artifactId> <groupId>org.kuang</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>hello-shiro</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.9.0</version> </dependency> <!-- configure logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>1.7.21</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.17.2</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.17.2</version> </dependency> </dependencies> </project>
-
配置文件
log4j2.xml
<Configuration name="ConfigTest" status="ERROR" monitorInterval="5"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> </Appenders> <Loggers> <Logger name="org.springframework" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="org.apache" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="net.sf.ehcache" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Logger name="org.apache.shiro.util.ThreadContext" level="warn" additivity="false"> <AppenderRef ref="Console"/> </Logger> <Root level="info"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>
shiro.ini
# # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # # ============================================================================= # Quickstart INI Realm configuration # # For those that might not understand the references in this file, the # definitions are all based on the classic Mel Brooks' film "Spaceballs". ;) # ============================================================================= # ----------------------------------------------------------------------------- # Users and their assigned roles # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc # ----------------------------------------------------------------------------- [users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
-
java文件
Quickstart.java
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; //import org.apache.shiro.ini.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; //import org.apache.shiro.lang.util.Factory; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple Quickstart application showing how to use Shiro's API. * * @since 0.9 RC2 */ public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } }
-
运行测试
上面的代码都是分析GitHub源码,复制粘贴过来的。
Shiro的Subject分析
Quickstart.java
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.config.IniSecurityManagerFactory;
//import org.apache.shiro.ini.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
//import org.apache.shiro.lang.util.Factory;
import org.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
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("Subject=>Session[" + value + "]");
}
//判断当前用户是否被认证
if (!currentUser.isAuthenticated()) {
//会根据用户名和密码生成一个 Token:令牌
UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa");
token.setRememberMe(true);//设置记住我
try {
currentUser.login(token);//执行了登录操作
} catch (UnknownAccountException uae) {
log.info("There is no user with username of " + token.getPrincipal());
} catch (IncorrectCredentialsException ice) {
log.info("Password for account " + token.getPrincipal() + " was incorrect!");
} catch (LockedAccountException lae) {
log.info("The account for username " + token.getPrincipal() + " is locked. " +
"Please contact your administrator to unlock it.");
}
// ... catch more exceptions here (maybe custom ones specific to your application?
catch (AuthenticationException ae) {
//unexpected condition? error?
}
}
//currentUser.getPrincipal() 获得当前用户的认证
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.");
}
//细粒度
//a (very powerful) Instance Level permission:
if (currentUser.isPermitted("winnebago:drive:eagle5")) {
log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " +
"Here are the keys - have fun!");
} else {
log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!");
}
//注销
//all done - log out!
currentUser.logout();
//结束
System.exit(0);
}
}
这些是shiro的主要内容
Subject currentUser = SecurityUtils.getSubject(); //获取当前的用户对象 Subject
Session session = currentUser.getSession(); //通过当前用户拿到 Session
currentUser.isAuthenticated() //判断当前用户是否被认证
currentUser.getPrincipal() //获得当前用户的认证
currentUser.hasRole("...") //可以获得当前用户是否拥有什么角色
currentUser.isPermitted("...") //获得当前用户的权限
currentUser.logout(); //注销
这些在SpringSecurity中基本都有~
SpringBoot整合Shiro环境搭建
新建一个SpringBoot module,添加spring-web、thymeleaf、shiro-spring依赖
shiro-spring的依赖
<!--
Subject 用户
SecurityManager 管理所有用户
Realm 连接数据
-->
<!--导入shiro整合spring的包-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.9.0</version>
</dependency>
UserRealm.java
package com.kuang.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;
//自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
return null;
}
}
ShiroConfig.java
package com.kuang.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
//DefaultWebSecurityManager
@Bean(name="securityManager")//可以指定bean的名称
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){//被spring接管
return new UserRealm();
}
}
add.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>add</h1>
</body>
</html>
update.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>update</h1>
</body>
</html>
MyController.java
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.xml.ws.RespectBinding;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro!");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
}
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<p th:text="${msg}"></p>
<hr>
<a th:href="@{/user/add}">add</a> | <a th:href="@{/user/update}">update</a>
</body>
</html>
Shiro实现登录拦截
ShiroConfig.java
package com.kuang.config;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
anno:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/user/add","authc");
filterMap.put("/user/update","authc");
bean.setFilterChainDefinitionMap(filterMap);
//如果没有权限,一般会返回登录页面
// shiro不像SpringSecurity那样有默认的登录页面,所以我们要自己新建一个登录页面
bean.setLoginUrl("/toLogin");
return bean;
}
//DefaultWebSecurityManager
@Bean(name="securityManager")//可以指定bean的名称
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){//被spring接管
return new UserRealm();
}
}
login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录</h1>
<hr>
<form action="">
<p>用户名:<input type="text" name="username"></p>
<p>密码:<input type="password" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
MyController.java
package com.kuang.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.xml.ws.RespectBinding;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro!");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
}
Shiro实现用户认证
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登录</h1>
<p th:text="${msg}" style="color: red"></p>
<hr>
<form th:action="@{/login}">
<p>用户名:<input type="text" name="username"></p>
<p>密码:<input type="password" name="password"></p>
<p><input type="submit"></p>
</form>
</body>
</html>
MyController.java
package com.kuang.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.xml.ws.RespectBinding;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro!");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@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);//执行登录的方法,如果没有异常就说明OK了
return "index";
} catch (UnknownAccountException e) {//异常:用户名不存在
model.addAttribute("msg","用户名错误!");
return "login";//返回到登录页面
} catch (IncorrectCredentialsException e){//异常:密码不存在
model.addAttribute("msg","密码错误!");
return "login";
}
}
}
UserRealm.java
package com.kuang.config;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
//自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
//用户名,密码 正常是数据库中取,我们现在自己伪造一下数据
String name = "root";
String password = "123456";
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
if(!userToken.getUsername().equals(name)){//用户名认证
return null;//抛出异常 UnknownAccountException
}
//密码认证,shiro会自动帮你做
return new SimpleAuthenticationInfo("",password,"");
//SimpleAuthenticationInfo 是 接口AuthenticationInfo 的实现类
}
}
Shiro整合MyBatis
新增依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
application.yaml
spring:
datasource:
username: root
password: 123456
# 假如时区报错了,就增加一个时区的配置就ok了,serverTimezone=UTC
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
# com.mysql.jdbc.Driver 这个已经过时
driver-class-name: com.mysql.jdbc.Driver
# 可以配置自己想使用的数据源,指定了之后,SpringBoot默认的数据源就不会用了
type: com.alibaba.druid.pool.DruidDataSource
#Spring Boot 默认是不注入这些属性值的,需要自己绑定
#druid 数据源专有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置监控统计拦截的filters, stat:监控统计、 log4j:日志记录、 wall:防御sql注入
#如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority
#则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
application.properties
mybatis.type-aliases-package=com.kuang.pojo
mybatis.mapper-locations=classpath:mapper/*.xml
User.java
package com.kuang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
UserMapper.java
package com.kuang.mapper;
import com.kuang.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Repository
@Mapper
public interface UserMapper {
public User queryUserByName(String name);
}
UserMapper.xml
<?xml version="1.0" encoding="utf8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.mapper.UserMapper">
<select id="queryUserByName" parameterType="String" resultType="User">
select * from mybatis.user where name = #{name};
</select>
</mapper>
UserService.java
package com.kuang.service;
import com.kuang.pojo.User;
public interface UserService {
public User queryUserByName(String name);
}
UserServiceImpl.java
package com.kuang.service;
import com.kuang.mapper.UserMapper;
import com.kuang.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl implements UserService{
@Autowired
UserMapper userMapper;
@Override
public User queryUserByName(String name) {
return userMapper.queryUserByName(name);
}
}
UserRealm.java
package com.kuang.config;
import com.kuang.pojo.User;
import com.kuang.service.UserServiceImpl;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
//自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userServiceImpl;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
return null;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//userToken.getUsername() 从token中拿到用户名,并且从数据库中查找是否存在该用户名的用户
User user = userServiceImpl.queryUserByName(userToken.getUsername());
if(user==null){//没有这个人
return null;//UnknownAccountException 会报出这个异常
}
//密码认证,shiro会自动帮你做
return new SimpleAuthenticationInfo("",user.getPwd(),"");
//SimpleAuthenticationInfo 是 接口AuthenticationInfo 的实现类
}
}
Shiro实现请求授权
UserRealm.java
package com.kuang.config;
import com.kuang.pojo.User;
import com.kuang.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
//自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userServiceImpl;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
//给用户授予权限 new SimpleAuthorizationInfo()
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这一个对象
Subject subject = SecurityUtils.getSubject();
//可以从下面的登录认证返回的值的代码中的user中拿到user
User currentUser = (User) subject.getPrincipal();//这样就拿到user对象了
//设置当前用户的权限,这个权限是数据库中用户对应的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//userToken.getUsername() 从token中拿到用户名,并且从数据库中查找是否存在该用户名的用户
User user = userServiceImpl.queryUserByName(userToken.getUsername());
if(user==null){//没有这个人
return null;//UnknownAccountException 会报出这个异常
}
//密码认证,shiro会自动帮你做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
//SimpleAuthenticationInfo 是 接口AuthenticationInfo 的实现类
}
}
ShiroConfig.java
package com.kuang.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;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
anno:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
//授权,正常情况下,未授权会跳转到未授权的页面
filterMap.put("/user/add","perms[user:add]");//这个包含了必须认证后才能访问
filterMap.put("/user/update","perms[user:update]");//这个包含了必须认证后才能访问
//注意map放值的时候,如果key相同,则会覆盖key所对应的value
// filterMap.put("/user/add","authc");
// filterMap.put("/user/update","authc");
// filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//如果没有权限,一般会返回登录页面
// shiro不像SpringSecurity那样有默认的登录页面,所以我们要自己新建一个登录页面
bean.setLoginUrl("/toLogin");
//设置未授权的页面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
//DefaultWebSecurityManager
@Bean(name="securityManager")//可以指定bean的名称
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){//被spring接管
return new UserRealm();
}
}
MyController.java
package com.kuang.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.xml.ws.RespectBinding;
@Controller
public class MyController {
@RequestMapping({"/","/index"})
public String toIndex(Model model){
model.addAttribute("msg","hello,Shiro!");
return "index";
}
@RequestMapping("/user/add")
public String add(){
return "user/add";
}
@RequestMapping("/user/update")
public String update(){
return "user/update";
}
@RequestMapping("/toLogin")
public String toLogin(){
return "login";
}
@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);//执行登录的方法,如果没有异常就说明OK了
return "index";
} catch (UnknownAccountException e) {//异常:用户名不存在
model.addAttribute("msg","用户名错误!");
return "login";//返回到登录页面
} catch (IncorrectCredentialsException e){//异常:密码不存在
model.addAttribute("msg","密码错误!");
return "login";
}
}
@RequestMapping("/noauth")
@ResponseBody
public String unauthorized(){
return "未经授权无法访问此页面";
}
}
User.java(数据库新增了perms字段,用来存储用户对应的权限)
package com.kuang.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
private String perms;
}
Shiro整合Thymeleaf
导入shiro-thymeleaf依赖
<!--shiro-thymeleaf整合-->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.0.0</version>
</dependency>
ShiroConfig.java
package com.kuang.config;
import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;
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;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager") DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
//设置安全管理器
bean.setSecurityManager(defaultWebSecurityManager);
//添加shiro的内置过滤器
/*
anno:无需认证就可以访问
authc:必须认证了才能访问
user:必须拥有 记住我 功能才能用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
Map<String, String> filterMap = new LinkedHashMap<>();
//授权,正常情况下,未授权会跳转到未授权的页面
filterMap.put("/user/add","perms[user:add]");//这个包含了必须认证后才能访问
filterMap.put("/user/update","perms[user:update]");//这个包含了必须认证后才能访问
//注意map放值的时候,如果key相同,则会覆盖key所对应的value
// filterMap.put("/user/add","authc");
// filterMap.put("/user/update","authc");
// filterMap.put("/user/*","authc");
bean.setFilterChainDefinitionMap(filterMap);
//如果没有权限,一般会返回登录页面
// shiro不像SpringSecurity那样有默认的登录页面,所以我们要自己新建一个登录页面
bean.setLoginUrl("/toLogin");
//设置未授权的页面
bean.setUnauthorizedUrl("/noauth");
return bean;
}
//DefaultWebSecurityManager
@Bean(name="securityManager")//可以指定bean的名称
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联 realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 realm 对象,需要自定义类
@Bean
public UserRealm userRealm(){//被spring接管
return new UserRealm();
}
//ShiroDialect:用来整合 shiro thymeleaf
@Bean
public ShiroDialect getShiroDialect(){
return new ShiroDialect();
}
}
index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<!--从session中判断值,如果用户未登录则显示登录按钮-->
<div th:if="${session.loginUser==null}">
<a th:href="@{/toLogin}">登录</a>
</div>
<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>
UserRealm.java
package com.kuang.config;
import com.kuang.pojo.User;
import com.kuang.service.UserServiceImpl;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
//自定义的 UserRealm
public class UserRealm extends AuthorizingRealm {
@Autowired
UserServiceImpl userServiceImpl;
//授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("执行了=>授权doGetAuthorizationInfo");
//给用户授予权限 new SimpleAuthorizationInfo()
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//拿到当前登录的这一个对象
Subject subject = SecurityUtils.getSubject();
//可以从下面的登录认证返回的值的代码中的user中拿到user
User currentUser = (User) subject.getPrincipal();//这样就拿到user对象了
//设置当前用户的权限,这个权限是数据库中用户对应的权限
info.addStringPermission(currentUser.getPerms());
return info;
}
//认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
System.out.println("执行了=>认证doGetAuthenticationInfo");
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
//userToken.getUsername() 从token中拿到用户名,并且从数据库中查找是否存在该用户名的用户
User user = userServiceImpl.queryUserByName(userToken.getUsername());
if(user==null){//没有这个人
return null;//UnknownAccountException 会报出这个异常
}
//登录成功存到session
Subject currentSubject = SecurityUtils.getSubject();
currentSubject.getSession().setAttribute("loginUser",user);
//密码认证,shiro会自动帮你做
return new SimpleAuthenticationInfo(user,user.getPwd(),"");
//SimpleAuthenticationInfo 是 接口AuthenticationInfo 的实现类
}
}
Swagger
学习目标:
- 了解Swagger的概念及作用
- 在SpringBoot中集成Swagger自动生成API文档
Swagger简介
前后端分离时代
-
前端:前端控制层、视图层
- 可以伪造后端数据,JSON。即不需要后端,前端工程依旧能跑起来
-
后端:后端控制层、服务层、数据访问层
-
前后端通过API接口进行交互
-
前后端相对独立,且松耦合
-
前后端甚至可以部署在不同的服务器上
产生的问题
- 前后端集成联调的时候,前端人员或者后端人员无法做到“及时协商,尽早解决”,最终导致问题集中爆发
解决方案
-
首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
-
早些年:制定word计划文档
-
前后端分离时代:
- 前端测试后端接口:postman
- 后端提供接口,需要实时更新最新的消息及改动!
- 这时候swagger应运而生!
swagger
-
swagger号称世界上最流行的API框架
-
Restful Api 文档在线自动生成器,即API文档与API定义同步更新
-
可以直接运行,在线测试API接口(其实就是controller requsetmapping)
-
支持多种语言(如:Java,PHP等)
-
官网:https://swagger.io/
在项目中使用Swagger需要这两个jar包
- springfox-swagger2
- springfox-swagger-ui
SpringBoot集成Swagger
新建一个SpringBoot项目,导入springweb依赖
导入依赖
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
配置Swagger
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//什么都没有配置
//它有默认值
}
测试运行,如果启动报错,可能是版本问题。把spring-boot的版本改为 2.5.7 试一下!
发起请求:http://localhost:8080/swagger-ui.html
配置Swagger
Swagger的bean实例 Docket
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo());
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
运行效果
Swagger配置扫描接口
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//RequestHandlerSelectors 配置要扫描接口的方式
//RequestHandlerSelectors.any() 扫描全部
//RequestHandlerSelectors.none() 都不扫描
//RequestHandlerSelectors.withMethodAnnotation() 扫描方法上的注解
//basePackage() 指定要扫描的包
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//paths() 过滤什么路径。只扫描什么路径
.paths(PathSelectors.ant("/kuang/**"))
.build();
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
配置是否启动Swagger
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
//enable() 是否启动Swagger,如果为false则不启动,不能在浏览器中访问Swagger
.enable(false)
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//.paths(PathSelectors.ant("/kuang/**"))
.build();
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
如果我只希望我的Swagger在生产环境中使用,在发布的时候不使用,怎么办?
思路:
- 判断是不是生产环境
- 注入enable()
application.properties
# 激活 application-dev.properties
spring.profiles.active=dev
application-dev.properties
server.port=8081
application-pro.properties
server.port=8082
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
//environment.acceptsProfiles() 判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
System.out.println(flag);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.enable(flag) //是否启动Swagger,如果为false则不启动,不能在浏览器中访问Swagger
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//.paths(PathSelectors.ant("/kuang/**"))
.build();
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
配置API文档的分组
groupName() 配置分组
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
//environment.acceptsProfiles() 判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
System.out.println(flag);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("狂神")//配置分组
.enable(flag) //是否启动Swagger,如果为false则不启动,不能在浏览器中访问Swagger
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//.paths(PathSelectors.ant("/kuang/**"))
.build();
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
如何配置多个分组?
SwaggerConfig.java
package com.kuang.swagger.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.core.env.Profiles;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.ArrayList;
@Configuration
@EnableSwagger2 //开启Swagger2
public class SwaggerConfig {
@Bean
public Docket docket1(){
return new Docket(DocumentationType.SWAGGER_2).groupName("A");
}
@Bean
public Docket docket2(){
return new Docket(DocumentationType.SWAGGER_2).groupName("B");
}
@Bean
public Docket docket3(){
return new Docket(DocumentationType.SWAGGER_2).groupName("C");
}
//配置了Swagger的Docket的bean实例
@Bean
public Docket docket(Environment environment){
//设置要显示的Swagger环境
Profiles profiles = Profiles.of("dev","test");
//environment.acceptsProfiles() 判断是否处在自己设定的环境当中
boolean flag = environment.acceptsProfiles(profiles);
System.out.println(flag);
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.groupName("狂神")//配置分组
.enable(flag) //是否启动Swagger,如果为false则不启动,不能在浏览器中访问Swagger
.select()
.apis(RequestHandlerSelectors.basePackage("com.kuang.swagger.controller"))
//.paths(PathSelectors.ant("/kuang/**"))
.build();
}
//配置Swagger信息 apiInfo
private ApiInfo apiInfo(){
//作者信息
Contact DEFAULT_CONTACT = new Contact("秦疆", "https://www.baidu.com", "24736743@qq.com");
//ApiInfo这个类没有set方法,只能通过构造器
return new ApiInfo(
"狂神的SwaggerAPI文档",
"即使再小的帆也能远航",
"v1.0",
"https://www.baidu.com",
DEFAULT_CONTACT,
"Apache 2.0",
"http://www.apache.org/licenses/LICENSE-2.0",
new ArrayList()
);
}
}
Swagger实体类配置
Swagger实体类配置:说白了就是给API文档添加注释,方便更好地看懂API文档
User.java
package com.kuang.swagger.pojo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
//Api("注释") 和 @ApiModel("注释") 是一样的
@ApiModel("用户实体类") //给实体类加一个文档注释
public class User {
@ApiModelProperty("用户名") //这个也是加注释
public String username;
@ApiModelProperty("密码")
public String password;
}
HelloController.java
package com.kuang.swagger.controller;
import com.kuang.swagger.pojo.User;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello!";
}
//只要我们的接口中,返回值中存在实体类,它就会被扫描到Swagger中
@PostMapping("/user")
public User user(){
return new User();
}
@GetMapping("/hello2")
//@ApiOperation() 给接口(controller相当于前端和后端之间的接口)加注释
@ApiOperation("Hello控制类")
public String hello2(@ApiParam("用户名") String username){
return "hello"+username;
}
}
运行效果
总结
- 我们可以通过Swagger给一些比较难理解的属性或者接口,增加注释信息
- 接口文档实时更新
- 可以在线测试
- 前端人员或者后端人员能够做到及时协商,尽早解决问题!
- Swagger是一个优秀的工具,几乎所有大公司都有使用它
注意点:
- 在正式发布的时候,关闭Swagger。不能让用户看见我们的接口。同时能节省运行内存
任务
异步任务
定时任务
邮件发送(这也是一个任务)
异步任务
新建一个SpringBoot项目,添加springweb依赖
AsyncService.java
package com.kuang.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncService {
@Async //告诉Spring这是一个异步方法
public void hello(){
try {
//休眠3秒
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("数据正在处理...");
}
}
AsyncController.java
package com.kuang.controller;
import com.kuang.service.AsyncService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@RequestMapping("/hello")
public String hello(){
asyncService.hello();//停止三秒,停止的时候网站会等待
return "OK";
}
}
Springboot09TestApplication.java
package com.kuang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@EnableAsync //开启异步功能的注解
@SpringBootApplication
public class Springboot09TestApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09TestApplication.class, args);
}
}
邮件任务
导入依赖
<!--javax.mail-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
swootxnbmhunbjfj
application.properties
spring.mail.username=398156587@qq.com
# 我的授权码
spring.mail.password=swootxnbmhunbjfj
# 发送的服务器
spring.mail.host=smtp.qq.com
# 开启加密验证,这是qq邮箱需要的
spring.mail.properties.mail.smtp.ssl.enable=true
Springboot09TestApplication.java
package com.kuang;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSenderImpl;
import org.springframework.mail.javamail.MimeMessageHelper;
import javax.annotation.Resource;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
@SpringBootTest
class Springboot09TestApplicationTests {
@Resource
JavaMailSenderImpl mailSender;
@Test
void contextLoads() {
//发送一封简单邮件
SimpleMailMessage simpleMailMessage = new SimpleMailMessage();
simpleMailMessage.setSubject("小狂神你好呀");//邮件的主题
simpleMailMessage.setText("谢谢你的狂神说JAVA系列课程");//邮件的内容
simpleMailMessage.setTo("qq398156587@163.com");//发送给谁
simpleMailMessage.setFrom("398156587@qq.com");//从什么邮箱发的
mailSender.send(simpleMailMessage);
}
@Test
void contextLoads2() throws MessagingException {
//发送一封复杂邮件
MimeMessage mimeMessage = mailSender.createMimeMessage();
//组装
MimeMessageHelper help = new MimeMessageHelper(mimeMessage,true);
//正文
help.setSubject("小狂神你好呀~plus");
help.setText("<p style='color:red'>谢谢你的狂神说JAVA系列课程</p>",true);//true,开启支持解析html
//附件
help.addAttachment("39.jpg",new File("C:\\Users\\CaoGuoWei\\Desktop\\Temp\\新建文件夹\\39.jpg"));
help.setTo("qq398156587@163.com");//发送给谁
help.setFrom("398156587@qq.com");//从什么邮箱发的
mailSender.send(mimeMessage);
}
}
定时任务
我们会用到两个核心的接口类
- TaskScheduler 任务调度程序
- TaskExecutor 任务执行者
Springboot09TestApplication.java
package com.kuang;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@EnableAsync //开启异步功能的注解
@EnableScheduling //开启定时功能的注解
@SpringBootApplication
public class Springboot09TestApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot09TestApplication.class, args);
}
}
ScheduledService.java
package com.kuang.service;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class ScheduledService {
//在一个特定的时间执行这个方法
//cron表达式
//秒 分 时 日 月 星期(0-7:0和7都代表周日;?代表不管星期几)
//cron = "0 57 19 * * ?" 这个代表每天的 19:57:00 执行
//cron = "0 0/5 10,18 * * ?" 这个代表每天的10点和18点,每隔5分钟执行一次
//cron = "0 15 10 ? * 1-6" 这个代表每个月的周一到周六的 10:15 执行
@Scheduled(cron = "0 57 19 * * ?")
public void hello(){
System.out.println("hello,你被执行了");
}
}
SpringBoot集成Redis
去看狂神的Redis教学视频
自定义RedisTemplate
去看狂神的Redis教学视频
分布式系统理论
分布式理论
- 在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
- 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
分布式系统(distributed system)是建立在网络之上的软件系统。
首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。(所以分布式不应该在一开始设计系统时就考虑到)因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。。。
- Dubbo(开源分布式服务框架)
随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
在Dubbo的官网文档有这样一张图
单一应用架构
-
当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。
-
适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
-
缺点
- 性能扩展比较难
- 协同开发问题
- 不利于升级维护
垂直应用架构
- 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。
- 通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性
- 缺点
- 公用模块无法重复利用,开发性的浪费
分布式服务架构
- 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键
流动计算架构
- 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
RPC
什么是RPC
- RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
也就是说有两台服务器A、B。一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。
为什么要用RPC呢
就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调用远程函数;
说白了就是不同于调用本地的而是调用远程的资源和方法
RPC原理
基本步骤
RPC的两个核心模块:通讯、序列化
序列化:数据传输需要转换
这时候我们就需要用到Dubbo,通过它来简化我们的操作
- Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
Dubbo
什么是Dubbo
Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
官网:Apache Dubbo
-
服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
-
服务消费者(Consumer):调用远程服务的服务消费者,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
-
注册中心(Registry):注册中心返回服务提供者地址列表给服务消费者,如果有变更,注册中心将基于长连接推送变更数据给服务消费者
-
监控中心(Monitor):服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给服务消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从服务提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
Zookeeper是什么?
官方文档上这么解释zookeeper,它是一个分布式服务框架,是Apache Hadoop的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。
Zookeeper 相当于上图的 Registry(注册中心)
上面的解释有点抽象,简单来说 zookeeper = 文件系统 + 监听通知机制
文件系统:Zookeeper维护一个类似文件系统的数据结构
每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。
有四种类型的znode:
-
PERSISTENT-持久化目录节点
客户端与zookeeper断开连接后,该节点依旧存在 -
PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号 -
EPHEMERAL-临时目录节点
客户端与zookeeper断开连接后,该节点被删除 -
EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号
监听通知机制
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。
就这么简单,下面我们看看Zookeeper能做点什么呢?
zookeeper功能非常强大,可以实现诸如分布式应用配置管理、统一命名服务、状态同步服务、集群管理等功能,我们这里拿比较简单的分布式应用配置管理为例来说明。
假设我们的程序是分布式部署在多台机器上,如果我们要改变程序的配置文件,需要逐台机器去修改,非常麻烦,现在把这些配置全部放到zookeeper上去,保存在 zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 zookeeper 的通知,然后从 zookeeper 获取新的配置信息应用到系统中。
window下载安装zookeeper
Index of /dist/zookeeper/zookeeper-3.4.14 (apache.org)
选择这个下载:zookeeper-3.4.14.tar.gz
运行 /bin/zkServer.cmd
初次运行会遇到闪退问题,会报错,原因是没有 zoo.cfg 配置文件
解决办法:在conf目录下拷贝一份zoo_sample.cfg,改名为zoo.cfg即可
我们查看一下zoo.cfg
先运行运行 zkServer.cmd 再运行 zkCli.cmd 测试一下
zkCli.cmd
window下载安装dubbo-admin
dubbo本身并不是一个服务软件。它其实就是一个jar包,能够帮你的java程序连接到zookeeper,并利用zookeeper消费、提供服务。
但是为了让用户更好的管理监控众多的dubbo服务,官方提供了一个可视化的监控程序dubbo-admin,不过这个监控即使不装也不影响使用。
1、下载dubbo-admin
地址 :https://github.com/apache/dubbo-admin/tree/master
2、解压进入目录
查看 dubbo-admin\src\main\resources\application.properties 指定zookeeper地址
3、在项目目录下打包dubbo-admin
管理员身份进入cmd
mvn clean package -Dmaven.test.skip=true
第一次打包的过程有点慢,需要耐心等待!直到成功!
打包完成后,在 dubbo-admin\target 目录下有一个jar包,可以通过cmd执行,必须先保证zookeeper的服务先打开!
我们先打开 zkServer.cmd
然后使用cmd运行jar包
java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
执行完毕,我们去访问一下 http://localhost:7001/ , 这时候我们需要输入登录账户和密码,账户和密码默认都是root
登录成功后,查看界面
dubbo-admin(这个可以不要):是一个监控管理后台,它可以查看我们注册了哪些服务,哪些服务被消费了
zookeeper(必须要):是一个注册中心
Dubbo(必须要):是一个jar包
整合Dubbo、Zookeeper、SpringBoot
创建一个空项目,配置好项目结构
新建一个Module,选择SpringBoot,添加springweb依赖。命名为 provider-server (相当于服务提供者)
TicketService.java
package com.kuang.service;
public interface TicketService {
public String getTicket();
}
TicketServiceImpl.java
package com.kuang.service;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;
//注意!!!这个@Service是dubbo中的,而不是spring中的
@Service//加了这个注解,就可以被扫描到,在项目一启动的时候就自动注册到注册中心,我们这里用的注册中心是zookeeper
@Component//使用了Dubbo后尽量不要用spring的那个@Service注解了,我们用万能的@Component
public class TicketServiceImpl implements TicketService{
@Override
public String getTicket() {
return "《狂神说Java》";
}
}
application.properties
server.port=8001
# 服务应用的名字
dubbo.application.name=provider-server
# 注册中心的地址,127.0.0.1 相当于 localhost
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 哪些服务要被注册
dubbo.scan.base-packages=com.kuang.service
添加依赖
<!--导入依赖:Dubbo + zookeeper-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--zookeeper的日志可能会和SpringBoot的日志有冲突,我们要排除日志冲突-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
运行测试一下:
-
先启动 zkServer.cmd
-
再去cmd中启动 dubbo-admin-0.0.1-SNAPSHOT.jar
-
最后启动 module中的主程序入口 ProviderServerApplication
-
运行效果如下,说明服务已经注册到注册中心了
新建一个Module,选择SpringBoot,添加springweb依赖。命名为 consumer-server (相当于服务消费者)
UserService.java
package com.kuang.service;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
@Service //注意:这个是spring中的@Service,作用就是放到spring容器中
public class UserService {
//想拿到provider-server提供的票,要去注册中心拿到服务
@Reference //引用。有两种办法:真实开发会通过Pom坐标引用;我们自己也可以定义路径相同的接口名简单测试一下
TicketService ticketService;
public void buyTicket(){
String ticket = ticketService.getTicket();
System.out.println("在注册中心拿到了=>" + ticket);
}
}
TicketService.java
package com.kuang.service;
public interface TicketService {
public String getTicket();
}
application.properties
server.port=8002
# 消费者去注册中心拿服务的时候需要暴露自己的名字
dubbo.application.name=consumer-server
# 注册中心的地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
添加依赖,和上面导的依赖一模一样
<!--导入依赖:Dubbo + zookeeper-->
<!-- Dubbo Spring Boot Starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.9</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--zookeeper的日志可能会和SpringBoot的日志有冲突,我们要排除日志冲突-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>2.12.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
<!--排除这个slf4j-log4j12-->
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
</exclusions>
</dependency>
ConsumerServerApplicationTests.java
package com.kuang;
import com.kuang.service.UserService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
void contextLoads() {
userService.buyTicket();
}
}
运行测试一下:
-
先启动 zkServer.cmd
-
再去cmd中启动 dubbo-admin-0.0.1-SNAPSHOT.jar
-
接着启动 module 中的 provider-server(服务提供者)的主程序入口 ProviderServerApplication;确保服务注册到注册中心
-
上面三个步骤是上一章节的步骤
-
最后我们测试一下,启动 module 中的 consumer-server (相当于服务消费者)的测试类 ConsumerServerApplicationTests;看一下运行效果!
总结:提供者提供服务,消费者消费服务!!!