开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

jsp技术被淘汰了?那还要不要学它?

发表于 2020-07-19

今天是刘小爱自学Java的第92天。

感谢你的观看,谢谢你。

话不多说,开始今天的学习:


首先声明:jsp这个技术基本被淘汰了,不太重要,用到它的可能性很低,但是有些企业还是会用到的。

花一天时间对其做一个简单的了解。

一、jsp概述

1jsp出现的原因

jsp这个技术的出现是为了解决一个什么问题?

先看如下情况:


在Java代码中,服务器要响应一个HTML页面给浏览器,需要将标签拼接在代码中。

这样一顿操作下来就会显得十分地繁琐,操作麻烦不说,阅读性还差。

那有没有什么方法解决这个问题呢?

jsp技术就应运而生了,在jsp文件中,既能写Java代码,又能写HTML代码,特别地厉害。

2jsp定义

JSP全名为Java Server Pages,翻译为java服务器页面,其本质是一个简化的Servlet。

它是由Sun公司倡导、许多公司参与一起建立的一种动态网页技术标准。

大白话就是一个既能书写Java代码又能书写HTML代码的文件。


3jsp为什么被淘汰了?

jsp被淘汰本身并不是因为技术落后的原因,它之所以被淘汰是因为行业趋势。

现在强调前后端分离,前端写前端的代码,后端写后端的代码,没有必要将前端和后端代码融合在一起,所以jsp使用就受限了。

当然也并不是完全就没人用了,只是用的少了。

二、jsp语法

1在jsp中书写代码


①注释格式

在jsp中的注释格式为:<%–注释–%>

②Java代码编写

格式为:<%Java代码%>,在该格式里面就能编写Java代码。

这样编写以后,在浏览器上输入对应的路径,就能发现能用Java语法在浏览器上输入内容了。

以上也就完成了在jsp文件中写Java代码了。

2jsp执行流程

jsp文件为何可以写Java代码,其底层是怎么样的一个执行流程呢?画图讲解:


①浏览器访问demo01.jsp

根据对应的路径来访问jsp文件,该jsp文件会被转化成两个文件:

demo01_jsp.java和demo01_jsp.class。

这两个文件对于Java开发者来说简直不要太熟悉了:一个是Java源码文件,一个是其对应的字节码文件。

也就是说jsp文件其实底层被转换成了Java文件,再执行的Java代码。

②关于转换后的Java源码

打开对应的Java源码文件,当然上图中我只截图了一部分做一个说明,其实源码远不止这么点。

从截图中的部分代码可以看出:

  • <%%>中的代码被直接解析成java代码。
  • html部分都被out.write(“”)方法以字符串的形式拼接,然后响应给浏览器。

绕来绕去其实还是拼接,和最先开始的方法一样,只不过说jsp中拼接被封装了,不用我们写。

3三种书写Java代码的方式


①脚本声明

格式:<%! 书写Java代码 %>

中间有一个感叹号,这里面也是可以编写Java代码的。

查看其对应的Java源码文件,会发现这块代码对应于源码中的成员变量和成员方法

②脚本片段

格式:<%书写Java代码 %>

它比①就少了一个感叹号,其对应的是源码中_jspService方法的Java代码。

在Java中,方法里面是不能定义一个新的方法的,所以方法定义只能用①的格式来编写。

③脚本表达式

格式:<%=”表达式” %>

其对应的Java代码就是out.print()。

三、EL表达式

EL表达式就是专门来取代上面三种格式中③脚本表达式的。

格式为:${str}。其就相当于<%=str%>,其中str为一个变量。

1四大域对象

根据其范围从小到大排列:

  • page域:只能在当前页面有效。
  • request域:只在一次请求或请求域中有效。
  • session域:一次会话(一次或多次请求和响应)过程中有效。
  • application域:整个项目过程中都有效。

2从四大域对象中取值


①设定域对象的值

使用的方法都是setAttribute()方法,参数以键值对的方式存值,

②普通方式取值

以page域对象为例,其格式为:

${pageScope.pageKey}

pageKey为page域对象对应的key,使用这种方式就能取出域对象中的值了。

③简写方式取值

以page域对象为例,格式为:${pageKey}

将pageScope简化了,直接就是一个key。

但是这样就会有一个问题,key可能会重复。

毕竟key是人为命名的,page域对象中的key和request域对象中的key可能一样。

如果key重复了,会按照从小到大逐级查找。

3从Cookie中取值


①保存Cookie到浏览器

在LoginServlet中保存两个Cookie到浏览器:

  • usernameCookie:名为username,值为请求中的数据,即登录界面输入的用户名。
  • passwordCookie:名为password,值为请求中的数据,也就是登录界面输入的密码。

②取出Cookie对应的值

以usernameCookie为例,这个Cookie中的key为usename,根据key来取值。

格式为:${cookie.username.value}

这样做有什么好处?做一个测试:


在第一次登录输入用户名和密码后,其数据会被保存在cookie中,从而被页面读取到。

再次刷新时,用户名和密码会自动出现,就不用再次输入用户名和密码了。

最后

谢谢你的观看。

如果可以的话,麻烦帮忙点个赞,谢谢你。

本文使用 mdnice 排版

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

ArrayList常见面试点

发表于 2020-07-18

ArrayList是Java程序员最常用的数据结构这句话说的一点都不过分,平日开发中拿来接受参数,包装数据使用非常频繁,但我们,因为它使用太简单,以至于我们好像并不是很在意ArrayList的底层实现,今天我们就来看看ArrayList的源码,以常见的面试套路来剖析它的底层原理。

面试官:你平时用ArrayList会是在那些场景,为什么用它?

我:我们一般开发时一般在接受集合类型的数据时用到,比如前端的参数,Dao层的返回值,以及业务处理集合类型的数据时用它来承载数据。因为它的特点是有序而且查询速度快,所以用它的频率很高。

  • 那你知道为什么它的查询效率为什么这么快吗?

我:因为ArrayList底层采用的是动态数组实现,我们可以通过数组索引下标定位元素所在的位置

  • 恩,那你说说JDK1.8中ArrayList的数据结构,以及它的添加元素的过程吧

我:JDK1.8中ArrayList提供了三个构造函数,无参构造默认是指向空数组,带参构造可以设置指定容量的数组,初始化时就新建一个指定容量的数组。添加元素时先检查element数组容量,如果容量不足就会扩容,如果容量充足就在数组末尾添加元素,然后集合size++。

  • 如果我创建ArrayList时,给定容量是20,那初始化以后它的容量是20吗?

我:额,是的,如果构造时指定了初始容量那么初始化时它的数组长度就是20,只不过它的size为0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    // 带参构造,自定义容量
public ArrayList(int initialCapacity) {
// 如果指定容量大于0,那么就初始化数组,数组长度就是指定的容量大小
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 如果指定容量为0,那么数组默认引用空数组对象,否则抛出异常
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}

/**
* Constructs an empty list with an initial capacity of ten.
*/
// 无参构造,默认引用空数组
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

当然还有一个带参构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码    //构造传入集合
public ArrayList(Collection<? extends E> c) {
// 将集合迭代为数组输出
elementData = c.toArray();
// 如果容量为空则引用空数组对象
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
// 数组类型不匹配的情况发生在toArray()中,实际元素量大于预期量时,迭代产生新的数组
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
  • 那你讲讲ArrayList是怎么扩容的吧

首先它会计算出数组需要的最小容量,然后调用grow(int minCapacity)方法进行扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码    // 计算出最小需要的容量大小
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组还处于空数组阶段,那么判断size+1的值也就是minCapacity和默认初始容量10的大小,取最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
// 添加元素的时候会调用此方法进行最小容量的计算
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}

private void ensureExplicitCapacity(int minCapacity) {
// 集合的变动次数
modCount++;

//如果当前数组的长度不足以容纳最小容量的元素,那么就扩容
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

我们进入grow(int minCapacity)方法看看,原来此方法就是ArrayList扩容的核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码    // 扩容机制,传入当前需要的最小容量大小
private void grow(int minCapacity) {
// overflow-conscious code
// 扩容前,数组的长度
int oldCapacity = elementData.length;
// 新的数组长度 = 旧的数组长度 + 旧数组长度/2 = oldCapacity*1.5 (新数组长度为旧数组长度1.5倍) 赋值为int时向下取整
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 如果新数组长度小于最小容量大小,则新数组长度=最小容量大小
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果新数组长度大于int范围,则返回int最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 复制一个新数组,指向原数组,完成扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}
  • 不光是ArrayList,其它集合也一样,为什么在add或者remove方法中有一个modCount,它是拿来干什么的

我:modCount是ArrayList的抽象父类AbstractList中的一个变量,用来记录集合机构被修改的次数。源码文档中的解释是它用来在迭代遍历集合的时候判断集合的修改状态,如果在遍历过程中发现modCount发生了改变,就会抛出ConcurrentModificationExceptions

  • 恩,不错。那你再讲讲ArrayList的删除元素吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    // 删除指定位置的元素
public E remove(int index) {
// 检查index是否越界
rangeCheck(index);

// 集合的变更次数
modCount++;
// 获取并返回此位置的老数据
E oldValue = elementData(index);

// 元素要移动的距离,如果numMoved>0标识删除的是集合内部的元素,numMoved=0标识删除的是集合末尾元素,就不用移动
int numMoved = size - index - 1;
if (numMoved > 0)
// 将index后续的元素复制到数组对象上
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 将数组末尾置为null
elementData[--size] = null; // clear to let GC do its work

return oldValue;
}

其实和add(int,E)方法原理类似,根据复制一个新数组,add是将新数组追加到index后面,然后指向原数组完成添加。remove是新数组追加到index之前,然后将数组末尾置为null。

  • 好,那你知道为什么集合类实现了Serializable接口,自己却还要重新定义序列化方法?

我:完了,回去等通知吧,当场领盒饭。

不单单是ArrayList是将容纳数据的element数组用transient关键字修饰,其它很多集合都一样。transient修饰的变量语义为序列化时忽略,那么集合类为什么要这样做呢?网上有很多说法,有说虚拟机版本和平台的问题,也有说容量浪费的问题。因为集合类都有扩容机制,而且每次扩容以后容量相比以前要大很多,而一般情况下容量是撑不满的,也意味着有大量的内存空间被浪费,而序列化手段是将程序对象转换为可转移的二进制文件或数据,让然体积越小越好。

补充:ArrayList的克隆是浅克隆,是复制了原来的数组

clear()方法并不是将element数组置为null,而是将数组中的元素依次置为null

1
2
3
4
5
6
7
8
9
10
复制代码    // 清空元素,并没有将数组置为null,而是将数组内每个元素置为null
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

RandomAccess接口是个空接口,语义为支持随机查找,推荐使用for循环遍历效率高于迭代器

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何保证缓存与数据库双写时的数据一致性? 如何保证缓存与数据

发表于 2020-07-17

如何保证缓存与数据库双写时的数据一致性?

在做系统优化时,想到了将数据进行分级存储的思路。因为在系统中会存在一些数据,有些数据的实时性要求不高,比如一些配置信息。基本上配置了很久才会变一次。而有一些数据实时性要求非常高,比如订单和流水的数据。所以这里根据数据要求实时性不同将数据分为三级。

  • 第1级:订单数据和支付流水数据;这两块数据对实时性和精确性要求很高,所以不添加任何缓存,读写操作将直接操作数据库。
  • 第2级:用户相关数据;这些数据和用户相关,具有读多写少的特征,所以我们使用redis进行缓存。
  • 第3级:支付配置信息;这些数据和用户无关,具有数据量小,频繁读,几乎不修改的特征,所以我们使用本地内存进行缓存。

但是只要使用到缓存,无论是本地内存做缓存还是使用 redis 做缓存,那么就会存在数据同步的问题,因为配置信息缓存在内存中,而内存时无法感知到数据在数据库的修改。这样就会造成数据库中的数据与缓存中数据不一致的问题。接下来就讨论一下关于保证缓存和数据库双写时的数据一致性。

解决方案

那么我们这里列出来所有策略,并且讨论他们优劣性。

  1. 先更新数据库,后更新缓存
  2. 先更新数据库,后删除缓存
  3. 先更新缓存,后更新数据库
  4. 先删除缓存,后更新数据库

先更新数据库,后更新缓存

这种场景一般是没有人使用的,主要原因是在更新缓存那一步,为什么呢?因为有的业务需求缓存中存在的值并不是直接从数据库中查出来的,有的是需要经过一系列计算来的缓存值,那么这时候后你要更新缓存的话其实代价是很高的。如果此时有大量的对数据库进行写数据的请求,但是读请求并不多,那么此时如果每次写请求都更新一下缓存,那么性能损耗是非常大的。

举个例子比如在数据库中有一个值为 1 的值,此时我们有 10 个请求对其每次加一的操作,但是这期间并没有读操作进来,如果用了先更新数据库的办法,那么此时就会有十个请求对缓存进行更新,会有大量的冷数据产生,如果我们不更新缓存而是删除缓存,那么在有读请求来的时候那么就会只更新缓存一次。

先更新缓存,后更新数据库

这一种情况应该不需要我们考虑了吧,和第一种情况是一样的。

先删除缓存,后更新数据库

该方案也会出问题,具体出现的原因如下。

先删除缓存,后更新数据库

先删除缓存,后更新数据库

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  1. 请求 A 会先删除 Redis 中的数据,然后去数据库进行更新操作
  2. 此时请求 B 看到 Redis 中的数据时空的,会去数据库中查询该值,补录到 Redis 中
  3. 但是此时请求 A 并没有更新成功,或者事务还未提交

那么这时候就会产生数据库和 Redis 数据不一致的问题。如何解决呢?其实最简单的解决办法就是延时双删的策略。

延时双删

延时双删

但是上述的保证事务提交完以后再进行删除缓存还有一个问题,就是如果你使用的是 Mysql 的读写分离的架构的话,那么其实主从同步之间也会有时间差。

主从同步时间差

主从同步时间差

此时来了两个请求,请求 A(更新操作) 和请求 B(查询操作)

  1. 请求 A 更新操作,删除了 Redis
  2. 请求主库进行更新操作,主库与从库进行同步数据的操作
  3. 请 B 查询操作,发现 Redis 中没有数据
  4. 去从库中拿去数据
  5. 此时同步数据还未完成,拿到的数据是旧数据

此时的解决办法就是如果是对 Redis 进行填充数据的查询数据库操作,那么就强制将其指向主库进行查询。

从主库中拿数据

从主库中拿数据

先更新数据库,后删除缓存

问题:这一种情况也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。

先更新数据库,后删除缓存

先更新数据库,后删除缓存

此时解决方案就是利用消息队列进行删除的补偿。具体的业务逻辑用语言描述如下:

  1. 请求 A 先对数据库进行更新操作
  2. 在对 Redis 进行删除操作的时候发现报错,删除失败
  3. 此时将Redis 的 key 作为消息体发送到消息队列中
  4. 系统接收到消息队列发送的消息后再次对 Redis 进行删除操作

但是这个方案会有一个缺点就是会对业务代码造成大量的侵入,深深的耦合在一起,所以这时会有一个优化的方案,我们知道对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么我们可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。

利用订阅 binlog 删除缓存

利用订阅 binlog 删除缓存

总结

每种方案各有利弊,比如在第二种先删除缓存,后更新数据库这个方案我们最后讨论了要更新 Redis 的时候强制走主库查询就能解决问题,那么这样的操作会对业务代码进行大量的侵入,但是不需要增加的系统,不需要增加整体的服务的复杂度。最后一种方案我们最后讨论了利用订阅 binlog 日志进行搭建独立系统操作 Redis,这样的缺点其实就是增加了系统复杂度。其实每一次的选择都需要我们对于我们的业务进行评估来选择,没有一种技术是对于所有业务都通用的。没有最好的,只有最适合我们的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PHP+MySQL+Bootstrap 美食主题博客项目

发表于 2020-07-17

这个项目是我大三上的PHP课期末项目大作业。 作业已经交了,现在放上来给大家参考学习!

一:技术栈介绍

主题:美食博客

前端:html,js,css, bootstrap,jq

后端:php mvc

数据库:mysql

本项目美食部分接口调用地址:美食接口文档

github源码:github.com/zoyoy1203/p…

网盘源码:pan.baidu.com/s/1CEHItobT…

提取码:jh36

二:实现功能总结

  1. 登录,注册,退出登录,验证码。
  2. API接口调用:菜谱推荐,菜谱分类,菜谱分类详情,菜谱详情。
  3. 个人信息展示:头像,座右铭修改。
  4. 个人动态发布展示。
  5. 动态点赞评论功能。
  6. 动态展示,搜索,关键字标红功能。
  7. 用户列表显示,添加删除好友功能。
  8. 错误信息提示功能。

三:总体结构

该项目采用简易版mvc的结构。

由于后来我都是直接在控制层里声明使用数据表结构数据,所以后面我把Model层去掉了。只留下Controller控制层和View视图层。

其他目录结构如下图:public(静态样式文件)upload(存放上传的图片) util (里面只有一个verCode.php用来绘制验证码图片)

在这里插入图片描述

根目录下的index.php文件用来对不同url的请求进行Controller控制层下不同类和方法的调用。
本项目Controller文件下只有一个UserController类,里面包含了项目所有的处理方法。只需按照/phpProject/?a=regis (a=后接相应的调用方法) 这个格式进行请求则可。

四:作品展示

1. 登录注册功能

在这里插入图片描述

在这里插入图片描述

详细介绍:
登录注册页面都是通过form表单提交数据到action="/phpProject/?a=loginPost"
action="/phpProject/?a=regisPost"

然后在Controller文件下的UserController.php里对应的方法中进行处理。

登录注册处理过程中,如果发生错误,则会在页面上提示相应的错误原因:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

功能要求:1.用户注册:判断重名,验证码验证,至少要有用户名和密码字段。(完成!)

项目移动过程中,需要注意,util文件夹下的verCode.php中

$font = "D:/xampp/htdocs/phpProject/public/font/segoepr.ttf"; // 路径问题

需要修改为当前电脑对应的路径,不然验证码无字体显示。

验证码生成的四个字符存储在 $_SESSION["code"]中,以便于后续的判断处理。

另外,为了解决随机颜色导致验证码个别字符融入背景色的问题,登录页面的验证码设置了点击验证码图片切换字符的功能。

主要代码如下:

1
2
3
4
5
6
7
8
复制代码<img src="util/verCode.php" alt="看不清楚,换一张" onclick="javascript:newgdcode(this,this.src);" style="width: 100px;height:50px;"  alt=""/>

<script language="javascript">
function newgdcode(obj,url) {
obj.src = url+ '?nowtime=' + new Date().getTime();
//后面传递一个随机参数,否则在IE7和火狐下,不刷新图片
}
</script>

功能要求:2.用户登录:以SESSION方式。(完成!)

登录成功后,会将登录用户名,用户id,用户头像地址分别存入SESSION:

_SESSION['username']_SESSION[‘userid’] $_SESSION[‘avatar’]

在后续页面菜单栏右侧会显示登录的用户名。

首页和菜谱页面下的子页面不需要用户登录也可以显示。

其他页面如:个人中心,动态,好友列表等页面,需要用户登录才能显示。如果没有登录则跳转到登录页面。

功能要求:6.使用API接口制作一项功能,如天气、菜谱、影视、火车票查询等(完成!)

2. 首页

(调用豆果美食数据接口:接口文档)

在这里插入图片描述

在这里插入图片描述

3. 菜谱分类页面,菜谱页面,菜谱详情页面 (调用豆果美食数据接口:接口文档)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

4. 个人中心页面

功能要求: 5.查看自己的分享:已登录用户要能查看自己发布的分享。(完成!)

在这里插入图片描述

在这里插入图片描述

个人中心页面左侧显示个人信息:可以修改个人头像,座右铭;右侧显示个人发布的动态,按时间先后显示。

5. 动态页面

功能要求: 4.首页:显示所有用户发布的分享,每一条分享显示发布人、时间。(完成)

在这里插入图片描述

在这里插入图片描述

功能要求:3.内容分享:已登录用户可以发布自己的图文分享,文字不超过200中文字,支持最多3张图片上传(6分)(完成)
在这里插入图片描述

在这里插入图片描述

字体图片检测主要代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码if($_POST['text']){
if(strlen($_POST['text'])<=400){
$content = $_POST['text'];
}else{
$errinfo = '评论内容超过200中文字!';
$this -> news1($errinfo,$errinfo1);
die;
}
}

if(count($_FILES['img']['name'])>2){
$errinfo = '上传图片超过3张!';
$this -> news1($errinfo,$errinfo1);
die;
}

功能要求: 7.所有用户可以搜索分享内容:用户可以搜索分享内容,并列表显示,搜索的关键字加粗或红色。(4分)(完成)

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

搜索动态并关键字标红主要思路:
Input框输入搜索内容(搜索用户可填可不填,也可只查询相应用户动态)。点击搜索按钮,提交form表单action="/phpProject/?a=searchNew"

在 searchNew方法里连接数据库查询相应动态:

1
2
复制代码// 查询动态
$sql = "SELECT news.*,`user`.avatar,`user`.nickname FROM `user`,news WHERE `user`.id=news.user_id ORDER BY news.createtime DESC,news.id DESC";

如果有搜索内容关键字,则在查询到的每条动态内容数据上对关键字进行替换即可:

1
2
复制代码// 动态内容关键字标红
$result['content'] = str_replace($s_content,"<span style='color:red;'><b>$s_content</b></span>",$result['content']);

动态点赞评论功能的主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
复制代码// 点赞
public function addLike() {
$id = $_GET['id'];
$like = $_GET['like'];
// 根据like值判断是执行点赞语句还是取消点赞语句
if($like==1){
$sql = "DELETE FROM like_news WHERE news_id= ".$id." AND user_id=".$_SESSION['userid'];
}else if($like==0){
$sql = "INSERT INTO like_news(news_id,user_id) VALUES(".$id.",".$_SESSION['userid'].")";
}
$res = mysqli_query($this->link,$sql);

}
// 添加评论
public function addcomments() {
if(!empty($_POST)){
$newid = $_POST['newid'];
$userid = $_SESSION['userid'];
$content = $_POST['content'];

$sql = "INSERT INTO `comment`(new_id,user_id,content) VALUES(".$newid.",".$userid.",'".$content."')"; // 向评论表插入用户评论信息
$res = mysqli_query($this->link,$sql);
}

}
其中,为了点赞评论后,页面不刷新,用户体验效果好,这里使用了`AJAX`异步请求数据。(修改座右铭,添加删除好友等功能都使用了该方法)
点赞JS代码示例如下:
$(".addlike").on("click",function(){
let uid = $(this).children(".id").text();
let avatar = $(this).children(".avatar").text();
let newsid = $(this).children(".newsid").text();
let like = $(this).children(".like").text();

var that = $(this);
$.get("/phpProject/?a=addLike",{id:newsid,like:like},function(data){

if(like ==1){
// $(".uid:contains(id)").parent("avatar_img").remove();
console.log($(that.next()).find(".avatar_img>.uid:contains(uid)").text());
var dom = $(that.next()).find(".avatar_img .uid");
console.log(dom);
$.each(dom, function(key, val) {
console.log(val.innerHTML);
if(val.innerHTML == uid){
$(val).parent().remove();
}
});

$(dom).parent("avatar_img").remove();

// uidDom.parent("avatar_img").remove();
that.children(".like").text("0");
that.children(".like_text").text("点赞");
}
if(like == 0){
var html = "";
html += "<div class='avatar_img'><div class='uid'style='display: none;' >";
html += uid;
html += "</div><img style='width:30px;height:30px;' src='";
html += avatar;
html += "' ></div>";
that.next().append(html)
that.children(".like").text("1");
that.children(".like_text").text("取消点赞");
}
});
});
6. 好友列表页面,更多好友页面(具有查看所有用户,添加删除好友功能)

在这里插入图片描述

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java后端开发三年!你还不了解Spring 依赖注入,凭什

发表于 2020-07-16

前言

前两天和一个同学吃饭的时候同学跟我说了一件事,说他公司有个做了两年的人向他提出要涨薪资,他就顺口问了一个问题关于spring依赖注入的,那个要求涨薪的同学居然被问懵了。。。事后回家想了想这一块确实有点难度的就写篇文章把我自己知道的和网上整理的分享给大家,至少大家在被问到这一块的时候能答上来,不会因为这个被卡涨薪。话不多说,满满的干货都在下面了!

1.什么是Spring的依赖注入?

依赖注入,是IOC的一个方面,是个通常的概念,它有多种解释。这概念是说你不用创建对象,而只需要描述它如何被创建。你不在代码里直接组装你的组件和服务,但是要在配置文件里描述哪些组件需要哪些服务,之后一个容器(IOC容器)负责把他们组装起来。

  1. IOC的作用

降低程序间的耦合(依赖关系)
依赖关系的管理:
以后都交给spring来维护
在当前类需要用到其他类的对象,由spring为我们提供,我们只需要在配置文件中说明依赖关系的维护,就称之为依赖注入。

3.Spring依赖注入的几种方式

能注入的数据:有三类

基本类型和String。
其他bean类型(在配置文件中或者注解配置过的bean)。
复杂类型/集合类型。
注入的方式:有三种

使用构造函数提供。
使用set方法提供。
使用注解提供。

构造函数注入

顾名思义,就是使用类中的构造函数,给成员变量赋值。注意,赋值的操作不是我们自己做的,而是通过配置的方式,让 spring 框架来为我们注入。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码<!--构造函数注入:
使用的标签:constructor-arg
标签出现的位置:bean标签的内部
标签中的属性
type:用于指定要注入的数据的数据类型,该数据类型也是构造函数中某个或某些参数的类型
index:用于指定要注入的数据给构造函数中指定索引位置的参数赋值。索引的位置是从0开始
name:用于指定给构造函数中指定名称的参数赋值
================以上三个用于指定给构造函数中哪个参数赋值===================
value:用于提供基本类型和String类型的数据
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象

优势:
在获取bean对象时,注入数据是必须的操作,否则对象无法创建成功。
弊端:
改变了bean对象的实例化方式,使我们在创建对象时,如果用不到这些数据,也必须提供。
-->
<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl">
<constructor-arg name="name" value="泰斯特"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
<constructor-arg name="birthday" ref="now"></constructor-arg>
</bean>

<!-- 配置一个日期对象 -->
<bean id="now" class="java.util.Date"></bean>

Set方式注入

顾名思义,就是在类中提供需要注入成员的 set 方法。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码<!-- set方法注入 --->   更常用的方式
涉及的标签:property
出现的位置:bean标签的内部
标签的属性
name:用于指定注入时所调用的set方法名称
value:用于提供基本类型和String类型的数据
ref:用于指定其他的bean类型数据。它指的就是在spring的Ioc核心容器中出现过的bean对象
优势:
创建对象时没有明确的限制,可以直接使用默认构造函数
弊端:
如果有某个成员必须有值,则获取对象是有可能set方法没有执行。
-->
<bean id="accountService2" class="com.itheima.service.impl.AccountServiceImpl2">
<property name="name" value="tom" ></property>
<property name="age" value="23"></property>
<property name="birthday" ref="now"></property>
</bean>

集合方式注入

顾名思义,就是给类中的集合成员传值,它用的也是set方法注入的方式,只不过变量的数据类型都是集合。
我们这里介绍注入数组,List,Set,Map,Properties。

复杂类型的注入/集合类型的注入

用于给List结构集合注入的标签:
list,array,set
用于个Map结构集合注入的标签:
map,props

代码如下:

User类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码public class User {
private String name;
private Integer age;
private Date birth;

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

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

public void setBirth(Date birth) {
this.birth = birth;
}

public User(){
System.out.println("我被创建了...");
}
public void show(){
System.out.println("user中的show方法调用了。。。");
}

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

Person类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public class Person {
private String name;
private int age;

public Person() {
}

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

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

CollectionDemo类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
复制代码public class CollectionDemo {
private String[] arr;
private List<String> myList;
private Set<String> mySet;
private Map<String,String> myMap;
private Properties myProp;

public void setArr(String[] arr) {
this.arr = arr;
}

public void setMyList(List<String> myList) {
this.myList = myList;
}

public void setMySet(Set<String> mySet) {
this.mySet = mySet;
}

public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}

public void setMyProp(Properties myProp) {
this.myProp = myProp;
}

public String[] getArr() {
return arr;
}

public List<String> getMyList() {
return myList;
}

public Set<String> getMySet() {
return mySet;
}

public Map<String, String> getMyMap() {
return myMap;
}

public Properties getMyProp() {
return myProp;
}
}

配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
复制代码<!-- 基于xml形式装配bean -->
<bean id="user" class="com.atguigu.java1.User"></bean>

<!--使用get方法创建bean-->
<bean id="user2" class="com.atguigu.java1.User">
<property name="name" value="张"></property>
<property name="age">
<value>20</value>
</property>
<property name="birth" ref="now"></property>
</bean>
<bean id="now" class="java.util.Date"></bean>

<!--集合和数组类型的依赖注入-->
<bean id="demo" class="com.atguigu.java1.CollectionDemo">
<property name="arr">
<array>
<value>111</value>
<value>222</value>
<value>333</value>
</array>
</property>
<property name="myList">
<list>
<value>111</value>
<value>222</value>
<value>333</value>
</list>
</property>
<property name="mySet">
<set>
<value>111</value>
<value>222</value>
<value>333</value>
</set>
</property>
<property name="myMap">
<map>
<entry key="aaa" value="aaa"></entry>
<entry key="bbb" value="bbb"></entry>
<entry key="ccc" value="ccc"></entry>
</map>
</property>
<property name="myProp">
<props>
<prop key="aaa">aaa</prop>
<prop key="bbb">bbb</prop>
<prop key="ccc">ccc</prop>
</props>
</property>
</bean>

<!--使用默认构造器创建bean-->
<bean id="person" class="com.atguigu.java1.Person">
<constructor-arg name="name" value="张三丰"></constructor-arg>
<constructor-arg name="age" value="18"></constructor-arg>
</bean>

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
复制代码/**
* 测试基于xml形式的spring ioc获取对象
*/
@Test
public void test3(){
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
User user= (User) ioc.getBean("user");//在此处打断点验证对象是什么时候被创建的。
user.show();
}

/**
* 采用默认构造器的形式创建bean对象
*/
@Test
public void test(){
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
Person p= (Person) ioc.getBean("person");
Person p2= (Person) ioc.getBean("person");
System.out.println(p.toString());
}
/**
* 使用get方法进行依赖注入
*/
@Test
public void test4(){
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
User user= (User) ioc.getBean("user2");//在此处打断点验证对象是什么时候被创建的。
System.out.println(user.toString());
}

/**
* 集合和数组的依赖注入
*/
@Test
public void test5(){
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
CollectionDemo demo= (CollectionDemo) ioc.getBean("demo");
System.out.println(Arrays.toString(demo.getArr()));
System.out.println(demo.getMyList());
System.out.println(demo.getMySet());
System.out.println(demo.getMyMap());
System.out.println(demo.getMyProp());
}

4.使用spring的ioc实现账户的CRUD

4.1 基于xml形式

1.引用外部属性文件

2.SPEL表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
复制代码Spring Expression Language,Spring表达式语言,简称SpEL。支持运行时查询并可以操作对象图。
和JSP页面上的EL表达式、Struts2中用到的OGNL表达式一样,SpEL根据JavaBean风格的getXxx()、setXxx()方法定义的属性访问对象图,完全符合我们熟悉的操作习惯。

2.基本语法
SpEL使用#{…}作为定界符,所有在大框号中的字符都将被认为是SpEL表达式。

3.使用字面量
●整数:<property name="count" value="#{5}"/>
●小数:<property name="frequency" value="#{89.7}"/>
●科学计数法:<property name="capacity" value="#{1e4}"/>
●String类型的字面量可以使用单引号或者双引号作为字符串的定界符号
<property name=”name” value="#{'Chuck'}"/>
<property name='name' value='#{"Chuck"}'/>
●Boolean:<property name="enabled" value="#{false}"/>

4.引用其他bean
<bean id="emp04" class="com.atguigu.parent.bean.Employee">
<property name="empId" value="1003"/>
<property name="empName" value="jerry"/>
<property name="age" value="21"/>
<property name="detp" value="#{dept}"/>
</bean>

5.引用其他bean的属性值作为自己某个属性的值
<bean id="emp05" class="com.atguigu.parent.bean.Employee">
<property name="empId" value="1003"/>
<property name="empName" value="jerry"/>
<property name="age" value="21"/>
<property name="deptName" value="#{dept.deptName}"/>
</bean>

6.调用非静态方法
<!-- 创建一个对象,在SpEL表达式中调用这个对象的方法 -->
<bean id="salaryGenerator" class="com.atguigu.spel.bean.SalaryGenerator"/>

<bean id="employee" class="com.atguigu.spel.bean.Employee">
<!-- 通过对象方法的返回值为属性赋值 -->
<property name="salayOfYear" value="#{salaryGenerator.getSalaryOfYear(5000)}"/>
</bean>

7.调用静态方法
<bean id="employee" class="com.atguigu.spel.bean.Employee">
<!-- 在SpEL表达式中调用类的静态方法 -->
<property name="circle" value="#{T(java.lang.Math).PI*20}"/>
</bean>

8.运算符
①算术运算符:+、-、*、/、%、^
②字符串连接:+
③比较运算符:<、>、==、<=、>=、lt、gt、eq、le、ge
④逻辑运算符:and, or, not, |
⑤三目运算符:判断条件?判断结果为true时的取值:判断结果为false时的取值
⑥正则表达式:matches

代码如下:

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

<bean id="accountDao" class="com.atguigu.dao.impl.AccountDaoImpl">
<property name="runner" ref="runner"></property>
</bean>
<bean id="accountService" class="com.atguigu.service.impl.AccountServiceImpl">
<property name="accountDao" ref="accountDao"></property>
</bean>
<bean id="account" class="com.atguigu.domain.Account"></bean>

<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
</beans>

持久层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
复制代码/*
账户的持久层实现类
*/
public class AccountDaoImpl implements IAccountDao {

private QueryRunner runner;

public void setRunner(QueryRunner runner) {
this.runner = runner;
}

public List<Account> findAllAccount() {
try{
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public Account findAccountById(Integer accountId) {
try{
return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void saveAccount(Account account) {
try{
runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void updateAccount(Account account) {
try{
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void deleteAccount(Integer accountId) {
try{
runner.update("delete from account where id=?",accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}

业务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码/*
账户的业务层实现类
*/
public class AccountServiceImpl implements IAccountService{

private IAccountDao accountDao;

public void setAccountDao(IAccountDao accountDao) {
this.accountDao = accountDao;
}

public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}

public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}

public void saveAccount(Account account) {
accountDao.saveAccount(account);
}

public void updateAccount(Account account) {
accountDao.updateAccount(account);
}

public void deleteAccount(Integer acccountId) {
accountDao.deleteAccount(acccountId);
}
}

测试类

1
2
3
4
5
6
7
8
复制代码public class Test1 {
ApplicationContext ioc=new ClassPathXmlApplicationContext("applicationContext.xml");
@Test
public void test1(){
IAccountService service= (IAccountService) ioc.getBean("accountService");
service.deleteAccount(2);
}
}

4.2 xml和注解的混搭

XML配置形式:

1
2
3
4
复制代码<bean id="accountService" class="com.itheima.service.impl.AccountServiceImpl" scope="" 
init-method="" destroy-method="">
<property name="" value="" | ref=""></property>
</bean>

1.用于创建对象的

他们的作用就和在XML配置文件中编写一个标签实现的功能是一样的。

Component:
作用:用于把当前类对象存入spring容器中
属性:
value:用于指定bean的id。当我们不写时,它的默认值是当前类名,且首字母改小写。

Controller:一般用在表现层

Service:一般用在业务层

Repository:一般用在持久层

以上个注解他们的作用和属性与Component是一模一样。
他们是spring框架为我们提供明确的层使用的注解,使我们的层对象更加清晰。

2.用于注入数据的

他们的作用就和在xml配置文件中的bean标签中写一个标签的作用是一样的。

Autowired:
作用:自动照类型注入。只要容器中唯一的一个bean对象类型和要注入的变量类型匹配,就可以注入成功。
如果ioc容器中没任何bean的类型和要注入的变量类型匹配,则报错。
如果Ioc容器中多个类型匹配时:

出现位置:
可以是变量上,也可以是方法上。

细节:
在使用注解注入时,set方法就不是必须的了。

Qualifier:
作用:在照类中注入的基础之上再照名称注入。在给类成员注入时不能单独使用。但是在给方法参数注入时可以。

属性:
value:用于指定注入bean的id。

Resource:
作用:直接照bean的id注入。它可以独立使用。

属性:
name:用于指定bean的id。
以上注入都只能注入其他bean类型的数据,而基本类型和String类型无法使用上述注解实现。
另外,集合类型的注入只能通过XML来实现。

Value:
作用:用于注入基本类型和String类型的数据。

属性:
value:用于指定数据的值。它可以使用spring中SpEL(也就是spring的el表达式
SpEL的写法:${表达式}

3.用于改变作用范围的

他们的作用就和在bean标签中使用scope属性实现的功能是一样的。

Scope:
作用:用于指定bean的作用范围。

属性:
value:指定范围的取值。常用取值:singleton prototype

4.和生命周期相关(了解)

他们的作用就和在bean标签中使用init-method和destroy-methode的作用是一样的。

PreDestroy
作用:用于指定销毁方法。
PostConstruct
作用:用于指定初始化方法。

代码如下:

配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.2.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">

<!--设置自动扫描的包-->
<context:component-scan base-package="com.atguigu"></context:component-scan>

<bean id="runner" class="org.apache.commons.dbutils.QueryRunner" scope="prototype">
<constructor-arg name="ds" ref="dataSource"></constructor-arg>
</bean>

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="jdbc:mysql://localhost:3306/eesy"></property>
<property name="username" value="root"></property>
<property name="password" value="123456"></property>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
</bean>
</beans>

持久层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
复制代码/**
* 账户的持久层实现类
*/
@Repository(value = "accountDao")
public class AccountDaoImpl implements IAccountDao {
@Autowired
private QueryRunner runner;

public List<Account> findAllAccount() {
try{
return runner.query("select * from account",new BeanListHandler<Account>(Account.class));
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public Account findAccountById(Integer accountId) {
try{
return runner.query("select * from account where id = ? ",new BeanHandler<Account>(Account.class),accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void saveAccount(Account account) {
try{
runner.update("insert into account(name,money)values(?,?)",account.getName(),account.getMoney());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void updateAccount(Account account) {
try{
runner.update("update account set name=?,money=? where id=?",account.getName(),account.getMoney(),account.getId());
}catch (Exception e) {
throw new RuntimeException(e);
}
}

public void deleteAccount(Integer accountId) {
try{
runner.update("delete from account where id=?",accountId);
}catch (Exception e) {
throw new RuntimeException(e);
}
}
}

业务层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
复制代码/**
* 账户的业务层实现类
*/
@Service("accountService")
public class AccountServiceImpl implements IAccountService{
@Autowired
private IAccountDao accountDao;

public List<Account> findAllAccount() {
return accountDao.findAllAccount();
}

public Account findAccountById(Integer accountId) {
return accountDao.findAccountById(accountId);
}

public void saveAccount(Account account) {
accountDao.saveAccount(account);
}

public void updateAccount(Account account) {
accountDao.updateAccount(account);
}

public void deleteAccount(Integer acccountId) {
accountDao.deleteAccount(acccountId);
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
复制代码public class AccountServiceTest {
@Test
public void testFindAll() {
//1.获取容易
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.得到业务层对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
//3.执行方法
List<Account> accounts = as.findAllAccount();
for(Account account : accounts){
System.out.println(account);
}
}

@Test
public void testFindOne() {
//1.获取容易
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.得到业务层对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
//3.执行方法
Account account = as.findAccountById(1);
System.out.println(account);
}

@Test
public void testSave() {
Account account = new Account();
account.setName("test");
account.setMoney(12345f);
//1.获取容易
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.得到业务层对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
//3.执行方法
as.saveAccount(account);
}

@Test
public void testUpdate() {
//1.获取容易
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.得到业务层对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
//3.执行方法
Account account = as.findAccountById(4);
account.setMoney(23456f);
as.updateAccount(account);
}

@Test
public void testDelete() {
//1.获取容易
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.得到业务层对象
IAccountService as = ac.getBean("accountService",IAccountService.class);
//3.执行方法
as.deleteAccount(4);
}
}

4.3 纯注解配置

1.注解

该类是一个配置类,它的作用和bean.xml是一样的。

spring中的新注解:

Configuration:

作用:指定当前类是一个配置类。
细节:当配置类作为AnnotationConfigApplicationContext对象创建的参数时,该注解可以不写。

ComponentScan:

作用:用于通过注解指定spring在创建容器时要扫描的包。

属性:
value:它和basePackages的作用是一样的,都是用于指定创建容器时要扫描的包。
我们使用此注解就等同于在xml中配置了:

1
2
3
复制代码<!--告知spring在创建容器时要扫描的包,配置所需要的标签不是在beans的约束中,
而是一个名称为context名称空间和约束中-->
<context:component-scan base-package="com.itheima"></context:component-scan>

Bean:

作用:用于把当前方法的返回值作为bean对象存入spring的ioc容器中。

属性:
name:用于指定bean的id。当不写时,默认值是当前方法的名称。

细节:
当我们使用注解配置方法时,如果有方法参数,spring框架会去容器中查找没可用的bean对象。
查找的方式和Autowired注解的作用是一样的。

Import:
作用:用于导入其他的配置类。

属性:
value:用于指定其他配置类的字节码。
当我们使用Import的注解之后,Import注解的类就父配置类,而导入的都是子配置类

PropertySource:

作用:用于指定properties文件的位置。

属性:
value:指定文件的名称和路径。
关键字:classpath,表示类路径下。

2.spring整合junit4

说明:
1、应用程序的入口
main方法

2、junit单元测试中,没有main方法也能执行
junit集成了一个main方法
该方法就会判断当前测试类中哪些方法有 @Test注解
junit就让有Test注解的方法执行

3、junit不会管我们是否采用spring框架
在执行测试方法时,junit根本不知道我们是不是使用了spring框架,
所以也就不会为我们读取配置文件/配置类创建spring核心容器。

4、由以上三点可知
当测试方法执行时,没有Ioc容器,就算写了Autowired注解,也无法实现注入。

使用Junit单元测试:
Spring整合junit的配置:测试我们的配置
1、导入spring整合junit的jar(坐标)

2、使用Junit提供的一个注解把原有的main方法替换了,替换成spring提供的
@Runwith(SpringJUnit4ClassRunner.class)

3、告知spring的运行器,spring和ioc创建是基于xml还是注解的,并且说明位置
@ContextConfiguration

参数说明:

locations:指定xml文件的位置,加上classpath关键字,表示在类路径下。
classes:指定注解类所在地位置。
注意:当我们使用spring 5.x版本的时候,要求junit的jar必须是4.12及以上。

代码如下:

配置类

1
2
3
4
5
6
7
8
9
10
11
复制代码/**
* @author Guohai
* @createTime 2020-07-13 17:14
*/
@Configuration
@ComponentScan("com.atguigu")
@Import(JdbcConfig.class)
@PropertySource("classpath:c3p0.properties")
public class SpringConfig {

}

配置子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
复制代码/**
* @author Guohai
* @createTime 2020-07-13 17:16
*/
public class JdbcConfig {
@Bean(name="runner")
@Scope(value = "prototype")
public QueryRunner getRunner(@Qualifier("ds1") DataSource dataSource) {
QueryRunner runner = new QueryRunner(dataSource);
return runner;
}

private static DataSource dataSource = null;

@Bean(name="ds1")
public DataSource getDataSource() {
try {
Properties prop = new Properties();
InputStream is = JdbcConfig.class.getClassLoader().getResourceAsStream("jdbc.properties");
prop.load(is);
dataSource = DruidDataSourceFactory.createDataSource(prop);
return dataSource;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean(name="ds2")
public DataSource getDataSource2(){
try {
ComboPooledDataSource dataSource=new ComboPooledDataSource();
dataSource.setDriverClass(driver);
dataSource.setJdbcUrl(url);
dataSource.setUser(username);
dataSource.setPassword(password);
return dataSource;
} catch (PropertyVetoException e) {
e.printStackTrace();
}
return null;
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfiguration.class)
public class AccountServiceTest {

@Autowired
private IAccountService as = null;

@Test
public void testFindAll() {
//3.执行方法
List<Account> accounts = as.findAllAccount();
for(Account account : accounts){
System.out.println(account);
}
}

@Test
public void testFindOne() {
//3.执行方法
Account account = as.findAccountById(1);
System.out.println(account);
}

@Test
public void testSave() {
Account account = new Account();
account.setName("test anno");
account.setMoney(12345f);
//3.执行方法
as.saveAccount(account);
}

@Test
public void testUpdate() {
//3.执行方法
Account account = as.findAccountById(4);
account.setMoney(23456f);
as.updateAccount(account);
}

@Test
public void testDelete() {
//3.执行方法
as.deleteAccount(4);
}
}

5.Spring的自动装配

在spring中,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象,使用autowire来配置自动装载模式。

在Spring框架xml配置中共有5种自动装配:
(1)no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
(2)byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
(3)byType:通过参数的数据类型进行自动装配。
(4)constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
(5)autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。

最后

大家看完有什么不懂的可以在下方留言讨论,也可以关注我私信问我,我看到后都会回答的。也欢迎大家关注我的公众号:前程有光,金三银四跳槽面试季,整理了1000多道将近500多页pdf文档的Java面试题资料,文章都会在里面更新,整理的资料也会放在里面。谢谢你的观看,觉得文章对你有帮助的话记得关注我点个赞支持一下!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringBoot 发送邮件功能实现

发表于 2020-07-16

背景

有个小伙伴问我你以前发邮件功能怎么弄的。然后我就给他找了个demo,正好在此也写一下,分享给大家。

理清痛点

发送邮件,大家可以想一下,坑的地方在哪?
我觉得是三个吧。
第一:邮件白名单问题。
第二:邮件超时问题。
第三:邮件带附件问题。
我下面的demo都会介绍这些问题及解决。

实现方案

准备工作

我们先要准备一个可以发送的邮箱,我这里以我的163邮箱为例,现在发送邮件的规则,要求你输入一种叫做授权码的东西,注意这个东西不是密码。
获取授权码的步骤:

当选择开启,通过验证之后就可以获取到验证码。选择重置验证码也可以获取。以前17年的时候就写过一个demo,现在19年又开了一个,因为以前的忘了。

SpringBoot项目引入邮件包

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>

yml配置邮件相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
复制代码spring:
mail:
# 邮件服务地址
host: smtp.163.com
# 端口,可不写默认
port: 25
# 编码格式
default-encoding: utf-8
# 用户名
username: xxx@163.com
# 授权码,就是我们刚才准备工作获取的代码
password: xxx
# 其它参数
properties:
mail:
smtp:
# 如果是用 SSL 方式,需要配置如下属性,使用qq邮箱的话需要开启
ssl:
enable: true
required: true
# 邮件接收时间的限制,单位毫秒
timeout: 10000
# 连接时间的限制,单位毫秒
connectiontimeout: 10000
# 邮件发送时间的限制,单位毫秒
writetimeout: 10000

针对于上面提的超时问题,捕获超时异常就可解决。

邮件发送工具类

主要通过以下工具类就可以满足发送java邮件的需要。当我们进行好 yml 配置后,SpringBoot会帮助我们自动配置 JavaMailSender 我们通过这个java类就可以实现操作java来发送邮件。以下列举了几种常用的邮件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
复制代码@Service
public class MailService {
private static final Logger logger = LoggerFactory.getLogger(MailServiceImpl.class);

@Autowired
private JavaMailSender mailSender;

private static final String SENDER = "xxx@163.com";

/**
* 发送普通邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
*/
@Override
public void sendSimpleMailMessge(String to, String subject, String content) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(SENDER);
message.setTo(to);
message.setSubject(subject);
message.setText(content);
try {
mailSender.send(message);
} catch (Exception e) {
logger.error("发送简单邮件时发生异常!", e);
}
}

/**
* 发送 HTML 邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
*/
@Override
public void sendMimeMessge(String to, String subject, String content) {
MimeMessage message = mailSender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(SENDER);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
mailSender.send(message);
} catch (MessagingException e) {
logger.error("发送MimeMessge时发生异常!", e);
}
}

/**
* 发送带附件的邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
* @param filePath 附件路径
*/
@Override
public void sendMimeMessge(String to, String subject, String content, String filePath) {
MimeMessage message = mailSender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(SENDER);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = file.getFilename();
helper.addAttachment(fileName, file);

mailSender.send(message);
} catch (MessagingException e) {
logger.error("发送带附件的MimeMessge时发生异常!", e);
}
}

/**
* 发送带静态文件的邮件
*
* @param to 收件人
* @param subject 主题
* @param content 内容
* @param rscIdMap 需要替换的静态文件
*/
@Override
public void sendMimeMessge(String to, String subject, String content, Map<String, String> rscIdMap) {
MimeMessage message = mailSender.createMimeMessage();
try {
//true表示需要创建一个multipart message
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(SENDER);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);

for (Map.Entry<String, String> entry : rscIdMap.entrySet()) {
FileSystemResource file = new FileSystemResource(new File(entry.getValue()));
helper.addInline(entry.getKey(), file);
}
mailSender.send(message);
} catch (MessagingException e) {
logger.error("发送带静态文件的MimeMessge时发生异常!", e);
}
}
}

发送邮件的demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbooEmailDemoApplicationTests {

@Autowired
private MailService mailService;

private static final String TO = "xxx@qq.com";
private static final String SUBJECT = "测试邮件";
private static final String CONTENT = "test content";

/**
* 测试发送普通邮件
*/
@Test
public void sendSimpleMailMessage() {
mailService.sendSimpleMailMessge(TO, SUBJECT, CONTENT);
}

/**
* 测试发送html邮件
*/
@Test
public void sendHtmlMessage() {
String htmlStr = "<h1>Test</h1>";
mailService.sendMimeMessge(TO, SUBJECT, htmlStr);
}

/**
* 测试发送带附件的邮件
* @throws FileNotFoundException
*/
@Test
public void sendAttachmentMessage() throws FileNotFoundException {
File file = ResourceUtils.getFile("classpath:test.txt");
String filePath = file.getAbsolutePath();
mailService.sendMimeMessge(TO, SUBJECT, CONTENT, filePath);
}

/**
* 测试发送带附件的邮件
* @throws FileNotFoundException
*/
@Test
public void sendPicMessage() throws FileNotFoundException {
String htmlStr = "<html><body>测试:图片1 <br> <img src=\'cid:pic1\'/> <br>图片2 <br> <img src=\'cid:pic2\'/></body></html>";
Map<String, String> rscIdMap = new HashMap<>(2);
rscIdMap.put("pic1", ResourceUtils.getFile("classpath:pic01.jpg").getAbsolutePath());
rscIdMap.put("pic2", ResourceUtils.getFile("classpath:pic02.jpg").getAbsolutePath());
mailService.sendMimeMessge(TO, SUBJECT, htmlStr, rscIdMap);
}
}

白名单问题

如果是发送给固定邮箱,可以直接在固定邮箱里面设置白名单,如果频繁的发送给多个邮箱,最好设置以下发送时间间隔,不要不断的给某一个邮箱发送。

总结

至此SpringBoot发送邮件介绍完毕,大家可以直接使用代码来进行发送。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot 2x基础教程:EhCache缓存的

发表于 2020-07-16

上一篇我们学会了如何使用Spring Boot使用进程内缓存在加速数据访问。可能大家会问,那我们在Spring Boot中到底使用了什么缓存呢?

在Spring Boot中通过@EnableCaching注解自动化配置合适的缓存管理器(CacheManager),Spring Boot根据下面的顺序去侦测缓存提供者:

  • Generic
  • JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  • EhCache 2.x
  • Hazelcast
  • Infinispan
  • Couchbase
  • Redis
  • Caffeine
  • Simple

除了按顺序侦测外,我们也可以通过配置属性spring.cache.type来强制指定。我们也可以通过debug调试查看cacheManager对象的实例来判断当前使用了什么缓存。在上一篇中,我们也展示了如何去查看当前使用情况。

当我们不指定具体其他第三方实现的时候,Spring Boot的Cache模块会使用ConcurrentHashMap来存储。而实际生产使用的时候,因为我们可能需要更多其他特性,往往就会采用其他缓存框架,所以接下来我们会分几篇分别介绍几个常用优秀缓存的整合与使用。

使用EhCache

本篇我们将介绍如何在Spring Boot中使用EhCache进程内缓存。这里我们将沿用上一篇的案例结果来进行改造,以实现EhCache的使用。

先回顾下这个基础案例的三个部分:

User实体的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Entity
@Data
@NoArgsConstructor
public class User {

@Id
@GeneratedValue
private Long id;

private String name;
private Integer age;

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

User实体的数据访问实现(涵盖了缓存注解)

1
2
3
4
5
6
7
java复制代码@CacheConfig(cacheNames = "users")
public interface UserRepository extends JpaRepository<User, Long> {

@Cacheable
User findByName(String name);

}

测试验证用例(涵盖了CacheManager的注入,可用来观察使用的缓存管理类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter51ApplicationTests {

@Autowired
private UserRepository userRepository;

@Autowired
private CacheManager cacheManager;

@Test
public void test() throws Exception {
// 创建1条记录
userRepository.save(new User("AAA", 10));

User u1 = userRepository.findByName("AAA");
System.out.println("第一次查询:" + u1.getAge());

User u2 = userRepository.findByName("AAA");
System.out.println("第二次查询:" + u2.getAge());
}

}

接下来我们通过下面的几步操作,就可以轻松的把上面的缓存应用改成使用ehcache缓存管理。

第一步:在pom.xml中引入ehcache依赖

1
2
3
4
xml复制代码<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>

在Spring Boot的parent管理下,不需要指定具体版本,会自动采用Spring Boot中指定的版本号。

第二步:在src/main/resources目录下创建:ehcache.xml

1
2
3
4
5
6
7
8
9
xml复制代码<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">

<cache name="users"
maxEntriesLocalHeap="200"
timeToLiveSeconds="600">
</cache>

</ehcache>

完成上面的配置之后,再通过debug模式运行单元测试,观察此时CacheManager已经是EhCacheManager实例,说明EhCache开启成功了。或者在测试用例中加一句CacheManager的输出,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码@Autowired
private CacheManager cacheManager;

@Test
public void test() throws Exception {
System.out.println("CacheManager type : " + cacheManager.getClass());

userRepository.save(new User("AAA", 10));

User u1 = userRepository.findByName("AAA");
System.out.println("第一次查询:" + u1.getAge());

User u2 = userRepository.findByName("AAA");
System.out.println("第二次查询:" + u2.getAge());
}

执行测试输出可以得到:

1
2
3
4
5
6
7
8
bash复制代码CacheManager type : class org.springframework.cache.ehcache.EhCacheCacheManager
Hibernate: select next_val as id_val from hibernate_sequence for update
Hibernate: update hibernate_sequence set next_val= ? where next_val=?
Hibernate: insert into user (age, name, id) values (?, ?, ?)
2020-07-14 18:09:28.465 INFO 58538 --- [ main] o.h.h.i.QueryTranslatorFactoryInitiator : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from user user0_ where user0_.name=?
第一次查询:10
第二次查询:10

可以看到:

  1. 第一行输出的CacheManager type为org.springframework.cache.ehcache.EhCacheCacheManager,而不是上一篇中的ConcurrentHashMap了。
  2. 第二次查询的时候,没有输出SQL语句,所以是走的缓存获取

整合成功!

代码示例

本文的相关例子可以查看下面仓库中的chapter5-2目录:

  • Github:github.com/dyc87112/Sp…
  • Gitee:gitee.com/didispace/S…

如果您觉得本文不错,欢迎Star支持,您的关注是我坚持的动力!

本文首发:Spring Boot 2.x基础教程:EhCache缓存的使用,转载请注明出处。
欢迎关注我的公众号:程序猿DD,获得独家整理的学习资源和日常干货推送。
本系列教程点击直达目录

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringSecurity启动流程源码解析 掘金新人第

发表于 2020-07-16

别辜负生命,别辜负自己。

楔子

前面两期我讲了SpringSecurity认证流程和SpringSecurity鉴权流程,今天是第三期,是SpringSecurity的收尾工作,讲SpringSecurity的启动流程。

就像很多电影拍火了之后其续作往往是前作的前期故事一样,我这个第三期要讲的SpringSecurity启动流程也是不择不扣的”前期故事”,它能帮助你真正认清SpringSecurity的整体全貌。

在之前的文章里,在说到SpringSecurity中的过滤器链的时候,往往是把它作为一个概念了解的,就是我们只是知道有这么个东西,也知道它到底是干什么用的,但是我们却不知道这个过滤器链是由什么类什么时候去怎么样创建出来的。

今天这期就是要了解SpringSecurity的自动配置到底帮我们做了什么,它是如何把过滤器链给创建出来的,又是在默认配置的时候怎么加入了我们的自定义配置。

祝有好收获(边赞边看,法力无限)。

  1. 📚EnableWebSecurity

我们先来看看我们一般是如何使用SpringSecurity的。

我们用SpringSecurity的时候都会先新建一个SpringSecurity相关的配置类,用它继承WebSecurityConfigurerAdapter,然后打上注解@EnableWebSecurity,然后我们就可以通过重写
WebSecurityConfigurerAdapter里面的方法来完成我们自己的自定义配置。

就像这样:

1
2
3
4
5
6
7
8
9
复制代码@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {

}

}

我们已经知道,继承WebSecurityConfigurerAdapter是为了重写配置,那这个注解是做了什么呢?

从它的名字@EnableWebSecurity我们可以大概猜出来,它就是那个帮我们自动配置了SpringSecurity的好心人。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {

/**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

emmm,我猜大家应该有注解相关的知识吧,ok,既然你们都有注解相关的知识,我就直接讲了。

这个@EnableWebSecurity中有两个地方是比较重要的:

  • 一是@Import注解导入了三个类,这三个类中的后两个是SpringSecurity为了兼容性做的一些东西,兼容SpringMVC,兼容SpringSecurityOAuth2,我们主要看的其实是第一个类,导入这个类代表了加载了这个类里面的内容。
  • 二是@EnableGlobalAuthentication这个注解,@EnableWebSecurity大家还没搞明白呢,您这又来一个,这个注解呢,其作用也是加载了一个配置类-AuthenticationConfiguration,看它的名字大家也可应该知道它加载的类是什么相关的了吧,没错就是AuthenticationManager相关的配置类,这个我们可以以后再说。

综上所述,@EnableWebSecurity可以说是帮我们自动加载了两个配置类:WebSecurityConfiguration和AuthenticationConfiguration(@EnableGlobalAuthentication注解加载了这个配置类)。

其中WebSecurityConfiguration是帮助我们建立了过滤器链的配置类,而AuthenticationConfiguration则是为我们注入AuthenticationManager相关的配置类,我们今天主要讲的是WebSecurityConfiguration。

  1. 📖源码概览

既然讲的是WebSecurityConfiguration,我们照例先把源码给大家看看,精简了一下无关紧要的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
复制代码@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
private WebSecurity webSecurity;

private Boolean debugEnabled;

private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

private ClassLoader beanClassLoader;

@Autowired(required = false)
private ObjectPostProcessor<Object> objectObjectPostProcessor;


@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}


@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}

webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);

Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}

}

如代码所示,首先WebSecurityConfiguration是个配置类,类上面打了@Configuration注解,这个注解的作用大家还知道吧,在这里就是把这个类中所有带@Bean注解的Bean给实例化一下。

这个类里面比较重要的就两个方法:springSecurityFilterChain和setFilterChainProxySecurityConfigurer。

springSecurityFilterChain方法上打了@Bean注解,任谁也能看出来就是这个方法创建了springSecurityFilterChain,但是先别着急,我们不能先看这个方法,虽然它在上面。

  1. 📄SetFilterChainProxySecurityConfigurer

我们要先看下面的这个方法:setFilterChainProxySecurityConfigurer,为啥呢?

为啥呢?

因为它是@Autowired注解,所以它要比springSecurityFilterChain方法优先执行,从系统加载的顺序来看,我们需要先看它。

@Autowired在这里的作用是为这个方法自动注入所需要的两个参数,我们先来看看这两个参数:

  • 参数objectPostProcessor是为了创建WebSecurity实例而注入进来的,先了解一下即可。
  • 参数webSecurityConfigurers是一个List,它实际上是所有WebSecurityConfigurerAdapter的子类,那如果我们定义了自定义的配置类,其实就是把我们的配置也读取到了。

这里其实有点难懂为什么参数中SecurityConfigurer<Filter, WebSecurity>这个类型可以拿到WebSecurityConfigurerAdapter的子类?

因为WebSecurityConfigurerAdapter实现了WebSecurityConfigurer<WebSecurity>接口,而WebSecurityConfigurer<WebSecurity>又继承了SecurityConfigurer<Filter, T>,经过一层实现,一层继承关系之后,WebSecurityConfigurerAdapter终于成为了SecurityConfigurer的子类。

而参数中SecurityConfigurer<Filter, WebSecurity>中的两个泛型参数其实是起到了一个过滤的作用,仔细查看我们的WebSecurityConfigurerAdapter的实现与继承关系,你可以发现我们的WebSecurityConfigurerAdapter正好是这种类型。

ok,说完了参数,我觉得我们可以看看代码了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
复制代码@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {

// 创建一个webSecurity实例
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
}

// 根据order排序
webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE);

Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}

// 保存配置
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}

// 成员变量初始化
this.webSecurityConfigurers = webSecurityConfigurers;
}

根据我们的注释,这段代码做的事情可以分为以为几步:

  1. 创建了一个webSecurity实例,并且赋值给成员变量。
  2. 紧接着对webSecurityConfigurers通过order进行排序,order是加载顺序。
  3. 进行判断是否有相同order的配置类,如果出现将会直接报错。
  4. 保存配置,将其放入webSecurity的成员变量中。

大家可以将这些直接理解为成员变量的初始化,和加载我们的配置类配置即可,因为后面的操作都是围绕它初始化的webSecurity实例和我们加载的配置类信息来做的。

这些东西还可以拆出来一步步的来讲,但是这样的话真是一篇文章写不完,我也没有那么大的精力能够事无巨细的写出来,我只挑选这条痕迹清晰的主脉络来讲,如果大家看完能明白它的一个加载顺序其实就挺好了。

就像Spring的面试题会问SpringBean的加载顺序,SpringMVC则会问SpringMVC一个请求的运行过程一样。

全部弄得明明白白,必须要精研源码,在初期,我们只要知道它的一条主脉络,在之后的使用中,哪出了问题你可以直接去定位到可能是哪有问题,这样就已经很好了,学习是一个循环渐进的过程。

  1. 📃SpringSecurityFilterChain

初始化完变量,加载完配置,我们要开始创建过滤器链了,所以先走setFilterChainProxySecurityConfigurer是有原因的,如果我们不把我们的自定义配置加载进来,创建过滤器链的时候怎么知道哪些过滤器需要哪些过滤器不需要。

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

springSecurityFilterChain方法逻辑就很简单了,如果我们没加载自定义的配置类,它就替我们加载一个默认的配置类,然后调用这个build方法。

看到这熟悉的方法名称,你就应该知道这是建造者模式,不管它什么模式,既然调用了,我们点进去就是了。

1
2
3
4
5
6
7
复制代码public final O build() throws Exception {
if (this.building.compareAndSet(false, true)) {
this.object = doBuild();
return this.object;
}
throw new AlreadyBuiltException("This object has already been built");
}

build()方法是webSecurity的父类AbstractSecurityBuilder中的方法,这个方法又调用了doBuild()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
复制代码@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;

// 空方法
beforeInit();
// 调用init方法
init();

buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;

// 空方法
beforeConfigure();
// 调用configure方法
configure();

buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;

// 调用performBuild
O result = performBuild();

buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT;

return result;
}
}

通过我的注释可以看到beforeInit()和beforeConfigure()都是空方法,
实际有用的只有init(),configure()和performBuild()方法。

我们先来看看init(),configure()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this);
}

for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
configurer.init((B) this);
}
}

private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();

for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}

源码中可以看到都是先获取到我们的配置类信息,然后循环调用配置类自己的init(),configure()方法。

前面说过,我们的配置类是继承了WebSecurityConfigurerAdapter的子类,而WebSecurityConfigurerAdapter又是SecurityConfigurer的子类,所有SecurityConfigurer的子类都需要实现init(),configure()方法。

所以这里的init(),configure()方法其实就是调用WebSecurityConfigurerAdapter自己重写的init(),configure()方法。

其中WebSecurityConfigurerAdapter中的configure()方法是一个空方法,所以我们只需要去看WebSecurityConfigurerAdapter中的init()方法就好了。

1
2
3
4
5
6
7
8
复制代码public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}

这里也可以分为两步:

  1. 执行了getHttp()方法,这里面初始化加入了很多过滤器。
  2. 将HttpSecurity放入WebSecurity,将FilterSecurityInterceptor放入WebSecurity,就是我们鉴权那章讲过的FilterSecurityInterceptor。

那我们主要看第一步getHttp()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码protected final HttpSecurity getHttp() throws Exception {
http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}

// 我们一般重写这个方法
configure(http);
return http;
}

getHttp()方法里面http调用的那一堆方法都是一个个过滤器,第一个csrf()很明显就是防止CSRF攻击的过滤器,下面还有很多,这就是SpringSecurity默认会加入过滤器链的那些过滤器了。

其次,还有一个重点就是倒数第二行代码,我也加上了注释,我们一般在我们自定义的配置类中重写的就是这个方法,所以我们的自定义配置就是在这里生效的。

所以在初始化的过程中,这个方法会先加载自己默认的配置然后再加载我们重写的配置,这样两者结合起来,就变成了我们看到的默认配置。(如果我们不重写configure(http)方法,它也会一点点的默认配置,大家可以去看源码,看了就明白了。)

init(),configure()(空方法)结束之后,就是调用performBuild()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
复制代码protected Filter performBuild() throws Exception {

int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();

List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);

for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}

// 调用securityFilterChainBuilder的build()方法
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}

FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}

filterChainProxy.afterPropertiesSet();

Filter result = filterChainProxy;

postBuildAction.run();
return result;
}

这个方法主要需要看的是调用securityFilterChainBuilder的build()方法,这个securityFilterChainBuilder是我们在init()方法中add的那个,所以这里的securityFilterChainBuilder其实就是HttpSecurity,所以这里其实是调用了HttpSecurity的bulid()方法。

又来了,WebSecurity的bulid()方法还没说完,先来了一下HttpSecurity的bulid()方法。

HttpSecurity的bulid()方法进程和之前的一样,也是先init()然后configure()最后performBuild()方法,值得一提的是在HttpSecurity的performBuild()方法里面,会对过滤器链中的过滤器进行排序:

1
2
3
4
5
复制代码@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}

HttpSecurity的bulid()方法执行完了之后将DefaultSecurityFilterChain返回给WebSecurity的performBuil()方法,performBuil()方法再将其转换为FilterChainProxy,最后WebSecurity的performBuil()方法执行结束,返回一个Filter注入成为name="springSecurityFilterChain"的Bean。

经过以上这些步骤之后,springSecurityFilterChain方法执行完毕,我们的过滤器链就创建完成了,SpringSecurity也可以跑起来了。

后记

看到这的话,其实你已经很有耐性了,但可能还觉得云里雾里的,因为SpringSecurity(Spring大家族)这种工程化极高的项目项目都是各种设计模式和编码思想满天飞,看不懂的时候只能说这什么玩意,看得懂的时候又该膜拜这是艺术啊。

这些东西它不容易看懂但是比较解耦容易扩展,像一条线下来的代码就容易看懂但是不容易扩展了,福祸相依。

而且这么多名称相近的类名,各种继承抽象,要好好理解下来的确没那么容易,这篇其实想给这个SpringSecurity来个收尾,逼着自己写的,我这个人喜欢有始有终,这段东西也的确复杂,接下来的几篇打算写几个实用的有意思的也轻松的放松一下。

如果你对SpringSecurity源码有兴趣可以跟着来我这个文章,点开你自己的源码点一点,看一看,加油。

自从上篇征文发了之后,感觉多了很多前端的关注者,掘金果然还是前端多啊,没事,虽然我不怎么写前端,说不定哪天改行了呢哈哈。

我也不藏着掖着,其实我现在是写后端的,我对前端呢只能说是略懂略懂,不过无聊了也可以来看看我的文章,点点赞刷刷阅读干干啥的👍,说不定某一天突然看懂了某篇文还前端劝退后端入行,加油了大家。

别辜负生命,别辜负自己。

你们的每个点赞收藏与评论都是对我知识输出的莫大肯定,如果有文中有什么错误或者疑点或者对我的指教都可以在评论区下方留言,一起讨论。

我是耳朵,一个一直想做知识输出的伪文艺程序员,下期见。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RAP2接口管理工具使用教程

发表于 2020-07-16

本人最近想为项目写一份接口文档,当时首先想到的是swagger,没想单swagger2对spring5的反应式编程很不友好,转而找其他更优方案

简介及安装

rap2是阿里妈妈前端团队出品的开源接口管理工具RAP,目前是第二版,他的优点是既可以方便管理一整套标准的接口文档,又能够提供mockjs数据。这样就不需要后端先行“到底”,前端才开始开发,既提升团队效率又减少前后端的耦合

安装

最新版的rap2,官方提供了两种安装方式,强烈推荐第一种docker部署的方式,简单直接。下面是安装过程

步骤

  1. 本地环境要求

安装Docker
2. 拉取rap2项目到本地任意目录(注:项目中已包含redis,mysql)

1
复制代码git clone git@github.com:thx/rap2-delos.git
  1. 进入项目,修改docker-compose.yml文件,个人根据需要修改,我这里直接使用项目中的redis和mysql,为了不和本地冲突,改了映射端口。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码// ...
services:
# frontend
dolores:
image: rapteam/rap2-dolores:latest
ports:
#冒号前可以自定义前端端口号,冒号后不要动
- 4000:38081
// ...
redis:
image: redis:4
ports:
- 6479:6379
// ...
mysql:
image: mysql:5.7
# expose 33306 to client (navicat)
ports:
- 3406:3306
  1. 拉取镜像并启动(注:以下命令后是在rap目录下执行)
1
复制代码docker-compose up -d
  1. 启动后,第一次运行需要手动初始化mysql数据库(注意)
1
复制代码docker-compose exec delos node scripts/init
  1. 部署成功后 访问

http://localhost:4000 # 前端
http://localhost:38080 # 后端
7. 其他:关闭rap服务

1
复制代码docker-compose down

手动部署可参考官网

RAP2中配置自定义接口

步骤

  1. 前端登录http://localhost:4000/
  2. 右上角新建仓库

  1. 新建模块->新建接口

  1. 点击导入,直接将示例json粘贴即可

  1. 到这里,一个最基础的接口文档就完成了

mock数据

虽然直接导入json已经很方便,但是里面的模拟数据都是固定的。熟悉mockJs的朋友都知道,动态的响应数据,更适合开发调试。幸运的是,rap也支持Mock.js 的语法规范

详细的语法规则可查看官方git,或者官方示例(推荐)。以下只做一些常见示范的补充:

类型 生成规则 初始值 场景
Number @natural 生成随机数
Number 1-10 1-10的随机数
String @name 随机英文名
String @cname 随机中文名
String @city 随机一个国内地级市名称
Function @datetime(“yyyy-MM-dd HH:mm:ss “) 指定格式的日期

不难看出,这些初始值的使用都是@占位符或者@占位符(参数 [, 参数])

类型 占位符 备注
Basic(基础类) boolean, natural, integer, float, character, string, range, date, time, datetime, now
Image(图片) image, dataImage 图片地址
Color(颜色值) color 16进制字符串
Text paragraph, sentence, word, title, cparagraph, csentence, cword, ctitle 段落,标题等
Name first, last, name, cfirst, clast, cname 姓名,姓,名占位符最前面是c的代表产生中文数据
Web url, domain, email, ip, tld 地址,域名,邮箱,ip地址
Address area, region 地区,方向
Helper capitalize, upper, lower, pick, shuffle
Miscellaneous guid, id

示例

导出

  • 整理完接口模块后,点击导航栏下的导出按钮,就可以制作文档或脚本了,非常方便

遇到的问题

  1. rap2运行,在前端页面登录时遇到了问题,登录时token一直无效,注册没有响应,后来我直接将rap2项目中Loggers表清空就可以了
  2. 默认rap2安装后,mysql是没有密码的,因此在登录时直接回车即可
1
2
3
4
复制代码#进入mysql,直接回车
docker exec -it docker_mysql mysql -uroot -p
# 配置密码
mysql> SET PASSWORD FOR 'root' = PASSWORD('new_password');
  1. 重新部署并修改配置,直接修改docker-compose.yml保存,重新执行步骤5

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

React实战 - 如何更优雅的使用 Antd 的 Moda

发表于 2020-07-15

前言

首先,让我们来看一看 Ant Design 官网的第一个关于 Modal 的 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
复制代码import { Modal, Button } from 'antd';

class App extends React.Component {
  state = { visible: false };

  showModal = () => {
    this.setState({
      visible: true,
    });
  };

  handleOk = e => {
    console.log(e);
    this.setState({
      visible: false,
    });
  };

  handleCancel = e => {
    console.log(e);
    this.setState({
      visible: false,
    });
  };

  render() {
    return (
      <div>
        <Button type="primary" onClick={this.showModal}>
          Open Modal
        </Button>
        <Modal
          title="Basic Modal"
          visible={this.state.visible}
          onOk={this.handleOk}
          onCancel={this.handleCancel}
        >
          <p>Some contents...</p>
          <p>Some contents...</p>
          <p>Some contents...</p>
        </Modal>
      </div>
    );
  }
}

当然,一般来说,我们写的 Modal 不会像官网里的例子这么的简单,毕竟这么简单的话会更倾向于使用类似于 Modal.confirm 等 API 直接调用弹出就好了。我们可能会对 Modal 进行二次封装,里面写一些代码逻辑及可能是固定的譬如 title 直接写在组件内,然后把一些像是 visible、onOk 及 onCancel这种 API 用 props 暴露出去。

这种把 visible 提升到父组件的方式固然能解决问题,可是这种方式也导致了一个问题。每次我们打开弹窗的时候,因为 visible 是在父组件中的状态,所以父组件也会重新 render 一次,甚至,如果父组件中的其他子组件没有做优化的话(没有使用 memo 或者没有设置 shouldComponentUpdate),也会跟着重新 render 一次。

那么有没有什么方法可以解决这个问题呢?当然可以,我们只要把 visible 的状态留在和 Modal 有关的子组件里面就可以了。而在父组件中,其实我们所需要的只是 打开弹窗 以及 接收子组件的回调 两个需求。那么有哪些方式可以实现把 visible 留在子组件中呢?下面我们逐一介绍,因为我想不到什么命名,所以下面就一二三四了,emmm,就这样。

具体实现

在线代码

codesandbox 地址

方案一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
复制代码import React, { memo, useState } from "react";
import { Modal } from "antd";

type Modal1Props = {
  children: React.ReactElement;
  onOk?(): void;
  onCancel?(): void;
  [others: string]: any;
};

const Modal1 = memo<Modal1Props>(({ children, onOk, onCancel, ..._restProps }) => {
  const [visible, setVisible] = useState(false);

  const wrapWithClose = (method?: () => void) => () => {
    setVisible(false);
    method && method();
  };

  // ------

  return (
    <>
      <Modal
        title="方案一"
        visible={visible}
        onOk={wrapWithClose(onOk)}
        onCancel={wrapWithClose(onCancel)}
      >
        <div>...</div>
      </Modal>
      {React.cloneElement(children, {
        onClick: (...args: any[]) => {
          const { onClick } = children.props;
          setVisible(true);
          onClick && onClick(...args);
        }
      })}
    </>
  );
});

export default Modal1;

第一种方案就是比较投机取巧,但是它也有它的缺点,就是打开弹窗这个操作只能由某一个元素完成且不能更多了。

方案二

对于在父组件中操作子组件状态这种事情,我们自然而然的就会想到使用 ref,下面就让我们来看看要怎么用 ref 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码import React, { useState, useImperativeHandle, useRef } from "react";
import { Modal } from "antd";

type Payload = {
  onOk?(): void;
  onCancel?(): void;
  [others: string]: any;
};

export type Modal2RefType = {
  show(payload: Payload): void;
};

const Modal2 = React.forwardRef<Modal2RefType>((_props, ref) => {
  const [visible, setVisible] = useState(false);
  const payloadRef = useRef<Payload>({});

  useImperativeHandle(
    ref,
    () => ({
      show: payload => {
        payloadRef.current = payload;
        setVisible(true);
      }
    }),
    []
  );

  const wrapWithClose = (method?: () => void) => () => {
    setVisible(false);
    method && method();
  };

  return (
    <Modal
      title="方案二"
      visible={visible}
      onOk={wrapWithClose(payloadRef.current.onOk)}
      onCancel={wrapWithClose(payloadRef.current.onCancel)}
    >
      <div>...</div>
    </Modal>
  );
});

export default Modal2;

使用 ref 的方式也很简单,这里我们将一些额外的参数使用 show 这个方法来传递,而不是像方案一中那样用 props,但是我们使用时需要一个额外的变量来存储,只能说,这还不够完美。

方案三

对于在父组件中控制子组件这件事,我们当然可以使用“无所不能”的发布订阅,因为发布订阅并不是我们这里所要讲的内容,所以就简单的导个包吧,我们这里使用了 eventemitter3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
复制代码import React, { memo, useState, useRef, useEffect } from "react";
import { Modal } from "antd";
import EventEmitter from "eventemitter3";

const eventEmitter = new EventEmitter();

type Payload = {
  onOk?(): void;
  onCancel?(): void;
  [others: string]: any;
};

type ModalType = React.NamedExoticComponent & { show(payload: Payload): void };

const Modal3: ModalType = memo(
  (_props, ref) => {
    const [visible, setVisible] = useState(false);
    const payloadRef = useRef<Payload>({});

    useEffect(() => {
      const handler = (payload: Payload) => {
        setVisible(true);
        payloadRef.current = payload;
      };

      eventEmitter.on("show", handler);

      return () => eventEmitter.off("show", handler);
    }, []);

    const wrapWithClose = (method?: () => void) => () => {
      setVisible(false);
      method && method();
    };

    return (
      <Modal
        title="方案三"
        visible={visible}
        onOk={wrapWithClose(payloadRef.current.onOk)}
        onCancel={wrapWithClose(payloadRef.current.onCancel)}
      >
        <div>...</div>
      </Modal>
    );
  },
  () => true
) as any;

Modal3.show = (payload: Payload) => eventEmitter.emit("show", payload);

export default Modal3;

在上面的代码中,因为直接把 eventEmitter 一起 export 出去会显得不那么优雅(不知道怎么描述的时候就用优雅就对了,大概)。而且还需要用的人知道,要调用 emit 方法触发 show 事件,实在是不优雅,所以我们直接在 Modal3 上绑定一个 show 方法来调用。

当看完上面的代码,我想应该有人会发现,其实我们根本就没有必要为此而引入一个 eventEmitter,这实在是有一种杀鸡用了牛刀的感觉。我们为什么不直接在 useEffect 内把 handler 直接赋值给 Modal3.show 呢?于是,我们就有了方案四

方案四

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
复制代码import React, { memo, useState, useRef, useEffect } from "react";
import { Modal } from "antd";

type Payload = {
  onOk?(): void;
  onCancel?(): void;
  [others: string]: any;
};

type ModalType = React.NamedExoticComponent & { show(payload: Payload): void };

const Modal4: ModalType = memo(
  (_props, ref) => {
    const [visible, setVisible] = useState(false);
    const payloadRef = useRef<Payload>({});

    useEffect(() => {
      const lastShow = Modal4.show;

      Modal4.show = (payload: Payload) => {
        setVisible(true);
        payloadRef.current = payload;
      };

      return () => (Modal4.show = lastShow);
    }, []);

    const wrapWithClose = (method?: () => void) => () => {
      setVisible(false);
      method && method();
    };

    return (
      <Modal
        title="方案四"
        visible={visible}
        onOk={wrapWithClose(payloadRef.current.onOk)}
        onCancel={wrapWithClose(payloadRef.current.onCancel)}
      >
        <div>...</div>
      </Modal>
    );
  },
  () => true
) as any;

Modal4.show = (payload: Payload) => console.log("Modal4 is not mounted.");

export default Modal4;

更多思考

上面提到了好几种解决方法,其实我们还可以把状态进一步提升,使用 Context 来传递,在父组件中接收 show 这个不会变化的 API,在 Modal 所在的组件中接收会变化的 visible 以及 payload,当然我觉得这样做过于复杂,所以没有列举。看到这里,我想大家也都知道,我肯定是最推荐方案四啦,之所以全都写出来,是为了告诉大家,我们应该有更多的思考,而不是用某一种方法解决了,就等于真正的掌握了。当然上面的都是我能想到的方法,当然也会有我想不到的,如果你想到了什么其他的方法,烦请赐教。

除此之外,我想留更多的问题给大家:

  • 上面的代码中有很多可以复用的逻辑,那么如何复用?
  • 我们可以发现,上面的实现都是无论 show 多少次都是同一个弹窗,那么有哪些方法可以实现类似于 Modal.confirm 的效果呢?
  • Ant Design 中的 message 组件又该如何实现,更进一步的,如果要限制同事出现的 message 的数量,又该怎么做?

第一次写文章因为不太会表达,所以就贴了很多代码,求原谅,以后我会慢慢锻炼自己多写一些文字的。上面提到的问题,如果有需要,我也会逐一给大家解答,以更多文章的方式。

那么最后,如果觉得文章有用的话,就点个关注吧。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…793794795…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%