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

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


  • 首页

  • 归档

  • 搜索

Container killed by YARN for e

发表于 2021-11-25

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

大家好,我是怀瑾握瑜,一只大数据萌新,家有两只吞金兽,嘉与嘉,上能code下能teach的全能奶爸

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~


经常我们提交任务到 yarn上后出现 内存溢出的错误 类似

1
csharp复制代码ExecutorLostFailure (executor 30 exited caused by one of the running tasks) Reason: Container killed by YARN for exceeding memory limits. 40.2 GB of 40 GB physical memory used. Consider boosting spark.yarn.executor.memoryOverhead.

这个错误总会使你的job夭折。它的意思是:因为超出内存限制,集群停掉了,然后就会无尽的重试重试,直到彻底崩了

问题:出现以上问题原因.

数据出现了倾斜等原因导致其中一个contaimer 内存负载太大 运行失败

container 内存分配图

建议解决方式

1、增加分区数,或对分区的具体逻辑修改呢(源码)避免出现数据倾斜这个通过scala 语法进行修改

2、但是对spark sql 进行任务调度时一般难控制,可采用增加spark.yarn.executor.memoryOverhead设置为最大值,可以考虑一下4096。这个数值一般都是2的次幂

影响其几个参数:

yarn.scheduler.maximum-allocation-mb

这个参数表示每个container能够申请到的最大内存,一般是集群统一配置。Spark中的executor进程是跑在container中,所以container的最大内存会直接影响到executor的最大可用内存。当你设置一个比较大的内存时,日志中会报错,同时会打印这个参数的值。如下图 ,6144MB,即6G 本集群cm 设置100G 从前ambari 中设置10G

spark.yarn.executor.memoryOverhead

executor执行的时候,用的内存可能会超过executor-memoy,所以会为executor额外预留一部分内存。spark.yarn.executor.memoryOverhead代表了这部分内存。这个参数如果没有设置,会有一个自动计算公式(位于ClientArguments.scala中),

1
ini复制代码--conf spark.yarn.executor.memoryOverhead=4096

其中,MEMORY_OVERHEAD_FACTOR默认为0.1,executorMemory为设置的executor-memory, MEMORY_OVERHEAD_MIN默认为384m。参数MEMORY_OVERHEAD_FACTOR和MEMORY_OVERHEAD_MIN一般不能直接修改,是Spark代码中直接写死的

executor-memory计算

val executorMem = args.executorMemory + executorMemoryOverhead

Spark中executor-memory

程序中可以通过 –executor-memory 来设置executor执行时所需的memory

1
css复制代码--executor-memory 40G

1) 如果没有设置spark.yarn.executor.memoryOverhead,

executorMem= X+max(X*0.1,384)

如果设置了spark.yarn.executor.memoryOverhead(整数,单位是M)

executorMem=X +spark.yarn.executor.memoryOverhead

设置executorMem需要满足的条件:

xecutorMem< yarn.scheduler.maximum-allocation-mb


结束语

如果您喜欢我的文章,可以[关注⭐]+[点赞👍]+[评论📃],您的三连是我前进的动力,期待与您共同成长~

可关注公众号【怀瑾握瑜的嘉与嘉】,获取资源下载方式

本文转载自: 掘金

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

会使用自定义注解 ≈ 好的程序员 ? 教你结合 AOP 切

发表于 2021-11-25

一、前言

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战 。听说有人不会自定义注解?不会写还不会 Copy ? ☞ 【ExcelUtil】二次封装,注解驱动,用起来不要太舒服! - 掘金 (juejin.cn) ,好了不开玩笑了,今天帅小伙就带着大伙梳理一遍注解也就是 @interface 正确的打开方式,除此之外,结合 AOP 切面统一打印出入参日志,对于每个访问注解绑定的接口方法的请求都一目了然,不仅方便接口的调试,还能给你一个优雅、整齐且大方的控制台日志记录。

二、效果演示

2.1 访问接口

01.gif

2.2 控制台日志输出

演示效果.png

三、如何设计一个注解

3.1 概念

知其然,要知其所以然,所以我们先来康康官方对注解的描述是什么:

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.

翻译过来的大意是:

注释是一种元数据的形式,可以添加到Java源代码中。类,方法,变量,参数和包可以注释。注释对他们注释的代码的操作没有直接影响。

综上来说,注解其实相当于 Java 的一种特殊的数据类型,也可以把它当做一个可以自定义的标记去理解,和类、接口、枚举类似,可以使用在很多不同的地方并且对原有的操作代码没有任何影响,仅做中间收集和处理。

3.2 小试牛刀

1
2
3
4
5
6
7
8
9
10
11
java复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Excel {

public String name();

int sort() default Integer.MAX_VALUE;

... ...
}

说明(从上往下):

  • 使用该注解在程序运行时被 JVM 保留,并且被编译器记录到 class 文件中,所以能够通过 Java 反射机制读取到注解中的属性等
  • 该注解仅能使用在字段上,不能用在类、方法、变量上
  • 该注解有两个属性,一个是 name 另一个是 sort 属性,属性?你可能就会问了后边不是带一对圆括号嘛,不应该是方法吗?看似接口中定义的抽象方法,实则看没看到 default 关键字,官方管定义在注解内的是 注解类型元素 ,不过我习惯管它们叫属性,因为在使用注解时,总是以键值对的形式传参
  • 访问修饰符必须为 public,不写默认为 public
  • 圆括号不是定义方法参数的地方,也不能在括号中定义任何参数,这仅仅是一个特殊的写法罢了
  • default 表示未设置该属性时的默认值,值需和类型保持一致
  • 如果没有 default 默认值,表示该类型元素必须在后续赋值

3.3 注解注解的注解类

此外,在 JDK 中提供了 4 个标准的用来对注解类型进行注解的注解类(元注解),分别是:

  • @Target 是专门用来限定某个自定义注解能够被应用在哪些 Java 元素上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
/** 类,接口(包括注释类型)或枚举声明 */
TYPE,

/** Field declaration (includes enum constants) */
/** 字段声明(包括枚举常量) */
FIELD,

/** Method declaration */
/** 方法声明 */
METHOD,

/** Formal parameter declaration */
/** 形参声明 */
PARAMETER,

/** Constructor declaration */
/** 构造器声明 */
CONSTRUCTOR,

/** Local variable declaration */
/** 本地变量声明 */
LOCAL_VARIABLE,

/** Annotation type declaration */
/** 注解类型声明 */
ANNOTATION_TYPE,

/** Package declaration */
/** 包声明 */
PACKAGE,

/**
* Type parameter declaration
*
* @since 1.8
*/
/** 类型参数声明 */
TYPE_PARAMETER,

/**
* Use of a type
*
* @since 1.8
*/
/** 使用类型 */
TYPE_USE
}
  • @Retention 该注解有 “保留”、“保持” 之义,用来定义注解的留存策略,可指定的留存策略只有 3 个:
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
java复制代码
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*
* 编译器丢弃注解,即被编译器忽略
*/
SOURCE,

/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*
* 注释将被编译器记录在 class 文件中,但在运行时不需要被虚拟机保留。这是一个默认的行为。
*/
CLASS,

/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* 注释将由编译器记录在类文件中,并在运行时由虚拟机保留,因此可以通过反射读取。
*
* @see java.lang.reflect.AnnotatedElement
*/
RUNTIME
}

一般使用无特殊需要,使用 RetentionPolicy.RUNTIME 就够了。

  • @Documented 是被用来指定自定义注解是否能随着被定义的 Java 文件生成到 JavaDoc 文档当中
  • @Inherited 是指定某个自定义注解如果写在了父类的声明部分,那么子类的声明部分也能自动拥有该注解

注意:@Inherited 注解只对那些 @Target 被定义为 ElementType.TYPE 的自定义注解起作用。

3.4 使用流程

自定义注解三步走.png

四、代码实现

4.1 第一步

编写设计注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:50
*/
@Retention(RetentionPolicy.RUNTIME)
/* 注解用在方法上 */
@Target(ElementType.METHOD)
public @interface MyAnnotation {

/**
* 接口方法描述
*/
public String description() default "默认描述";
}

这步没什么好讲的,上面的概念理解掌握了,轻轻松松写出这个注解应该是没有什么问题!

4.2 第二步

使用切面注解进行标记,因为是对请求相关的日志打印,所以我们随便写一个控制层接口方法进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-24
* @time: 13:43
*/
@RestController
@RequestMapping(value = "/test")
public class TestController {

private final static Logger log = LoggerFactory.getLogger(TestController.class);

@GetMapping("/hello/{say}")
@MyAnnotation(description = "测试接口")
public String sayHello(@PathVariable("say") String content) {
log.info("Client is saying:{}", content);
return content;
}
}

4.3 第三步

最后一步也是最关键的一步,在运行时解析注解执行切面操作,所以对应地写一个切面类:

image.png

新建切面类后,考虑到日志的打印,这段代码必不可少:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码
/**
* @description: TODO
* @author: HUALEI
* @date: 2021-11-19
* @time: 15:56
*/
@Aspect
@Component
public class MyAnnotationAspect {

private static final Logger logger = LoggerFactory.getLogger(MyAnnotationAspect.class);

......
......
}

@Aspect 和 @Component 注解必不可少,@Component 大伙应该在熟悉不过了,将该类注入到 Spring 容器中;而另一个 @Aspect 注解的作用是把当前类标识成一个切面供容器去读取。

注意: 打印日志推荐使用的包是 slf4j.Logger 。

1
2
3
4
5
6
7
8
9
10
java复制代码
/**
* 配置织入点
*
* 切到所有被 @MyAnnotation 注解修饰的方法
*/
@Pointcut("@annotation(com.xxx.xxx.annotation.MyAnnotation)")
// @annotation(annotationType) 匹配指定注解为切入点的方法,annotationType 为注解的全路径
public void myAnnotationPointCut() {
}

配置织入点,切到所有被 @MyAnnotation 注解修饰的方法,不需要再方法体内编写实际的代码!

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
java复制代码
/**
* 环绕增强,可自定义目标方法执行的时机
* 实现记录所有被 @MyAnnotation 注解修饰接口请求功能
*
* @param pjp 连接点对象
* @return 目标方法的返回值
* @throws Throwable 异常
*/
@Around("myAnnotationPointCut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 请求开始时间戳
// long begin = System.currentTimeMillis();

TimeInterval timer = DateUtil.timer();

// 通过请求上下文(执行目标方法之前)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

HttpServletRequest request = attributes.getRequest();

// 获取连接点的方法签名对象
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;

// 获取接口方法
Method method = methodSignature.getMethod();
// 通过接口方法获取该方法上的 @MyAnnotation 注解对象
MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);

// 通过注解获取接口方法描述信息
String description = myAnnotation.description();

// 请求开始(前置通知)
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求开始 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<");
// 请求链接
logger.info("请求链接:{}", request.getRequestURL().toString());
// 接口方法描述信息
logger.info("接口描述:{}", description);
// 请求类型
logger.info("请求类型:{}", request.getMethod());
// 请求方法
logger.info("请求方法:{}.{}", signature.getDeclaringTypeName(), signature.getName());
// 请求远程地址
logger.info("请求远程地址:{}", request.getRemoteAddr());
// 请求入参
logger.info("请求入参:{}", JSONUtil.toJsonStr(pjp.getArgs()));

// 请求结束时间戳
// long end = System.currentTimeMillis();

// 请求耗时
logger.info("请求耗时:{}", timer.intervalPretty());

// 请求返回结果(执行目标方法之后)
Object processedResult = pjp.proceed();
// 请求返回
logger.info("请求返回:{}", JSONUtil.toJsonStr(processedResult));

// 请求结束(后置通知)
logger.info(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 请求结束 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" + System.lineSeparator());

return processedResult;
}

pjp 连接点对象,JoinPoint 的子接口,可以获取当前切入的方法的参数、代理类等信息,因此可以记录一些信息、验证一些信息等,它有两个重要的方法:

  • Object proceed() throws Throwable 执行目标方法
  • Object proceed(Object[] var1) throws Throwable 传入的新的参数去执行目标方法

整个代码都有注解,这里就不赘述代码逻辑了!

棒.gif

4.4 扩展

除了上面用到的 @PointCut 和 @Around 注解,还有另外 4 个使用 AOP 常用的注解:

  • @Before :前置增强,在切点之前织入相关代码
  • @After :final 增强,不管是抛出异常或者正常退出都会执行
  • @AfterReturning :后置增强,方法正常退出时执行
  • @AfterThrowing :异常抛出增强,切点方法抛出异常时执行

执行顺序:@Around => @Before => 执行接口方法中的代码 => @After => @AfterReturning

有兴趣的同学,可以环绕增强中的代码拆分到前置和后置增强中,以便更好地理解这四个常用注解使用场景! (๑•̀ㅂ•́)و✧

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。

本文转载自: 掘金

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

一文带你掌握Mybatis二级缓存源码中Cache标签解析

发表于 2021-11-25

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

Mybatis二级缓存源码中Cache标签解析

Mybatis缓存结构图

image.png

有上图可知:⼆级缓存构建在⼀级缓存之上,在收到查询请求时,MyBatis⾸先会查询⼆级缓存,若⼆级缓存未命中,再去查询⼀级缓存,⼀级缓存没有,再查询数据库。
顺序就是 二级缓存 —》一级缓存 —》数据库

与一级缓存不同的是,⼆级缓存和具体的命名空间绑定,⼀个Mapper中有⼀个Cache,相同Mapper中的MappedStatement共⽤⼀个Cache,⼀级缓存则是和SqlSession绑定。一个Mapper可能有多个SqlSession,但是只有一个Cache。

如何启用二级缓存

  • 开启全局⼆级缓存配置
1
2
3
xml复制代码<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
  • 在需要使⽤⼆级缓存的Mapper配置⽂件中配置标签
1
xml复制代码<cache></cache>
  • 在具体CURD标签上配置useCache=true
1
2
3
xml复制代码<select id="findById" resultType="com.test.pojo.User" useCache="true">
select * from user where id = #{id}
</select>

标签< cache />的源码解析

如何解析cache标签属性

根据Mybatis前两篇Mybatis源码剖析,可以知道xml的解析⼯作主要交给XMLConfigBuilder.parse()⽅法来实现。

image.png

因为标签chache配置在mapper的映射配置文件中,因此,我们直接看关于mappers标签的解析

image.png

XMLMapperBuilder配置文件的作用是,解析映射配置文件,执行mapperParser.parse()方法进行解析

image.png

通过configurationElement方法中有cache标签的解析处理‘

image.png

通过源码可知:针对与cache相关方法有两个,一个是cacheRefElement方法,主要是解析cache-ref标签,该标签作用是,在当前mapper中引入其他缓存,一个是cacheRefElement方法,主要是解析cache标签

image.png

通过cacheRefElement方法就可以解析cache标签里面所有的属性,然后通过这些属性使用useNewCache方法构造一个cache对象,至此cache标签解析已经完成

image.png

如何构建Cache对象

image.png

我们看到⼀个Mapper.xml只会解析⼀次标签,也就是只创建⼀次Cache对象,放进configuration中,并将cache赋值给MapperBuilderAssistant.currentCache

我们看到将Mapper中创建的Cache对象,加⼊到了每个MappedStatement对象中,也就是同⼀个Mapper中所有的。

至此,Cache标签源码解析已经完成,明天会进一步分析

本文转载自: 掘金

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

跟着leedcode刷算法 -- 树2

发表于 2021-11-25

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

image.png

题3

二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

示例 1:

输入:root = [1,2,3,null,null,4,5]
输出:[1,2,3,null,null,4,5]
示例 2:

输入:root = []
输出:[]
示例 3:

输入:root = [1]
输出:[1]
示例 4:

输入:root = [1,2]
输出:[1,2]

提示:

  • 树中结点数在范围 [0, 104] 内
  • -1000 <= Node.val <= 1000
  • 相关标签*
  • 树
  • 深度优先搜索
  • 广度优先搜索
  • 设计
  • 字符串
  • 二叉树

解题思路:
深度优先 DFS 递归

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
python复制代码# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None

class Codec:

def serialize(self, root):
"""Encodes a tree to a single string.

:type root: TreeNode
:rtype: str
"""
def dfs(node):
if node:
res.append(str(node.val))
dfs(node.left)
dfs(node.right)
else:
res.append("#")

res = []
dfs(root)
return ",".join(res)


def deserialize(self, data):
"""Decodes your encoded data to tree.

:type data: str
:rtype: TreeNode
"""
def dfs():
v = next(res)
if v == "#":
return None
node = TreeNode(int(v))
node.left = dfs()
node.right = dfs()
return node
res = iter(data.split(","))
return dfs()

# Your Codec object will be instantiated and called as such:
# ser = Codec()
# deser = Codec()
# ans = deser.deserialize(ser.serialize(root))

执行结果:

image.png

题4

天际线问题

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度,请返回由这些建筑物形成的 天际线 。

每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildings[i] = [lefti, righti, heighti] 表示:

  • lefti 是第 i 座建筑物左边缘的 x 坐标。
  • righti 是第 i 座建筑物右边缘的 x 坐标。
  • heighti 是第 i 座建筑物的高度。
  • 天际线 应该表示为由 “关键点” 组成的列表,格式 [[x1,y1],[x2,y2],…] ,并按 x 坐标 进行 排序 。关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点,y 坐标始终为 0 ,仅用于标记天际线的终点。此外,任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
  • 注意:输出天际线中不得有连续的相同高度的水平线。例如 […[2 3], [4 5], [7 5], [11 5], [12 7]…] 是不正确的答案;三条高度为 5 的线应该在最终输出中合并为一个:[…[2 3], [4 5], [12 7], …]

示例 1:

image.png

  • 输入:buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
  • 输出:[[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
  • 解释:
  • 图 A 显示输入的所有建筑物的位置和高度,
  • 图 B 显示由这些建筑物形成的天际线。图 B 中的红点表示输出列表中的关键点。
    示例 2:
  • 输入:buildings = [[0,2,3],[2,5,3]]
  • 输出:[[0,3],[5,0]]

提示:

  • 1 <= buildings.length <= 104
  • 0 <= lefti < righti <= 231 - 1
  • 1 <= heighti <= 231 - 1
  • buildings 按 lefti 非递减排序
  • 相关标签*
  • 树状数组
  • 线段树
  • 数组
  • 分治
  • 有序集合
  • 扫描线
  • 堆(优先队列)

这里借助一下SortedList 方法来维持高度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
ini复制代码from sortedcontainers import SortedList

class Solution:
def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
res = []
ans = []
for l, r, h in buildings:
ans.append((l, -h))
ans.append((r, h))
ans.sort()
level = SortedList([0])
response = 0
for i, j in ans:
if j < 0:
level.add(j)
else:
level.remove(-j)
curr_max = -level[0]
if curr_max != response:
res.append([i, curr_max])
response = curr_max
return res

执行结果:

image.png

本文转载自: 掘金

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

如何利用 JuiceFS 的性能工具做文件系统分析和调优

发表于 2021-11-25

JuiceFS 是一款面向云原生环境设计的高性能 POSIX 文件系统,在 AGPL v3.0 开源协议下发布。作为一个云上的分布式文件系统,任何存入 JuiceFS 的数据都会按照一定规则拆分成数据块存入对象存储(如 Amazon S3),相对应的元数据则持久化在独立的数据库中。这种结构决定了 JuiceFS 的存储空间可以根据数据量弹性伸缩,可靠地存储大规模的数据,同时支持在多主机之间共享挂载,实现跨云跨地区的数据共享和迁移。

从 v0.13 发布以来, JuiceFS 新增了多项与性能监测和分析相关的功能,从某种程度上说,开发团队希望 JuiceFS 既能提供大规模分布式计算场景下的出色性能,也能广泛地覆盖更多日常的使用场景。

本文我们从单机应用入手,看依赖单机文件系统的应用是否也可以用在 JuiceFS 之上,并分析它们的访问特点进行针对性的调优。

测试环境

接下来的测试我们会在同一台亚马逊云服务器上进行,配置情况如下:

  • 服务器配置:Amazon c5d.xlarge: 4 vCPUs, 8 GiB 内存, 10 Gigabit 网络, 100 GB SSD
  • JuiceFS:使用本地自建的 Redis 作为元数据引擎,对象存储使用与服务器相同区域的 S3。
  • EXT4:直接在本地 SSD 上创建
  • 数据样本:使用 Redis 的源代码作为测试样本

测试项目一:Git

常用的 git 系列命令有 clone、status、add、diff 等,其中 clone 与编译操作类似,都涉及到大量小文件写。这里我们主要测试 status 命令。

分别将代码克隆到本地的 EXT4 和 JuiceFS,然后执行 git status 命令的耗时情况如下:

  • Ext4:0m0.005s
  • JuiceFS:0m0.091s

可见,耗时出现了数量级的差异。如果单从测试环境的样本来说,这样的性能差异微乎其微,用户几乎是察觉不到的。但如果使用规模更大的代码仓库时,二者的性能差距就会逐渐显现。例如,假设两者耗时都乘以 100 倍,本地文件系统需要约 0.5s,尚在可接受范围内;但 JuiceFS 会需要约 9.1s,用户已能感觉到明显的延迟。为搞清楚主要的耗时点,我们可以使用 JuiceFS 客户端提供的 profile 工具:

1
bash复制代码$ juicefs profile /mnt/jfs

在分析过程中,更实用的方式是先记录日志,再用回放模式:

1
2
3
4
bash复制代码$ cat /mnt/jfs/.accesslog > a.log
# 另一个会话:git status
# Ctrl-C 结束 cat
$ juicefs profile --interval 0 a.log

结果如下:

image.png

这里可以清楚地看到有大量的 lookup 请求,我们可以通过调整 FUSE 的挂载参数来延长内核中元数据的缓存时间,比如使用以下参数重新挂载文件系统:

1
bash复制代码$ juicefs mount -d --entry-cache 300 localhost /mnt/jfs

然后我们再用 profile 工具分析,结果如下:

image.png

可以看到,lookup 请求减少了很多,但都转变成了 getattr 请求,因此还需要属性的缓存:

1
bash复制代码$ juicefs mount -d --entry-cache 300 --attr-cache 300 localhost /mnt/jfs

此时测试发现 status 命令耗时下降到了 0m0.034s,profile 工具结果如下:

image.png

可见一开始最耗时的 lookup 已经减少了很多,而 readdir 请求变成新的瓶颈点。我们还可以尝试设置 --dir-entry-cache,但提升可能不太明显。

测试项目二:Make

大型项目的编译时间往往需要以小时计,因此编译时的性能显得更加重要。依然以 Redis 项目为例,测试编译的耗时为:

  • Ext4:0m29.348s
  • JuiceFS:2m47.335s

我们尝试调大元数据缓存参数,整体耗时下降约 10s。通过 profile 工具分析结果如下:

image.png

显然这里的数据读写是性能关键,我们可以使用 stats 工具做进一步的分析:

1
bash复制代码$ juicefs stats /mnt/jfs

其中一段结果如下:

image.png

从上图可见 fuse 的 ops 与 meta 接近,但平均 latency 远大于 meta,因此可以判断出主要的瓶颈点在对象存储侧。不难想象,编译前期产生了大量的临时文件,而这些文件又会被编译的后几个阶段读取,以通常对象存储的性能很难直接满足要求。好在 JuiceFS 提供了数据 writeback 模式,可以在本地盘上先建立写缓存,正好适用于编译这种场景:

1
bash复制代码$ juicefs mount -d --entry-cache 300 --attr-cache 300 --writeback localhost /mnt/jfs

此时编译总耗时下降到 0m38.308s,已经与本地盘非常接近了。后阶段的 stats 工具监控结果如下:

image.png

可见,读请求基本全部在 blockcache 命中,而不再需要去访问对象存储;fuse 和 meta 侧的 ops 统计也得到了极大提升,与预期吻合。

总结

本文以本地文件系统更擅长的 Git 仓库管理和 Make 编译任务为切入点,评估这些任务在 JuiceFS 存储上的性能表现,并使用 JuiceFS 自带的 profile 与 stats 工具进行分析,通过调整文件系统挂载参数做针对性的优化。

毫无疑问,本地文件系统与 JuiceFS 等分布式文件系统存在着天然的特征差异,二者面向的应用场景也截然不同。本文选择了两种特殊的应用场景,只是为了在差异鲜明的情境下介绍如何为 JuiceFS 做性能调优,旨在抛砖引玉,希望大家举一反三。如果你有相关的想法或经验,欢迎在 JuiceFS 论坛或用户群分享和讨论。

推荐阅读
知乎 x JuiceFS:利用 JuiceFS 给 Flink 容器启动加速

项目地址: Github (github.com/juicedata/j…)如有帮助的话欢迎关注我们哟! (0ᴗ0✿)

本文转载自: 掘金

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

一起来看看什么是JMM模型

发表于 2021-11-25

一起来看看什么是JMM模型

JMM(Java Memory Model)是java内存模型,是一种规范、规则,一种抽象的概念。通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。那么JMM规定的访问方式是怎样的,JMM规定,所有的变量必须存储在主内存当中,每个线程访问主内存的变量时,不能直接访问,而是在本线程内开辟一块工作内存,将主内存中的变量复制到工作内存中,线程才可以进行访问。

未命名文件 (4).png

  • 主内存:存储Java实例对象。
  • 工作内存:存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝)。

JVM与JMM的区别

JVM是java虚拟机,是负责管理内存的。JMM是java内存模型,是一种抽象的概念,并不真实存在,规定了程序中各个变量的访问方式,侧重于围绕原子性、有序性、可见性。

JMM与硬件内存架构的关系

前面我们说到,JMM是并不真实存在的,在硬件内存中并不知道JMM内存模型的存在,因此,多线程的执行最终还是会回归到硬件架构中。

为什么需要JMM

假设我们遇到了以下场景:

现在我有两个线程,分别是线程A、线程B,在JMM规范下,他们是有各自的工作内存的,假设现在他们分别要对主内存共享变量x=6进行修改操作,A想要修改x为1,B想要修改x为2,那么x最终的结果是不确定的,要看先执行谁,这就导致了主内存与工作内存数据不一致性的问题。因此我们需要JMM协议去规范这一些问题。

未命名文件 (5).png

并发编程的原子性、有序性、可见性问题

原子性

原子性顾名思义,是指操作是不可中断的、一气呵成的。

JMM保证原子性:JMM协议是通过synchronized关键字去保证原子性的。

有序性

在cpu执行指令时,他会对指令进行优化,也就是我写在下面的代码有可能在cpu指令优化的作用下提前执行,但是前提都是保证我们的执行结果是正确的。但有时指令优化也会带来一系列的问题,因此需要去保证有序性。

JMM保证有序性:JMM协议是通过volatile关键字去保证原子性的。

可见性

可见性是指一个线程能否读到变量x最新的值,因为变量x有可能会被其他线程修改值。

JMM保证可见性:JMM协议是通过volatile关键字去保证原子性的。

本文转载自: 掘金

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

SpringSecurity认证基本原理与认证2种方式 一、

发表于 2021-11-25

[TOC]

Spring Security功能的实现主要是由一系列过滤器相互配合完 成。也称之为过滤器链,Spring Security默认加载15个过滤器, 但是随着配置可以增加或者删除一些过滤器.

一、过滤器链介绍

image-20211124155050759

过滤器是一种典型的AOP思想,下面简单了解下这些过滤器链,后续再源码剖析中在涉及到过滤器链在 仔细讲解

1.org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter

根据请求封装获取WebAsyncManager,从WebAsyncManager获取/注册的安全上下文可调 用处理拦截器

2.org.springframework.security.web.context.SecurityContextPersistenceFilter

SecurityContextPersistenceFilter主要是使用SecurityContextRepository在session中保存 或更新一个SecurityContext,并将SecurityContext给以后的过滤器使用,来为后续fifilter 建立所需的上下文。SecurityContext中存储了当前用户的认证以及权限信息。

3.org.springframework.security.web.header.HeaderWriterFilter

向请求的Header中添加相应的信息,可在http标签内部使用security:headers来控制

4.org.springframework.security.web.csrf.CsrfFilter

csrf又称跨域请求伪造,SpringSecurity会对所有post请求验证是否包含系统生成的csrf的 token信息,如果不包含,则报错。起到防止csrf攻击的效果。

5.org.springframework.security.web.authentication.logout.LogoutFilter

匹配URL为/logout的请求,实现用户退出,清除认证信息。

6.org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter

表单认证操作全靠这个过滤器,默认匹配URL为/login且必须为POST请求

7.org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter

如果没有在配置文件中指定认证页面,则由该过滤器生成一个默认认证页面。

8.org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter

由此过滤器可以生产一个默认的退出登录页面

9.org.springframework.security.web.authentication.www.BasicAuthenticationFilter

此过滤器会自动解析HTTP请求中头部名字为Authentication,且以Basic开头的头信息

10.org.springframework.security.web.savedrequest.RequestCacheAwareFilter

通过HttpSessionRequestCache内部维护了一个RequestCache,用于缓存 HttpServletRequest

  1. org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter

针对ServletRequest进行了一次包装,使得request具有更加丰富的API

12.org.springframework.security.web.authentication.AnonymousAuthenticationFilter

当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到 SecurityContextHolder中。spring security为了兼容未登录的访问,也走了一套认证流程, 只不过是一个匿名的身份。

  1. org.springframework.security.web.session.SessionManagementFilter

securityContextRepository限制同一用户开启多个会话的数量

14.org.springframework.security.web.access.ExceptionTranslationFilter

异常转换过滤器位于整个springSecurityFilterChain的后方,用来转换整个链路中出现的异 常

  1. org.springframework.security.web.access.intercept.FilterSecurityInterceptor

获取所配置资源访问的授权信息,根据SecurityContextHolder中存储的用户信息来决定其 是否有权限。

二、认证方式

2.1 HttpBasic认证

HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋 的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种“防君子不防小人”的登录验 证。

在使用的Spring Boot早期版本为1.X版本,依赖的Security 4.X版本,那么就无需任何配置,启动 项目访问则会弹出默认的httpbasic认证。现在使用的是spring boot2.0以上版本(依赖Security 5.X版本),HttpBasic不再是默认的验证模式,在spring security 5.x默认的验证模式已经是表单 模式。

HttpBasic模式要求传输的用户名密码使用Base64模式进行加密。如果用户名是 “admin” , 密码是“ admin”,则将字符串”admin:admin” 使用Base64编码算法加密。加密结果可能是: YWtaW46YWRtaW4=。HttpBasic模式真的是非常简单又简陋的验证模式,Base64的加密算法是 可逆的,想要破解并不难

image-20211124161756023

2.2 formLogin登录认证模式

Spring Security的HttpBasic模式,该模式比较简单,只是进行了通过携带Http的Header进行 简单的登录验证,而且没有定制的登录页面,所以使用场景比较窄。对于一个完整的应用系统,与 登录验证相关的页面都是高度定制化的,非常美观而且提供多种登录方式。这就需要Spring Security支持我们自己定制登录页面, spring boot2.0以上版本(依赖Security 5.X版本)默认会生 成一个登录页面.

2.3 表单认证

2.3.1 在config包下编写SecurityConfiguration配置类

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
java复制代码/**
* @author wuzhixuan
* @version 1.0.0
* @ClassName SecurityConfiguration.java
* @Description Security配置类
* @createTime 2021年11月24日 16:35:00
*/

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}

@Override
public void configure(WebSecurity web) throws Exception {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic() // 开启base验证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要登录认证才能访问*/
http.formLogin().loginPage("/toLoginPage") // 开启表单验证
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll() // 放行当前请求
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问;
}
}

image-20211124164629344

重启服务发现css,js等静态文件没有成功加载

2.3.2 解决静态资源被拦截问题

1
2
3
4
5
java复制代码@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring().antMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico");
}

Spring Security 中,安全构建器 HttpSecurity 和 WebSecurity 的区别是 :

  • WebSecurity 不仅通过 HttpSecurity 定义某些请求的安全控制,也通过其他方式定义其他某些 请求可以忽略安全控制;
  • HttpSecurity 仅用于定义需要安全控制的请求(当然 HttpSecurity 也可以指定某些请求不需要 安全控制);
  • 可以认为 HttpSecurity 是 WebSecurity 的一部分, WebSecurity 是包含 HttpSecurity 的更大 的一个概念;
  • 构建目标不同
    • WebSecurity 构建目标是整个 Spring Security 安全过滤器 FilterChainProxy`,
    • HttpSecurity 的构建目标仅仅是 FilterChainProxy 中的一个 SecurityFilterChain 。

image-20211124170821308

改造登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码protected void configure(HttpSecurity http) throws Exception {
/*http.httpBasic() // 开启base验证
.and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要登录认证才能访问*/
http.formLogin() // 开启表单验证
.loginPage("/toLoginPage") // 自定义登录页面
.loginProcessingUrl("/login") // 登录请求url
.usernameParameter("username") // 修改自定义表单name值
.passwordParameter("password")
.successForwardUrl("/") // 登录成功跳转的路径
.and().authorizeRequests()
.antMatchers("/toLoginPage").permitAll() // 放行当前请求
.anyRequest().authenticated(); //所有请求都需要登录认证才能访问;
// 关闭csrf防护
http.csrf().disable();
// 允许iframe加载页面
http.headers().frameOptions().sameOrigin();
}

发现行内框架iframe这里出现问题了. Spring Security下,X-Frame-Options默认为DENY,非Spring Security环境下,X-Frame-Options的默认大多也是DENY,这种情况下,浏览器拒绝当前页面加载任何 Frame页面,设置含义如下:

  • DENY:浏览器拒绝当前页面加载任何Frame页面 此选择是默认的.
  • SAMEORIGIN:frame页面的地址只能为同源域名下的页面

image-20211124171005902

设置之后看到成功展示了

2.4 基于数据库实现认证功能

之前我们所使用的用户名和密码是来源于框架自动生成的, 那么我们如何实现基于数据库中的用户名和 密码功能呢? 要实现这个得需要实现security的一个UserDetailsService接口, 重写这个接口里面 loadUserByUsername即可

  • 编写MyUserDetailsService并实现UserDetailsService接口,重写loadUserByUsername方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class MyUserDetailsService implements UserDetailsService {

@Autowired
private UserService userService;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException(username);// 用户名没有找到
}
// 先声明一个权限集合, 因为构造方法里面不能传入null
Collection<? extends GrantedAuthority> authorities = new ArrayList<>();

return new org.springframework.security.core.userdetails.User(username,
"{noop}" + user.getPassword(),// {noop}表示不加密认证
true, // 用户是否启用 true 代表启用
true,// 用户是否过期 true 代表未过期
true,// 用户凭据是否过期 true 代表未过期
true,// 用户是否锁定 true 代表未锁定
authorities
);
}
}
  • 在SecurityConfiguration配置类中指定自定义用户认证
1
2
3
4
5
6
7
8
9
java复制代码/**
* 身份验证管理器
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService);
}

2.5 密码加密验证

在基于数据库完成用户登录的过程中,我们所是使用的密码是明文的,规则是通过对密码明文添加 {noop} 前缀。那么下面 Spring Security 中的密码编码进行一些探讨。

Spring Security 中 PasswordEncoder 就是我们对密码进行编码的工具接口。该接口只有两个功能: 一个是匹配验证。另一个是密码编码。

image-20211124172444242

  • BCrypt算法介绍

BCrypt强哈希方法 每次加密的结果都不一样,所以更加的安全。

2.6 推出登录

org.springframework.security.web.authentication.logout.LogoutFilter
匹配URL为/logout的请求,实现用户退出,清除认证信息。
只需要发送请求,请求路径为/logout即可, 当然这个路径也可以自行在配置类中自行指定, 同时退出操作也有对应的自定义处理LogoutSuccessHandler,退出登录成功后执行,退出的同时如果有remember-me的数据,同时一并删除

1
java复制代码/*** 自定义登录成功,失败,退出处理类 */ @Service public class MyAuthenticationService implements AuthenticationSuccessHandler, AuthenticationFailureHandler, LogoutSuccessHandler { private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); ................ @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { System.out.println("退出成功后续处理...."); redirectStrategy.sendRedirect(request, response, "/toLoginPage"); } }

SecurityConfig配置修改

1
java复制代码.and().logout().logoutUrl("/logout")//设置退出url .logoutSuccessHandler(myAuthenticationService)//自定义退出处理

2.7 图形验证码验证

图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台

spring security添加验证码大致可以分为三个步骤:

  • 根据随机数生成验证码图片;
  • 将验证码图片显示到登录页面
  • 认证流程中加入验证码校验

Spring Security的认证校验是UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下:

image-20211125111756522

自定义验证码过滤器ValidateCodeFilter

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
java复制代码/**
* @author wuzhixuan
* @version 1.0.0
* @ClassName ValidateCodeFilter.java
* @Description 验证码验证filter 需要继承OncePerRequestFilter确保在一次请求只通过一次filter,而不 需要重复执行
* @createTime 2021年11月25日 11:19:00
*/
@Component

public class ValidateCodeFilter extends OncePerRequestFilter {

@Autowired
MyAuthenticationService myAuthenticationService;
@Autowired
StringRedisTemplate stringRedisTemplate;

@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {

// 判断是否是登录请求,只有登录请求才校验验证码
if (httpServletRequest.getRequestURI().equals("/login") && httpServletRequest.getMethod().equals("POST")) {
try {
validate(httpServletRequest);
} catch (ValidateCodeException e) {
myAuthenticationService.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}

//如果不是登录请求,直接调用后面的过滤器链
filterChain.doFilter(httpServletRequest, httpServletResponse);
}

private void validate(HttpServletRequest request) {

//获取ip
String remoteAddr = request.getRemoteAddr();
//拼接redis的key
String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + remoteAddr;

//从redis中获取imageCode
String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
String imageCode = request.getParameter("imageCode");

if (!StringUtils.hasText(imageCode)) { throw new ValidateCodeException("验证码的值不能为空!"); }

if (redisImageCode == null) { throw new ValidateCodeException("验证码已过期!"); }

if (!redisImageCode.equals(imageCode)) { throw new ValidateCodeException("验证码不正确!"); }

// 从redis中删除imageCode
stringRedisTemplate.delete(redisKey);
}
}

securityConfig加入自定义验证码拦截器

1
2
java复制代码// 加在用户名密码过滤器的前面
http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);

前端验证码代码

1
2
htm复制代码<img src="/code/image" alt="" width="100" height="32" class="passcode"
style="height:43px;cursor:pointer;"onclick="this.src=this.src+'?'">

获取验证码后台接口

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

import com.lagou.domain.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
* 处理生成验证码的请求
*/
@RestController
@RequestMapping("/code")
public class ValidateCodeController {

public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
public final static int expireIn = 60; // 验证码有效时间 60s

//使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
@Autowired
public StringRedisTemplate stringRedisTemplate;

@RequestMapping("/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
//获取访问IP
String remoteAddr = request.getRemoteAddr();
//生成验证码对象
ImageCode imageCode = createImageCode();
//生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr)
.set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
//通过IO流将生成的图片输出到登录页面上
ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
}

/**
* 用于生成验证码对象
*
* @return
*/
private ImageCode createImageCode() {

int width = 100; // 验证码图片宽度
int height = 36; // 验证码图片长度
int length = 4; // 验证码位数
//创建一个带缓冲区图像对象
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
//获得在图像上绘图的Graphics对象
Graphics g = image.getGraphics();

Random random = new Random();

//设置颜色、并随机绘制直线
g.setColor(getRandColor(200, 250));
g.fillRect(0, 0, width, height);
g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
g.setColor(getRandColor(160, 200));
for (int i = 0; i < 155; i++) {
int x = random.nextInt(width);
int y = random.nextInt(height);
int xl = random.nextInt(12);
int yl = random.nextInt(12);
g.drawLine(x, y, x + xl, y + yl);
}

//生成随机数 并绘制
StringBuilder sRand = new StringBuilder();
for (int i = 0; i < length; i++) {
String rand = String.valueOf(random.nextInt(10));
sRand.append(rand);
g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
g.drawString(rand, 13 * i + 6, 16);
}
g.dispose();
return new ImageCode(image, sRand.toString());
}

/**
* 获取随机演示
*
* @param fc
* @param bc
* @return
*/
private Color getRandColor(int fc, int bc) {
Random random = new Random();
if (fc > 255) {
fc = 255;
}
if (bc > 255) {
bc = 255;
}
int r = fc + random.nextInt(bc - fc);
int g = fc + random.nextInt(bc - fc);
int b = fc + random.nextInt(bc - fc);
return new Color(r, g, b);
}

}

本文由博客一文多发平台 OpenWrite 发布!

本文转载自: 掘金

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

LeetCode401 二进制手表

发表于 2021-11-25

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

题目描述:

401. 二进制手表 - 力扣(LeetCode) (leetcode-cn.com)

二进制手表顶部有 4 个 LED 代表 小时(0-11),底部的 6 个 LED 代表 分钟(0-59)。每个 LED 代表一个 0 或 1,最低位在右侧。

  • 例如,下面的二进制手表读取 “3:25” 。

image.png
(图源:WikiMedia - Binary clock samui moon.jpg ,许可协议:Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0) )

给你一个整数 turnedOn ,表示当前亮着的 LED 的数量,返回二进制手表可以表示的所有可能时间。你可以 按任意顺序 返回答案。

小时不会以零开头:

  • 例如,”01:00” 是无效的时间,正确的写法应该是 “1:00” 。

分钟必须由两位数组成,可能会以零开头:

  • 例如,”10:2” 是无效的时间,正确的写法应该是 “10:02” 。

示例一

1
2
css复制代码输入:turnedOn = 1
输出:["0:01","0:02","0:04","0:08","0:16","0:32","1:00","2:00","4:00","8:00"]

示例二

1
2
ini复制代码输入: turnedOn = 9
输出: []

提示:

  • 0 <= turnedOn <= 10

思路分析

枚举时分

由图可知,上一行代表小时,有4个比特表示。

下一行代表分钟,有6个比特表示。

而比特 0 代表灯灭,1 代表灯亮,所以题目给出亮灯的个数,即比特值中 1 的个数,

而求 1 的个数,我们之前已经做过了,可以直接使用sdk的api,也可以手动实现。

要求解本题,简单点的话我们可以2遍循环

枚举小时和分钟的所有可能值,然后求出当中 1 的个数,如果等于 turnedOn ,则为答案之一。

然后还有个要注意的就是 小时不以 0 开头,分钟 个位数的情况下必须得有 0.

AC代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Kotlin复制代码class Solution {

    fun readBinaryWatch(turnedOn: Int): List<String> {
        val ans = ArrayList<String>()
        for(h in 0..11) {
            for(m in 0..59){
                if(Integer.bitCount(h) + Integer.bitCount(m) == turnedOn) {
                    var m2 = m.toString()
                    if(m < 10) {
                        m2 = "0$m"
                    }
                    ans.add("$h:$m2")
                }
            }
        }
        return ans
    }
}

总结

这题不错,但这表肯定没人买 - -

参考

二进制手表 - 二进制手表 - 力扣(LeetCode) (leetcode-cn.com)

本文转载自: 掘金

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

零基础玩转SLS日志查询-SLS Query Builder

发表于 2021-11-25

简介:日志服务(Log Service,简称 SLS) 是阿里云提供的行业领先的日志大数据解决方案,一站式提供数据收集、清洗、分析、可视化、告警等功能。智能查询分析是数据中台重要的一环,SLS支持秒级查询10亿到千亿级别的日志数据,为万级开发者提供每日百亿级的查询服务。SLS查询语句是日志服务的专有语法,为了帮助用户简单、快速地构建查询语句,降低用户的学习成本,SLS推出了查询辅助输入(Query Builder)功能,让用户无需关注语法细节也可完成查询。

背景

日志服务(SLS)

日志服务(Log Service,简称 SLS) 是阿里云提供的行业领先的日志大数据解决方案,一站式提供数据收集、清洗、分析、可视化、告警等功能,全面提升海量日志处理能力,实时挖掘数据价值,智能助力研发/运维/运营/安全等场景。

智能查询分析是数据中台重要的一环,SLS支持秒级查询10亿到千亿级别的日志数据,为万级开发者提供每日百亿级的查询服务。SLS非常适合于做监控报表/告警/运营探索式交互分析,更可以通过API调用集成数据分析能力,集成到第三方的可视化平台,BI工具,或自研程序。

SLS查询分析

SLS的日志查询分为两部分:查询语句和分析语句。查询语句和分析语句以竖线(|)分割,查询语句的语法为日志服务专有语法,分析语句采用标准的SQL92语法。

1
复制代码查询语句|分析语句

其中,查询语句可单独使用,分析语句必须与查询语句一起使用。即分析功能是基于查询结果或全量数据进行的。

示例:

1
2
3
4
5
6
7
8
vbnet复制代码// 仅查询
status > 200

// 仅统计
* | SELECT status, count(*) AS PV GROUP BY status

// 查询 + 统计
status > 200 | SELECT status, count(*) AS PV GROUP BY status

Query Builder介绍

SLS查询语句是日志服务的专有语法,为了帮助用户简单、快速地构建查询语句,降低用户的学习成本,SLS推出了查询辅助输入(Query Builder)功能,让用户无需关注语法细节也可完成查询。

(注:当前版本只支持查询语句,SQL分析语句的辅助输入功能SQL Builder将在后续版本推出,敬请期待)

1. 功能入口

登录日志服务控制台,在Project列表区域,点击进入目标Project。在日志存储 > 日志库页签中,点击进入目标Logstore,在查询和分析语句输入框中,点击右侧图标,唤起Query Builder面板。随后在配置查询条件面板中,配置查询条件即可。

2. 使用模式

  • 简洁模式

简洁模式中的多个查询条件是平铺展示的,各个查询条件之间为同级关系。

  • 高级模式

高级模式中的多个查询条件是换行展示的,各个查询条件之间可设置层级关系,即对应于查询语句中的括号运算符。用户可以通过点击且、或两侧的<图标或>图标,定义各个查询条件之间的层级关系。

  • 双向同步

为了最大程度的保证用户使用的灵活性,Query Builder的查询条件与用户手动输入的查询语句是实时双向同步的。用户既可以通过Query Builder生成查询语句,也可以在查询框中修改生成的查询语句。Query Builder会实时解析用户手动输入的查询语句并同步到面板上。

3. 功能概述

3.1 查询类型

  • 全文查询和字段查询

  • 精确查询和模糊查询

3.2 逻辑连接符

示例

3.3 运算符

Query Builder功能支持如下运算符。更多信息,请参见运算符。

  • 针对全文查询,提供包含、不包含运算符。
  • 针对text类型的字段,提供包含、不包含、(字段)存在、(字段)不存在运算符。
  • 针对long类型或double类型的字段,提供=、!=、>、<、>=、<=、(字段)存在、(字段)不存在运算符。

4. 使用示例

示例1

  • 查询包含GET、PUT、POST中任意值的日志

  • 查询同时包含GET、PUT、POST中的日志

示例2

  • 查询user_agent字段的值中包含Chrome的日志

  • 查询slbid字段的值以slb-开头且client_ip字段的值是在127.0.0.x范围内的日志

示例3

查询请求时间小于1秒的成功请求的日志中包含SLS字符串的日志

常见问题

简洁模式和高级模式有什么区别?

  • 简洁模式中的多个查询条件是平铺展示的,各个查询条件之间为同级关系。
  • 高级模式中的多个查询条件是换行展示的,各个查询条件之间可设置层级关系,即对应于查询语句中的括号运算符。

是否支持构建SQL分析语句?

目前,Query Builder功能只支持构建查询语句,不支持构建SQL分析语句。我们将在后续版本推出针对SQL分析语句的构建能力,敬请期待后续版本。

如果您在输入框中输入了SQL分析语句,Query Builder面板将消失且面板唤起按钮被禁用。删除SQL分析语句部分后,可重新打开Query Builder面板。

更多Query Builder使用过程中的问题参考常见问题。

写在最后

SLS Query Builder可以帮助用户简单、快速地构建查询语句,让用户无需关注语法细节也可完成查询。正如前面提到,SLS的查询分析能力由查询语句和分析语句两部分构成,分析语句能够在秒级查询的基础上进行实时统计分析,适合于做监控报表/告警/运营探索式交互分析,更可以通过API调用集成数据分析能力,集成到第三方的可视化平台,BI工具,或自研程序。

相较于查询语句,分析语句具有更强大的能力和更丰富的应用场景。SLS分析语句采用标准的SQL92语法,相较于查询语法,使用门槛和学习成本更高,因此我们也将在不久的将来推出针对SQL分析语句的辅助输入工具SQL Builder,敬请期待!

原文链接

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

本文转载自: 掘金

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

AsposePDF for Java2111去除水印和数

发表于 2021-11-25

前言
工欲善其事,必先利其器

源码分析

1. 下载Aspose.PDF for Java21.11官方jar包

2. 开始分析

  1. 调用授权方法
1
2
3
java复制代码InputStream is = new FileInputStream("..license.xml");
License license = new License();
license.setLicense(is);

license.xml文件内容这里是个过期的文件主要是格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<License>
<Data>
<Products>
<Product>Aspose.Total for Java</Product>
<Product>Aspose.Words for Java</Product>
</Products>
<EditionType>Enterprise</EditionType>
<SubscriptionExpiry>20991231</SubscriptionExpiry>
<LicenseExpiry>20991231</LicenseExpiry>
<SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
</Data>
<Signature>
sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=
</Signature>
</License>
  1. 分析License类的setLicense方法
    setLicense代码太长我就不贴了,源码里面有。

主要分析里面的核心代码,setLicense第两个重载方法主要验证在139和140行

1
2
java复制代码l9f var10 = new l9f();
var10.lI(var9);

进入l9f类的lT方法

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复制代码void lI(InputStream var1) throws Exception {
l19n.lI(0);
if (var1 == null) {
throw new NullPointerException("stream");
} else {
String[] var2 = new String[]{new String(new byte[]{97, 115, 112, 111, 115, 101, 46, 116, 111, 116, 97, 108}), new String(new byte[]{99, 111, 110, 104, 111, 108, 100, 97, 116, 101, 46, 116, 111, 116, 97, 108})};
Document var3 = com.aspose.pdf.internal.l1f.ld.lc().parse(var1);
boolean var4 = true;
if (!this.lI(var3)) {
var4 = false;
}

this.ly();
Iterator var5 = (com.aspose.pdf.internal.l1f.lu.lI() ? (ArrayList)l0t.get() : l9f.lf.lf).iterator();

while(var5.hasNext()) {
String var6 = (String)var5.next();
if (this.lv.equals(var6)) {
throw new IllegalStateException(new String(new byte[]{84, 104, 105, 115, 32, 108, 105, 99, 101, 110, 115, 101, 32, 105, 115, 32, 100, 105, 115, 97, 98, 108, 101, 100, 44, 32, 112, 108, 101, 97, 115, 101, 32, 99, 111, 110, 116, 97, 99, 116, 32, 65, 115, 112, 111, 115, 101, 32, 116, 111, 32, 111, 98, 116, 97, 105, 110, 32, 97, 32, 110, 101, 119, 32, 108, 105, 99, 101, 110, 115, 101, 46}));
}
}

if (this.lc() != l9f.lj.lj) {
String var12 = this.lc() == l9f.lj.lI ? var2[0] : var2[1];
if (l19n.lj() <= 0) {
boolean var13 = false;
String var7 = "";
String[] var8 = this.lh;
int var9 = var8.length;
int var10 = 0;

while(var10 < var9) {
String var11 = var8[var10];
var7 = l10l.lu(var11);
if (!var12.equals(var7) && !(var12 + " for java").equals(var7) && !(var12 + " product family").equals(var7)) {
if (this.lc() == l9f.lj.lI && l10l.lu(com.aspose.pdf.internal.l1f.lI.lt).equals(var7)) {
var13 = true;
break;
}

if (this.lc() == l9f.lj.lI && var11.equals(com.aspose.pdf.internal.l1f.lI.lt.concat(new String(new byte[]{32, 102, 111, 114, 32, 74, 97, 118, 97})))) {
var13 = true;
break;
}

if (this.lc() == l9f.lj.lI && l10l.ly(var11).equals(l10l.ly(com.aspose.pdf.internal.l1f.lI.lt.concat(new String(new byte[]{32, 80, 114, 111, 100, 117, 99, 116, 32, 70, 97, 109, 105, 108, 121}))))) {
var13 = true;
break;
}

if (this.lc() != l9f.lj.lI || !var11.equals(com.aspose.pdf.internal.l1f.lI.lj.concat(new String(new byte[]{32, 102, 111, 114, 32, 74, 97, 118, 97}))) && !var11.equals(com.aspose.pdf.internal.l1f.lI.lj.concat(new String(new byte[]{32, 80, 114, 111, 100, 117, 99, 116, 32, 70, 97, 109, 105, 108, 121})))) {
++var10;
continue;
}

var13 = true;
break;
}

var13 = true;
break;
}

if (!var13) {
throw new IllegalStateException(new String(new byte[]{84, 104, 101, 32, 108, 105, 99, 101, 110, 115, 101, 32, 105, 115, 32, 110, 111, 116, 32, 118, 97, 108, 105, 100, 32, 102, 111, 114, 32, 116, 104, 105, 115, 32, 112, 114, 111, 100, 117, 99, 116, 46, 32, 70, 111, 114, 32, 102, 114, 101, 101, 32, 116, 101, 99, 104, 110, 105, 99, 97, 108, 32, 115, 117, 112, 112, 111, 114, 116, 44, 32, 112, 108, 101, 97, 115, 101, 32, 112, 111, 115, 116, 32, 116, 104, 105, 115, 32, 101, 114, 114, 111, 114, 32, 97, 110, 100, 32, 116, 104, 101, 32, 102, 105, 108, 101, 32, 105, 110, 32, 116, 104, 101, 32, 65, 115, 112, 111, 115, 101, 46, 80, 68, 70, 32, 70, 111, 114, 117, 109, 115, 32, 104, 116, 116, 112, 58, 47, 47, 119, 119, 119, 46, 97, 115, 112, 111, 115, 101, 46, 99, 111, 109, 47, 99, 111, 109, 109, 117, 110, 105, 116, 121, 47, 102, 111, 114, 117, 109, 115, 47, 97, 115, 112, 111, 115, 101, 46, 112, 100, 102, 45, 112, 114, 111, 100, 117, 99, 116, 45, 102, 97, 109, 105, 108, 121, 47, 50, 48, 47, 115, 104, 111, 119, 102, 111, 114, 117, 109, 46, 97, 115, 112, 120}));
} else {
Date var14 = (new SimpleDateFormat("yyyy.MM.dd", Locale.ENGLISH)).parse(com.aspose.pdf.internal.l1f.lI.lu);
if (var14.after(this.lc)) {
throw new IllegalStateException(new String(new byte[]{84, 104, 101, 32, 115, 117, 98, 115, 99, 114, 105, 112, 116, 105, 111, 110, 32, 105, 110, 99, 108, 117, 100, 101, 100, 32, 105, 110, 32, 116, 104, 105, 115, 32, 108, 105, 99, 101, 110, 115, 101, 32, 97, 108, 108, 111, 119, 115, 32, 102, 114, 101, 101, 32, 117, 112, 103, 114, 97, 100, 101, 115, 32, 117, 110, 116, 105, 108, 32}) + (new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH)).format(this.lc) + ", " + new String(new byte[]{98, 117, 116, 32, 116, 104, 105, 115, 32, 118, 101, 114, 115, 105, 111, 110, 32, 111, 102, 32, 116, 104, 101, 32, 112, 114, 111, 100, 117, 99, 116, 32, 119, 97, 115, 32, 114, 101, 108, 101, 97, 115, 101, 100, 32, 111, 110, 32}) + (new SimpleDateFormat("dd MMM yyyy", Locale.ENGLISH)).format(var14) + ". " + new String(new byte[]{80, 108, 101, 97, 115, 101, 32, 114, 101, 110, 101, 119, 32, 116, 104, 101, 32, 115, 117, 98, 115, 99, 114, 105, 112, 116, 105, 111, 110, 32, 111, 114, 32, 117, 115, 101, 32, 97, 32, 112, 114, 101, 118, 105, 111, 117, 115, 32, 118, 101, 114, 115, 105, 111, 110, 32, 111, 102, 32, 116, 104, 101, 32, 112, 114, 111, 100, 117, 99, 116, 46}));
} else if ((new Date()).after(this.ly)) {
throw new IllegalStateException(new String(new byte[]{84, 104, 101, 32, 108, 105, 99, 101, 110, 115, 101, 32, 104, 97, 115, 32, 101, 120, 112, 105, 114, 101, 100, 46}));
} else if (l19n.lj() > 0) {
throw new PdfException("Invalid license signature. Please make sure the license file was not modified.");
} else {
this.l0if = l10if.lf;
Helper.help1();
lI(this);
}
}
}
}
}
}

仔细观察你会发现这个方法就是验证的,主要是做验证失败抛异常和添加一些标识,这个分析下来就简单了,我们直接找到验证成功的最终执行176~179行,然后尝试修改试一下。

1
2
3
java复制代码this.l0if = l10if.lf;
Helper.help1();
lI(this);

3. 分析结果

  • 修改void lI(InputStream var1)方法为
1
2
3
4
5
java复制代码void lI(InputStream var1) throws Exception {
this.l0if = l10if.lf;
Helper.help1();
lI(this);
}

执行操作

1. 添加Javassist修改class字节码文件

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.28.0-GA</version>
</dependency>

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
java复制代码/**
* 修改pdf jar包里面的校验
*/
public static void modifyPDFJar() {
try {
//这一步是完整的jar包路径,选择自己解压的jar目录
ClassPool.getDefault().insertClassPath("D:\\aspose.pdf-21.11-java\\lib\aspose.pdf-21.11.jar");
//获取指定的class文件对象
CtClass zzZJJClass = ClassPool.getDefault().getCtClass("com.aspose.pdf.l9f");
//从class对象中解析获取所有方法
CtMethod[] methodA = zzZJJClass.getDeclaredMethods();
for (CtMethod ctMethod : methodA) {
//获取方法获取参数类型
CtClass[] ps = ctMethod.getParameterTypes();
//筛选同名方法,入参是Document
if (ps.length == 1 && ctMethod.getName().equals("lI") && ps[0].getName().equals("java.io.InputStream")) {
System.out.println("ps[0].getName==" + ps[0].getName());
//替换指定方法的方法体
ctMethod.setBody("{this.l0if = com.aspose.pdf.l10if.lf;com.aspose.pdf.internal.imaging.internal.p71.Helper.help1();lI(this);}");
}
}
//这一步就是将破译完的代码放在桌面上
zzZJJClass.writeFile("C:\\Users\\roc\\Desktop\\");

} catch (Exception e) {
System.out.println("错误==" + e);
}
}

运行修改方法后会在桌面生成 com 修改后的文件夹

3. 修改jar包里面的数据

为了不修改原jar包建议复制一份重命名。

  1. 打开jar包将桌面com文件夹覆盖到jar包com文件夹

image.png
2. 删除jar包里面的.RSA和.SF文件

image.png

4. 重新导入修改后的jar包进行测试

  1. maven移除旧的jar包,导入修改后的jar包
  2. 调用测试方法进行测试转换后的文件是否去除水印和数量限制成功
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码String sourceFile = "D:\\b.pdf";//输入的文件
String targetFile = "D:\\转换后.docx";//输出的文件
/**
* PDF转Word操作
*
* @param sourceFile 源文件
* @param targetFile 目标文件
*/
public static void pdf2doc(String sourceFile, String targetFile) {
try {
long old = System.currentTimeMillis();
FileOutputStream os = new FileOutputStream(targetFile);
com.aspose.pdf.Document doc = new com.aspose.pdf.Document(sourceFile);//加载源文件数据
doc.save(os, com.aspose.pdf.SaveFormat.DocX);//设置转换文件类型并转换
os.close();
long now = System.currentTimeMillis();
System.out.println("共耗时:" + ((now - old) / 1000.0) + "秒"); //转化用时
} catch (Exception e) {
e.printStackTrace();
}
}

声明
本方法只做个人研究学习使用,切勿用于商用。
其他参考
Aspose.Words for Java21.11去除水印和数量限制

Aspose.Cells for Java21.11去除水印和数量限制

Aspose.Slides for Java21.10去除水印和数量限制

本文转载自: 掘金

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

1…196197198…956

开发者博客

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