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

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


  • 首页

  • 归档

  • 搜索

面试高频算法题之栈&队列

发表于 2021-11-30

这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战

栈和队列

全文概览

image-20211129163913713

基础知识

####栈

栈是一种先进后出的数据结构。这里有一个非常典型的例子,就是堆叠盘子。我们在放盘子的时候,只能从下往上一个一个的放;在取的时候,只能从上往下一个一个取,不能从中间随意取出。

image-20211129094451883

栈是一种操作受限的线性表,只允许在一端处理数据。主要包括两种操作,即入栈和出栈,也就是在栈顶插入一个数据和从栈顶删除一个数据。

栈既可以用数组实现,也可以用链表来实现。用数组实现的栈,我们叫作顺序栈,用链表实现的栈,我们叫作链式栈。

队列

队列是一种先进先出的数据结构。你可以把它想象成排队买票,先来的先买,后来的人只能站末尾,不允许插队。

image-20211129101242533

队列跟栈一样,也是一种操作受限的线性表数据结构。主要包括两个操作,即出队和入队,也就是从队首取一个元素和在队尾插入一个元素。

队列可以用数组来实现,也可以用链表来实现。用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。

用两个栈实现队列

剑指 Offer 09. 用两个栈实现队列

问题描述

用两个栈来实现一个队列,完成 n 次在队列尾部插入整数 (push) 和在队列头部删除整数 (pop) 的功能。 队列中的元素为 int 类型。保证操作合法,即保证pop操作时队列内已有元素。

示例:

输入:[“PSH1”,”PSH2”,”POP”,”POP”]

返回值:1,2

说明:

“PSH1”:代表将1插入队列尾部
“PSH2”:代表将2插入队列尾部
“POP”:代表删除一个元素,先进先出 => 返回1
“POP”:代表删除一个元素,先进先出 => 返回2

分析问题

首先,我们需要知道队列和栈的区别。

  1. 队列是一种先进先出的数据结构。队列中的元素是从后端入队,从前端出队。就和排队买票一样。
  2. 栈是一种后进先出的数据结构。栈中的元素是从栈顶压入,从栈顶弹出。

为了使用两个栈实现队列的先进先出的特性,我们需要用一个栈来反转元素的入队顺序。

入队操作Push:

因为栈是后进先出的,而队列是先进先出的,所以要想使用栈来实现队列的先进先出功能,我们需要把新入栈的元素放入栈底。为了实现这个操作,我们需要先把栈S1中的元素移动到S2,接着再把新来的元素压入S2,然后再把S2中的所有元素再弹出,压入到S1。这样就实现了把新入栈的元素放入栈底的功能。

image-20210928153029729

1
2
3
4
5
6
7
python复制代码    def push(self, node):
# write code here
while self.stack1:
self.stack2.append(self.stack1.pop())
self.stack2.append(node)
while self.stack2:
self.stack1.append(self.stack2.pop())

出队操作Pop:

我们直接从S1弹出就可以了,因为经过反转后,S1中的栈顶元素就是最先入栈的元素,也就是队首元素。

1
2
3
ruby复制代码    def pop(self):
if self.stack1:
return self.stack1.pop()

我们来看完整的代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码class Solution:
def __init__(self):
self.stack1 = []
self.stack2 = []
def push(self, node):
# write code here
while self.stack1:
self.stack2.append(self.stack1.pop())
self.stack2.append(node)
while self.stack2:
self.stack1.append(self.stack2.pop())

def pop(self):
if self.stack1:
return self.stack1.pop()

我们可以看到入队操作的时间复杂度是O(n),空间复杂度也是O(n)。出队时间复杂度是O(1),空间复杂度也是O(1)。

优化

在上面的算法中,不知道你有没有发现,每次在push一个新元素时,我们都需要把S1中的元素移动到S2中,然后再从S2移回到S1中。这显然是冗余的。其实,我们在入队时只需要插入到S1中即可。而出队的时候,由于第一个元素被压在了栈S1的底部,要想实现队列的先进先出功能,我们就需要把S1的元素进行反转。我们可以把栈S1的元素Pop出去,然后压入S2。这样就把S1的栈底元素放在了栈S2的栈顶,我们直接从S2将它弹出即可。一旦 S2 变空了,我们只需把 S1 中的元素再一次转移到 S2 就可以了。

image-20210928153046516

下面我们来看一下代码实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码class Solution:
def __init__(self):
self.stack1 = []
self.stack2 = []
def push(self, node):
# write code here
self.stack1.append(node)

def pop(self):
if not self.stack2:
while self.stack1:
self.stack2.append(self.stack1.pop())
return self.stack2.pop()

有效的括号

LeetCode 20. 有效的括号

问题描述

给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。

有效字符满足的条件是:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。

示例:

输入:s = “()[]{}”

输出:true

分析问题

这个问题我们可以借助 “栈” 这种数据结构来解决。在遍历字符串的过程中,当我们遇见一个左括号时,我们就入栈,当我们遇到一个右括号时,我们就取出栈顶元素去判断他们是否是同类型的。如果不是的话,那就代表字符串s不是有效串,我们直接返回False。如果是,接着去遍历,直到遍历结束为止。当遍历完字符串s后,如果栈为空,就代表字符串是有效的。这里需要注意一点,为了加快判断左、右括号是否是同类型的,我们引入哈希表存储每一种括号。哈希表的键为右括号,值为相同类型的左括号。

下面我们来看一下代码实现。

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
python复制代码def isValid(s):
#如果字符串不是偶数,直接返回false
#因为字符只包含括号,所以只有偶数时才有可能匹配上
if len(s) % 2 == 1:
return False

dict = {
")": "(",
"]": "[",
"}": "{",
}

stack = list()

for ch in s:
#代表遍历到右括号
if ch in dict:
#看栈顶元素是否能匹配上,如果没有匹配上,返回false
if not stack or stack[-1] != dict[ch]:
return False
#如果匹配上,弹出栈顶元素
stack.pop()
else:
#匹配到左括号,入栈
stack.append(ch)
#栈为空,代表s是有效串,否则是无效串
return not stack

包含min函数的栈

剑指 Offer 30. 包含min函数的栈

问题描述

定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数,并且调用 min函数、push函数 及 pop函数 的时间复杂度都是 O(1)。

push(value):将value压入栈中

pop():弹出栈顶元素

top():获取栈顶元素

min():获取栈中最小元素

示例:

1
2
3
4
5
6
7
lua复制代码minStack.push(-2); -->将-2入栈
minStack.push(0); -->将0入栈
minStack.push(-3); -->将-3入栈
minStack.min(); -->返回栈中最小元素-3
minStack.pop(); -->弹出栈顶元素-3
minStack.top(); -->返回栈顶元素0
minStack.min(); -->返回栈中最小元素-2

分析问题

对于普通的栈来说,执行push和pop的时间复杂度是O(1),而执行min函数的时间复杂度是O(N),因为要想找到最小值,就需要遍历整个栈,为了降低min函数的时间复杂度,我们引入了一个辅助栈。

  • 数据栈A:栈A用来存储所有的元素,保证入栈push、出栈pop、获取栈顶元素top的操作。
  • 辅助栈B:栈B用来存储栈A中所有非严格递减的元素,即栈A中的最小值始终在栈B的栈顶,这样可以保证以O(1)的时间复杂度来返回栈中的最小值。

image-20211010122012798

image-20211010122025833

image-20211010122040550

下面我们来看一下代码实现。

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
python复制代码class Solution:
def __init__(self):
#数据栈
self.A = []
#辅助栈
self.B = []
#push操作
def push(self, x):
self.A.append(x)
#如果辅助栈B为空,或者栈顶元素大于x,则入栈
if not self.B or self.B[-1] >= x:
self.B.append(x)

def pop(self):
#弹出数据栈A中的元素
s = self.A.pop()
#如果弹出的元素和栈B的栈顶元素相同,则为了保持一致性
#将栈B的栈顶元素弹出
if s == self.B[-1]:
self.B.pop()

def top(self):
#返回数据栈A中的栈顶元素
return self.A[-1]

def min(self):
#返回辅助栈B中的栈顶元素
return self.B[-1]

表达式求值

问题描述

请写一个整数计算器,支持加减乘三种运算和括号。

示例:

输入:”(2 * (3 - 4)))* 5”

返回值:-10

分析问题

因为只支持加、减、乘、括号,所以我们根据优先级可以分为3类,即括号>乘>加、减,假设先把括号去掉,那么就剩下乘和加减运算,根据运算规则,我们需要先计算乘、再计算加、减,因此我们可以这么来考虑,我们先进行乘法运算,并将这些乘法运算后的整数值返回原表达式的相应位置,则随后整个表达式的值,就等于一系列整数加减后的值。而对于被括号分割的表达式,我们可以递归的去求解,具体算法如下。

遍历字符串s,并用变量preSign记录每个数字之前的运算符,初始化为加号。

  1. 遇到空格时跳过。
  2. 遇到数字时,继续遍历求出这个完整的数字的值,保存到num中。
  3. 遇到左括号时,需要递归的求出这个括号内的表达式的值。
  4. 遇到运算符或者表达式的末尾时,就根据上一个运算符的类型来决定计算方式。
    • 如果是加号,不需要进行计算,直接push到栈里
    • 如果是减号,就去当前数的相反数,push到栈里
    • 如果是乘号,就需要从栈内pop出一个数和当前数求乘法,再把计算结果push到栈中
  5. 最后把栈中的结果求和即可。

下面我们来看一下代码实现。

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
ini复制代码class Solution:
def calculate(self, s):
n = len(s)
#存取部分数据和
stack = []
preSign = '+'
num = 0
i=0
while i<n:
c=s[i]
if c==' ':
i=i+1
continue
if c.isdigit():
num = num * 10 + ord(c) - ord('0')

#如果遇到左括号,递归求出括号内表达式的值
if c=='(':
j=i+1
counts=1
#截取出括号表达式的值
while counts>0:
if s[j]=="(":
counts=counts+1
if s[j]==")":
counts=counts-1
j=j+1
#剥去一层括号,求括号内表达式的值
num=self.calculate(s[i+1:j-1])
i=j-1

if not c.isdigit() or i==n-1:
if preSign=="+":
stack.append(num)
elif preSign=="-":
stack.append(-1*num)
elif preSign=="*":
tmp=stack.pop()
stack.append(tmp*num)

num=0
preSign=c
i=i+1
return sum(stack)

s=Solution()
print(s.calculate("(3+4)*(5+(2-3))"))

滑动窗口的最大值

LeetCode 239. 滑动窗口最大值

问题描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。返回滑动窗口中的最大值。

示例:

输入:[2,3,4,2,6,2,5,1],3

输出:[4,4,6,6,6,5]

分析问题

这道题的关键点在于求滑动窗口中的最大值。大小为k的滑动窗口,我们可以通过遍历的方式来求出其中的最大值,需要O(k)的时间复杂度。对于大小为n的数组nums,一共有n-k+1个窗口,因此该算法的时间复杂度是O(nk)。

image-20211028134716254

通过观察,我们可以知道,对于两个相邻的滑动窗口,有k-1个元素是共用的,只有一个元素是变化的,因此我们可以利用此性质来优化我们的算法。

image-20211028135405455

对于求最大值问题,我们可以使用优先级队列(大顶推)来求解。首先,我们将数组的前k个元素放入优先级队列中。每当我们向右移动窗口时,我们就可以把一个新的元素放入队列中,此时堆顶元素就是堆中所有元素的最大值,然而这个最大值有可能不属于当前的滑动窗口中,我们需要将该元素进行移除处理(如果最大值不在当前滑动窗口中,它只能在滑动窗口的左边界的左侧,所以滑动窗口向右移动的过程中,该元素再也不会出现在滑动窗口中了,所以我们可以对其进行移除处理)。我们不断地移除堆顶的元素,直到其确实出现在滑动窗口中。此时,堆顶元素就是滑动窗口中的最大值。

为了方便判断堆顶元素与滑动窗口的位置关系,我们可以在优先队列中存储二元组 (num,index),表示元素num在数组中的下标为index。

小trick:因为python中只提供了小顶堆,所以我们需要对元素进行取反处理,例如对于列表[1, -3],我们对元素进行取反,然后插入小顶堆中,此时堆中是这样的[-1,3],我们取出堆顶元素-1,然后取反为1,正好可以得到列表中的最大值1。

我们nums=[2,3,4,2,6,2,5,1],k=3为例,来看一下具体的过程。

  1. 首先,我们将nums的前3个元素放入优先级队列中,队首元素下标值index=2>0,在窗口中,所以加入结果中,此时res=[4]。

image-20211028181619295
2. 下一个元素2入队,此时队首元素下标index=2>1,在窗口中,所以加入结果中,此时res=[4,4]。

image-20211028181644285
3. 下一个元素6入队,此时队首元素下标index=4>2,在窗口中,所以加入结果中,此时res=[4,4,6]。

image-20211028181720875
4. 下一个元素2入队,此时队首元素下标index=4>3,在窗口中,所以加入结果中,此时res=[4,4,6,6]。

image-20211028181754576
5. 下一个元素5入队,此时队首元素下标index=4=4,在窗口中,所以加入结果中,此时res=[4,4,6,6,6]。

image-20211028181811290
6. 下一个元素1队列,此时队首元素下标index=4<5,不在窗口中,所以我们将其弹出,此时队首元素的下标变为6,在窗口中,所以加入结果中,此时res=[4,4,6,6,6,5]。

image-20211028181832592

进阶

这道题我们也可以使用双端队列来求解。我们在遍历数组的过程中,不断的对元素对应的下标进行出队入队操作,在出入队的过程中,我们需要保证队列中存储的下标对应的元素是从大到小排序的。具体来说,当有一个新的元素对应的下标需要入队时,如果该元素比队尾对应的元素的值大,我们需要弹出队尾,然后循环往复,直到队列为空或者新的元素小于队尾对应的元素。

由于队列中下标对应的元素是严格单调递减的,因此队首下标对应的元素就是滑动窗口中的最大值。但是此时的最大值可能在滑动窗口左边界的左侧,并且随着窗口向右移动,它永远不可能出现在滑动窗口中了。因此我们还需要不断从队首弹出元素,直到队首元素在窗口中为止。

我们还是以nums=[2,3,4,2,6,2,5,1],k=3为例,来看一下具体的过程。我们首先初始化一个空队列que。

  1. 此时队列为que空,元素2对应的下标0入队。并且此时未形成窗口,不取值。

image-20211028182724320
2. 此时队列que=[0],队尾元素为0,它对应数组中的元素是nums[0] < nums[1]的,所以我们把队尾0弹出,此时队列为空,我们将1入队。并且此时未形成窗口,不取值。

image-20211028182735893
3. 此时队列que=[1],队尾元素为1,它对应的数组中的元素是nums[1] < nums[2]的,所以我们把队尾1弹出,此时队列为空,我们将2入队。并且此时队首元素2在窗口[0,2]中,所以取出队首元素。

image-20211028182754689
4. 此时队列que=[2],队尾元素为2,它对应的数组中的元素是nums[2] > nums[3]的,所以我们将3入队。并且此时队首元素2在窗口[1,3]中,所以取出队首元素。

image-20211028182820316
5. 此时队列que=[2,3],队尾元素为3,它对应的数组中的元素是nums[3] < nums[4]的,所以我们把队尾3弹出,并且此时队尾元素对应的数组中的元素是nums[2] < nums[4],所以我们把队尾2弹出,此时队列为空,我们将4入队。并且此时队首元素4在窗口[2,4]中,所以取出队首元素。

image-20211028182843010
6. 此时队列que=[4],队尾元素为4,它对应的数组中的元素是nums[4] > nums[5]的,所以我们将5入队。并且此时队首元素4在窗口[3,5]中,所以我们取出队首元素。

image-20211028182937255
7. 此时队列que=[4,5],队尾元素为5,它对应的数组中的元素是nums[5] < nums[6]的,所以我们把队尾5弹出,此时队尾元素对应的数组中的元素时nums[4] > nums[6] ,所以我们将6入队。并且此时队首元素4在窗口[4,6]中,所以我们取出队首元素。

image-20211028183110577
8. 此时队列que=[4,6],队尾元素为6,它对应的数组中的元素是nums[6] > nums[7]的,所以我们将7入队。而此时队首元素4不在窗口[5,7]中,所以我们将其移除队列,此时队首元素6在窗口[5,7]中,所以我们将其取出。

image-20211028183151850

下面我们来看一下代码实现。

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
perl复制代码import collections
class Solution:
def maxSlidingWindow(self, nums, k):
n = len(nums)
#申请一个双端队列
q = collections.deque()

#初始化第一个窗口
for i in range(k):
#如果队列不为空且比队尾元素大,将队尾出队
while q and nums[i] >= nums[q[-1]]:
q.pop()
#直到队列为空,或者比队尾元素小,入队
q.append(i)

#将队首元素加入结果中
ans = [nums[q[0]]]

#窗口逐步向右移动
for i in range(k, n):
#如果队列不为空且比队尾元素大,将队尾出队
while q and nums[i] >= nums[q[-1]]:
q.pop()
#直到队列为空,或者比队尾元素小,入队
q.append(i)
#如果队首元素不在该窗口内,出队操作
while q[0] <= i - k:
q.popleft()
#将队首元素加入结果中
ans.append(nums[q[0]])

return ans


s=Solution()
print(s.maxSlidingWindow([2,3,4,2,6,2,5,1],3))

栈和排序

问题描述

给你一个由1~n,n个数字组成的一个排列和一个栈,要求按照排列的顺序入栈。如何在不打乱入栈顺序的情况下,仅利用入栈和出栈两种操作,输出字典序最大的出栈序列。

排列:指 1 到 n 每个数字出现且仅出现一次。

示例:

输入:[2,4,5,3,1]

输出:[5,4,3,2,1]

分析问题

由于我们只能使用出栈和入栈两种操作,要想使得出栈序列字典序最大,首先想到的就是令高位尽可能地大,我们出栈的时机就是:当前入栈元素若是大于之后将要入栈的元素,那么就将其出栈。当元素出栈后,还需要判断栈顶元素与之后将要入栈元素之间的大小关系,如果此时栈顶元素大于之后将要入栈的元素,那么就将其出栈,不断判断直到栈为空或条件不满足。

为了快速判断“当前入栈元素是否大于之后将要入栈的元素”,我们需要创建一个辅助数组temp,其中temp[i]表示i之后的最大元素。借助辅助数组,我们可以以O(1)的时间复杂度去判断当前入栈元素是否大于之后将要入栈的元素。

image-20211109214153107

image-20211109215553532

image-20211109215610927

image-20211109215644288

image-20211109215700561

image-20211109215716016

image-20211109215736573

image-20211109215756969

image-20211109215816076

下面我们来看一下代码的实现。

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
ini复制代码import sys
class Solution:
def solve(self , a):
n=len(a)
res=[]
if n==0:
return res
stack=[]
temp=[0]*n
temp[n-1]=-sys.maxsize-1
#从右往左遍历数组a,然后取填充temp
#使得temp[i]表示i之后的最大元素
for i in range(n-2,-1,-1):
temp[i]=max(a[i+1],temp[i+1])

#遍历数组a
for i in range(0,n):
if a[i] > temp[i]: #若当前元素大于之后将要入栈的元素,将其加入结果中
res.append(a[i])
# 若栈不为空,且栈顶元素大于temp[i],
# 栈顶出栈,加入结果中
while stack and stack[-1] > temp[i]:
res.append(stack[-1])
stack.pop()
else:
stack.append(a[i])

while stack:
res.append(stack[-1])
stack.pop()
return res

该算法的时间复杂度是O(n),空间复杂度也是O(n)。

单调栈

问题描述

给定一个长度为 n 的可能含有重复值的数组 arr ,找到每一个 i 位置左边和右边离 i 位置最近且值比 arr[i] 小的位置。请设计算法,返回一个二维数组,表示所有位置相应的信息。位置信息包括:两个数字 l 和 r,如果不存在,则值为 -1,下标从 0 开始。

示例:

输入:[3,4,1,5,6,2,7]

输出:[[-1,2],[0,2],[-1,-1],[2,5],[3,5],[2,-1],[5,-1]]

分析问题

这道题最简单的解法就是暴力求解,即通过两层for循环来求解。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码class Solution:
def foundMonotoneStack(self , nums):
n=len(nums)
res=[]
#遍历一遍数组
for i in range(0,n):
l=-1
r=-1
#从左往右寻找l,寻找比nums[i]小的最近的nums[l]
for j in range(0,i):
if nums[j] < nums[i]:
l=j

#从右往左寻找l,寻找比nums[i]小的最近的nums[r]
for j in range(n-1,i,-1):
if nums[j] < nums[i]:
r=j

res.append([l,r])
return res

该算法的时间复杂度是O(n^2),空间复杂度是O(1)。

显然暴力求解的时间复杂度太高,那我们该如何优化呢?其实这道题我们可以使用单调栈来求解,首先我们来看一下单调栈的定义。

单调栈是指栈内元素是具有有单调性的栈,和普通栈相比,单调栈在入栈的时候,需要将待入栈的元素和栈顶元素进行对比,看待加入栈的元素入栈后是否会破坏栈的单调性,如果不会,直接入栈,否则一直弹出到满足条件为止。

本题中,我们维护一个存储数组下标的单调栈。然后遍历数组,执行如下操作。

我们以求每一个i左边离i最近且小于arr[i]的位置为例,来看一下算法的执行流程。首先我们从左往右遍历数组。

  • 假设遍历到的元素是 arr[i],栈顶元素 top 对应的数组中的元素是 arr[top],然后我们拿 arr[i] 和 arr[top] 进行对比。
  • 如果 arr[top] > arr[i],就说明 top 不是第 i 个元素的解,也不会是 i 以后任何元素的解(因为 i 比 top 距离后面的数更近,同时arr[i] < arr[top]),所以我们就把top弹出,直到栈为空或者栈顶元素(数组的下标)对应数组中的元素小于 arr[i]。
  • 如果arr[top] < arr[i],就说明第 i 个数的解就是 top,因为栈内的元素都是单调递增的,所以 top 是离 i 最近的数,即 top 就是所求值。然后因为 i 可能是 i 右侧的候选解,所以把 i 加入栈中。

image-20211110161325006

image-20211110161410870

image-20211110161448715

image-20211110161517313

同理,我们从右往左遍历数组,就可以得到每一个i右边离i最近且小于arr[i]的位置。

下面我们来看一下代码的实现。

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
ini复制代码class Solution:
def foundMonotoneStack(self , nums):
n=len(nums)
res=[]
l=[0]*n
r=[0]*n
#单调栈
stack=[]
#从左往右遍历数组
for i in range(0,n):
#如果栈顶元素top对应的数组中的元素num[top]>nums[i]
#则出栈,直到栈为空或者栈顶元素对应的数组中的元素比nums[i]小
while stack and nums[stack[-1]] >=nums[i]:
stack.pop()

l[i]=stack[-1] if stack else -1
#i入栈,因为i有可能成为i右边的答案
stack.append(i)

stack=[]
#从右往左遍历数组
for i in range(n-1,-1,-1):
# 如果栈顶元素top对应的数组中的元素num[top]>nums[i]
# 则出栈,直到栈为空或者栈顶元素对应的数组中的元素比nums[i]小
while stack and nums[stack[-1]] >= nums[i]:
stack.pop()
r[i] = stack[-1] if stack else -1
# i入栈,因为i有可能成为i左边的答案
stack.append(i)

for i in range(0,len(l)):
res.append([l[i],r[i]])

return res

s=Solution()
print(s.foundMonotoneStack([3,4,1,5,6,2,7]))

该算法的时间复杂度是O(n),空间复杂度是O(n)。

##每日温度

739. 每日温度

问题描述

请根据每日气温列表 temperatures ,计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例:

输入:temperatures = [73,74,75,71,69,72,76,73]

输出:[1,1,4,2,1,1,0,0]

分析问题

既然是求每一天需要等几天才会有更高的温度,那么最直观的想法就是针对气温列表 temperatures 中的每个温度值,向后依次进行搜索,找到第一个比当前温度更高的值的位置索引,然后减去当前温度所在的位置索引,就是要求的结果。

下面我们来看一下代码的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码class Solution(object):
def dailyTemperatures(self,temperatures):
#求出温度列表的长度
n = len(temperatures)
result=[0]*n
#遍历每一个温度值
for i in range(n):
if temperatures[i]<100:
#想后搜索第一个大于当前温度值的元素
for j in range(i+1,n):
if temperatures[j] > temperatures[i]:
result[i]=j-i
break

return result

该算法的时间复杂度是O(n^2),空间复杂度是O(n)。

显然该算法的时间复杂度太高,那我们有什么优化的方法吗。

优化

这里可以使用单调栈来优化,即维护一个温度值下标的单调栈,使得从栈顶到栈底的的元素对应的温度值依次递减。具体来说,我们正向遍历温度列表 temperatures。对于温度列表中的每个元素 temperatures[i]。如果栈为空,则直接将 i 进栈;如果栈不为空,则比较栈顶元素 prev 对应的温度值 temperatures[prev] 和 当前温度 temperatures[i]。

  • 如果 temperatures[prev] < temperatures[i],则将栈顶元素 prev 移除,此时 prev 对应的等待天数为 i - prev。重复上述操作直到栈为空或者栈顶元素对应的温度大于等于当前温度,然后将 i 进栈。
  • 如果 temperatures[prev] > temperatures[i],则直接将元素 i 入栈。

下面我们来思考一个问题,为什么可以在出栈的时候更新等待天数 result[prev] 呢?因为即将进栈的元素 i 对应的温度值 temperatures[i] 一定是 temperatures[prev] 右边第一个比它大的元素。

下面我们来看一个具体的例子,假设温度列表 temperatures = [73,74,75,71,69,72,76,73] 。

初始时,单调栈 stack 为空;等待天数 result 为 [0,0,0,0,0,0,0,0]。

image-20211129124347950

image-20211129124411253

image-20211129124507019

image-20211129124531093

image-20211129124608472

image-20211129124634335

image-20211129124752957

image-20211129124817867

image-20211129124851144

下面我们来看一下代码的实现。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码n = len(temperatures)
#初始化一个空的栈
stack = []
result = [0] * n
for i in range(n):
temperature = temperatures[i]
#如果 temperatures[i] 大于栈顶元素对应的温度值,则栈顶元素出栈。
while stack and temperature > temperatures[stack[-1]]:
prev = stack.pop()
result[prev] = i - prev
stack.append(i)
return result

该算法的时间复杂度是O(n),空间复杂度也是O(n)。

本文转载自: 掘金

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

零信任策略下云上安全信息与事件管理最佳实践 一 SIEM概念

发表于 2021-11-30

简介:随着企业数字化转型的深入推进,网络安全越来越被企业所重视。为了构建完备的安全防御体系,企业通常会引入了防火墙(Firewall)、防病毒系统(Anti-Virus System,AVS)、入侵防御系统(Intrusion Prevention System,IPS)、入侵检测系统(Intrusion Detection System,IDS)、审计系统等大量安全产品,然而这些安全产品往往各自为政、缺乏联动,难以形成有价值的、全面系统的安全态势分析报告,也就难以应对复杂多变的安全威胁。

作者 | 烨陌

来源 | 阿里技术公众号

一 SIEM概念及发展趋势

随着企业数字化转型的深入推进,网络安全越来越被企业所重视。为了构建完备的安全防御体系,企业通常会引入了防火墙(Firewall)、防病毒系统(Anti-Virus System,AVS)、入侵防御系统(Intrusion Prevention System,IPS)、入侵检测系统(Intrusion Detection System,IDS)、审计系统等大量安全产品,然而这些安全产品往往各自为政、缺乏联动,难以形成有价值的、全面系统的安全态势分析报告,也就难以应对复杂多变的安全威胁。

安全信息和事件管理 (SIEM,Security Information and Event Management) 正好可以满足这方面的需求。SIEM可以收集和存储来自各种网络、安全设备等日志和事件,并能够持续分析接入的数据,用以持续地进行威胁检测和合规性检测,帮助提升企业威胁响应能力;另外,SIEM也可以综合所采集的安全日志和事件,提供系统全面地安全报告,以便企业完整地评估系统风险。

1 SIEM简介

Gartner在2021年度的《SIEM市场魔力象限分析(MQ)报告》中将SIEM定义为满足以下客户需求的解决方案:

  • 实时收集安全事件日志和telemetry数据,用于威胁检测和合规性检测;
  • 实时并持续分析接入数据,以检测攻击和其他感兴趣的活动;
  • 调查安全事件以确定其潜在的严重性和对业务的影响;
  • 报告上述活动;
  • 存储相关事件和日志。

2 SIEM发展趋势

与自然界事物的发展规律类似,SIEM也有一个从简单到高级的发展过程。

早期的日志管理系统

日志采集的需求由来已久,在计算机领域,日志一般用于记录计算机操作系统或应用程序运行状态或者外部请求事件。SIEM产生之前,安全场景主要是利用一些日志管理工具收集来自各种网络设备的日志,并进行统一存储,以便当异常事件发生时,可以进行事后的日志审计。

SIM和SEM

到了上世纪90年代末,日志分析的需求逐渐强烈,开始出现了SIM(Security Information Management,安全信息管理)和SEM(Security Event Management,安全事件管理)两种技术。在SIM和SEM发展的早期,两者是分开的,比较公认的理解是:SIM注重安全事件的历史分析和报告,包括取证分析;而SEM则更关注实时事件监控和应急处理,更多的强调事件归一化、关联分析。

SIEM

随着企业在IT建设上的持续投入,企业拥有了更多的网络设备和安全设备,会有更加复杂的网络环境,同时对企业安全也越来越重视。随之而来的是更多的安全数据需要处理,并且希望能够从众多数据中提取出威胁事件和安全情报,用于达到安全防火或合规审计的需求。之前单一的基于日志分析的模式不在适用,需要一种新型的工具满足安全分析场景的需求,SIEM的出现正好匹配上这些需求。

SIEM可以收集企业和组织中所有IT资源(包括网络、系统和应用)产生的安全信息(包括日志、告警等)进行统一的实时监控、历史分析,对来自外部的威胁和内部的违规、误操作行为进行监控、审计分析、调查取证、出具各种报表报告,达到IT资源合规性管理的目标,同时提升企业和组织的安全运营、威胁管理和应急响应能力。

SIEM AS A SERVICE

随着企业数字化转型的深入,企业往往需要有更高级的安全分析的能力,同时SIEM也变得越来越复杂,想要掌握整套的SIEM系统使用能力要求也越来越高。托管式的SIEM出现,很大程度上降低了本地部署的运维成本,提供了更多开箱即用的功能,可以一定程度上降低SIEM的使用门槛,助力企业的安全能力建设。

SIEM AS A UTILITY

未来SIEM可能会作为网络设备的基础功能,作为一个内置工具存在。目前越来越多的云厂商开始将SIEM方案内嵌到自家的云产品中,作为一个基本的功能与云产品基础能力打包售卖。

3 SIEM如何保护企业组织安全?

识别未知威胁:SIEM可以通过针对日志或者安全事件提供实时的数据监控能力,并结合人工智能、威胁情报能力,帮助企业发现潜在的安全风险。

威胁追本溯源:因为安全事件的发生往往会有个很长的持续周期(例如数据库拖库的表象是一次数据库拖库行为,背后可能隐藏着更早之前的某个时间的跳板机密码的泄露),SIEM提供了对于历史数据的分析能力,能够长达几个月甚至更长时间内协助企业发现安全事件发生的蛛丝马迹,还原事件现场。

支持自定义审计:通过SIEM提供的开放的规则引擎,企业可以根据自身的业务场景配置一些持续的审计监控规则,实时监控网络安全。

及时威胁响应:当威胁事件发生时,监控规则会将探测到的异常通过告警等形式通知给相关人员及时进行响应处理,形成问题闭环。

二 数字化时代企业安全面临的全新挑战

1 企业数字化带来了新的安全挑战

传统的网络安全架构理念是基于边界的安全架构,企业构建网络安全体系时,首先要做的是寻找安全边界,把网络划分为外网、内网等不同的区域,然后在边界上部署防火墙、入侵检测、WAF等产品。然而这种网络安全架构是基于内网比外网更安全的假设建立起来,在某种程度上预设了对内网中的人、设备和系统的信任,忽视加强内网安全措施。不法分子一旦突破企业的边界安全防护进入内网,会像进入无人之境,将带来严重的后果。此外,内部人员100%安全的假说也是不成立的,我们可以从《内部威胁成本全球报告》里看到,不管是内部威胁的数量,还是成本从2018年到2020年都有大幅的提升。

此外,随着云计算、大数据、物联网、移动办公等新技术与业务的深度融合,网络安全边界也逐渐变得更加模糊,传统边界安全防护理念面临巨大挑战。在这样的背景下,零信任架构(Zero Trust Architecture, ZTA)应运而生。它打破传统的认证,即信任边界防护、静态访问控制、以网络为中心等防护思路,建立起一套以身份为中心,以持续认证、动态访问控制、审计以及监测为链条,以最小化实时授权为核心,以多维信任算法为基础,认证达末端的动态安全架构。

我们可以看到,零信任策略下,监控无边界、持续监控的需求也给SIEM提出了新的挑战。

2 数字化对工程师的挑战

随着DevOps的逐渐深入人心,工程师的开发职责也逐步发生了变化,开发、测试、运维逐步成为趋势。但是DevOps模式下,安全产品、安全能力其实是外置的,在整个软件生命周期中安全防护只是安全团队的责任,在开发的最后阶段才会介入。然而在DevOps有效推进快速迭代的今天,过时的安全措施则可能会拖累整个开发流程,由此催生出了“DevSecOps”的概念。

DevSecOps认为安全防护是整个 IT 团队的共同责任,需要贯穿至整个生命周期的每一个环节。DevSecOps更多关注的是过程安全,这种安全前置的理念,可以把安全植入到开发、测试、部署的各个环节,从源头上屏蔽掉一些风险。

DevSecOps分为了如下几个阶段,每个阶段都有自己的安全要求。左边的“Dev 段”,聚焦软件开发过程的安全保障;右边的“Ops 段”,聚焦软件运行时安全。具体阶段如下:

  • Plan+Create 阶段,从宏观上可以认为是在进行软件的安全设计与开发前准备,更注重安全规则的制定、安全需求分析、软件设计时的安全考虑;
  • Verify+Preproduction 阶段,即是对开发阶段进行安全保障,可以进行 AST、Fuzz、SCA 等;
  • Predict+Respond 阶段,可以理解为软件的在网安全监测,比如监测和响应安全事件等;
  • Configure+Detect 阶段,可以理解为对应用程序的运行时的安全保障,比如容器和基础设施安全、RASP、WAF 等。

我们可以看到,“Ops 段”涉及的威胁探测、应急响应、威胁预测与SIEM的特性是比较符合的,为了应对DevSecOps中安全融合、快速迭代的要求,对SIEM也提出了轻量化、便捷化的需求。

3 SIEM的全新挑战

基于上述的趋势,我们可以看到零信任(从不信任,始终验证)理念、DevSecOps都给SIEM提出了新的发展要求。新一代的SIEM需要满足零信任下无边界持续动态监控的诉求,就需要监控更广泛的数据,并提供更强的关联分析能力。为了提升DevSecOps的效率,就需要做的更轻量,作为一个基础的工具与DevSecOps进行融合,提供更多开箱即用的功能。同时,与可观测平台一体化融合也是一个发展的趋势。

4 云上一体化SIEM平台

为了适应这些挑战,我们认为一个云上一体化的SIEM平台需要具备如下特征:

  • 平台能力:对包括日志、Metric、Trace和事件的数据提供统一的采集、存储能力。在统一存储的基础上,提供统一的数据处理、分析,并且具备机器学习分析能力。基于可视化的安全态势,告警检测事件管理能力。
  • 业务场景:基于统一的平台上层业务,支撑上层业务方(开发运营、监控、安全、用户运营)。
  • 生态对接:可以对接上下游系统的安全生态支持。

三 Cloud SIEM核心技术及行业方案

1 SIEM的核心技术及挑战

要实现一个SIEM系统,需要经过采集(Collection)-> 探测(Detection)-> 调查(Investigation)-> 响应(Response)四个阶段。四个阶段面临的挑战如下:

  • 采集:如何将数据便捷接入的问题,以及如何低成本存储。
  • 探测:如何通过各种数据的关联分析,捕获未知的威胁。
  • 调查:如何审计安全事件及还原威胁过程。
  • 响应:如何将安全事件通知给用户的能力。

2 常用的行业方案

Splunk的思路是基于“将数据转化为一切(Data-to-Everything)”的平台,提供了一整套融合了SIEM、UEBA、SOAR的完整解决方案。

Elastic以开源为基础,通过Logstash、Elasticsearch、Kibana组合奠定了数据的基本采集、分析、可视化能力。其中,Logstash作为一个日志聚合器,可以收集和处理来自几乎任何数据源的数据;Elasticsearch是存储引擎,用于解析大量数据;Kibana作为可视化层,用于可视化处理及问题分析。Elastic 7.14 版发布了首个免费开放的Limitless XDR,能够在一个平台中提供一体化的 SIEM 和 Endpoint Security 功能。

Exabeam 的 SIEM 解决方案可作为 SaaS(Exabeam Fusion SIEM)使用,也可用于混合、联合部署。它包括 Exabeam数据湖、高级分析、威胁捕获、实体分析、案例管理和事件响应。基于定制部署的模块化架构,用户可以灵活购买。Exabeam 的机器学习 (ML) 驱动的用户和实体行为检测,能为用户提供风险评分和自动的上下文富化能力。

从上述的行业方案我们可以看出SIEM厂商普遍是平台化、SaaS的发展思路。

3 SIEM核心特性及落地方案

基于上文提到的SIEM平台化的思路,我们可以看到SIEM系统一些核心的特性(左图),而右图是我们最佳实践的落地方案。

四 构建Cloud SIEM方案的最佳实践

接下来我们将重点阐述构建Cloud SIEM方案一些最佳实践,主要从如下四个方面展开。

  • 广泛的数据接入:数据采集(特别是云上场景:跨账号、多云)、处理、存储能力。
  • 统一的查询分析能力:交互式的查询分析语法、ML算法支持、可视化分析能力。
  • 威胁探测和响应:使用内置告警规则和自定义规则进行威胁探测,将发现的威胁事件通知给用户,并能够进行事件管理。
  • 安全生态集成:如何与第三方平台集成。

1 广泛的数据接入

构建Cloud SIEM方案,首先要解决的问题是海量数据的统一接入问题。然而目前行业中涉及的接入方案众多,例如日志可能会使用logstash、FluentD等,指标会使用Prometheus等。这也造成了数据接入管理的诸多痛点:

  • 运维成本高:完整的数据接入需要数个软件的协同,从而也带了极高的运维成本。
  • 学习成本高:每个软件都有自己的使用插件及配置规则,学习成本非常高。

我们的方案是建立了标准化的数据接入方式,支持SDK及Agent采集两种方式,可以方便的将各类数据(日志、Metric、Trace、Meta)统一地采集到统一存储系统中。除了支持服务器与应用日志采集外,对于开源软件、标准协议也有很好的支持。最主要的是,针对阿里云云原生场景提供了一键式的采集方案,例如,日志RDS审计日志、K8s审计日志等;并且与阿里云资源目录集成支持跨账号采集的能力(因为很多企业可能会有多个账号,每个账号对应一个部门的业务)。

Cloud SIEM场景下,往往需要采集多种数据源的数据(格式可能比较杂乱),同时也有长时间跨度、海量数据的分析需求,而我们的做法是提供了一套低代码、可扩展的数据加工服务,通过Schema On Write的方式提前进行数据规整,能够为后续的分析处理提供很大的便捷。

数据加工服务可以对结构化或非结构化的日志进行实时的ETL处理。该功能目前包含200+算子,广泛应用于数据规整、数据聚合、富化、分发等场景。对于安全场景,数据加工也有很好的安全类算子支持。例如,数据加工提供的数据脱敏算子,可以有效地减少敏感数据在加工、传输、使用等环节中的暴露,降低敏感数据泄露的风险,保护用户权益。常见脱敏场景有为手机号、银行卡号、邮箱、IP、AK、身份证号网址、订单号、字符串等敏感信息脱敏。

2 统一的数据查询分析能力

Cloud SIEM系统一个重要的能力就是对采集到的数据,进行实时的合规监控分析,支持对历史数据的合规审计,对来自外部的威胁和内部的违规进行审计分析。但是,安全威胁的方法往往是一个逐步的过程,可能需要几个月或更长的时间才会真正暴露出来;此外,安全威胁可能需要多种数据的联动分析才能发现。

为了应对这些挑战,我们将日志、指标、Meta等数据全部接入到统一的存储中,在此之上,我们构建了一套统一的查询分析引擎,用于支撑查询分析、可视化、监控告警、AI 等上层能力。

基于统一的存储,我们构建的统一的查询分析引擎,以标准 SQL 为基础,进行了SQL 函数扩展,并融合了 PromQL,从而让不同的数据之间进行联合查询也变成了可能。

SLS SQL = Search + SQL92(Agg,WIndow,GroupBy…)+ PromQL + …

以下就是一个复杂分析的例子:

  • 我们可以通过标准 SQL 语句对日志进行分析。
  • 还可以通过 PromQL 扩展的 SQL 函数对指标数据进行分析
  • 还可以通过嵌套查询,对指标数据的分析结果进行再聚合
  • 此外还可以再通过机器学习函数,给查询和分析赋予 AI 的能力

虽然不同阶段的数据产生自不同的系统,也有着不同的格式,但是由于它们的存储和分析是一致的,我们可以非常轻松地实现统一的安全态势及安全事件监控。

同时,提供了大量基于AI的巡检、预测、聚类、根因分析等算法,以SQL/DSL函数的形式向用户提供,在人工分析和自动巡检告警中都能使用到。

3 威胁探测与响应

通过上文提到的统一的数据接入、统一的查询分析能力,我们可以做到对安全威胁的基本的探测能力。但是要构建完备的监控体系,接下来就要解决如何持续监控的问题。基于这个问题,我们开发了一套一站式智能运维告警系统。它提供对日志、时序等各类数据的告警监控,亦可接受三方告警,对告警进行降噪、事件管理、通知管理等。

我们提供了超过数百个内置告警规则,开箱即用并持续增加中。这些规则库有覆盖了CIS(覆盖了账号安全、数据库安全等)和安全场景的最佳实践,用户仅需开启对应规则,即可享受到全天候的安全保障。

当告警规则探测到异常发生时,需要尽快的将威胁事件通知给相应的开发人员。我们对接了丰富的通知渠道,便于威胁事件的全方位触达。

  • 多渠道:支持短信、语音、邮件、钉钉、企业微信、飞书、Slack等多种通知渠道,同时还支持通过自定义 Webhook 进行扩展。同一个告警,支持同时通过多个渠道、每个渠道使用不同的通知内容进行发送。例如通过语音和钉钉来进行告警通知,既可以保证触达强度,又可以保证通知内容的丰富程度。
  • 动态通知:可以根据告警属性动态分派通知。例如:测试环境的告警,通过短信通知到张三,并且只在工作时间通知;而生产环境的告警,通过电话通知到张三和李四,并且无论何时,都要进行通知。
  • 通知升级:长时间未解决的告警要进行升级。例如某告警触发后,通过短信通知到了某员工,但是该问题长时间未被处理,导致告警一直没有恢复,此时需要通知升级,通过语音的方式通知到该员工的领导。

安全事件发生后,如果不及时处理或不慎遗漏都会造成更大的安全风险扩展。因此,一定要建立完备的反馈机制,将安全问题处理形成闭环。基于这个问题,我们提供了安全事件管理中心,便于用户全局查看安全事件,并进行相应的管理动作。当开发或安全人员接收到安全告警事件通知后,可以登陆安全事件管理中心进行事件的确认、处理人的指派、处理动作记录等操作。

最后,我们提供了安全态势大盘,帮助用户全局了解安全事件、安全态势,便于进行告警链路查看及排错使用。此外,报表还可自由扩展。

4 安全生态集成

现代企业上云后,有时会将业务部署在多家云厂商上,那么安全场景可能就涉及多家云厂商数据的同步问题。我们提供了与第三方SIEM方案(例如Splunk)对接的方式,以便确保阿里云上的所有法规、审计、与其他相关日志能够导入到用户的安全运维中心(SOC)中。

五 总结

我们总体上介绍了SIEM的背景趋势、以及数字化时代新的挑战,并且介绍了构建云上SIEM的最佳实践。

另外,我们可以看到云上SIEM也在朝着平台化、SaaS化的方向发展,并且不断的优化以适应新的业务挑战。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

C++ 中用于动态内存的 的 new 和 delete 运算

发表于 2021-11-30

「这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战」

C/C++ 中的动态内存分配是指由程序员手动进行内存分配。动态分配的内存在堆上分配,非静态和局部变量在堆栈上分配内存。

什么是应用程序?

  • 动态分配内存的一种用途是分配可变大小的内存,这对于编译器分配的内存是不可能的,除了可变长度数组。
  • 最重要的用途是提供给程序员的灵活性。我们可以在需要和不再需要时自由分配和释放内存。这种灵活性在很多情况下都有帮助。此类情况的示例是Linked List、Tree等。

它与分配给普通变量的内存有何不同?

对于“int a”、“char str[10]”等普通变量,内存会自动分配和释放。对于像“int *p = new int[10]”这样的动态分配内存,程序员有责任在不再需要时释放内存。如果程序员不释放内存,则会导致内存泄漏(直到程序终止内存才会释放)。

在 C++ 中如何分配/释放内存?

C 使用malloc() 和 calloc()函数在运行时动态分配内存,并使用 free() 函数释放动态分配的内存。C++ 支持这些函数并且还有两个操作符new和delete以更好、更简单的方式执行分配和释放内存的任务。

这篇文章是关于 new 和 delete 操作符的。

new 运算符

new 运算符表示在 Free Store 上分配内存的请求。如果有足够的内存可用,new 操作符会初始化内存并将新分配和初始化的内存的地址返回给指针变量。

  • 使用 new 运算符的语法:要分配任何数据类型的内存,语法为:
1
c++复制代码pointer-variable = new data-type;
  • 这里,指针变量是数据类型类型的指针。数据类型可以是任何内置数据类型,包括数组或任何用户定义的数据类型,包括结构和类。

例子:

1
2
3
4
5
6
7
8
c++复制代码// 用 NULL 初始化的指针然后为变量请求内存
int *p = NULL;
p = 新整数;

或

// 结合指针的声明和它们的赋值
int *p = new int;
  • 初始化内存: 我们还可以使用 new 运算符为内置数据类型初始化内存。对于自定义数据类型,需要一个构造函数(以数据类型作为输入)来初始化值。这是两种数据类型初始化的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
c++复制代码指针变量 =新数据类型(值);
示例:
int *p = new int(25);
float *q = new float(75.25);

// 自定义数据类型
struct cust
{
int p;
cust(int q) : p(q) {}
};

cust* var1 = new cust;// 工作正常,不需要构造函数

或

cust* var1 = new cust(); // 工作正常,不需要构造函数


cust* var = new cust(25) // 如果注释此行,请注意错误
  • 分配内存块: new 运算符也用于分配数据类型的内存块(数组)。
1
c++复制代码指针变量=新数据类型[大小];
  • 其中 size(a variable) 指定数组中元素的数量。
1
2
c++复制代码示例:
int *p = new int[10]
  • 为连续 10 个 int 类型的整数动态分配内存,并返回指向序列第一个元素的指针,该元素被分配给 p(a pointer)。p[0] 指的是第一个元素,p[1] 指的是第二个元素,依此类推。

普通数组声明与使用 new

声明普通数组和使用 new 分配内存块之间存在差异。最重要的区别是,普通数组由编译器释放(如果数组是本地的,则在函数返回或完成时释放)。然而,动态分配的数组总是保留在那里,直到它们被程序员释放或程序终止。

如果在运行时没有足够的内存可用怎么办?

如果堆中没有足够的内存可供分配,则新请求通过抛出类型为 std::bad_alloc 的异常指示失败,除非“nothrow”与 new 运算符一起使用,在这种情况下它返回一个 NULL 指针。因此,在使用 new 程序之前检查 new 产生的指针变量可能是个好主意。

1
2
3
4
5
c++复制代码int *p = new(nothrow) int; 
if (!p)
{
cout << "内存分配失败\n";
}

删除操作符

由于释放动态分配的内存是程序员的责任,因此 C++ 语言为程序员提供了删除运算符。

句法:

1
2
c++复制代码// 释放指针变量指向的内存
delete pointer-variable;

这里,pointer-variable 是指向new创建的数据对象的指针。

例子:

1
2
c++复制代码  delete p;
delete q;

要释放指针变量指向的动态分配数组,请使用以下形式的delete:

1
2
3
4
5
6
c++复制代码// 释放指针变量所指向的内存块
delete[] pointer-variable;

示例:
// 它将释放 p 指向的整个数组。
delete[] p;
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
c++复制代码#include <iostream>
using namespace std;

int main ()
{
int* p = NULL;

p = new(nothrow) int;
if (!p)
cout << "allocation of memory failed\n";
else
{
*p = 29;
cout << "Value of p: " << *p << endl;
}

float *r = new float(75.25);

cout << "Value of r: " << *r << endl;

int n = 5;
int *q = new(nothrow) int[n];

if (!q)
cout << "allocation of memory failed\n";
else
{
for (int i = 0; i < n; i++)
q[i] = i+1;

cout << "Value store in block of memory: ";
for (int i = 0; i < n; i++)
cout << q[i] << " ";
}

delete p;
delete r;

delete[] q;

return 0;
}

输出:

1
2
3
c++复制代码Value of p: 29
Value of r: 75.25
Value store in block of memory: 1 2 3 4 5

我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:cloud.tencent.com/developer/s…

本文转载自: 掘金

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

基于mybatis的数据库脱敏

发表于 2021-11-30
  • 背景
  • 思路
  • 实现
  • 思考

背景

最近接到需求需要对数据库中的电话、身份证号等敏感信息进行脱敏加密处理,再加上之前面试时也被问到相关问题,所有在此记录。脱敏对象是数据库的字段,所以在数据库存取的出入口进行加解密操作是最合适的,项目中使用mybatis作为ORM框架,所以使用基于mybatis的数据库脱敏。

思路

对数据库中的数据进行脱敏处理,核心思想就是在入库时对敏感字段进行加密,在出库时对敏感字段解密。看清了这个问题,我们的关注点就有两个。

  1. 何时?入库和出库
  2. 何地?入参和查询结果

mybatis框架中的plugin,能够对上面两个关注点进行很好的控制,再结合自定义注解,对需要脱敏的字段进行标注,就能够满足我们的需求。

实现

理论知识储备

  • mybatis interceptor执行流程与原理(blog.csdn.net/weixin_3949…)
  • 自定义注解
  • 反射
  1. 定义自定义注解,用于标识敏感字段
1
2
3
4
5
6
7
8
9
10
less复制代码/**
* 标识字段入库信息需要加密
* @see com.vcg.veer.sign.utils.DesUtils
* @author zhouyao
* @date 2021/10/27 9:22 上午
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Encrypt {
}
  1. mybatis插件逻辑(对项目中使用的pagehelper和mybatis-processor插件兼容)
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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
ini复制代码
/**
* 敏感字段入库、出库处理
*
* @author zhouyao
* @date 2021/10/27 9:25 上午
**/
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
}
)
public class EncryptInterceptor implements Interceptor {

private final String EXAMPLE_SUFFIX = "Example";


@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
Class<?> argClass = parameter.getClass();
String argClassName = argClass.getName();
//兼容mybatis-processor
if (needHandleExample(argClassName)){
handleExample(args);
}else{
//自定义的mapper文件增删查改参数处理
handleCustomizeMapperParams(args);
}

//update 方法
if (args.length == 2 ){
return invocation.proceed();
}
//兼容pagehelper
if(args.length == 4){
RowBounds rowBounds = (RowBounds) args[2];
ResultHandler resultHandler = (ResultHandler) args[3];
Executor executor = (Executor) invocation.getTarget();
CacheKey cacheKey;
BoundSql boundSql;
//4 个参数时
boundSql = ms.getBoundSql(parameter);
cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
List<Object> queryResult = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
//处理需要解密的字段
decryptFieldIfNeeded(queryResult);
return queryResult;
}

return invocation.proceed();
}

/**
* 对数据进行解密
* @param queryResult
*/
private void decryptFieldIfNeeded(List<Object> queryResult) throws IllegalAccessException {
if (CollectionUtils.isEmpty(queryResult)) {
return;
}
Object o1 = queryResult.get(0);
Class<?> resultClass = o1.getClass();
Field[] resultClassDeclaredFields = resultClass.getDeclaredFields();
List<Field> needDecryptFieldList = new ArrayList<>();
for (Field resultClassDeclaredField : resultClassDeclaredFields) {
Encrypt encrypt = resultClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt == null){
continue;
}
Class<?> type = resultClassDeclaredField.getType();
if (!String.class.isAssignableFrom(type)){
throw new IllegalStateException("@Encrypt should annotated on String field");
}
needDecryptFieldList.add(resultClassDeclaredField);
}
if (CollectionUtils.isEmpty(needDecryptFieldList)){
return;
}
for (Field field : needDecryptFieldList) {
field.setAccessible(true);
for (Object o : queryResult) {
String fieldValue = (String) field.get(o);
if (!StringUtils.hasText(fieldValue)){
continue;
}
field.set(o,DesUtils.decrypt(fieldValue));
}
}
}

/**
* 处理自定义mapper参数
* @param args
*/
private void handleCustomizeMapperParams(Object[] args) throws Exception {
Object param = args[1];
encryptObjectField(param);
}

private void encryptObjectField(Object param) throws Exception {
Class<?> paramClass = param.getClass();
//mybatis @param注解会处理为多参数
if (Map.class.isAssignableFrom(paramClass)){
Map mapParam = (Map) param;
Set<Object> params = new HashSet<>();
params.addAll(mapParam.values());
for (Object o : params) {
encryptObjectField(o);
}
return;
}
Field[] paramClassDeclaredFields = paramClass.getDeclaredFields();
// 遍历参数的所有字段查找需要加密的字段
for (Field paramClassDeclaredField : paramClassDeclaredFields) {
Encrypt encrypt = paramClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt != null){
//加密
encryptField(param,paramClassDeclaredField);
}
}
}

/**
* 给指定字段加密
* @param targetObj
* @param paramClassDeclaredField
*/
private void encryptField(Object targetObj, Field paramClassDeclaredField) throws Exception {
paramClassDeclaredField.setAccessible(true);
Class<?> type = paramClassDeclaredField.getType();
Object fieldValue = paramClassDeclaredField.get(targetObj);
if (fieldValue == null){
return;
}

if (Collection.class.isAssignableFrom(type)) {
try {
Collection<String> collection = (Collection<String>) fieldValue;
List<String> tempList = new ArrayList<>();
Iterator<String> iterator = collection.iterator();
while (iterator.hasNext()) {
String next = iterator.next();
tempList.add(DesUtils.encrypt(next));
iterator.remove();
}
collection.addAll(tempList);
}catch (Exception ex){
//加密字段参数只支持String类型
throw new IllegalArgumentException("Encrypted fields only support String type");
}
}
else if(String.class.isAssignableFrom(type)){
//基础数据类型直接设值
paramClassDeclaredField.set(targetObj, DesUtils.encrypt(fieldValue.toString()));
}
else if (isBasicType(type)) {
//加密字段参数只支持String类型
throw new IllegalArgumentException("Encrypted fields only support String type");
} else {
//递归调用
encryptObjectField(fieldValue);
}
}

private boolean isBasicType(Class<?> clz) {
try {
return ((Class) clz.getField("TYPE").get(null)).isPrimitive();
} catch (Exception e) {
return false;
}
}

//兼容processor
private void handleExample(Object[] args) throws Exception {
Object arg = args[1];
Class<?> argClass = arg.getClass();
String argClassName = argClass.getName();
//兼容 mybatis-processor
if (argClassName.endsWith(EXAMPLE_SUFFIX)) {
//实体类的类名
String modelClassName = argClassName.substring(0, argClassName.length() - 7);
Class<?> modelClass;
try {
modelClass = Class.forName(modelClassName);
}catch(ClassNotFoundException ex){
return;
}

Method getCriteria = argClass.getDeclaredMethod("getCriteria");
getCriteria.setAccessible(true);
Object criteria = getCriteria.invoke(arg);
Class<?> criteriaClass = criteria.getClass();
Method getAllCriteria = criteriaClass.getDeclaredMethod("getAllCriteria");
Set<Object> criterions = (Set<Object>) getAllCriteria.invoke(criteria);
for (Object criterionObj : criterions) {
Class<?> criterionClass = criterionObj.getClass();
Method getCondition = criterionClass.getDeclaredMethod("getCondition");
String condition = (String) getCondition.invoke(criterionObj);
//列名
String[] conditionParts = condition.split(" ");
if (conditionParts.length != 2){
continue;
}
String columnName = conditionParts[0];
//操作 >=< like
String operateType = conditionParts[1];
Field[] modelClassDeclaredFields = modelClass.getDeclaredFields();
for (Field modelClassDeclaredField : modelClassDeclaredFields) {
Column annotation = modelClassDeclaredField.getAnnotation(Column.class);
if (annotation == null){
continue;
}
if (columnName.equalsIgnoreCase(annotation.name())){
Encrypt encrypt = modelClassDeclaredField.getDeclaredAnnotation(Encrypt.class);
if (encrypt != null) {
//加密字段只能用等于比较
if (!"=".equalsIgnoreCase(operateType)) {
throw new IllegalArgumentException("encrypt field only can be operate by '='");
}
Field value = criterionClass.getDeclaredField("value");
value.setAccessible(true);

List<Integer> list = new ArrayList<>();
list.add(1);
//重新设置参数
value.set(criterionObj,list);

break;
}
break;
}

}
}
}
}

/**
* 判断是否需要处理Example类型的查询
* @param argClassName
* @return
*/
private boolean needHandleExample(String argClassName) {
return argClassName.endsWith(EXAMPLE_SUFFIX);
}

private Object decryptIfNeeded(Invocation invocation) throws InvocationTargetException, IllegalAccessException {
return invocation.proceed();
}

@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}

@Override
public void setProperties(Properties properties) {
Interceptor.super.setProperties(properties);
}
}
  1. 插件的使用

在项目启动时注册插件(注意,根据mybatis插件的执行原理,此插件需要在最后注册,才能保证最先解析参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
//加解密插件
EncryptInterceptor encryptInterceptor = new EncryptInterceptor();

//分页插件
PageInterceptor pageInterceptor = new PageInterceptor();
Properties properties = new Properties();
properties.setProperty("reasonable", "true");
properties.setProperty("supportMethodsArguments", "true");
properties.setProperty("returnPageInfo", "check");
properties.setProperty("params", "count=countSql");
pageInterceptor.setProperties(properties);

//添加插件
bean.setPlugins(pageInterceptor,encryptInterceptor);

对需要加密处理的字段标注@Encrypt注解(入参和结果DTO对象字段都需要标注)

1
2
typescript复制代码    @Encrypt
private String mobile;

思考

通过mybatis的插件对数据库的增删改查实现脱敏处理还是比较简单的。重点就在于:

  1. 拦截Executor对象的query和update方法,获取查询/更新参数和查询结果集
  2. 通过反射对参数中标注自定义注解的字段进行加/解密处理

在开发过程中也遇到了由于使用了pagehelper插件,导致自定义拦截器不生效的问题,最后查阅pagehelper的文档解决了(需要根据pagehelper定义的拦截器编写规范来开发)。

完整的代码参考:

github.com/zhouyao423/…

参考文档:

pagehelper interceptor高级用法

mybatis interceptor

本文转载自: 掘金

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

走进Java接口测试之简单快速的Mock Server Mo

发表于 2021-11-30

「这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战」

一、引言

在上文中,我们介绍 Mock 的基本概念,本文我们将详细介绍其中一个快速简单Mock Server Moco。

二、简介

简单来说 Moco 就是类似一个 Mock 的工具框架,一个简单搭建模拟服务器的程序库 / 工具,下载就是一个JAR包。
在 Moco 的 github 上面有这段话。

Integration, especially based on HTTP protocol, e.g. web service, REST etc, is wildly used in most of our development.
In the old days, we just deployed another WAR to an application server, e.g. Jetty or Tomcat etc. As we all know, it’s so boring to develop a WAR and deploy it to any application server, even if we use an embeded server. And the WAR needs to be reassembled even if we just want to change a little bit.

翻译过来:

集成,特别是基于 HTTP 协议的集成,例如 web 服务、REST 等,在我们的大多数开发中都被广泛使用。
在过去,我们只是将另一场 WAR 包部署到应用服务器上,例如 Jetty 或Tomcat 等。众所周知,开发一个 WAR 包并将其部署到任何应用服务器上是非常枯燥的,即使我们使用的是嵌入式服务器。war包也需要被重新打包即使我们只是想稍微改变一下。
简单来说,Moco 就是解决了开发前端时没有后端支持,开发接口时依赖没有到位的尴尬场景。当然 Moco 的灵活性,让其有越来越多的应用场景,比如我们在开发接口测试的时候。

特点:

  • 只需要简单的配置 request、response 等即可满足要求,支持 http、https、socket 。可以说是非常的灵活性。
  • 支持在 request 中设置 Headers , Cookies , StatusCode 等。
  • 对 GET、POST、PUT、DELETE 等请求方式均支持,很适合 web 开发。
  • 无需环境配置,有 Java 环境即可。
  • 修改配置后,立刻生效。只需要维护接口,也就是契约即可。
  • 对可能用到的数据格式都支持,如 Json、text、xml、file 等。
  • 还能与其他工具集成,如 Junit、Maven、Gradle 等。

三、原理

Moco 本身支持 API 和独立运行两种方式。通过 API ,开发人员可以在Junit、TestNg 等测试框架里使用 Moco,这样极大地降低了接口测试的复杂度。
Moco 根据一些配置,启动一个真正的 HTTP 服务(监听本地指定端口)。当发起的请求满足一个条件时,就会收到一个 response 。Moco 底层并没有依赖于像 Servlet 这样的重型框架,而是基于 Netty 的网络应用框架编写的,这样就绕过了复杂的应用服务器,所以它的速度是极快的。

四、使用

加载配置启动 Moco HTTP Server

1
bash复制代码java -jar <moco-runner-path> http -p <port> -c <configfile-path>

启动命令参数含义:

  • moco-runner-path :moco-runner-0.11.0-standalone.jar 包路径。
  • port :HTTP 服务监听端口。
  • configfile-path :配置文件路径

下面介绍不同的 HTTP 服务,以及如何设置 JSON 文件的参数

在本地启动一个 http 服务器,其中监听端口是 12306,配置文件是 JSON 文件。只需要本机发起一个request,如:http://localhost:12306

1、约定请求 URI

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
json复制代码[
{
"description":"这是一个请求URI",
"request":{
"uri":"/7d"
},
"response":{
"text":"success!"
}
}
]

启动命令

1
bash复制代码java -jar moco-runner-0.11.0-standalone.jar http -p 12306 -c ./src/main/resources/startupURI.json

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码09 十二月 2018 11:06:50 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Postman-Token: 7b5a1a47-a287-4674-b94e-c455fc5c645a
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 11:06:50 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

2、约定请求 Queries

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个请求queries",
"request":{
"uri":"/7d",
"queries":{
"name":"zuozewei"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码09 十二月 2018 11:21:04 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Postman-Token: 2d36e386-e022-4478-8acd-258eff4ff684
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 11:21:04 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

3、约定请求 Get 方法

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码[
{
"description":"这是一个get请求",
"request":{
"uri":"/7d",
"method":"get"
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Get 请求
在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码09 十二月 2018 11:26:42 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Postman-Token: ae3250b6-0ec0-4875-8970-d37e5b840820
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 11:26:42 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

4、约定请求 Post 方法

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码[
{
"description":"这是一个post请求",
"request":{
"uri":"/7d",
"method":"post"
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码09 十二月 2018 11:29:30 [nioEventLoopGroup-3-2] INFO  Request received:

POST /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Postman-Token: 73f38af1-4efb-473a-b9d2-de0392c65bbe
X-Lantern-Version: 5.1.0

09 十二月 2018 11:29:30 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

5、约定请求 Headers

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码[
{
"description":"这是一个带headers的post请求",
"request":{
"uri":"/7d",
"method":"post",
"headers":{
"content-type":"application/json"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 11:34:43 [nioEventLoopGroup-3-2] INFO  Request received:

POST /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: application/json
Postman-Token: 0a82d74b-303f-42a3-9da0-32fd6c604166
X-Lantern-Version: 5.1.0

09 十二月 2018 11:34:43 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

6、约定请求 Cookies

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码[
{
"description":"这是一个带cookies的post请求",
"request":{
"uri":"/7d",
"method":"post",
"cookies":{
"login":"7dgroup"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 12:26:46 [nioEventLoopGroup-3-3] INFO  Request received:

POST /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Cookie: login=7dgroup
Postman-Token: 36a12412-6eb1-44a4-a2d8-ea222eba8968
X-Lantern-Version: 5.1.0

09 十二月 2018 12:26:46 [nioEventLoopGroup-3-3] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

7、约定请求 Forms

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码[
{
"description":"这是一个带forms参数的post请求",
"request":{
"uri":"/7d",
"method":"post",
"forms":{
"name":"zuozewei"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
bash复制代码09 十二月 2018 12:50:47 [nioEventLoopGroup-3-3] INFO  Request received:

POST /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 167
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------977669308391204172275520
Postman-Token: 308d06bf-c110-4736-9ac4-ee2fe8a4a036
X-Lantern-Version: 5.1.0

<content is binary>

09 十二月 2018 12:50:47 [nioEventLoopGroup-3-3] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

8、约定请求 URI (Match)

对于 Restful 风格的 url ,支持正则匹配。
JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个请求Match URI",
"request":{
"uri":
{
"match":"/\\w*/7d"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 13:05:48 [nioEventLoopGroup-7-2] INFO  Request received:

POST /wzuozewei/7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------767805110351846142172059
Postman-Token: 5d7b5c65-1f8b-46ae-8868-62def1a5de31
X-Lantern-Version: 5.1.0

09 十二月 2018 13:05:48 [nioEventLoopGroup-7-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

9、约定请求 URI (StartsWith)

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个请求StartsWith URI",
"request":{
"uri":
{
"startsWith":"/7d"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 13:12:43 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d/zuozewei HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------445269276531904972620891
Postman-Token: f9deca3a-9b59-426c-ad48-00ebb4800321
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:12:43 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

10、约定请求 URI (endsWith)

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个请求endsWith URI",
"request":{
"uri":
{
"endsWith":"/7d"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 13:16:48 [nioEventLoopGroup-3-2] INFO  Request received:

GET /zuozewei/7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------516453569550782372688423
Postman-Token: 774378a6-5e57-4cc2-a015-f4b3bd2cb84d
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:16:48 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

11、约定请求 URI (Contain)

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个请求Contain URI",
"request":{
"uri":
{
"contain":"/7d"
}
},
"response":{
"text":"success!"
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 13:20:28 [nioEventLoopGroup-3-2] INFO  Request received:

GET /zuozewei/7d/12345 HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------030965700716204296542028
Postman-Token: 7615db1b-77e1-40f7-bdc3-e464c4e7269a
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:20:28 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 10
Content-Type: text/plain; charset=utf-8

success!

12、约定指定 Json 响应

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码[
{
"description":"这是一个指定Json响应的post请求",
"request":{
"uri":"/7d",
"method":"post"
},
"response":{
"json":{
"name":"success",
"code":"1"
}
}
}
]

通过 Postman 验证服务,测试 Post 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码09 十二月 2018 13:25:19 [nioEventLoopGroup-3-2] INFO  Request received:

POST /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Content-Length: 0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------703341725381001692596870
Postman-Token: e5686919-85b9-44d0-8a73-61bf804b6377
X-Lantern-Version: 5.1.0

09 十二月 2018 13:25:19 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Content-Length: 29
Content-Type: application/json; charset=utf-8

{"name":"success","code":"1"}

13、约定响应 Status

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
json复制代码[
{
"description":"这是指定响应status的get请求",
"request":{
"uri":"/7d",
"method":"get"
},
"response":{
"status":200
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码09 十二月 2018 13:29:07 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------465777039297587100709267
Postman-Token: 791fa21c-386f-4389-aaa9-ba06d9e53aff
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:29:07 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200

14、约定响应 Headers

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个get请求",
"request":{
"uri":"/7d",
"method":"get"
},
"response":{
"headers":{
"content-type":"application/json"
}
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码09 十二月 2018 13:34:22 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------774041889819140984857561
Postman-Token: 0a51f958-0338-4afa-8ff6-af45d61e12a7
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:34:22 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
content-type: application/json

15、约定响应 Cookies

JSON 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
"description":"这是一个响应Cookies的get请求",
"request":{
"uri":"/7d",
"method":"get"
},
"response":{
"cookies":{
"login":"7dgroup"
}
}
}
]

通过 Postman 验证服务,测试 Get 请求

在这里插入图片描述

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码09 十二月 2018 13:39:00 [nioEventLoopGroup-3-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------315176206881627055625168
Postman-Token: f6d1ae6b-c1c2-474a-827c-f02ed3f23482
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:39:00 [nioEventLoopGroup-3-2] INFO Response return:

HTTP/1.1 200
Set-Cookie: login=7dgroup; Path=/

16、约定重定向 RedirectTo

JSON 脚本

1
2
3
4
5
6
7
8
9
10
json复制代码[
{
"description":"这是一个重定向的get请求",
"request":{
"uri":"/7d",
"method":"get"
},
"redirectTo":"http://blog.csdn.net/zuozewei"
}
]

通过浏览器验证服务,测试 Get 请求
http://127.0.0.1:12306/7d

Moco 服务日志

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码09 十二月 2018 13:43:58 [nioEventLoopGroup-4-2] INFO  Request received:

GET /7d HTTP/1.1
Host: 127.0.0.1:12306
User-Agent: PostmanRuntime/7.4.0
Accept: */*
Accept-Encoding: gzip, deflate
Cache-Control: no-cache
Content-Type: multipart/form-data; boundary=--------------------------167408129884853494096695
Cookie: login=7dgroup
Postman-Token: f83696d4-37ba-45b6-aff6-6f20982673ac
X-Lantern-Version: 5.1.0
Content-Length: 0

09 十二月 2018 13:43:58 [nioEventLoopGroup-4-2] INFO Response return:

HTTP/1.1 302
Location: http://blog.csdn.net/zuozewei

五、小结

Moco 的使用很简单,配置也很方便,目前更是提供了 http、rest、socket 服务。但是也仅仅是能 stub 接口,模拟出简单的场景。如果接收到请求后需要做一些处理,如需查询数据库、进行运算、或者一些复杂的操作,就无能为力了。所以是否选用 Moco,就取决于测试人员是否只是需要一个简单的模拟服务器。

本文源码:

  • github.com/zuozewei/bl…

本文转载自: 掘金

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

关于函数的学习(五) — 传递任意数量的实参

发表于 2021-11-30

传递任意数量的实参

有时候,你预先不知道函数需要接受多少个实参,好在Python允许函数从调用语句中收集任意数量的实参。
例如,来看一个制作比萨的函数,它需要接受很多配料,但你无法预先确定顾客要多少种配料。下面的函数只有一个形参*toppings,但不管调用语句提供了多少实参,这个形参都将它们统统收入囊中:

1
2
3
4
5
handlebars复制代码def make_pizza(*toppings):
"""打印顾客点的所有配料"""
print(toppings)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

形参名*toppings中的星号让Python创建一个名为toppings的空元组,并将收到的所有值都封装到这个元组中。函数体内的print语句通过生成输出来证明Python能够处理使用一个值调用函数的情形,也能处理使用三个值来调用函数的情形。它以类似的方式处理不同的调用,注意,Python将实参封装到一个元组中,即便函数只收到一个值也如此:

1
2
handlebars复制代码('pepperoni',)
('mushrooms', 'green peppers', 'extra cheese')

现在,我们可以将这条print语句替换为一个循环,对配料列表进行遍历,并对顾客点的比萨进行描述:

1
2
3
4
5
6
7
handlebars复制代码def make_pizza(*toppings):
"""概述要制作的比萨"""
print("\nMaking a pizza with the following toppings:")
for topping in toppings:
print("- "+topping)
make_pizza('pepperoni')
make_pizza('mushrooms', 'green peppers', 'extra cheese')

不管收到的是一个值还是三个值,这个函数都能妥善地处理:

1
2
3
4
5
6
handlebars复制代码Making a pizza with the following toppings:
- pepperoni
Making a pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

不管函数收到的实参是多少个,这种语法都管用。

1.结合使用位置实参和任意数量实参

如果要让函数接受不同类型的实参,必须在函数定义中将接纳任意数量实参的形参放在最后。Python先匹配位置实参和关键字实参,再将余下的实参都收集到最后一个形参中。
例如,如果前面的函数还需要一个表示比萨尺寸的实参,必须将该形参放在形参*toppings的前面:

1
2
3
4
5
6
7
8
handlebars复制代码def make_pizza(size, *toppings):
"""概述要制作的比萨"""
print("\nMaking a "+str(size)+
"-inch pizza with the following toppings:")
for topping in toppings:
print("- "+topping)
make_pizza(16, 'pepperoni')
make_pizza(12, 'mushrooms', 'green peppers', 'extra cheese')

基于上述函数定义,Python将收到的第一个值存储在形参size中,并将其他的所有值都存储在元组toppings中。在函数调用中,首先指定表示比萨尺寸的实参,然后根据需要指定任意数量的配料。
现在,每个比萨都有了尺寸和一系列配料,这些信息按正确的顺序打印出来了——首先是尺寸,然后是配料:

1
2
3
4
5
6
handlebars复制代码Making a 16-inch pizza with the following toppings:
- pepperoni
Making a 12-inch pizza with the following toppings:
- mushrooms
- green peppers
- extra cheese

2.*使用任意数量的关键字实参

有时候,需要接受任意数量的实参,但预先不知道传递给函数的会是什么样的信息。在这种情况下,可将函数编写成能够接受任意数量的键*—值对——调用语句提供了多少就接受多少。一个这样的示例是创建用户简介:你知道你将收到有关用户的信息,但不确定会是什么样的信息。在下面的示例中,函数build_profile()接受名和姓,同时还接受任意数量的关键字实参:

1
2
3
4
5
6
7
8
9
10
11
12
handlebars复制代码def build_profile(first, last, **user_info):
"""创建一个字典,其中包含我们知道的有关用户的一切"""
profile = {}
profile['first_name'] = first
profile['last_name'] = last
for key, value in user_info.items():
profile[key] = value
return profile
user_profile = build_profile('albert', 'einstein',
location='princeton',
field='physics')
print(user_profile)

函数build_profile()的定义要求提供名和姓,同时允许用户根据需要提供任意数量的名称—值对。形参user_info中的两个星号让Python创建一个名为user_info的空字典,并将收到的所有名称—值对都封装到这个字典中。在这个函数中,可以像访问其他字典那样访问user_info中的名称—值对。
在build_profile()的函数体内,我们创建了一个名为profile的空字典,用于存储用户简介。我们将名和姓加入到这个字典中,因为我们总是会从用户那里收到这两项信息。我们遍历字典user_info中的键—值对,并将每个键—值对都加入到字典profile中。最后,我们将字典profile返回给函数调用行。
我们调用build_profile(),向它传递名(’albert’)、姓(’einstein’)和两个键—值对(location=’princeton’和field=’physics’),并将返回的profile存储在变量user_profile中,再打印这个变量:

1
2
handlebars复制代码{'first_name': 'albert', 'last_name': 'einstein',
'location': 'princeton', 'field': 'physics'}

在这里,返回的字典包含用户的名和姓,还有求学的地方和所学专业。调用这个函数时,不管额外提供了多少个键—值对,它都能正确地处理。
编写函数时,你可以以各种方式混合使用位置实参、关键字实参和任意数量的实参。

本文转载自: 掘金

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

Java 中的抽象简介

发表于 2021-11-30

「这是我参与11月更文挑战的第30天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🏆CSDN全栈领域优质创作者、🥇HDZ核心组成员

🌊 粉丝福利:往期获奖记录 每周送六本书,不定期送各种小礼品

数据抽象是一种仅向用户显示基本细节的属性。不向用户显示琐碎或非必需的单元。例如:汽车被视为汽车而不是其单个组件。

数据抽象也可以定义为仅识别对象所需特征而忽略不相关细节的过程。对象的属性和行为将其与其他类似类型的对象区分开来,也有助于对对象进行分类/分组。

考虑一个男人开车的真实例子。男人只知道踩油门会提高车速或踩刹车会停车,但他不知道踩油门车速实际上是如何增加的,他不知道汽车的内部机制汽车或在汽车中执行油门、刹车等。这就是抽象。

在java中,抽象是通过接口和抽象类来实现的。我们可以使用接口实现 100% 的抽象。

抽象类和抽象方法:

  1. 抽象类是用抽象关键字声明的类。
  2. 抽象方法是声明而没有实现的方法。
  3. 一个抽象类可能有也可能没有所有的抽象方法。其中一些可以是具体的方法
  4. 定义为抽象的方法必须始终在子类中重新定义,从而强制覆盖或使子类本身成为抽象的。
  5. 任何包含一个或多个抽象方法的类也必须用抽象关键字声明。
  6. 抽象类不能有对象。也就是说,抽象类不能直接用new operator实例化。
  7. 抽象类可以具有参数化构造函数,并且默认构造函数始终存在于抽象类中。

何时在示例中使用抽象类和抽象方法

在某些情况下,我们希望定义一个超类来声明给定抽象的结构,而无需提供每个方法的完整实现。也就是说,有时我们会想要创建一个只定义一个泛化形式的超类,该泛化形式将被其所有子类共享,而将其留给每个子类来填充细节。

考虑一个经典的“形状”示例,可能用于计算机辅助设计系统或游戏模拟。基本类型是“形状”,每个形状都有颜色、大小等。由此,衍生出(继承)特定类型的形状——圆形、方形、三角形等——每一种都可能有额外的特征和行为。例如,某些形状可以翻转。某些行为可能会有所不同,例如当您要计算形状的面积时。类型层次体现了形状之间的相似性和差异性。

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
java复制代码abstract class Shape {
String color;

abstract double area();
public abstract String toString();

public Shape(String color){
System.out.println("Shape constructor called");
this.color = color;
}
public String getColor() { return color; }
}
class Circle extends Shape {
double radius;

public Circle(String color, double radius){
super(color);
System.out.println("Circle constructor called");
this.radius = radius;
}

@Override double area(){
return Math.PI * Math.pow(radius, 2);
}

@Override public String toString(){
return "Circle color is " + super.getColor()
+ "and area is : " + area();
}
}
class Rectangle extends Shape {

double length;
double width;

public Rectangle(String color, double length,double width){
super(color);
System.out.println("Rectangle constructor called");
this.length = length;
this.width = width;
}

@Override double area() { return length * width; }

@Override public String toString(){
return "Rectangle color is " + super.getColor()
+ "and area is : " + area();
}
}
public class Test {
public static void main(String[] args){
Shape s1 = new Circle("Red", 2.2);
Shape s2 = new Rectangle("Yellow", 2, 4);
System.out.println(s1.toString());
System.out.println(s2.toString());
}
}

输出

1
2
3
4
5
6
java复制代码Shape constructor called
Circle constructor called
Shape constructor called
Rectangle constructor called
Circle color is Redand area is : 15.205308443374602
Rectangle color is Yellowand area is : 8.0

封装与数据抽象

  1. 封装是数据隐藏(信息隐藏),抽象是细节隐藏(实现隐藏)。
  2. 封装将数据和作用于数据的方法组合在一起,而数据抽象则处理将接口暴露给用户并隐藏实现细节。

抽象的优点

  1. 它降低了查看事物的复杂性。
  2. 避免代码重复并提高可重用性。
  3. 有助于提高应用程序或程序的安全性,因为只向用户提供重要的细节。

本文转载自: 掘金

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

快速理解Mybatis——仿框架实现数据库查询操作 1 前言

发表于 2021-11-30

​

1 前言

mybatis是一个优秀的基于java的持久层框架,它内部封装了jdbc,使开发者只需要关注sql语句本身,而不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。

通过xml或注解的方式将要执行的各种statement配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由框架执行sql并将结果映射为java对象并返回。

采用ORM(Object Relational Mapping)思想解决了实体和数据库映射的问题,屏蔽了jdbc api底层访问细节,使我们不用与jdbc api打交道,就可以完成对数据库的持久化操作。

评论领取源码

2 Mybatis框架快速入门

2.1 搭建Mybatis开发环境(使用.xml配置)

数据库准备

1
2
3
4
5
6
7
8
9
10
11
12
mysql复制代码USE DATABASE eesy;

CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL COMMENT '用户名称',
`birthday` DATETIME DEFAULT NULL COMMENT '生日',
`sex` CHAR(1) DEFAULT NULL COMMENT '性别',
`address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

INSERT INTO `user`(`id`,`username`,`birthday`,`sex`,`address`) VALUES (41,'老王','2018-02-27 17:47:08','男','北京'),(42,'小二王','2018-03-02 15:09:37','女','北京金燕龙'),(43,'小二王','2018-03-04 11:34:34','女','北京金燕龙'),(45,'传智播客','2018-03-04 12:04:06','男','北京金燕龙'),(46,'老王','2018-03-07 17:37:26','男','北京'),(48,'小马宝莉','2018-03-08 11:44:00','女','北京修正');

2.1.1 添加Mybatis3.4.5的坐标

首先创建maven工程,之后在pom.xml中添加坐标,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xml复制代码<dependencies>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.6</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>

2.1.2 数据库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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
java复制代码package domain;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {

private Integer id;
private String username;
private Date birthday;
private String sex;
private String address;

public Integer getId() {
return id;
}

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

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public Date getBirthday() {
return birthday;
}

public void setBirthday(Date birthday) {
this.birthday = birthday;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}

public String getAddress() {
return address;
}

public void setAddress(String address) {
this.address = address;
}
}

2.1.3 dao层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package dao;

import domain.User;

import java.util.List;

public interface UserDao {

/**
* 查询所有用户
* @return
*/
List<User> findAll();
}

2.1.4 编写Dao层接口的映射文件UserDao.xml

创建位置:必须和dao接口在相同的包中。

名称:必须以持久层接口名称命名文件名,扩展名是.xml

)​

mapper标签namespace属性的取值必须是dao接口的全限定类名;

操作配置(select),id属性的取值必须是dao接口的方法名

1
2
3
4
5
6
7
8
9
10
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.UserDao">
<!-- 配置查询所有操作,查询结果封装在User类里 -->
<select id="findAll" resultType="domain.User">
select * from user;
</select>
</mapper>

2.1.5 编写 SqlMapConfig.xml 配置文件

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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!--mybatis的主配置文件-->
<configuration>
<!--配置环境-->
<environments default="mysql">
<!--配置mysql的环境-->
<environment id="mysql">
<!--配置事务类型-->
<transactionManager type="JDBC"></transactionManager>
<!--配置数据源(连接池)-->
<dataSource type="POOLED">
<!--配置连接数据库的4个基本信息-->
<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>

<!-- 告知mybatis映射配置的位置 -->
<mappers>
<mapper resource="dao/UserDao.xml"/>
</mappers>
</configuration>

2.1.6 测试类

程序套路比较固定,基本步骤如下:

  1. 读取配置文件
  2. 创建SqlSessionFactory工厂
  3. 创建SqlSession
  4. 创建dao接口的代理对象 getMapper()
  5. 执行dao中的方法
  6. 释放资源
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
java复制代码package test;

import dao.UserDao;
import domain.User;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

public class MybatisTest {

public static void main(String[] args) throws IOException {
// 读取配置文件
InputStream in = Resources.getResourceAsStream("sqlMapConfig.xml");
// 创建SqlSessionFactory工厂
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);
// 使用工厂生产SqlSession
SqlSession session = factory.openSession();
// 使用SqlSession创建Dao接口的代理对象
UserDao userDao = session.getMapper(UserDao.class);
// 使用代理对象执行dao中的方法
List<User> users = userDao.findAll();
for (User user : users) {
System.out.println(user);
}
// 释放资源
session.close();
in.close();
}
}

2.2 使用注解开发方法

使用注解时,就不需要 resources/dao/UserDao.xml 文件了

在dao接口的方法上使用@Select注解,并且指定SQL语句

UserDao.java 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package dao;

import domain.User;
import org.apache.ibatis.annotations.Select;

import java.util.List;

public interface UserDao {

/**
* 查询所有用户
* @return
*/
@Select("select * from user")
List<User> findAll();
}

SqlMapConfig.xml 全局配置文件里,如果使用注解来配置的话,应该是用class属性指定被注解的dao全限定类名

1
2
3
xml复制代码<mappers>
<mapper class="dao.UserDao"/>
</mappers>

2.3 设计模式分析

在编写测试类时,按照了六个步骤来进行的,其中涉及到了构建者、工厂、代理等设计模式。

1
2
java复制代码SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(in);

创建工厂时使用了构建者模式,可以把对象的创建细节隐藏,使得使用者直接调用方法即可拿到对象。

1
java复制代码SqlSession session = factory.openSession();

生产SqlSession使用了工厂模式,可以降低类之间的依赖关系(解耦)。

1
java复制代码UserDao userDao = session.getMapper(UserDao.class);

创建Dao接口实现类使用了代理模式,可以在不修改源码的基础上对已有方法增强。

注意:

实际应用时,绝对路径和相对路径都不可用,(在部署成webapp后src目录没了)

所以实际中只用两种方法:

  1. 类加载器,但只能读取类路径的配置文件
  2. 使用ServletContext的getRealPath()获得绝对路径

mybatis在使用代理dao的方式实现增删改查时:

  1. 创建代理对象
  2. 在代理对象中调用selectList

3 自定义Mybatis框架

3.1 查询所有分析

selectList方法大致分为五步:

  1. 根据配置文件创建Connection对象
  2. 获取预处理对象PreparedStatement
  3. 执行查询操作
  4. 遍历查询结果集并封装为List
  5. 返回List对象

其中第一步需要配置文件SqlMapConfig.xml中的信息,解析xml的技术已经成熟,这里不做赘述。

1
2
3
4
xml复制代码<property name="driver" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/eesy_mybatis"/>
<property name="username" value="root"/>
<property name="password" value="root"/>

第二步获取预处理对象PreparedStatement

1
java复制代码conn.prepareStatement(queryString);

需要传入sql语句,需要用到UserDao.xml中定义的sql语句

1
2
3
4
5
6
xml复制代码<mapper namespace="dao.UserDao">
<!-- 配置查询所有操作 -->
<select id="findAll" resultType="domain.User">
select * from user;
</select>
</mapper>

第四步进行数据的封装,需要知道数据库中存储的数据对应的java中的类

对应上面代码中的 resultType

纵观整个解析的过程,如果想要让selectList方法执行,还必须要提供映射信息,映射信息包含两部分:sql和封装结果的实体类名,可以将这两个信息封装成一个映射类Mapper作为map的value,其中map的key为类似于”dao.UserDao.findAll”的字符串。

具体流程如下图:

)​

总结一下:

SqlMapConfig.xml 中包含连接数据库的信息、映射配置信息

UserDao.xml 中包含sql语句和封装的实体类信息

可以使用dom4j技术来解析xml

3.2 创建代理对象的分析

1
2
java复制代码// 使用SqlSession创建Dao接口的代理对象
UserDao userDao = (UserDao) session.getMapper(UserDao.class);

getMapper的具体实现如下:

1
2
3
4
5
java复制代码public <T> T getMapper(Class<T> daoInterfaceClass) {
return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass},
new MapperProxy(cfg.getMappers(), connection));
}

使用代理

第一个参数是类加载器:使用和被代理对象相同的类加载器

第二个参数是代理对象要实现的接口:和被代理对象实现相同的接口

第三个参数是如何代理:我们自己提供一个增强的方法

3.3 使用Mybatis时用到的类(接口)

class Resources

class SqlSessionFactoryBuilder

interface SqlSessionFactory

interface SqlSession

3.4 准备工作

将之前写的Mybatis测试类中所需要的方法创建出来

)​

3.4.1 Resources 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package mybatis.io;

import java.io.InputStream;

/**
* 使用类加载器读取配置文件的类
*/
public class Resources {

/**
* 根据传入的参数,获取字节输入流
* @param filePath
* @return
*/
public static InputStream getResourceAsStream(String filePath){
return Resources.class.getClassLoader().getResourceAsStream(filePath);
}

}

3.4.2 SqlSession 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package mybatis.sqlsession;

/**
* 自定义Mybatis中和数据库交互的核心类
* 里面可以创建dao接口的代理对象
*/
public interface SqlSession<T> {

/**
* 很具参数创建一个代理对象
* @param daoInterfaceClass dao的接口字节码
* @return
*/
T getMapper(Class<T> daoInterfaceClass);

/**
* 释放资源
*/
void close();
}

3.4.3 SqlSessionFactory 接口

1
2
3
4
5
6
7
8
9
10
java复制代码package mybatis.sqlsession;

public interface SqlSessionFactory {

/**
* 用于打开一个新SqlSession对象
* @return
*/
SqlSession openSession();
}

3.4.4 SqlSessionFactoryBuilder 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package mybatis.sqlsession;

import java.io.InputStream;

/**
* 用于创建SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {

public SqlSessionFactory build(InputStream in){
return null;
}

}

3.5 解析XML的工具类介绍

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
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
java复制代码package mybatis.utils;

import mybatis.io.Resources;
import mybatis.cfg.Configuration;
import mybatis.cfg.Mapper;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 用于解析配置文件
*/
public class XMLConfigBuilder {

/**
* 解析主配置文件,把里面的内容填充到DefaultSqlSession所需要的地方
* 使用的技术:
* dom4j+xpath
*/
public static Configuration loadConfiguration(InputStream config){
try{
//定义封装连接信息的配置对象(mybatis的配置对象)
Configuration cfg = new Configuration();

//1.获取SAXReader对象
SAXReader reader = new SAXReader();
//2.根据字节输入流获取Document对象
Document document = reader.read(config);
//3.获取根节点
Element root = document.getRootElement();
//4.使用xpath中选择指定节点的方式,获取所有property节点
List<Element> propertyElements = root.selectNodes("//property");
//5.遍历节点
for(Element propertyElement : propertyElements){
//判断节点是连接数据库的哪部分信息
//取出name属性的值
String name = propertyElement.attributeValue("name");
if("driver".equals(name)){
//表示驱动
//获取property标签value属性的值
String driver = propertyElement.attributeValue("value");
cfg.setDriver(driver);
}
if("url".equals(name)){
//表示连接字符串
//获取property标签value属性的值
String url = propertyElement.attributeValue("value");
cfg.setUrl(url);
}
if("username".equals(name)){
//表示用户名
//获取property标签value属性的值
String username = propertyElement.attributeValue("value");
cfg.setUsername(username);
}
if("password".equals(name)){
//表示密码
//获取property标签value属性的值
String password = propertyElement.attributeValue("value");
cfg.setPassword(password);
}
}
//取出mappers中的所有mapper标签,判断他们使用了resource还是class属性
List<Element> mapperElements = root.selectNodes("//mappers/mapper");
//遍历集合
for(Element mapperElement : mapperElements){
//判断mapperElement使用的是哪个属性
Attribute attribute = mapperElement.attribute("resource");
if(attribute != null){
System.out.println("使用的是XML");
//表示有resource属性,用的是XML
//取出属性的值
String mapperPath = attribute.getValue();//获取属性的值"dao/IUserDao.xml"
//把映射配置文件的内容获取出来,封装成一个map
Map<String,Mapper> mappers = loadMapperConfiguration(mapperPath);
//给configuration中的mappers赋值
cfg.setMappers(mappers);
}
}
//返回Configuration
return cfg;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
try {
config.close();
}catch(Exception e){
e.printStackTrace();
}
}

}

/**
* 根据传入的参数,解析XML,并且封装到Map中
* @param mapperPath 映射配置文件的位置
* @return map中包含了获取的唯一标识(key是由dao的全限定类名和方法名组成)
* 以及执行所需的必要信息(value是一个Mapper对象,里面存放的是执行的SQL语句和要封装的实体类全限定类名)
*/
private static Map<String,Mapper> loadMapperConfiguration(String mapperPath)throws IOException {
InputStream in = null;
try{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String,Mapper>();
//1.根据路径获取字节输入流
in = Resources.getResourceAsStream(mapperPath);
//2.根据字节输入流获取Document对象
SAXReader reader = new SAXReader();
Document document = reader.read(in);
//3.获取根节点
Element root = document.getRootElement();
//4.获取根节点的namespace属性取值
String namespace = root.attributeValue("namespace");//是组成map中key的部分
//5.获取所有的select节点
List<Element> selectElements = root.selectNodes("//select");
//6.遍历select节点集合
for(Element selectElement : selectElements){
//取出id属性的值 组成map中key的部分
String id = selectElement.attributeValue("id");
//取出resultType属性的值 组成map中value的部分
String resultType = selectElement.attributeValue("resultType");
//取出文本内容 组成map中value的部分
String queryString = selectElement.getText();
//创建Key
String key = namespace+"."+id;
//创建Value
Mapper mapper = new Mapper();
mapper.setQueryString(queryString);
mapper.setResultType(resultType);
//把key和value存入mappers中
mappers.put(key,mapper);
}
return mappers;
}catch(Exception e){
throw new RuntimeException(e);
}finally{
in.close();
}
}
}

需要在 pom.xml 中导入 dom4j、jaxen

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.6</version>
</dependency>

3.5.1 在 mybatis.cfg 包下创建Configuration类和Mapper类

Configuration类中存储了”dao.UserDao.findAll”与一个Mapper对象的映射

Mapper对象中封装 执行的SQL语句 以及 结果类型的全限定类名

由于可能有多条sql语句,所以Configuration中的setMappers方法应该不断地往map中追加数据,而不是替换,使用 putAll 追加数据时必须先把map new出来,不然空指针。

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
java复制代码package mybatis.cfg;

import java.util.Map;

public class Configuration {

private String driver;
private String url;
private String username;
private String password;

private Map<String, Mapper> mappers = new HashMap<String, Mapper>();

public String getDriver() {
return driver;
}

public void setDriver(String driver) {
this.driver = driver;
}

public String getUrl() {
return url;
}

public void setUrl(String url) {
this.url = url;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Map<String, Mapper> getMappers() {
return mappers;
}

public void setMappers(Map<String, Mapper> mappers) {
this.mappers.putAll(mappers); // 追加方式添加至map
}
}

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
java复制代码package mybatis.cfg;

/**
* 用于封装 执行的SQL语句 以及 结果类型的全限定类名
*/
public class Mapper {

private String queryString;
private String resultType; // 实体类全限定类名

public String getQuerryString() {
return queryString;
}

public void setQueryString(String querryString) {
this.queryString = querryString;
}

public String getResultType() {
return resultType;
}

public void setResultType(String resultType) {
this.resultType = resultType;
}
}

3.6 SqlSessionFactoryBuilder 类的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package mybatis.sqlsession;

import mybatis.cfg.Configuration;
import mybatis.sqlsession.defaults.DefaultSqlSessionFactory;
import mybatis.utils.XMLConfigBuilder;

import java.io.InputStream;
/**
* 用于创建SqlSessionFactory对象
*/
public class SqlSessionFactoryBuilder {

/**
* 根据参数的字节输入流构建一个SqlSessionFactory工厂
* @param in
* @return
*/
public SqlSessionFactory build(InputStream config){
Configuration cfg = XMLConfigBuilder.loadConfiguration(config);
return new DefaultSqlSessionFactory(cfg);
}
}

SqlSessionFactoryBuilder 类中有一个 public SqlSessionFactory build(InputStream config)方法,需要返回一个SqlSessionFactory的对象。

3.6.1 SqlSessionFactory 接口的实现类

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
java复制代码package mybatis.sqlsession.defaults;

import mybatis.cfg.Configuration;
import mybatis.sqlsession.SqlSession;
import mybatis.sqlsession.SqlSessionFactory;

/**
* SqlSessionFactory 接口的实现
*/
public class DefaultSqlSessionFactory implements SqlSessionFactory {

private Configuration cfg;

public DefaultSqlSessionFactory(Configuration cfg){
this.cfg = cfg;
}

/**
* 创建一个新的操作数据库对象
* @return
*/
public SqlSession openSession() {
return new DefaultSqlSession(cfg);
}
}

SqlSessionFactory 中有一个 public SqlSession openSession() 方法,需要返回一个SqlSession的对象。

3.6.2 SqlSession 接口的实现类

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
java复制代码package mybatis.sqlsession.defaults;

import mybatis.cfg.Configuration;
import mybatis.sqlsession.SqlSession;
import mybatis.sqlsession.proxy.MapperProxy;
import mybatis.utils.DataSourceUtil;

import javax.sql.DataSource;
import java.lang.reflect.Proxy;
import java.sql.Connection;

public class DefaultSqlSession implements SqlSession {

private Configuration cfg;
private Connection connection;

public DefaultSqlSession(Configuration cfg){
this.cfg = cfg;
connection = DataSourceUtil.getConnection(cfg);
}

/**
* 创建代理对象
* @param daoInterfaceClass dao的接口字节码
* @param <T>
* @return
*/
public <T> T getMapper(Class<T> daoInterfaceClass) {
// 使用代理
// 被代理对象的类加载器
// 相同的接口
// 如何代理
return (T) Proxy.newProxyInstance(daoInterfaceClass.getClassLoader(),
new Class[]{daoInterfaceClass},
new MapperProxy(cfg.getMappers(), connection));
}

/**
* 释放资源
*/
public void close() {
if(connection != null){
try{
connection.close();
}catch (Exception e){
e.printStackTrace();
}
}
}
}

DefaultSqlSession 中使用到的工具类 DataSourceUtil 为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码package mybatis.utils;

import mybatis.cfg.Configuration;

import java.sql.Connection;
import java.sql.DriverManager;

/**
* 创建数据源的工具类
*/
public class DataSourceUtil {

public static Connection getConnection(Configuration cfg){
try{
// 注册驱动
Class.forName(cfg.getDriver());
return DriverManager.getConnection(cfg.getUrl(), cfg.getUsername(), cfg.getPassword());
}catch (Exception e){
throw new RuntimeException(e);
}
}
}

该类的 public T getMapper(Class daoInterfaceClass) 方法通过代理模式来实现

1
2
3
java复制代码public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

loader: 用哪个类加载器去加载代理对象

interfaces:动态代理类需要实现的接口

h:动态代理方法在执行时,会调用h里面的invoke方法去执行

需要定义一个类 MapperProxy 来作为上面的h,该类必须实现 InvocationHandler 接口:

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
java复制代码package mybatis.sqlsession.proxy;

import mybatis.cfg.Mapper;
import mybatis.utils.Executor;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.Map;

public class MapperProxy implements InvocationHandler {

// key : 全限定类名 + 方法名
private Map<String, Mapper> mappers;
private Connection conn;

public MapperProxy(Map<String, Mapper> mappers, Connection conn){
this.mappers = mappers;
this.conn = conn;
}

// 对方法进行增强,其实就是调用selectList方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
// 获取方法所在类的名称
String className = method.getDeclaringClass().getName();

String key = className + "." + methodName;
Mapper mapper = mappers.get(key);
if (mapper == null){
throw new IllegalArgumentException("传入的参数有误");
}

// 调用工具类查询所有
return new Executor().selectList(mapper, conn);
}
}

MapperProxy 中的使用到的工具类 Execute 为(中间定义了selectList方法):

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
java复制代码package mybatis.utils;

import mybatis.cfg.Mapper;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.util.ArrayList;
import java.util.List;

/**
* 负责执行SQL语句,并且封装结果集
*/
public class Executor {

public <E> List<E> selectList(Mapper mapper, Connection conn) {
PreparedStatement pstm = null;
ResultSet rs = null;
try {
//1.取出mapper中的数据
String queryString = mapper.getQueryString();//select * from user
String resultType = mapper.getResultType();//com.itheima.domain.User
Class domainClass = Class.forName(resultType);
//2.获取PreparedStatement对象
pstm = conn.prepareStatement(queryString);
//3.执行SQL语句,获取结果集
rs = pstm.executeQuery();
//4.封装结果集
List<E> list = new ArrayList<E>();//定义返回值
while(rs.next()) {
//实例化要封装的实体类对象
E obj = (E)domainClass.newInstance();

//取出结果集的元信息:ResultSetMetaData
ResultSetMetaData rsmd = rs.getMetaData();
//取出总列数
int columnCount = rsmd.getColumnCount();
//遍历总列数
for (int i = 1; i <= columnCount; i++) {
//获取每列的名称,列名的序号是从1开始的
String columnName = rsmd.getColumnName(i);
//根据得到列名,获取每列的值
Object columnValue = rs.getObject(columnName);
//给obj赋值:使用Java内省机制(借助PropertyDescriptor实现属性的封装)
PropertyDescriptor pd = new PropertyDescriptor(columnName,domainClass);//要求:实体类的属性和数据库表的列名保持一种
//获取它的写入方法
Method writeMethod = pd.getWriteMethod();
//把获取的列的值,给对象赋值
writeMethod.invoke(obj,columnValue);
}
//把赋好值的对象加入到集合中
list.add(obj);
}
return list;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
release(pstm,rs);
}
}


private void release(PreparedStatement pstm,ResultSet rs){
if(rs != null){
try {
rs.close();
}catch(Exception e){
e.printStackTrace();
}
}

if(pstm != null){
try {
pstm.close();
}catch(Exception e){
e.printStackTrace();
}
}
}
}

3.7 基于注解的查询所有

修改 resources.SqlMapConfig.xml 中的语法为注解相应的形式:

1
2
3
xml复制代码<mappers>
<mapper class="dao.UserDao" />
</mappers>

给 dao.UserDao 添加注解:

1
2
java复制代码@Select("select * from user")
List<User> findAll();

新建一个 mybatis.annotations.Select 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package mybatis.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Select {
/**
* 配置sql语句
*/
String value();
}

在 utils.XMLConfigBuilder 中添加根据传入的参数,得到dao中所有被select注解标注的方法:

基于反射的原理,传入一个全限定类名,再通过这个类名获取字节码对象、方法,将带有注解的方法的相关信息加入到Mapper对象中。

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
java复制代码/**
* 根据传入的参数,得到dao中所有被select注解标注的方法。
* 根据方法名称和类名,以及方法上注解value属性的值,组成Mapper的必要信息
* @param daoClassPath
* @return
*/
private static Map<String,Mapper> loadMapperAnnotation(String daoClassPath)throws Exception{
//定义返回值对象
Map<String,Mapper> mappers = new HashMap<String, Mapper>();

//1.得到dao接口的字节码对象
Class daoClass = Class.forName(daoClassPath);
//2.得到dao接口中的方法数组
Method[] methods = daoClass.getMethods();
//3.遍历Method数组
for(Method method : methods){
//取出每一个方法,判断是否有select注解
boolean isAnnotated = method.isAnnotationPresent(Select.class);
if(isAnnotated){
//创建Mapper对象
Mapper mapper = new Mapper();
//取出注解的value属性值
Select selectAnno = method.getAnnotation(Select.class);
String queryString = selectAnno.value();
mapper.setQueryString(queryString);
//获取当前方法的返回值,还要求必须带有泛型信息
Type type = method.getGenericReturnType();//List<User>
//判断type是不是参数化的类型
if(type instanceof ParameterizedType){
//强转
ParameterizedType ptype = (ParameterizedType)type;
//得到参数化类型中的实际类型参数
Type[] types = ptype.getActualTypeArguments();
//取出第一个
Class domainClass = (Class)types[0];
//获取domainClass的类名
String resultType = domainClass.getName();
//给Mapper赋值
mapper.setResultType(resultType);
}
//组装key的信息
//获取方法的名称
String methodName = method.getName();
String className = method.getDeclaringClass().getName();
String key = className+"."+methodName;
//给map赋值
mappers.put(key,mapper);
}
}
return mappers;
}

此时运行test程序即可看到查询数据库的结果。

​

本文转载自: 掘金

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

会话怎么可以少了session和cookie

发表于 2021-11-30

​

磊哥最近应该是去南方玩耍了,应该是在抽空之中发来这样一篇小作文:

“南方的村落房屋是个性的。村民们像是商量好的似的,你用大红,我就紫红,一时间各种各样的红色扑进眼帘,让人叫不出颜色,人们却又实在地感受着巨大的差异。红色只是一个打底色,青蓝黄靛紫一股脑的泼洒其间,仿佛一个潮流张狂的都市女孩,大胆而又富有魅力。北方的村落房屋是整齐划一的。每家每户盖房子前都会认真地参考邻居的风格,他们认为统一是一种谐和的美丽。看着整片整片的红砖瓦房,就让人不禁想到大阅兵的方阵,横成排,竖成纵,动作划一,即使不会审美的人也从心底油然而生一种赞叹。”

初读一下,感觉和看过的某位作家的风格很像,颇具细心观察生活之智慧,让我忍不住引用过来。其实磊哥的思维一直是我很羡慕却又无法模仿的点,对问题的理解精准,讲话幽默而不乏深度,有丶类似于梓泉的风格,厉害。

说回正文。

有天阳光明媚,你正在某购物网开开心心的挑选商品凑单,终于选到了自己钟爱的物件凑够了9999.999元,而该网站上正好有恰好买到这个数字就减去9999元的超级活动!于是你欣欣然打开购物车,发现

)​

)​

只好怒删软件,世界再见。幸好这确实只是一场梦,醒来以后除去感动不说,你发现会话怎么可以少了小饼干和主菜!扯远了又。。。今天就简单谈一谈访问web时的cookie与session机制。

1 cookie

)​

众所周知,Web应用是使用大名鼎鼎的HTTP协议传输数据的。HTTP协议是无状态的协议,意味着一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接,如果不采取一些措施的话,服务器将无法从连接上跟踪会话(你辛辛苦苦凑的单,说不定就躺在了别人的购物车里)。cookie就是这样的一种措施,将数据保存到客户端。抽象一点,cookie可以看作是服务器发给每一个客户的通行证,上面记录了你的一些个人信息,于是你去浏览网页的时候,网线的另一头就可以根据你的通行证知道你是谁了(突然害怕)。

专业一点的解释如图:

)​

服务器上部署了两个Servlet程序CookieDemo1.java和CookieDemo2.java,姑且简称为CD1和CD2吧,CD1程序中有发送cookie的代码,CD2程序中有获取cookie的代码,当你打开浏览器访问CD1之后,服务器就会将响应头中set-cookie字段通过键值对的形式进行设置,再次在浏览器中访问CD2时,请求头中cookie字段就会包含刚刚设置的cookie信息(实际上,不管你是否有获取cookie的代码,这里的请求头中都包含这一信息),于是乎服务器就可以知道你究竟是哪一位小朋友了~

Java中操作cookie很方便,主要有以下3个方法,了解java语法的小朋友靠猜都能知道:

  • 创建cookie
1
arduino复制代码new Cookie(String name, String value)

  • 发送cookie
1
scss复制代码response.addCookie(Cookie cookie)

  • 获取cookie
1
ini复制代码Cookie[] cookies = request.getCookies()

对于数组中的每一个cookie,getName,getValue等方法肯定会有的鸭

这里你可能就有疑问了,cookie究竟会保存多长时间?我说默认情况下浏览器关闭后就会被销毁,然而回忆一下平时我们网上冲浪的场景,在不登陆浏览器的情况下,我们对其所做的设置并不会因为明天重新来玩电脑打开网页时而消失不见,所以一种直觉就是cookie可以由程序写入电脑硬盘里而实现长久存储,事实也确实如此:

1
scss复制代码setMaxAge(int seconds)

对于程序员来讲,cookie的共享问题应该是最需要关心的了,假如在一个tomcat服务器中,部署了多个web项目,如果需要共享可以将 setPath(String path) 方法path设置为”/“,当然为了安全考虑,默认情况下是不能共享的~。假如要使不同的tomcat服务器间共享cookie,那就必须调用 setDomain(String path) 方法设置一级域名相同啦,还是拿我最喜欢的百度来举例,百度贴吧tieba.baidu.com 和百度新闻news.baidu.com必定是部署在不同的服务器之上的,我们有 setDomain(“.baidu.com”) 来设置共享。

使用cookie的一个经典案例便是记住用户上一次访问的时间了,相信各位也会有意无意的注意到,尽管你不曾登陆过,有一些网页还是会提示上一次访问的时间,原理便也很简单,首次访问服务器时就会将cookie写回客户端保存,当你再次带着此物去访问时,服务器便可解析出相应的时间信息。公众号回复【cookie案例】即可获取此案例代码(仅供交流感情所用)。饼干虽小,足以果腹。

2 session

session可以用中文描述为:服务器端会话技术,在一次会话的多次请求间共享数据。和cookie对应,只不过是被保存在了服务器端的对象中,这个对象具体名字是HttpSession

实际上session的实现是依赖于cookie的,直接上图:

)​

假设服务器中部署了两个Servlet程序,SessionDemo1.java和SessionDemo2.java,我们将其简称为SD1和SD2,两个程序中都有获取session的逻辑,当用户通过浏览器访问了SD1时,服务器并没有发现有session的存在,所以会在内存中创建一个新的Session对象并分配了一个编号,这里称做id,于是乎,创建一个cookie,并设置JSESSIONID等于先前分配的编号。这样一来一去的,在下一次请求时SD2时,程序就会获得这个session。

Java中使用HttpSession也很简单,经典的增删改差四君子如下:

  • 查询:
1
javascript复制代码Object getAttribute(String name)

  • 增加:
1
javascript复制代码void setAttribute(String name, Object value)

  • 删除:
1
arduino复制代码void removeAttribute(String name)

为什么没有修改呢。。。修改和增加其实一样啊

看到这里可能又有疑问了。。。我们该如何操作使得用户关闭浏览器重新打开后再次访问服务器,获得同一个session?我们已经说了,session是基于cookie实现的,cookie保存了session对象的身份证号码,它可以设置存活于内存中的时间呀!于是乎,思路顺势而生:

1
2
3
4
5
ini复制代码Cookie c = new Cookie("JSESSIONID",session.getId());

c.setMaxAge(60*60);

response.addCookie(c);

同样问题有两面,服务器重启前后两次获得的session也不是同一个,但是要确保数据不丢失。tomcat自动完成活化和钝化两个操作,钝化是指将session对象系列化到硬盘上,活化则是其相反过程,将session文件转化为内存中的对象,所以这里“不是同一个”的意思应该是两次获取的session内存地址是不同的。

之所以将其称之为主菜,那必然是拥有比cookie更高的热量更优秀的一些特性。cookie存储数据在客户端浏览器,这必然会导致一些安全性的担忧,同样,个人电脑和服务器的性能始终是不用比较的,所以浏览器对于单个cookie 的大小有限制(4kb)且对同一个域名下的总cookie数量也有限制(20个),因此,命名之争落下帷幕。

有了这个工具,我们就可以实现一些“更高级”的操作,比如验证码,不难分析,我们登录时经常输入的验证码肯定是电脑自动生成的(如果是在服务器里存储成千上万张验证码的图片未免太过低级,并且浪费资源),这里就需要借助session在不同的服务器程序中共享数据来获得生成的验证码,如下:

)​

为了节约篇幅,公众号回复【session案例】即可获取此案例代码(仅供交流感情所用)。美食是世上美好的存在,但切忌暴饮暴食,要节约资源。

​

本文转载自: 掘金

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

webSerivce简明使用教程

发表于 2021-11-30

本文正在参与 “网络协议必知必会”征文活动

茫茫人海千千万万,感谢这一秒你看到这里。希望我的文章对你的有所帮助!

愿你在未来的日子,保持热爱,奔赴山海!

👀 前言

因为前段时间,需要使用到webService来调用公司的其他系统api接口,但是请求方式和我熟知的http请求不一样,是基于soap协议来传输xml数据格式,请求的参数极其复杂,需要封装多层xml数据格式,并且我不知道对方的api接口是什么语言,甚至不知道他们存在于什么平台。

那这与我们常见的httpapi接口有什么区别呢?

  • 不同协议:HTTPService基于http协议,而WebService基于soap协议;
  • 处理数据效率不同:HTTPService效率较高,传输的是字符串,而WebService是包装成了更复杂的对象以致于能处理较复杂的数据类型;
  • 是否可以跨域处理:HttpService方式不能处理跨域,如果调用一个其它应用的服务就要用webService,就像我现在是调其他系统的服务。

😀 概述

😆 介绍下

WebService主要是通过SOAP协议在Web上提供的软件服务,使用WSDL文档进行说明,并通过UDDI进行注册。WebService是一种跨编程语言和跨操作系统平台的远程调用技术,能使得运行在不同机器上的不同应用无须借助附加的、专门的第三方软件或硬件, 就可相互交换数据或集成。依据WebService规范实施的应用之间, 无论它们所使用的语言、 平台或内部协议是什么, 都可以相互交换数据。

所以呢,如果你想是使用不同语言,不同平台,不同地方,想进行数据传输,自我推荐下,选择WebService就没错的!

🔄 多维度了解

  • 从表面看,WebService就是一个应用程序向外界暴露出一个能通过Web进行调用的API,也就是说能用编程的方法通过Web来调用这个应用程序。我们把调用这个WebService的应用程序叫做客户端,而把提供这个WebService的应用程序叫做服务端。
  • 从内层看,WebService不是一种技术,更像是建立在可互操作的分布式应用程序的新平台,是一个平台,是一套标准,是一种规范。它定义了应用程序如何在Web上实现互操作性,你可以用任何你喜欢的语言,在任何你喜欢的平台上写WebService ,只要我们可以通过WebService标准对这些服务进行查询和访问。

👩‍👧‍👦 三个好兄弟

常伴于三个元素兄弟:UDDI,WSDL,SOAP

  • UDDI:UDDI 是一种用于描述、发现、集成Web Service的技术,它是Web Service协议栈的一个重要部分。通过UDDI,企业可以根据自己的需要动态查找并使用Web服务,也可以将自己的Web服务动态地发布到UDDI注册中心,供其他用户使用。UDDI利用SOAP消息机制(标准的XML/HTTP)来发布,编辑,浏览以及查找注册信息。它采用XML格式来封装各种不同类型的数据,并且发送到注册中心或者由注册中心来返回需要的数据。
  • WSDL:是为了描述Web服务发布的XML格式。就是用机器能阅读的方式提供的一个正式描述文档而基于XML(标准通用标记语言下的一个子集)的语言,用于描述WebService及其函数、参数和返回值。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的。
  • SOAP:简单对象访问协议,是交换数据的一种协议规范,是一种轻量的、简单的、基于XML标准通用标记语言下的一个子集)的协议。主要组成由Http协议和XML数据格式。WebService通过HTTP协议发送请求和接收结果时,发送的请求内容和结果内容都采用XML格式封装,并增加了一些特定的HTTP消息头,以说明HTTP消息的内容格式,这些特定的HTTP消息头和XML内容格式就是SOAP协议。SOAP提供了标准的RPC(远程调用技术)方法来调用WebService。

😎 选择的好处

  1. 跨平台调用。
  2. 跨语言调用。
  3. 远程调用。

😜 来开始使用吧

话不多说,看太多理论不如自己做个小demo测下来得爽快。所以Let’s GO!

做一个天气系统的demo,客户端发送城市名称,服务器端回应相应的天气。

📖 创建一个空项目

创建一个空项目weatherServer出来,我使用的IDEA版本为2020.3版本。当然一开始我比较详细介绍啦

🍉实现发布服务端

1. 创建服务端模块weatherServerTest

创建过程基本类似的。如果你想选择使用我,那么我会提议你先创建一个服务端,这样别人可以进行访问,或者使用他人的服务端都OK的啦。这里先会完成创建出一个服务端模块出来,后续的创建客户端模块基本类似。这里我们直接选择一个Maven项目即可,也可以自行选择一个普通Java项目都行。JDK为流行的JDK8即可。

最终得到的空模块服务端如下:

2. 提供天气服务

接下来就是开发服务端代码。提供简单的城市天气服务。

  1. 定义一个天气业务接口IWeatherService

代码如下:

1
2
3
4
5
6
7
8
9
10
java复制代码package com.ws.service;

public interface IWeatherSerice {
/**
* 通过城市名得到对应的天气
* @param city 城市
* @return 天气
*/
public String queryWeather(String city);
}
  1. 编写对应的接口实现类WeatherServiceImpl

对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码package com.ws.service.impl;

import com.ws.service.IWeatherSerice;
import javax.jws.WebService;

@WebService // 用该注解修改表示当前类是一个服务类 必须加上的,不然启动服务的时候会报错。
public class WeatherServiceImpl implements IWeatherSerice {

@Override
public String queryWeather(String city) {
// 这里直接返回对应的城市和晴天!!!
return city + "的天气为:晴天!";
}
}
  1. 创建一个用于发布服务的类WeatherServerDemo。

对应的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.ws.server;

import com.ws.service.impl.WeatherServiceImpl;
import javax.xml.ws.Endpoint;

public class WeatherServerDemo {

public static void main(String[] args) {
/**
* address: 服务地址 --> 提供访问的地址
* implementor: 服务类 --> 提供访问的接口的实现类
*/
Endpoint.publish("http://localhost:8086/weatherServer", new WeatherServiceImpl());
System.out.println("服务发布成功");
}
}
  1. 运行main方法,开启服务:

可以看到控制台,他没有运行完成后关闭,并且显示服务发布成功了。接下来就在线看看我们发布的服务。

3. 访问服务

访问刚才提供的访问的地址加上?wsdl,如:http://localhost:8086/weatherServer?wsdl

那如何看这个wsdl文档呢?不怕,让我来教你看几个重要的部分就一目了然啦!注意:wsdl文档需要从下往上看

  • :服务视图名称,WebService的服务端点
  • :Web Services的通信协议,还描述Web Services的方法、输入、输出。
  • :描述了WebService可执行的操作,通过binding指向portType
  • :描述了服务中发布的方法,包括参数,返回值等。
  • :定义了WebService中使用的数据类型

访问一个详细的参数页面:http://localhost:8086/weatherServer?xsd=1

🍊 实现客户端访问的几种方式

1. 创建客户端模块weatherClientTest

结构:

2. 生成对应的客户端代码

为什么需要下载客户端代码,首先前面两个方式需要用到服务端代码的接口,然后如果你是访问远程的服务端,那你也就访问不了。

这里会介绍jdk自带的命令:wsimport

1
2
3
4
5
6
7
java复制代码wsimport是jdk自带的webservice客户端工具,可以根据wsdl文档生成客户端调用代码(java代码).
wsimport.exe位于JAVA_HOME\bin目录下
常用参数为:
-d<目录> - 将生成.class文件。默认参数。
-s<目录> - 将生成.java文件。
-p<生成的新包名> -将生成的类,放于指定的包下
wsimport -s ./ http://localhost:8086/weatherServer?wsdl

生成步骤:

  • 进入到客户端模块的src的java目录下

  • 在该目录中,进入命令行模式,然后输入wsimport -s ./ http://localhost:8086/weatherServer?wsdl,下载刚才启动的服务端代码。注意:这一步得保证服务端是启动可访问状态。

  • 最终生成代码的结构在IDEA结构为:

​ 这里得到的代码跟wsdl文档对应的名称可以一一对应。当然如果你想改包名的话,可以使用-p参数。

3. 客户端访问方式1

第一种方式:通过得到的代码,service方式调用。

结构如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码package com.ws.client;

import com.ws.service.impl.WeatherServiceImpl;
import com.ws.service.impl.WeatherServiceImplService;

public class WeatherClientDemo1 {
public static void main(String[] args) {
// 1. 创建服务视图
WeatherServiceImplService weatherServiceImplService = new WeatherServiceImplService();

// 2. 得到服务实现类
WeatherServiceImpl weatherService = weatherServiceImplService.getPort(WeatherServiceImpl.class);

// 3. 调用接口的方法
String result = weatherService.queryWeather("广州");

// 4. 返回远程访问得到的结果 --> result = 广州的天气为:晴天!
System.out.println("result = " + result);
}
}

最终我们运行后得到的结果也正如所预料的一般:

4. 客户端访问方式2

之前这一步有什么缺陷呢,应该很清楚吧:我们固定了一个服务地址了,而且如果要改服务地址的话,可能还需要再生成一遍代码。

所以我们把服务地址写出来,结构如下:

代码如下:

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
java复制代码package com.ws.client;

import com.ws.service.impl.WeatherServiceImpl;
import com.ws.service.impl.WeatherServiceImplService;
import javax.xml.namespace.QName;
import javax.xml.ws.Service;
import java.net.MalformedURLException;
import java.net.URL;

public class WeatherClientDemo2 {
public static void main(String[] args) throws Exception {
// 1. 设置访问的服务端地址
URL url = new URL("http://localhost:8086/weatherServer?wsdl");

/*
2.设置服务名称和命名空间
namespaceURI: wsdl的命名空间(targetNamespace)
localPart: 是服务视图的名称(service的name值)
*/
QName qName = new QName("http://impl.service.ws.com/", "WeatherServiceImplService");

// 3. 生成服务视图
Service service = Service.create(url, qName);

// 4. 得到服务视图的实现类 --> WeatherServiceImpl
WeatherServiceImpl weatherServiceImpl = service.getPort(WeatherServiceImpl.class);

// 5. 调用接口方法得到结果! --> result = 深圳的天气为:晴天!
String result = weatherServiceImpl.queryWeather("深圳");
System.out.println("result = " + result);
}
}

这些对应的参数在wsdl的那个位置呢? 看我指给你:

这第二种方式是比较常用的方式,首先推荐!

5. 客户端访问方式3

在不需要下载服务端代码的,通过HTTPURLConnection的方式进行访问服务端。只是这里代码就相对比较长啦,而且需要自行定义XML字符串和解析字符串。

  1. 在pom.xml文件中加入一个dom4j的依赖包
1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码<dependencies>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>

<dependency>
<groupId>jaxen</groupId>
<artifactId>jaxen</artifactId>
<version>1.1.1</version>
</dependency>
</dependencies>

结构如下:


2. 创建对应的测试demo,大概结构如下:

代码如下:可以复制到IDEA仔细观看。

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
java复制代码package com.ws.client;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Node;

import java.io.DataOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Scanner;

public class WeatherClientDemo3 {
public static void main(String[] args) throws Exception {
// 1. 设置访问的服务端地址
URL url = new URL("http://localhost:8086/weatherServer?wsdl");

// 2.打开一个通向服务地址的连接
HttpURLConnection connection = (HttpURLConnection) url.openConnection();

// 3.设置参数, --> POST必须大写,否则抛出异常
connection.setRequestMethod("POST");
// 这里是text/xml不是text/html
connection.setRequestProperty("content-Type", "text/xml;charset=utf-8");

// 4.设置输入输出,默认是false没有读写的权限
connection.setDoOutput(true);
connection.setDoInput(true);

// 5.组织SOAP数据,发送请求
String soapXml = getXmlString("佛山");
System.out.println("soapXml = " + soapXml);

// 6. 将数据写入到输出流中
DataOutputStream dos = new DataOutputStream(connection.getOutputStream());
dos.write(soapXml.getBytes("utf-8"));
dos.flush();

// 7. 判断远程访问是否成功,如果响应码为200即为成功
if (connection.getResponseCode() == 200) {
// 8. 获取相应的输入流,得到对应的结果
InputStream ips = connection.getInputStream();
Scanner scanner = new Scanner(ips);
StringBuffer buffer = new StringBuffer();
while (scanner.hasNextLine()) {
buffer.append(scanner.nextLine());

}
scanner.close();
// 得到为xml字符串
System.out.println("buffer = " + buffer);
// 9. 解析xml字符串获得返回的字符串
String xml = parseXmlToString(buffer);
System.out.println("xml = " + xml);

}
}

private static String parseXmlToString(StringBuffer buffer) {
try {
// 得到对应的文档对象
Document document = DocumentHelper.parseText(buffer.toString());
// "//"从任意位置的节点上选择名称为 item 的节点。
Node node = document.selectSingleNode("//return");
return node.getText();

} catch (DocumentException e) {
e.printStackTrace();

}
return null;
}

private static String getXmlString(String string) {
String xml = "<?xml version=\"1.0\" ?>"
+ "<S:Envelope xmlns:S=\"http://schemas.xmlsoap.org/soap/envelope/\">"
+ "<S:Body>" + "<ns2:queryWeather xmlns:ns2=\"http://impl.service.ws.com/\">"
+ "<arg0>" + string
+ "</arg0>" + "</ns2:queryWeather>" + "</S:Body>"
+ "</S:Envelope>\"";
return xml;

}
}
  1. 得到的结果:

可以看出,可以得到相对应的结果。

6. 三种方式比较

  1. 第一种方法:它虽然代码比较简洁,但是耦合度较高,只能适用于当前下载的服务端的地址。一般我们服务端启动后,接口方法,参数这些都不会发生太大的变化。但是如果地址改变了也就需要重新下载服务端代码。
  2. 第二种方式:在第一种方式的基础上,将服务端访问地址抽出来,这样我们就可以在xml文件等配置文件中进行配置对应的服务访问地址,一旦需要修改,只需修改配置文件的访问地址即可。
  3. 第三种方式:它并不需要下载服务端代码,通过HTTPURLConnection的方式去远程访问服务端,但是需要自己去生成xml字符串,然后得到的结果也需要自己去解析出来。而一旦传入的xml字符串参数错了,就会响应失败,或者得到的字符串解析失败的问题。
  4. 个人推荐第二种方式,下载服务端代码,然后编写访问的服务端的地址和创建对应的接口去调方法。无需编写额外的xml字符串和解析字符串。

😬优缺点

🌈优点

  1. 通过我们的客户端第三种方式,就可以得知我是采用XML格式封装数据,并且XML它是跨平台,并且不需要特定的语言编写出来,所以我也是跨平台的。
  2. 通过客户端访问的方式,我们可以得知服务端可以在远程,可以在本地,我们可以通过SOAP协议实现异地调用。

🌩️ 缺点

因为我是采用XML格式封装数据的嘛,所以在传输过程中,可能需要传输额外的标签,然而标签越来越大的话,导致webservice性能下降。

🔥 使用场景

  1. 发布一个服务(对内/对外),不考虑客户端类型,不考虑语言,不考虑性能,建议使用WebService。
  2. 服务端已经确定使用WebService,客户端不能选择,就必须使用WebService。

💧 不适用场景

  1. 在系统考虑性能情况下,不建议使用WebService。
  2. 同构程序(一个公司中系统之间的接口)下不建议使用WebService,比如java用RMI(远程方法调用),不需要翻译成XML的数据。

🌸总结

相信各位看官都对webService如何使用及其它的概念和一些知识点都有了稍稍的了解吧,不确定用到人的是否很多,这里只做入门练习,当然如果我们要整合到SpringBoot项目中,甚至我们可以集成相关依赖来使用webService,例如我们可以使用SpringBoot使用CXF集成WebService。那我们应不应该学习这一门技术呢?其实我觉得看需求,如果需要用到了,我们可以快速了解下,用一用小demo快速入门,接着使用集成CXF来整合到项目中,从而达到这门技术的应用到实际项目中去!

我个人其实也没有接触过,只因项目中需要来调用这样webService接口的需求,所以我也是刚入门,而且现在各网页接口基本都是基于http请求来做的,所以我们可以当了解了解咯,走过路过,瞧一瞧,保证不吃亏呀!!!

让我们也一起加油吧!本人不才,如有什么缺漏、错误的地方,也欢迎各位人才大佬评论中批评指正!当然如果这篇文章确定对你有点小小帮助的话,也请亲切可爱的人才大佬们给个点赞、收藏下吧,一键三连,非常感谢!

学到这里,今天的世界打烊了,晚安!虽然这篇文章完结了,但是我还在,永不完结。我会努力保持写文章。来日方长,何惧车遥马慢!

感谢各位看到这里!愿你韶华不负,青春无悔!

本文转载自: 掘金

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

1…108109110…956

开发者博客

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