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

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


  • 首页

  • 归档

  • 搜索

springboot注解详解(一)Spring Web MV

发表于 2021-11-20

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

1、@RequestMapping

@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过RquestMappingHandlerMapping和RequestMappingHndlerAdapter两个类来提供对@RequestMapping注解的支持。

@RequestMapping注解对请求处理类中的请求处理方法进行标注;@RequestMapping注解拥有以下的六个配置属性:

• value:映射的请求URL或者其别名

• method:兼容HTTP的方法名

• params:根据HTTP参数的存在、缺省或值对请求进行过滤

• header:根据HTTP Header的存在、缺省或值对请求进行过滤

• consume:设定在HTTP请求正文中允许使用的媒体类型

• product:在HTTP响应体中允许使用的媒体类型

提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller或@RestController进行标记

下面是使用@RequestMapping的两个示例:

@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前。

2、@RequestBody

@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过HttpMessageConverter传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。

3、@GetMapping

@GetMapping注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是@RequestMapping(method=RequestMethod.GET)的快捷方式。

4、@PostMapping

@PostMapping注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping与@GetMapping一样,也是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.POST)的快捷方式。

5、@PutMapping

@PutMapping注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是@RequestMapping(method=HttpMethod.PUT)的快捷方式。

6、@DeleteMapping

@DeleteMapping注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是@RequestMapping(method=HttpMethod.DELETE)的快捷方式。

7、@PatchMapping

@PatchMapping注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是@RequestMapping(method=HttpMethod.PATCH)的快捷方式。

8、@ControllerAdvice

@ControllerAdvice是@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。@ControllerAdvice需要和@ExceptionHandler、@InitBinder以及@ModelAttribute注解搭配使用,主要是用来处理控制器所抛出的异常信息。首先,我们需要定义一个被@ControllerAdvice所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。此外,在有必要的时候,可以使用@InitBinder在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用@ControllerAdvice注解,就可以快速的创建统一的,自定义的异常处理类。

9、@ResponseBody

@ResponseBody会自动将控制器中方法的返回值写入到HTTP响应中。特别的,@ResponseBody注解只能用在被@Controller注解标记的类中。如果在被@RestController标记的类中,则方法不需要使用@ResponseBody注解进行标注。@RestController相当于是@Controller和@ResponseBody的组合注解。

10、@ExceptionHandler

@ExceptionHander注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被@ExceptionHandler标注的方法。

11、@ResponseStatus

@ResponseStatus注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。

12、@PathVariable

@PathVariable注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过@RequestMapping注解来指定URI的模板变量,然后使用@PathVariable注解将方法中的参数绑定到模板变量上。特别地,@PathVariable注解允许我们使用value或name属性来给参数取一个别名。

模板变量名需要使用“{ }”进行包裹,如果方法的参数名与URI模板变量名一致,则在@PathVariable中就可以省略别名的定义。

提示:如果参数是一个非必须的,可选的项,则可以在@PathVariable中设置require = false

13、@RequestParam

@RequestParam注解用于将方法的参数与Web请求的传递的参数进行绑定。使用@RequestParam可以轻松的访问HTTP请求参数的值。

该注解的其他属性配置与@PathVariable的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。

14、@Controller

@Controller是@Component注解的一个延伸,Spring会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。

15、@RestController

@RestController是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于@Controller和@ResponseBody的快捷方式。当使用此注解时,不需要再在方法上使用@ResponseBody注解。

16、@ModelAttribute

通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。

与@PathVariable和@RequestParam注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称。

特别地,如果使用@ModelAttribute对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。

在Spring调用具体的处理方法之前,被@ModelAttribute注解标注的所有方法都将被执行。

17、@CrossOrigin

@CrossOrigin注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。

18、@InitBinder

@InitBinder注解用于标注初始化WebDataBinider的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。

本文转载自: 掘金

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

594 最长和谐子序列 「滑动窗口」&「哈希计数」

发表于 2021-11-20

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

题目描述

这是 LeetCode 上的 594. 最长和谐子序列 ,难度为 简单。

Tag : 「模拟」、「双指针」、「滑动窗口」、「哈希表」

和谐数组是指一个数组里元素的最大值和最小值之间的差别 正好是 111。

现在,给你一个整数数组 numsnumsnums ,请你在所有可能的子序列中找到最长的和谐子序列的长度。

数组的子序列是一个由数组派生出来的序列,它可以通过删除一些元素或不删除元素、且不改变其余元素的顺序而得到。

示例 1:

1
2
3
4
5
ini复制代码输入:nums = [1,3,2,2,5,2,3,7]

输出:5

解释:最长的和谐子序列是 [3,2,2,2,3]

示例 2:

1
2
3
ini复制代码输入:nums = [1,2,3,4]

输出:2

示例 3:

1
2
3
ini复制代码输入:nums = [1,1,1,1]

输出:0

提示:

  • 1<=nums.length<=2∗1041 <= nums.length <= 2 * 10^41<=nums.length<=2∗104
  • −109<=nums[i]<=109-10^9 <= nums[i] <= 10^9−109<=nums[i]<=109

排序 + 滑动窗口

一个直观的想法是,先对 numsnumsnums 进行排序,然后从前往后使用「双指针」实现「滑动窗口」进行扫描,统计所有符合条件的窗口长度,并在所有长度中取最大值即是答案。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
Java复制代码class Solution {
public int findLHS(int[] nums) {
Arrays.sort(nums);
int n = nums.length, ans = 0;
for (int i = 0, j = 0; j < n; j++) {
while (j < n && nums[j] - nums[i] < 1) j++;
while (j < n && i < j && nums[j] - nums[i] > 1) i++;
if (i < n && j < n && nums[j] - nums[i] == 1) ans = Math.max(ans, j - i + 1);
}
return ans;
}
}
  • 时间复杂度:排序的复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn),通过双指针实现的滑动窗口复杂度为 O(n)O(n)O(n)。整体复杂度为 O(nlog⁡n)O(n\log{n})O(nlogn)
  • 空间复杂度:O(log⁡n)O(\log{n})O(logn)

哈希计数

题目规定的「和谐子序列」中的最值差值正好为 111,因而子序列排序后必然符合 [a,a,..,a+1,a+1][a,a,..,a+1,a+1][a,a,..,a+1,a+1] 形式,即符合条件的和谐子序列长度为相邻两数(差值为 111) 的出现次数之和。

我们可以使用「哈希表」记录所有 nums[i]nums[i]nums[i] 的出现次数,然后通过 O(n)O(n)O(n) 的复杂度找出所有可能的数对(两数差值为 111),并在所有符合条件的数对所能构成的「和谐子序列」长度中取最大值。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
Java复制代码class Solution {
public int findLHS(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
for (int i : nums) map.put(i, map.getOrDefault(i, 0) + 1);
int ans = 0;
for (int i : nums) {
if (map.containsKey(i - 1)) {
ans = Math.max(ans, map.get(i) + map.get(i - 1));
}
}
return ans;
}
}
  • 时间复杂度:O(n)O(n)O(n)
  • 空间复杂度:O(n)O(n)O(n)

最后

这是我们「刷穿 LeetCode」系列文章的第 No.594 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

本文转载自: 掘金

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

dart系列之 在dart中使用生成器 简介 两种返回类型的

发表于 2021-11-20

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

简介

ES6中在引入异步编程的同时,也引入了Generators,通过yield关键词来生成对应的数据。同样的dart也有yield关键词和生成器的概念。

什么时候生成器呢?所谓生成器就是一个能够持续产生某些数据的装置,也叫做generator。

两种返回类型的generator

根据是同步生成还是异步生成,dart返回的结果也是不同的。

如果是同步返回,那么返回的是一个Iterable对象.

如果是异步返回,那么返回的是一个Stream对象。

同步的generator使用sync*关键词如下:

1
2
3
4
dart复制代码Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}

异步的generator使用的是async* 关键词如下:

1
2
3
4
arduino复制代码Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}

生成关键词使用的是yield。

如果yield后面跟着的本身就是一个generator,那么需要使用yield*。

1
2
3
4
5
6
arduino复制代码Iterable<int> naturalsDownFrom(int n) sync* {
if (n > 0) {
yield n;
yield* naturalsDownFrom(n - 1);
}
}

Stream的操作

stream表示的是流,得到这个流之后,我们需要从流中取出对应的数据。

从Stream中取出数据有两种方式,第一种就是使用Stream本身的API来获取Stream中的数据。

最简单的就是调用stream的listen方法:

1
2
csharp复制代码  StreamSubscription<T> listen(void onData(T event)?,
{Function? onError, void onDone()?, bool? cancelOnError});

listen可以接数据的处理方法,具体使用如下:

1
2
3
4
5
6
scss复制代码 final startingDir = Directory(searchPath);
startingDir.list().listen((entity) {
if (entity is File) {
searchFile(entity, searchTerms);
}
});

默认的方法是onData方法。

另外一种就是今天要讲解的await for.

await for的语法如下:

1
2
3
arduino复制代码await for (varOrType identifier in expression) {
// Executes each time the stream emits a value.
}

要注意的是上面的expression必须是一个Stream对象。并且await for必须用在async中,如下:

1
2
3
4
5
6
7
csharp复制代码Future<void> main() async {
// ...
await for (final request in requestServer) {
handleRequest(request);
}
// ...
}

如果要想中断对stream的监听,则可以使用break或者return。

总结

以上就是dart中生成器的使用了。

本文转载自: 掘金

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

手撸通用灰度规则框架(一)

发表于 2021-11-20

什么是灰度

有时候,产品说,我们这个新版本上线后,先切20%的流量看看转化率。

有时候,领导说,这个重构的系统我们还是需要在生产小规模验证一下。

有时候,产品说,转化率符合预期,流量扩大到80%吧。

有时候,领导说,生产炸了,快、快全部切回老系统。

相信上面的场景很多人都遇到过,那么常见的解决这些问题的方式是什么呢?

使用配置文件,劣势,改配置需要发布。

使用db存储,劣势,每一次请求都要访问一次DB,不太合适
使用配置中心,劣势,不能做复杂的规则,例如10%的流量,排除掉某些个用户。

实现一个通用的灰度框架需要考虑哪些

1、规则是可以通过配置完成的

2、规则配置后是实时生效的,支持热加载的

3、不同的团队有不同的技术栈,需要支持市场上主流的配置中心

4、复杂规则可以自定义扩展、数据源支持自定义扩展

5、规则的执行是可跟踪的,跟踪的数据是可查看的,数据的存储是可以自定义的。

设计-先搭骨架

1、规则是由许多细小的表达式组成,例如 在0-20之间,在[1,2,3,4,6,7]中,等于“jack”,先设计一个class来描述这样的规则 ,我们可以设计成一个规则(Rule 包含 多个 Feature,规则的执行结果等于所有的Feature的执行结果)

1
2
3
4
5
6
7
8
9
10
java复制代码public class GrayRuleConfig
{
private List<Feature> features;
public static class Feature{
private String key;
private boolean enable;
private boolean unionAll;
private Map<String,String> metaData;
}
}

2、规则行为定义和Feature的行为

1
2
3
4
5
java复制代码public interface IRule {
String getKey();
boolean enable();
boolean check(Object value);
}
1
2
3
4
java复制代码public interface IFeature<F> {
F getFeature();
boolean check(Object targetValue);
}

3、规则和Feature的公有行为抽象出来,并定义好执行骨架

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
java复制代码public abstract class AbstractRule implements IRule {
private final ITraceRepository traceRepository;

public AbstractRule(ITraceRepository traceRepository) {
this.traceRepository = traceRepository;
}

public boolean check(Object value) {
boolean result = doCheck(value);
beforeGrayCheck(value);
try {
if (result) {
execResultPass();
} else {
execResultNoPass();
}
afterGrayCheck(value, result);
} catch (Exception ex) {
execGrayErrorAfter(ex, value);
}

return result;
}

protected void beforeGrayCheck(Object value) {
System.out.println(this.getClass() + ";beforeGrayCheck:" + value);
}

protected void afterGrayCheck(Object value, boolean result) {
System.out.println(this.getClass() + ";afterGrayCheck:" + value);

}

protected void execGrayErrorAfter(Exception ex, Object value) {
//todo log
System.out.println(this.getClass() + ";execGrayErrorAfter:" + value + "ex:" + ex);

}

private void execResultPass() {
traceRepository.pass(this.getKey());
}

private void execResultNoPass() {
traceRepository.noPass(this.getKey());
}
public abstract boolean doCheck(Object targetValue);
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
java复制代码public abstract class AbstractFeature<F> implements IFeature<F>  {
private ITraceRepository traceRepository;
private F feature;

public void setTraceRepository(ITraceRepository traceRepository){
this.traceRepository=traceRepository;
}
public ITraceRepository getTraceRepository(){
return this.traceRepository;
}
protected IFeature buildFeature(String featureDesc){
return buildFeature(featureDesc,null);
}
protected IFeature buildFeature(String featureDesc,ITraceRepository traceRepository){
this.feature=parseFeature(featureDesc);
this.traceRepository=traceRepository;
return this;
}
public F getFeature() {
return this.feature;
}

public boolean check(Object targetValue) {
return doCheck(targetValue);
}
private F parseFeature(String featureDesc){
if (StringUtils.isEmpty(featureDesc)){
throw new GrayException("featureDesc can not be null");
}
return doParseFeature(featureDesc);
}

protected abstract F doParseFeature(String featureDesc);
protected abstract boolean doCheck(Object value);

默认支持的规则实现

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复制代码public class DefaultRule extends AbstractRule {
private String key;
private boolean enable;

public boolean isUnionAll() {
return unionAll;
}

public void setUnionAll(boolean unionAll) {
this.unionAll = unionAll;
}

private boolean unionAll;

public DefaultRule(ITraceRepository traceRepository) {
super(traceRepository);
}
public void setKey(String key){
this.key=key;
}
public void setEnable(boolean enable){
this.enable=enable;
}
public boolean enable() {
return this.enable;
}
public String getKey() {
return this.key;
}

private List<IFeature> features;
public void setFeatures(List<IFeature> features){
this.features=features;
}
public List<IFeature> getFeatures() {
return this.features;
}

public boolean doCheck(Object targetValue) {
boolean result=false;
for (IFeature feature:this.features){
result=feature.check(targetValue);
if (result&&!unionAll){
break;
}
if (!result&&unionAll){
break;
}
}
return result;
}

规则部分,先写到这里,github有源码

本文转载自: 掘金

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

Java源码心得jdk8-HashMap和HashSet一

发表于 2021-11-20

原文日期:2017-10-15


HashMap源码分析

HashMap、HashSet和ArrayList一样,都是使用非常频繁的集合,理解这两个的实现原理,在使用的时候会更加得心应手。HashMap内部由两部分组成,首先是Node数组(Hash桶数组),每个Node元素(桶元素)又单独组成一个数据结构,可能是链表或者红黑树(当链表长度大于8时,将转换成红黑树。mark:红黑树原理将单独作为一个专题来学习),如下图。

blob.png

在深入了解HashMap前,先说明以下几个概念。

  • Hash桶(bucket)数组:HashMap的内部数组结构。
  • 桶(bucket)元素:内部数组元素,每个元素又可组成一个链表或者红黑树。
  • HashMap容量(table.length):内部数组长度。默认情况下为16,之后每次扩容都会翻一倍。
  • HashMap大小或者长度(size):实际key/balue键值对数量。
  • 加载因子(loadFactor):主要是用来计算扩容极限值,加载因子*容量=扩容极限值。
  • 扩容极限值(threshold):当HashMap长度大于扩容极限值,则集合开始扩容。

HashMap中有两个重要的数学运算,我把它们分别叫做hash和index运算。

第一次hash:key->hash,计算key的hash值。

1
java复制代码hash = (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

第二次index:hash->index,通过hash值计算在数组中的索引值。如下n为容量值

1
java复制代码index = (n - 1) & hash

初始化

HashMap的构造函数主要是初始化HashMap的容量值和加载因子。默认初始容量为16,加载因子为0.75。这里注意的是在构造函数中并不会立刻初始化内部数组,内部数组会在第一次put时,进行初始化,这样可以做到内存使用滞后。

1
2
3
java复制代码    public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}

put方法

1.首先计算key的hash值。

2.通过key和容量值-1进行与运算操作,得到数组的索引值。

3.判断索引下的元素是否为树节点。如果是树节点的话,进行红黑树的新增/更新节点操作。

4.否则进行链表新增/更新节点操作。如果该链表长度大于8,则将链表转换成红黑树。

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
48
49
50
51
52
java复制代码    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//数组节点为空,首次扩容
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
//i索引下没有元素,该节点作为桶下第一个元素
tab[i] = newNode(hash, key, value, null);
else {
//i索引下有元素,定位到桶的首节点
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//新节点与首节点key值相等,将在后面更新value
e = p;
else if (p instanceof TreeNode)
//key值不相等,如果首节点为树,则按红黑树处理
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//否则按链表方式处理,遍历链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//找不到相同key节点,在末尾添加新节点
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//此时节点长度大于8,也将链表转换成红黑树
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
//在链表中匹配到了key值相等的节点,跳出循环,再更新value值
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
//更新value值,并返回旧值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}

resize方法

扩容方法,当集合长度大于扩容极限值时,将进行集合扩容操作,以减少元素hash碰撞概率。每次扩容容量都是原先的两倍,扩容极限值也为原先两倍。

由于容量发生了变化,所以扩容后还需要将原本的结构中的元素重新hash和index处理,得到新的数组索引值。reindex原理:将索引m下的元素的hash值与oldCap(旧容量)做与运算,结果为0则将元素置于索引m下,否则将元素置于索引m+oldCap下。

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
java复制代码    final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//设置新的容量值和扩容极限值,新的容量值为旧的两倍,扩容极限值等于加载因子乘以新的容量值
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//扩容,元素重组
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
//桶下只有一个节点是,直接hash和新容量值做与运算计算新的索引值
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//树节点,进行红黑树拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//链表拆分
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
//相当于reIndex重新计算元素新的索引值,将原先链表拆分成两个链表
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}

待续中。。。 Java源码心得jdk8-HashMap和HashSet二

本文转载自: 掘金

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

通过docker配置密码、持久化的redis 单纯Docke

发表于 2021-11-20

单纯Docker创建持久化Redis

前言

之前服务器买了之后,使用docker跑了一个Redis实例之后就放在那边不管了,直到前段时间发现Redis会莫名的删除所有的key值,可能是被黑掉了- - 于是有了这篇文章,希望通过docker来简单的配置一个有密码保护的,可持久化的Redis。

步骤

首先,我们在/usr/local/下创建一个redis文件夹,用于存放redis的配置文件和数据

1
2
3
bash复制代码mkdir /usr/local/redis
mkdir /usr/local/redis/data
touch /usr/local/redis/redis.conf
  1. 编写配置文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
bash复制代码## default: 不以守护进程的方式运行 
daemonize no

## 开启aop持久化
appendonly yes
#default: 持久化文件
appendfilename "appendonly.aof"
#default: 每秒同步一次
appendfsync everysec

port 6379
# 绑定端口,不指定外网可能连不上服务器
bind 0.0.0.0
requirepass password
  1. 编写一个docker的指令

vim redis.sh

1
2
3
4
5
6
7
bash复制代码docker run 
-p 6379:6379
--name redis
-v /usr/local/redis/redis.conf:/etc/redis/redis.conf ##将redis.conf挂在到docker的etc/redis目录下
-v /usr/local/redis/data:/data
-d redis:6.0
redis-server /etc/redis/redis.conf ##追加指令 运行redis服务器并指定刚刚挂载的配置文件
  1. 使用sh redis.sh启动redis 并使用docker ps查看运行状态

docker中redis没有跑起来则通过docker logs redis查看报错信息

线上配置密码

对于已经开启的Redis如果不想通过配置文件设置密码的话,可以通过指令

config set requirepass [password]来线上配置命令

另外,值得一提的是通过指令进行配置的密码不会同步到redis.conf配置文件中喔

本文转载自: 掘金

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

(七)Nacos开发教程之总结篇

发表于 2021-11-20

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

前情回顾

到这里,我们已经学习过了Nacos服务的部署,数据持久化,实现服务治理,实现服务配置管理等相关知识,以及Nacos后台系统的操作也学习了个遍,可以说目前已经对Nacos也有了一定的认知和使用经验。

但是这些还远远不够,还需要更深入的应用到实际项目中,这些需要大家更多的思考Nacos给我们解决了哪些问题,这些问题在实际项目中又是如何来处理的,是否能使用Nacos来替代,使用Nacos后是否会使原服务可用性更强。

等等等等的问题,需要大家自行去思考,学习总归是学习,更重要的是如何更有效地使用技术。

今天是我们学习Nacos的最后一篇文章,统一的和大家分享一下选择Nacos的一些思路。

我为什么要选择Nacos

我为什么要去选择Nacos集成到我自己的微服务框架中,无非就是几个点。

  1. Nacos提供的后台系统,我非常受用,操作简单,比如Euraka的界面,感觉Nacos的界面更符合我的审美。
  2. Nacos同时提供了服务治理和配置管理两大功能,我不需要再次引入其他组件。
  3. 相比于SpringCloud Config组件中需要同git服务来维护配置,我更倾向于Nacos这种数据库存储配置文件,页面修改的方式,更加方便,开发人员不需要过多的操作。
  4. Nacos的独立部署,其实在微服务部署项目的同时,也是稍微减少了一些部署上面的复杂度,虽然微乎及微。
  5. Nacos是国产的,中国人不骗中国人,中文文档更符合国人编码思维。
  6. SpringCloud Euraka已然停止开源,虽然我可能不会过多的扒其中的源码,但是在我看来,停止开源就证明缺少了一些的监督和建议,也可能是大老板为此开始烧钱,然后出个商业版来牟利也说不定。

大概就是这些了,本篇文章没有项目中使用Nacos的一些案例,但是可以更加灵活的让大家理解Nacos其中的优点。

至于缺点嘛,确实有,大家可以去网上自行搜一下。

总结

学习到这里,Nacos就算是大概学习完了,更深入的需要大家在日常开发中去一一探索了,我也会尝试着总结一些更加深入的知识分享给大家,大家加油。

本文转载自: 掘金

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

18-1 jvm调优工具(一) 一、前言 二、Jmap使用

发表于 2021-11-20

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

一、前言

因为我的是mac电脑,所以运行程序都是在mac上,有时一些工具在mac上不是很好用。如果有不好用的情况,可以参考文章:

  1. mac安装多版本jdk

  1. 彻底解决Jmap在mac版本无法使用的问题

以上是我在mac上运行Jmap时遇到的问题,如果你也遇到了,可以查看。

二、Jmap使用

  1. Jmap -histo 进程号

这个命令是用来查看系统内存使用情况的,实例个数,以及占用内存。

命令:

1
yaml复制代码jmap -histo 3241

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码   
num     #instances         #bytes  class name
----------------------------------------------
1:       1101980      372161752 [B
  2:        551394      186807240 [Ljava.lang.Object;
  3:       1235341      181685128 [C
  4:         76692      170306096 [I
  5:        459168       14693376  java.util.concurrent.locks.AbstractQueuedSynchronizer$Node
  6:        543699       13048776  java.lang.String
  7:        497636       11943264  java.util.ArrayList
  8:        124271       10935848  java.lang.reflect.Method
  9:        348582        7057632 [Ljava.lang.Class;
 10:        186244        5959808  java.util.concurrent.ConcurrentHashMap$Node

这里显示的是,byte类型的数组,有多少个实例,占用多大内存。

  • num:序号
  • instances:实例数量
  • bytes:占用空间大小
  • class name:类名称,[C is a char[],[S is a short[],[I is a int[],[B is a byte[],[[I is a int
  1. Jmap -heap 进程号

注意:Jmap命令在mac不太好用,具体参考前言部分。

windows或者linux上运行的命令是

1
复制代码Jmap -heap 进程号

mac上运行的命令是:(jdk8不能正常运行,jdk9以上可以)

1
css复制代码jhsdb jmap --heap --pid 2139

执行结果

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
ini复制代码Attaching to process ID 2139, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.2+9
​
using thread-local object allocation.
Garbage-First (G1) GC with 8 thread(s)
​
Heap Configuration:
  MinHeapFreeRatio         = 40
  MaxHeapFreeRatio         = 70
  MaxHeapSize              = 4294967296 (4096.0MB)
  NewSize                  = 1363144 (1.2999954223632812MB)
  MaxNewSize               = 2576351232 (2457.0MB)
  OldSize                  = 5452592 (5.1999969482421875MB)
  NewRatio                 = 2
  SurvivorRatio            = 8
  MetaspaceSize            = 21807104 (20.796875MB)
  CompressedClassSpaceSize = 1073741824 (1024.0MB)
  MaxMetaspaceSize         = 17592186044415 MB
  G1HeapRegionSize         = 1048576 (1.0MB)
​
Heap Usage:
G1 Heap:
  regions  = 4096
  capacity = 4294967296 (4096.0MB)
  used     = 21654560 (20.651397705078125MB)
  free     = 4273312736 (4075.348602294922MB)
  0.5041845142841339% used
G1 Young Generation:
Eden Space:
  regions  = 15
  capacity = 52428800 (50.0MB)
  used     = 15728640 (15.0MB)
  free     = 36700160 (35.0MB)
  30.0% used
Survivor Space:
  regions  = 5
  capacity = 5242880 (5.0MB)
  used     = 5242880 (5.0MB)
  free     = 0 (0.0MB)
  100.0% used
G1 Old Generation:
  regions  = 1
  capacity = 210763776 (201.0MB)
  used     = 0 (0.0MB)
  free     = 210763776 (201.0MB)
  0.0% used

通过上述结果分析,我们查询的内容如下:

  • 进程号:2139
  • JDK版本号:11
  • 使用的垃圾收集器:G1(jdk11默认的)
  • G1垃圾收集器线程数:8
  • 还可以知道堆空间大小,已用大小,元数据空间大小等等。
  • 新生代,老年代region的大小。容量,已用,空闲等。
  1. Jmap -dump 导出堆信息

这个命令是导出堆信息,当我们线上有内存溢出的情况的时候,可以使用Jmap -dump导出堆内存信息。然后再导入可视化工具用jvisualvm进行分析。

导出命令

1
ini复制代码jmap -dump:file=a.dump 进程号

我们还可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

1
2
ruby复制代码1. -XX:+HeapDumpOnOutOfMemoryError 
2. -XX:HeapDumpPath=./ (路径)

下面有案例说明如何使用。

三、jvisualvm命令工具的使用

  1. 基础用法

上面我们有导出dump堆信息到文件中,可以使用jvisualvm工具导入dump堆信息,进行分析。

打开jvisualvm工具命令:

1
复制代码jvisualvm

打开工具界面如下:

点击文件->装入,可以导入文件,查看系统的运行情况了。

2.案例分析 - 堆空间溢出问题定位

下面通过工具来分析内存溢出的原因。

第一步:自定义一段可能会内存溢出的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import com.aaa.jvm.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
​
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
​
@SpringBootApplication
public class JVMApplication {
​
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
int i = 0;
int j = 0;
while (true) {
list.add(new User(i++, UUID.randomUUID().toString()));
new User(j--, UUID.randomUUID().toString());
}
}
}

第二步:配置参数

为了方便看到效果,所以我们会设置两组参数。

第一组:设置堆空间大小,将堆空间设置的小一些,可以更快查看内存溢出的效果

1
ruby复制代码 ‐Xms10M ‐Xmx10M ‐XX:+PrintGCDetails

设置的堆内存空间是10M,并且打印GC

第二组:设置内存溢出自动导出dump****文件(内存很大的时候,可能会导不出来)

1
2
ruby复制代码1. -XX:+HeapDumpOnOutOfMemoryError 
2. -XX:HeapDumpPath=./ (路径)

将这两组参数添加到项目启动配置中。

运行的过程中打印堆空间信息到文件中:

1
lua复制代码jmap -dump:file=a.dump,format=b 12152

后面我们可以使用工具导入堆文件进行分析(下面有说到)。

我们还可以设置内存溢出自动导出dump文件(内存很大的时候,可能会导不出来)

完整参数配置如下:

1
ruby复制代码-Xms10M -Xmx10M -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/zhangsan/Downloads

这里需要注意的是堆目录要写绝对路径,不能写相对路径。

第三步:启动项目,等待内存溢出

我们看到,运行没有多长时间就内存溢出了。

查看导出到文件的目录:

第四步:导入堆内存文件到jvisualvm工具

文件->装入->选择刚刚导出的文件

第五步:分析

我们主要看【类】这个模块。

通过上图我们可以明确看出,有三个类实例数特别多,分别是:byte[],java.lang.String,com.lxl.jvm.User。前两个我们不容易看出是哪里的问题,但是第三个类com.lxl.jvm.User我们就看出来了,问题出在哪里。接下来就重点排查调用了这个类的地方,有没有出现内存没有释放的情况。

这个程序很简单,那么byte[]和java.lang.String到底是什么呢?我们的User对象结构中字段类型是String。

1
2
3
4
arduino复制代码public class User {
   private int id;
   private String name;
}

既然有很多User,自然String也少不了。

那么byte[]是怎么回事呢?其实String类中有byte[]成员变量。所以也会有很多byte[]对象。

本文转载自: 掘金

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

Echo 框架:实现分布式日志追踪 介绍 安装 快速开始 概

发表于 2021-11-20

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

介绍

通过一个完整例子,基于 Echo 框架实现分布式日志追踪。

什么是 API 日志追踪?

一个 API 请求会跨多个微服务,我们希望通过一个唯一的 ID 检索到整个链路的日志。

我们将会使用 rk-boot 来启动 Echo 框架的微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-echo

快速开始

我们会创建 /v1/greeter API 进行验证,同时开启 logging, meta 和 tracing 中间件以达到目的。

  1. 创建 bootA.yaml & serverA.go

ServerA 监听 1949 端口,并且发送请求给 ServerB。

我们通过 rkechoctx.InjectSpanToNewContext() 方法把 Tracing 信息注入到 Context 中,发送给 ServerB。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码---
echo:
- name: greeter # Required
port: 1949 # Required
enabled: true # Required
interceptors:
loggingZap:
enabled: true # Optional, enable logging interceptor
meta:
enabled: true # Optional, enable meta interceptor
tracingTelemetry:
enabled: true # Optional, enable tracing interceptor
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
go复制代码// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
"context"
"github.com/labstack/echo/v4"
"github.com/rookie-ninja/rk-boot"
"github.com/rookie-ninja/rk-echo/boot"
"github.com/rookie-ninja/rk-echo/interceptor/context"
"net/http"
)

// Application entrance.
func main() {
// Create a new boot instance.
boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootA.yaml"))

// Register handler
boot.GetEntry("greeter").(*rkecho.EchoEntry).Echo.GET("/v1/greeter", GreeterA)

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

// GreeterA will add trace info into context and call serverB
func GreeterA(ctx echo.Context) error {
// Call serverB at 2008
req, _ := http.NewRequest(http.MethodGet, "http://localhost:2008/v1/greeter", nil)

// Inject current trace information into context
rkechoctx.InjectSpanToHttpRequest(ctx, req)

// Call server
http.DefaultClient.Do(req)

// Respond to request
return ctx.String(http.StatusOK, "Hello from serverA!")
}
  1. 创建 bootB.yaml & serverB.go

ServerB 监听 2008 端口。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码---
echo:
- name: greeter # Required
port: 2008 # Required
enabled: true # Required
interceptors:
loggingZap:
enabled: true # Optional, enable logging interceptor
meta:
enabled: true # Optional, enable meta interceptor
tracingTelemetry:
enabled: true # Optional, enable tracing interceptor
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
go复制代码// Copyright (c) 2021 rookie-ninja
//
// Use of this source code is governed by an Apache-style
// license that can be found in the LICENSE file.
package main

import (
"context"
"github.com/labstack/echo/v4"
"github.com/rookie-ninja/rk-boot"
"github.com/rookie-ninja/rk-echo/boot"
"net/http"
)

// Application entrance.
func main() {
// Create a new boot instance.
boot := rkboot.NewBoot(rkboot.WithBootConfigPath("bootB.yaml"))

// Register handler
boot.GetEntry("greeter").(*rkecho.EchoEntry).Echo.GET("/v1/greeter", GreeterB)

// Bootstrap
boot.Bootstrap(context.Background())

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

// GreeterB will add trace info into context and call serverB
func GreeterB(ctx echo.Context) error {
// Respond to request
return ctx.String(http.StatusOK, "Hello from serverB!")
}
  1. 文件夹结构

1
2
3
4
5
6
7
8
9
go复制代码.
├── bootA.yaml
├── bootB.yaml
├── go.mod
├── go.sum
├── serverA.go
└── serverB.go

0 directories, 6 files
  1. 启动 ServerA & ServerB

1
2
go复制代码$ go run serverA.go
$ go run serverB.go
  1. 往 ServerA 发送请求

1
2
shell复制代码$ curl localhost:1949/v1/greeter
Hello from serverA!
  1. 验证日志

两个服务的日志中,会有同样的 traceId,不同的 requestId。

我们可以通过 grep traceId 来追踪 RPC。

  • ServerA
1
2
3
4
5
6
7
8
9
ini复制代码------------------------------------------------------------------------
endTime=2021-11-19T23:56:47.681644+08:00
...
ids={"eventId":"e2670cdb-9a3c-42e9-ae8f-e01de3d8fbfa","requestId":"e2670cdb-9a3c-42e9-ae8f-e01de3d8fbfa","traceId":"eb466c6e0c46538027d8b8c2efc08baa"}
...
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE
  • ServerB
1
2
3
4
5
6
7
8
9
ini复制代码------------------------------------------------------------------------
endTime=2021-11-19T23:56:47.681362+08:00
...
ids={"eventId":"3c72b929-78bd-4ff1-b48c-3ad699429c45","requestId":"3c72b929-78bd-4ff1-b48c-3ad699429c45","traceId":"eb466c6e0c46538027d8b8c2efc08baa"}
...
operation=/v1/greeter
resCode=200
eventStatus=Ended
EOE

概念

当我们没有使用例如 jaeger 调用链服务的时候,我们希望通过日志来追踪分布式系统里的 RPC 请求。

rk-boot 的中间件会通过 openTelemetry 库来向日志写入 traceId 来追踪 RPC。

当启动了日志中间件,原数据中间件,调用链中间件的时候,中间件会往日志里写入如下三种 ID。

EventId

当启动了日志中间件,EventId 会自动生成。

1
2
3
4
5
6
7
8
yaml复制代码---
echo:
- name: greeter # Required
port: 1949 # Required
enabled: true # Required
interceptors:
loggingZap:
enabled: true
1
2
3
4
ini复制代码------------------------------------------------------------------------
...
ids={"eventId":"cd617f0c-2d93-45e1-bef0-95c89972530d"}
...

RequestId

当启动了日志中间件和原数据中间件,RequestId 和 EventId 会自动生成,并且这两个 ID 会一致。

1
2
3
4
5
6
7
8
9
10
yaml复制代码---
echo:
- name: greeter # Required
port: 1949 # Required
enabled: true # Required
interceptors:
loggingZap:
enabled: true
meta:
enabled: true
1
2
3
4
ini复制代码------------------------------------------------------------------------
...
ids={"eventId":"8226ba9b-424e-4e19-ba63-d37ca69028b3","requestId":"8226ba9b-424e-4e19-ba63-d37ca69028b3"}
...

即使用户覆盖了 RequestId,EventId 也会保持一致。

1
arduino复制代码rkechoctx.AddHeaderToClient(ctx, rkechoctx.RequestIdKey, "overridden-request-id")
1
2
3
4
ini复制代码------------------------------------------------------------------------
...
ids={"eventId":"overridden-request-id","requestId":"overridden-request-id"}
...

TraceId

当启动了调用链中间件,traceId 会自动生成。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码---
echo:
- name: greeter # Required
port: 1949 # Required
enabled: true # Required
interceptors:
loggingZap:
enabled: true
meta:
enabled: true
tracingTelemetry:
enabled: true
1
2
3
4
ini复制代码------------------------------------------------------------------------
...
ids={"eventId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","requestId":"dd19cf9a-c7be-486c-b29d-7af777a78ebe","traceId":"316a7b475ff500a76bfcd6147036951c"}
...

本文转载自: 掘金

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

Java jar 如何防止被反编译

发表于 2021-11-19

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

Java作为解释型的语言,其高度抽象的特性意味其很容易被反编译,容易被反编译,自然有防止反编译措施存在。今天就拜读了一篇相关的文章,受益匪浅,知彼知己嘛!!之所以会对java的反编译感兴趣,那是因为自己在学习的过程中,常常需要借鉴一下别人的成果(你懂的…)。或许反编译别人的代码不怎么道德,这个嘛……

常用的保护技术

由于Java字节码的抽象级别较高,因此它们较容易被反编译。本节介绍了几种常用的方法,用于保护Java字节码不被反编译。通常,这些方法不能够绝对防止程序被反编译,而是加大反编译的难度而已,因为这些方法都有自己的使用环境和弱点。

1. 隔离Java程序

最简单的方法就是让用户不能够访问到Java Class程序,这种方法是最根本的方法,具体实现有多种方式。例如,开发人员可以将关键的Java Class放在服务器端,客户端通过访问服务器的相关接口来获得服务,而不是直接访问Class文件。这样黑客就没有办法反编译Class文件。目前,通过接口提供服务的标准和协议也越来越多,例如 HTTP、Web Service、RPC等。但是有很多应用都不适合这种保护方式,例如对于单机运行的程序就无法隔离Java程序。这种保护方式见图1所示。

e319bdc6bccdabba8e5923e0885e15ac.png

图1隔离Java程序示意图  

2. 对Class文件进行加密

为了防止Class文件被直接反编译,许多开发人员将一些关键的Class文件进行加密,例如对注册码、序列号管理相关的类等。在使用这些被加密的类之前,程序首先需要对这些类进行解密,而后再将这些类装载到JVM当中。这些类的解密可以由硬件完成,也可以使用软件完成。

在实现时,开发人员往往通过自定义ClassLoader类来完成加密类的装载(注意由于安全性的原因,Applet不能够支持自定义的 ClassLoader)。自定义的ClassLoader首先找到加密的类,而后进行解密,最后将解密后的类装载到JVM当中。在这种保护方式中,自定义的ClassLoader是非常关键的类。由于它本身不是被加密的,因此它可能成为黑客最先攻击的目标。如果相关的解密密钥和算法被攻克,那么被加密的类也很容易被解密。这种保护方式示意图见图2。

c35e4f57d547ee9aa511ac1b265555ce.png

图2 对Class文件进行加密示意图

3. 转换成本地代码

将程序转换成本地代码也是一种防止反编译的有效方法。因为本地代码往往难以被反编译。开发人员可以选择将整个应用程序转换成本地代码,也可以选择关键模块转换。如果仅仅转换关键部分模块,Java程序在使用这些模块时,需要使用JNI技术进行调用。

当然,在使用这种技术保护Java程序的同时,也牺牲了Java的跨平台特性。对于不同的平台,我们需要维护不同版本的本地代码,这将加重软件支持和维护的工作。不过对于一些关键的模块,有时这种方案往往是必要的。

为了保证这些本地代码不被修改和替代,通常需要对这些代码进行数字签名。在使用这些本地代码之前,往往需要对这些本地代码进行认证,确保这些代码没有被黑客更改。如果签名检查通过,则调用相关JNI方法。这种保护方式示意图见图3。

9f24cfc5dcdababfd5bae7a65a57f6aa.png

图3 转换成本地代码示意图 

4. 代码混淆

代码混淆是对Class文件进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能(语义)。但是混淆后的代码很难被反编译,即反编译后得出的代码是非常难懂、晦涩的,因此反编译人员很难得出程序的真正语义。

从理论上来说,黑客如果有足够的时间,被混淆的代码仍然可能被破解,甚至目前有些人正在研制反混淆的工具。但是从实际情况来看,由于混淆技术的多元化发展,混淆理论的成熟,经过混淆的Java代码还是能够很好地防止反编译。下面我们会详细介绍混淆技术,因为混淆是一种保护Java程序的重要技术。图4是代码混淆的示图。   

b6ba714d03997ed3c0ffb10d954e4fdd.png

图4 代码混淆示意图

几种技术的总结 

以上几种技术都有不同的应用环境,各自都有自己的弱点,表1是相关特点的比较。

   表1 不同保护技术比较表

d19854ab46b1a5b651f4aaa49565dae3.png

到目前为止,对于Java程序的保护,混淆技术还是最基本的保护方法。Java混淆工具也非常多,包括商业的、免费的、开放源代码的。Sun公司也提供了自己的混淆工具。它们大多都是对Class文件进行混淆处理,也有少量工具首先对源代码进行处理,然后再对Class进行处理,这样加大了混淆处理的力度。

目前,商业上比较成功的混淆工具包括JProof公司的1stBarrier系列、Eastridge公司的JShrink和 4thpass.com的SourceGuard等。主要的混淆技术按照混淆目标可以进行如下分类,它们分别为符号混淆(Lexical Obfuscation)、数据混淆(Data Obfuscation)、控制混淆(Control Obfuscation)、预防性混淆(Prevent Transformation)。

符号混淆

在Class中存在许多与程序执行本身无关的信息,例如方法名称、变量名称,这些符号的名称往往带有一定的含义。例如某个方法名为 getKeyLength(),那么这个方法很可能就是用来返回Key的长度。符号混淆就是将这些信息打乱,把这些信息变成无任何意义的表示,例如将所有的变量从vairant_001开始编号;对于所有的方法从method_001开始编号。这将对反编译带来一定的困难。对于私有函数、局部变量,通常可以改变它们的符号,而不影响程序的运行。但是对于一些接口名称、公有函数、成员变量,如果有其它外部模块需要引用这些符号,我们往往需要保留这些名称,否则外部模块找不到这些名称的方法和变量。因此,多数的混淆工具对于符号混淆,都提供了丰富的选项,让用户选择是否、如何进行符号混淆。

数据混淆

5a08d7e03dd78c8b63ec88fc9daa95cb.png

  图5 改变数据访问   

数据混淆是对程序使用的数据进行混淆。混淆的方法也有多种,主要可以分为改变数据存储及编码(Store and Encode Transform)、改变数据访问(Access Transform)。

改变数据存储和编码可以打乱程序使用的数据存储方式。例如将一个有10个成员的数组,拆开为10个变量,并且打乱这些变量的名字;将一个两维数组转化为一个一维数组等。对于一些复杂的数据结构,我们将打乱它的数据结构,例如用多个类代替一个复杂的类等。

另外一种方式是改变数据访问。例如访问数组的下标时,我们可以进行一定的计算,图5就是一个例子。

在实践混淆处理中,这两种方法通常是综合使用的,在打乱数据存储的同时,也打乱数据访问的方式。经过对数据混淆,程序的语义变得复杂了,这样增大了反编译的难度。

控制混淆

控制混淆就是对程序的控制流进行混淆,使得程序的控制流更加难以反编译,通常控制流的改变需要增加一些额外的计算和控制流,因此在性能上会给程序带来一定的负面影响。有时,需要在程序的性能和混淆程度之间进行权衡。控制混淆的技术最为复杂,技巧也最多。这些技术可以分为如下几类:

增加混淆控制通过增加额外的、复杂的控制流,可以将程序原来的语义隐藏起来。例如,对于按次序执行的两个语句A、B,我们可以增加一个控制条件,以决定B的执行。通过这种方式加大反汇编的难度。但是所有的干扰控制都不应该影响B的执行。图6就给出三种方式,为这个例子增加混淆控制。

33cb3472c0051d49e1ea79d9ffc6e7d7.png

  图6 增加混淆控制的三种方式

控制流重组重组控制流也是重要的混淆方法。例如,程序调用一个方法,在混淆后,可以将该方法代码嵌入到调用程序当中。反过来,程序中的一段代码也可以转变为一个函数调用。另外,对于一个循环的控制流,为可以拆分多个循环的控制流,或者将循环转化成一个递归过程。这种方法最为复杂,研究的人员也非常多。

预防性混淆

这种混淆通常是针对一些专用的反编译器而设计的,一般来说,这些技术利用反编译器的弱点或者Bug来设计混淆方案。例如,有些反编译器对于 Return后面的指令不进行反编译,而有些混淆方案恰恰将代码放在Return语句后面。这种混淆的有效性对于不同反编译器的作用也不太相同的。一个好的混淆工具,通常会综合使用这些混淆技术。

案例分析

在实践当中,保护一个大型Java程序经常需要综合使用这些方法,而不是单一使用某一种方法。这是因为每种方法都有其弱点和应用环境。综合使用这些方法使得Java程序的保护更加有效。另外,我们经常还需要使用其它的相关安全技术,例如安全认证、数字签名、PKI等。

本文给出的例子是一个Java应用程序,它是一个SCJP(Sun Certificate Java Programmer)的模拟考试软件。该应用程序带有大量的模拟题目,所有的题目都被加密后存储在文件中。由于它所带的题库是该软件的核心部分,所以关于题库的存取和访问就成为非常核心的类。一旦这些相关的类被反编译,则所有的题库将被破解。现在,我们来考虑如何保护这些题库及相关的类。

在这个例子中,我们考虑使用综合保护技术,其中包括本地代码和混淆技术。因为该软件主要发布在Windows上,因此转换成本地代码后,仅仅需要维护一个版本的本地代码。另外,混淆对Java程序也是非常有效的,适用于这种独立发布的应用系统。

在具体的方案中,我们将程序分为两个部分,一个是由本地代码编写的题库访问的模块,另外一个是由Java开发的其它模块。这样可以更高程度地保护题目管理模块不被反编译。对于Java开发的模块,我们仍然要使用混淆技术。该方案的示意图参见图7。

955bb835b3f25453b9a43fddc08a28eb.png

  图7 SCJP保护技术方案图

对于题目管理模块,由于程序主要在Windows下使用,所以使用C++开发题库访问模块,并且提供了一定的访问接口。为了保护题库访问的接口,我们还增加了一个初始化接口,用于每次使用题库访问接口之前的初始化工作。它的接口主要分为两类:

1. 初始化接口
在使用题库模块之前,我们必须先调用初始化接口。在调用该接口时,客户端需要提供一个随机数作为参数。题库管理模块和客户端通过这个随机数,按一定的算法同时生成相同的SessionKey,用于加密以后输入和输出的所有数据。通过这种方式,只有授权(有效)的客户端才能够连接正确的连接,生成正确的 SessionKey,用于访问题库信息。非法的客户很难生成正确的SessionKey,因此无法获得题库的信息。如果需要建立更高的保密级别,也可以采用双向认证技术。

2. 数据访问接口
认证完成之后,客户端就可以正常的访问题库数据。但是,输入和输出的数据都是由SessionKey所加密的数据。因此,只有正确的题库管理模块才能够使用题库管理模块。图8时序图表示了题库管理模块和其它部分的交互过程。

484d92fccfb5d4b82cf9abead4667e6b.png

本文转载自: 掘金

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

1…271272273…956

开发者博客

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