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

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


  • 首页

  • 归档

  • 搜索

字节码增强艺术-javasist 前言 走近Javasist

发表于 2021-11-05

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

前言

在上篇文章通过实战走近Java Agent探针技术中,在进行热替换的时候,我们使用了javasist对目标类的字节码进行了增强,所谓字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。字节码增强技术的实现有很多,比如:ASM、cglib、javasist等、上篇中用到了javasist去修改我们目标类的行为,但是并没有着重介绍javasist的具体使用,本篇文章将带你走近字节码增强艺术:javasist

走近Javasist

官网地址:www.javassist.org/

GitHub:github.com/jboss-javas…

Javasist(Java Programming Assistant)是我们操作字节码更加简单。所谓Javasist其实就是一个类库,它提供了允许开发者在运行时去定义一个新class或者修改一个class文件。那么javasist提供了两种级别的API:源码级别和字节码级别。所谓源码级别,说白点就是更加方便我们傻瓜式去调用,我们不用去关注字节码实现规范细节,这也是Javasist的一大优势,编程简单,提升效率,谁不爱呢;而相应的字节码级别对我们的要求就会高一点,它允许我们像在编辑器中写代码一样去编写字节码文件。

写此文中,特意去看了一下Javasist社区 github.com/jboss-javas… ,基本一年两三个版本,所以还是蛮活跃的,暂时不必担心开源版本无人维护而难以选择。那么废话到这,接下来就进入实战环节了!

Javasist实战

首先引入pom依赖:

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

整个Javasist操作字节码有四个类是最为关键和核心的,其实如果你看一个类的组成(方法+属性),大概也就知道都有哪些东西:

类名 描述
Javassist.CtClass Javassist.CtClass就代表着一个class文件的抽象,一个CtClass对象就是操作一个class文件的句柄
ClassPool ClassPool可以看作是保存着CtClass对象的一个容器,当我们需要获取指定类对应的CtClass时,就可以通过指定类的全限定名去获取。要对字节码进行操作,第一步就是要从ClassPool中获取对应的CtClass。从开发视角看ClassPool是一个CtClass对象的哈希表,类名作为键,CtClass作为value
CtMethod 一个实例代表一个方法
CtField 一个实例代表一个属性

有了关键类之后,再来看下这些关键类(主要就看下ClassPool和CtClass)哪些关键API吧:

CtClass

标题
writeFile() translates the CtClass object into a class file and writes it on a local disk.
toBytecode() 获取字节码
toClass() 获取当前执行线程的contextClassLoader对该字节码文件进行加载
setName() 从名字上看是修改类名,但是这里其实是基于当前CtClass复制一个出来,所以这里set的是新类的名字
defrost() 如果一个CtClass对象被writeFile()、toClass()或toBytecode()转换成一个类文件,Javassist将冻结该CtClass对象。不允许进一步修改该CtClass对象。这是为了在开发人员试图修改已经加载的类文件时发出警告,因为JVM不允许重新加载类。那么我们可以通过此方法对该CtClass进行解冻,之后就可以修改了
detach() 需要注意的是ClassPool在运行期间,所有创建的CtClass都会永远保存。如果CtClass对象的数量变得惊人地大,ClassPool的这种规范可能会导致巨大的内存消耗(这种情况很少发生,因为Javassist试图以各种方式减少内存消耗)。为了避免这个问题,您可以显式地从ClassPool中删除一个不必要的CtClass对象。如果在一个CtClass对象上调用detach(),那么该CtClass对象将从ClassPool中删除。当然,除了这种方式去避免这种问题,还有另外一种方式:当前ClassPool直接不要了,当垃圾回收时,那么所有相关的CtClass也都没了

ClassPool

标题
getDefault() 通过getDefault()获取的ClassPool,其默认搜索路径是System Path
insertClassPath 假如我们的javasist运行在诸如Jboss,Tomcat这种自定义了自己加载器的实现,我们如果通过getDefault获取时,是无法获取到一些路径下的类的,此时我们可以通过该方法去指定一些搜索路径

从上面insertClassPath可以知道,是不是感觉和类加载有点像呢,说白了就是找class文件嘛,那怎么找?类加载机制提供了双亲委派,然而javasist也提供了类似的机制,我们可以给ClassPool指定父ClassPool,这样,每当我们调用get()时,都会优先从父ClassPool中去寻找,就像这样:

1
2
3
java复制代码ClassPool parent = ClassPool.getDefault();
ClassPool child = new ClassPool(parent);
child.insertClassPath("./classes");

然后javasist除了这,还支持从儿子往爷爷头上找,就是自己先找,自己找不到,再问自己爹找,这个可以通过属性childFirstLookup去控制。

代码实战

说了这么多,接下来直接上代码

通过Javasist修改已有类的行为

这里先定义一个我们目标要进行修改的类:

1
2
3
4
5
java复制代码public class TargetObject {
public void sayHello(){
System.out.println("hello");
}
}

接下来我将通过javasist改变sayHello的行为,就像是我们平常使用的AOP一样:

1
2
3
4
5
6
7
8
9
10
11
java复制代码ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get("com.cf.study.DailyStudy.javasisttest.TargetObject");
CtMethod method = ctClass.getDeclaredMethod("sayHello");

String beforeExecute = "System.out.println("before...");";
String afterExecute = "System.out.println("after...");";
method.insertBefore(beforeExecute);
method.insertAfter(afterExecute);

TargetObject targetObjectAgent = (TargetObject)ctClass.toClass().getConstructor().newInstance();
targetObjectAgent.sayHello();

输出:

1
2
3
erlang复制代码before...
hello
after...

参考

tech.meituan.com/2019/09/05/…
www.javassist.org/tutorial/tu…

本文转载自: 掘金

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

设计模式——观察者模式 概述 实现

发表于 2021-11-05

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

行为型模式,就是描述对象之间的交互关系和交互方法

概述

什么叫做观察者模式

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

什么时候使用观察者模式

当对象间存在一对多的关系,当前对象的修改需要通知到其他各个依赖它的对象时使用,广播通知。

例子

用菜鸟教程中的一个例子说明非常精确:“拍卖的时候,拍卖师观察最高标价,然后通知给其他竞价者竞价。”这个拍卖师就是一个观察者

其实我们平时使用MQ,也贯彻这种思想。就是一个数值的改变会通知依赖于这个数值的各个程序

实现

逻辑实现

父母的户口薄中的姓名更换,子女的户口薄“父亲”一列信息也会更换

  1. 创建父亲类(parent),其中属性为name、childrens、notifyAllObservers()发放进行通知子女信息。
  2. 创建观察者,观察者中有parent ,abstract void update();parentName方法
  3. 实现子女信息,其中update方法实现观察更新逻辑

代码实现

  1. 实现父亲类,注意,修改姓名之后需要通知到它所有的依赖者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public class Parent {
/**
* 受其他类所依赖的点
*/
private List<Children> childrens
= new ArrayList<Children>();
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
notifyAllObservers();
}
public void attach(Children children){
childrens.add(children);
}
public void notifyAllObservers(){
for (Children children : childrens) {
children.update();
}
}
}
  1. 实现孩子的抽象方法及孩子
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
java复制代码public abstract class Children {

protected Parent parent;
public abstract void update();
}

public class Daughter extends Children {

/**
* 监控参数
*/
private String parentName;

public Daughter(Parent parent) {
//实现该对象时候,将该对象依赖到Parent的对象中
this.parent = parent;
this.parent.attach(this);
}

@Override
public void update() {
//处理观察变化的逻辑
this.parentName = parent.getName();
System.out.println("i'm daughter ,my Father name is : "
+ parent.getName());
}
}
public class Son extends Children {

/**
* 监控参数
*/
private String parentName;

public Son(Parent parent) {
//实现该对象时候,将该对象依赖到Parent的对象中
this.parent = parent;
this.parent.attach(this);
}

@Override
public void update() {
//处理观察变化的逻辑
this.parentName = parent.getName();
System.out.println(" i'm son ,my Father name is : "
+ parent.getName());
}
}
  1. 测试实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码
public class ObServerDemo {


public static void main(String[] args) {
Parent parent = new Parent();
new Son(parent);
new Daughter(parent);

parent.setName("李刚");
parent.setName("王刚");
}
}

输出:

i'm son ,my Father name is : 李刚
i'm daughter ,my Father name is : 李刚
i'm son ,my Father name is : 王刚
i'm daughter ,my Father name is : 王刚

由此可见那个循环通知的操作,如果依赖者过多,会导致进行相关通知的逻辑太多。所以其实这块逻辑可以再优化下,将找个通知的过程异步出去,开一个单独的线程进行处理(又设计数据同步和锁的问题了)。

本文转载自: 掘金

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

Collections类的使用 1Collections常

发表于 2021-11-05

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

1.Collections常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。

常用方法如下:

  • public static void shuffle(List<?> list):打乱集合顺序。
  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class CollectionsDemo {
   public static void main(String[] args) {
       ArrayList<Integer> list = new ArrayList<Integer>();
 
       list.add(100);
       list.add(300);
       list.add(200);
       list.add(50);
       //排序方法
       Collections.sort(list);
       System.out.println(list);
  }
}

结果:

1
java复制代码[50,100, 200, 300]

我们的集合按照默认的自然顺序进行了排列,如果想要指定顺序那该怎么办呢?

2.Comparator比较器

我们还是先研究这个方法

public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

不过这次存储的是字符串类型。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class CollectionsDemo2 {
   public static void main(String[] args) {
       ArrayList<String>  list = new ArrayList<String>();
       list.add("cba");
       list.add("aba");
       list.add("sba");
       list.add("nba");
       //排序方法
       Collections.sort(list);
       System.out.println(list);
  }
}

结果:

1
java复制代码[aba, cba, nba, sba]

我们使用的是默认的规则完成字符串的排序,那么默认规则是怎么定义出来的呢?

说到排序了,简单的说就是两个对象之间比较大小,那么在JAVA中提供了两种比较实现的方式,一种是比较死板的采用java.lang.Comparable接口去实现,一种是灵活的当我需要做排序的时候在去选择的java.util.Comparator接口完成。

那么我们采用的public static <T> void sort(List<T> list)这个方法完成的排序,实际上要求了被排序的类型需要实现Comparable接口完成比较的功能,在String类型上如下:

1
java复制代码public final class String implements java.io.Serializable, Comparable<String>, CharSequence {

String类实现了这个接口,并完成了比较规则的定义,但是这样就把这种规则写死了,那比如我想要字符串按照第一个字符降序排列,那么这样就要修改String的源代码,这是不可能的了,那么这个时候我们可以使用

public static <T> void sort(List<T> list,Comparator<? super T> )方法灵活的完成,这个里面就涉及到了Comparator这个接口,位于位于java.util包下,排序是comparator能实现的功能之一,该接口代表一个比较器,比较器具有可比性!顾名思义就是做排序的,通俗地讲需要比较两个对象谁排在前谁排在后,那么比较的方法就是:

  • public int compare(String o1, String o2):比较其两个参数的顺序。

两个对象比较的结果有三种:大于,等于,小于。

如果要按照升序排序,则o1 小于o2,返回(负数),相等返回0,01大于02返回(正数)如果要按照降序排序则o1 小于o2,返回(正数),相等返回0,01大于02返回(负数)

操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class CollectionsDemo3 {
   public static void main(String[] args) {
       ArrayList<String> list = new ArrayList<String>();
       list.add("cba");
       list.add("aba");
       list.add("sba");
       list.add("nba");
       //排序方法 按照第一个单词的降序
       Collections.sort(list, new Comparator<String>() {
           @Override
           public int compare(String o1, String o2) {
               return o2.charAt(0) - o1.charAt(0);
          }
      });
       System.out.println(list);
  }
}

结果如下:

1
java复制代码[sba, nba, cba, aba]

3.简述Comparable和Comparator两个接口的区别

Comparable:强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的compareTo方法被称为它的自然比较方法。只能在类中实现compareTo()一次,不能经常修改类的代码实现自己想要的排序。实现此接口的对象列表(和数组)可以通过Collections.sort(和Arrays.sort)进行自动排序,对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。

Comparator:强行对某个对象进行整体排序。可以将Comparator 传递给sort方法(如Collections.sort或 Arrays.sort),从而允许在排序顺序上实现精确控制。还可以使用Comparator来控制某些数据结构(如有序set或有序映射)的顺序,或者为那些没有自然顺序的对象collection提供排序。

4.可变参数

在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.

格式:

1
scss复制代码修饰符 返回值类型 方法名(参数类型... 形参名){  }

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码  public class ChangeArgs {
   public static void main(String[] args) {
       int sum = getSum(6, 7, 2, 12, 2121);
       System.out.println(sum);
  }
   
   public static int getSum(int... arr) {
  int sum = 0;
      for (int a : arr) {
        sum += a;
      }
  return sum;
  }
}

注意:

​ 1.一个方法只能有一个可变参数

​ 2.如果方法中有多个参数,可变参数要放到最后。

应用场景: Collections

​ 在Collections中也提供了添加一些元素方法:

​ public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class CollectionsDemo {
public static void main(String[] args) {
     ArrayList<Integer> list = new ArrayList<Integer>();
     //原来写法
     //list.add(12);
     //list.add(14);
     //list.add(15);
     //list.add(1000);
     //采用工具类 完成 往集合中添加元素  
     Collections.addAll(list, 5, 222, 1,2);
     System.out.println(list);
}

5.练习

创建一个学生类,存储到ArrayList集合中完成指定排序操作。

Student 初始类

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
java复制代码public class Student{
   private String name;
   private int age;
​
   public Student() {
  }
​
   public Student(String name, int age) {
       this.name = name;
       this.age = age;
  }
​
   public String getName() {
       return name;
  }
​
   public void setName(String name) {
       this.name = name;
  }
​
   public int getAge() {
       return age;
  }
​
   public void setAge(int age) {
       this.age = age;
  }
​
   @Override
   public String toString() {
       return "Student{" +
              "name='" + name + ''' +
              ", age=" + age +
              '}';
  }
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class Demo {
​
   public static void main(String[] args) {
       // 创建四个学生对象 存储到集合中
       ArrayList<Student> list = new ArrayList<Student>();
​
       list.add(new Student("rose",18));
       list.add(new Student("jack",16));
       list.add(new Student("abc",16));
       list.add(new Student("ace",17));
       list.add(new Student("mark",16));
       /*
         按照年龄排序 升序
        */
//       Collections.sort(list);//要求 该list中元素类型必须实现比较器Comparable接口
​
       for (Student student : list) {
           System.out.println(student);
      }
  }
}

发现,当我们调用Collections.sort()方法的时候 程序报错了。

原因:如果想要集合中的元素完成排序,那么必须要实现比较器Comparable接口。

于是我们就完成了Student类的一个实现,如下:

1
2
3
4
5
6
7
java复制代码public class Student implements Comparable<Student>{
  ....
   @Override
   public int compareTo(Student o) {
       return this.age-o.age;//升序
  }
}

再次测试,代码就OK 了效果如下:

1
2
3
4
5
java复制代码Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}
Student{name='ace', age=17}
Student{name='rose', age=18}

6.扩展

如果在使用的时候,想要独立的定义规则去使用 可以采用Collections.sort(List list,Comparetor<T> c)方式,自己定义规则:

1
2
3
4
5
6
java复制代码Collections.sort(list, new Comparator<Student>() {
   @Override
   public int compare(Student o1, Student o2) {
       return o2.getAge()-o1.getAge();//以学生的年龄降序
  }
});

效果:

1
2
3
4
5
java复制代码Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='jack', age=16}
Student{name='abc', age=16}
Student{name='mark', age=16}

如果想要规则更多一些,可以参考下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码Collections.sort(list, new Comparator<Student>() {
           @Override
           public int compare(Student o1, Student o2) {
               // 年龄降序
               int result = o2.getAge()-o1.getAge();//年龄降序
​
               if(result==0){//第一个规则判断完了 下一个规则 姓名的首字母 升序
                   result = o1.getName().charAt(0)-o2.getName().charAt(0);
              }
​
               return result;
          }
      });

效果如下:

1
2
3
4
5
java复制代码Student{name='rose', age=18}
Student{name='ace', age=17}
Student{name='abc', age=16}
Student{name='jack', age=16}
Student{name='mark', age=16}

本文转载自: 掘金

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

最佳实践之Golang错误处理 1、原生错误处理 2、开源e

发表于 2021-11-05

1、原生错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:

1
2
3
go复制代码type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:

1
2
3
4
5
6
go复制代码func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}

在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

1
2
3
4
go复制代码result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}

2、开源error包

github.com/pkg/errors包在原生error包基础上增加了以下常用的功能:

  • 可以打印error的堆栈信息:打印错误需要%+v才能详细输出
  • 使用Wrap或Wrapf,初始化一个error
  • 使用errors.WithMessage可以在原来的error基础上再包装一层,包含原有error信息
  • errors.Is,用于判断error类型,可根据error类型不同做不同处理
  • errors.As,用于解析error

具体使用案例见全局错误处理一节。

3、工程中错误处理

3.1 需求整理

  • 自定义error信息,并进行编码整理
    • controller层可以判断自定义error类型,最终判断是按info处理,还是按error处理
  • 可以打印error初始发生的位置(获取error的调用栈)
  • 确认当前系统定位:
    • 用户,获取TagMessage
    • 上游服务,需要错误码映射
    • 日志监控、监控TagMessage

下面在一个工程化的项目中利用github.com/pkg/errors包,完整实现一套的错误处理机制

3.2 方式一:Map保存错误码与Message的映射

3.2.1 定义错误信息

新建error_handler.go

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
go复制代码package error_handle

import (
"github.com/pkg/errors"
)

// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
Code int `json:"code"` // 业务码
TagMessage string `json:"message"` // 描述信息
}

func (e *CustomError) Error() string {
return e.TagMessage
}

// 2、定义errorCode
const (
// 服务级错误码
ServerError = 10101
TooManyRequests = 10102
ParamBindError = 10103
AuthorizationError = 10104
CallHTTPError = 10105
ResubmitError = 10106
ResubmitMsg = 10107
HashIdsDecodeError = 10108
SignatureError = 10109

// 业务模块级错误码
// 用户模块
IllegalUserName = 20101
UserCreateError = 20102
UserUpdateError = 20103
UserSearchError = 20104

// 授权调用方
AuthorizedCreateError = 20201
AuthorizedListError = 20202
AuthorizedDeleteError = 20203
AuthorizedUpdateError = 20204
AuthorizedDetailError = 20205
AuthorizedCreateAPIError = 20206
AuthorizedListAPIError = 20207
AuthorizedDeleteAPIError = 20208

// 管理员
AdminCreateError = 20301
AdminListError = 20302
AdminDeleteError = 20303
AdminUpdateError = 20304
AdminResetPasswordError = 20305
AdminLoginError = 20306
AdminLogOutError = 20307
AdminModifyPasswordError = 20308
AdminModifyPersonalInfoError = 20309

// 配置
ConfigEmailError = 20401
ConfigSaveError = 20402
ConfigRedisConnectError = 20403
ConfigMySQLConnectError = 20404
ConfigMySQLInstallError = 20405
ConfigGoVersionError = 20406

// 实用工具箱
SearchRedisError = 20501
ClearRedisError = 20502
SearchRedisEmpty = 20503
SearchMySQLError = 20504

// 菜单栏
MenuCreateError = 20601
MenuUpdateError = 20602
MenuListError = 20603
MenuDeleteError = 20604
MenuDetailError = 20605

// 借书
BookNotFoundError = 20701
BookHasBeenBorrowedError = 20702
)

// 3、定义errorCode对应的文本信息
var codeTag = map[int]string{
ServerError: "Internal Server Error",
TooManyRequests: "Too Many Requests",
ParamBindError: "参数信息有误",
AuthorizationError: "签名信息有误",
CallHTTPError: "调用第三方 HTTP 接口失败",
ResubmitError: "Resubmit Error",
ResubmitMsg: "请勿重复提交",
HashIdsDecodeError: "ID参数有误",
SignatureError: "SignatureError",

IllegalUserName: "非法用户名",
UserCreateError: "创建用户失败",
UserUpdateError: "更新用户失败",
UserSearchError: "查询用户失败",

AuthorizedCreateError: "创建调用方失败",
AuthorizedListError: "获取调用方列表页失败",
AuthorizedDeleteError: "删除调用方失败",
AuthorizedUpdateError: "更新调用方失败",
AuthorizedDetailError: "获取调用方详情失败",
AuthorizedCreateAPIError: "创建调用方API地址失败",
AuthorizedListAPIError: "获取调用方API地址列表失败",
AuthorizedDeleteAPIError: "删除调用方API地址失败",

AdminCreateError: "创建管理员失败",
AdminListError: "获取管理员列表页失败",
AdminDeleteError: "删除管理员失败",
AdminUpdateError: "更新管理员失败",
AdminResetPasswordError: "重置密码失败",
AdminLoginError: "登录失败",
AdminLogOutError: "退出失败",
AdminModifyPasswordError: "修改密码失败",
AdminModifyPersonalInfoError: "修改个人信息失败",

ConfigEmailError: "修改邮箱配置失败",
ConfigSaveError: "写入配置文件失败",
ConfigRedisConnectError: "Redis连接失败",
ConfigMySQLConnectError: "MySQL连接失败",
ConfigMySQLInstallError: "MySQL初始化数据失败",
ConfigGoVersionError: "GoVersion不满足要求",

SearchRedisError: "查询RedisKey失败",
ClearRedisError: "清空RedisKey失败",
SearchRedisEmpty: "查询的RedisKey不存在",
SearchMySQLError: "查询mysql失败",

MenuCreateError: "创建菜单失败",
MenuUpdateError: "更新菜单失败",
MenuDeleteError: "删除菜单失败",
MenuListError: "获取菜单列表页失败",
MenuDetailError: "获取菜单详情失败",

BookNotFoundError: "书未找到",
BookHasBeenBorrowedError: "书已经被借走了",
}

func Text(code int) string {
return codeTag[code]
}

// 4、新建自定义error实例化
func NewCustomError(code int) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code: code,
TagMessage: codeTag[code],
}, "")
}

3.3 自定义Error使用

新建测试文件:error_handler_test.go

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
go复制代码package error_handle

import (
"fmt"
"github.com/pkg/errors"
"testing"
)

func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}

for _, bookName := range books {
err := searchBook(bookName)

// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var myError = new(CustomError)
// As - 解析错误内容
if errors.As(err, &myError) {
fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, myError.Code, myError.TagMessage)
}

// 特殊场景,指定错误(ErrorBookHasBeenBorrowed)时,打印即可,不返回错误
// Is - 判断错误是否为指定类型
if errors.Is(err, NewCustomError(BookHasBeenBorrowedError)) {
fmt.Printf("IS中的信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
err = nil
}else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err")
fmt.Printf("IS中的信息:%s 未找到,应该按Error处理! ,newErr is %s\n", bookName , newErr)
}
}
}
}

func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookHasBeenBorrowedError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}

3.3 方式二:借助generate简化代码(建议使用)

方式一维护错误码与错误信息的关系较为复杂,我们可以借助go generate来自动生成代码。

3.3.1 安装stringer

stringer不是Go自带工具,需要手动安装。执行如下命令即可

1
go复制代码go get golang.org/x/tools/cmd/stringer

3.3.1 定义错误信息

新建error_handler.go。在error_handler中,增加注释//go:generate stringer -type ErrCode -linecomment。执行go generate,会生成新的文件

image.png

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
go复制代码package error_handle

import (
"github.com/pkg/errors"
)

// 1、自定义error结构体,并重写Error()方法
// 错误时返回自定义结构
type CustomError struct {
Code ErrCode `json:"code"` // 业务码
Message string `json:"message"` // 业务码
}

func (e *CustomError) Error() string {
return e.Code.String()
}

type ErrCode int64 //错误码

// 2、定义errorCode
//go:generate stringer -type ErrCode -linecomment
const (
// 服务级错误码
ServerError ErrCode = 10101 // Internal Server Error
TooManyRequests ErrCode = 10102 // Too Many Requests
ParamBindError ErrCode = 10103 // 参数信息有误
AuthorizationError ErrCode = 10104 // 签名信息有误
CallHTTPError ErrCode = 10105 // 调用第三方HTTP接口失败
ResubmitError ErrCode = 10106 // ResubmitError
ResubmitMsg ErrCode = 10107 // 请勿重复提交
HashIdsDecodeError ErrCode = 10108 // ID参数有误
SignatureError ErrCode = 10109 // SignatureError

// 业务模块级错误码
// 用户模块
IllegalUserName ErrCode = 20101 // 非法用户名
UserCreateError ErrCode = 20102 // 创建用户失败
UserUpdateError ErrCode = 20103 // 更新用户失败
UserSearchError ErrCode = 20104 // 查询用户失败

// 配置
ConfigEmailError ErrCode = 20401 // 修改邮箱配置失败
ConfigSaveError ErrCode = 20402 // 写入配置文件失败
ConfigRedisConnectError ErrCode = 20403 // Redis连接失败
ConfigMySQLConnectError ErrCode = 20404 // MySQL连接失败
ConfigMySQLInstallError ErrCode = 20405 // MySQL初始化数据失败
ConfigGoVersionError ErrCode = 20406 // GoVersion不满足要求

// 实用工具箱
SearchRedisError ErrCode = 20501 // 查询RedisKey失败
ClearRedisError ErrCode = 20502 // 清空RedisKey失败
SearchRedisEmpty ErrCode = 20503 // 查询的RedisKey不存在
SearchMySQLError ErrCode = 20504 // 查询mysql失败

// 菜单栏
MenuCreateError ErrCode = 20601 // 创建菜单失败
MenuUpdateError ErrCode = 20602 // 更新菜单失败
MenuListError ErrCode = 20603 // 删除菜单失败
MenuDeleteError ErrCode = 20604 // 获取菜单列表页失败
MenuDetailError ErrCode = 20605 // 获取菜单详情失败

// 借书
BookNotFoundError ErrCode = 20701 // 书未找到
BookHasBeenBorrowedError ErrCode = 20702 // 书已经被借走了
)

// 4、新建自定义error实例化
func NewCustomError(code ErrCode) error {
// 初次调用得用Wrap方法,进行实例化
return errors.Wrap(&CustomError{
Code: code,
Message: code.String(),
}, "")
}

3.3.2 自定义Error使用

新建测试文件:error_handler_test.go

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
go复制代码package error_handle

import (
"fmt"
"github.com/pkg/errors"
"testing"
)

func TestText(t *testing.T) {
books := []string{
"Book1",
"Book222222",
"Book3333333333",
}

for _, bookName := range books {
err := searchBook(bookName)

// 特殊业务场景:如果发现书被借走了,下次再来就行了,不需要作为错误处理
if err != nil {
// 提取error这个interface底层的错误码,一般在API的返回前才提取
// As - 获取错误的具体实现
var customErr = new(CustomError)
// As - 解析错误内容
if errors.As(err, &customErr) {
//fmt.Printf("AS中的信息:当前书为: %s ,error code is %d, message is %s\n", bookName, customErr.Code, customErr.Message)
if customErr.Code == BookHasBeenBorrowedError {
fmt.Printf("IS中的info信息:%s 已经被借走了, 只需按Info处理!\n", bookName)
} else {
// 如果已有堆栈信息,应调用WithMessage方法
newErr := errors.WithMessage(err, "WithMessage err1")
// 使用%+v可以打印完整的堆栈信息
fmt.Printf("IS中的error信息:%s 未找到,应该按Error处理! ,newErr is: %+v\n", bookName, newErr)
}
}
}
}
}

func searchBook(bookName string) error {
// 1 发现图书馆不存在这本书 - 认为是错误,需要打印详细的错误信息
if len(bookName) > 10 {
return NewCustomError(BookNotFoundError)
} else if len(bookName) > 6 {
// 2 发现书被借走了 - 打印一下被接走的提示即可,不认为是错误
return NewCustomError(BookHasBeenBorrowedError)
}
// 3 找到书 - 不需要任何处理
return nil
}

4 总结

  1. CustomError 作为全局 error 的底层实现,保存具体的错误码和错误信息;
  2. CustomError向上返回错误时,第一次先用Wrap初始化堆栈,后续用WithMessage增加堆栈信息;
  3. 从error中解析具体错误时,用errors.As提取出CustomError,其中的错误码和错误信息可以传入到具体的API接口中;
  4. 要判断error是否为指定的错误时,用errors.Is + Handler Error的方法,处理一些特定情况下的逻辑;

Tips:

  1. 不要一直用errors.Wrap来反复包装错误,堆栈信息会爆炸,具体情况可自行测试了解
  2. 利用go generate可以大量简化初始化Erro重复的工作
  3. github.com/pkg/errors和标准库的error完全兼容,可以先替换、后续改造历史遗留的代码
  4. 一定要注意打印error的堆栈需要用%+v,而原来的%v依旧为普通字符串方法;同时也要注意日志采集工具是否支持多行匹配

我是简凡,一个励志用最简单的语言,描述最复杂问题的新时代农民工。求点赞,求关注,如果你对此篇文章有什么疑惑,欢迎在我的微信公众号中留言,我还可以为你提供以下帮助:

  • 帮助建立自己的知识体系
  • 互联网真实高并发场景实战讲解
  • 不定期分享Golang、Java相关业内的经典场景实践

我的博客:besthpt.github.io/

微信公众号:”简凡丶”

本文转载自: 掘金

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

开启mybatis学习之路——若依框架中的mybatis

发表于 2021-11-05

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

很多Java小伙伴自嘲是Spring工程师,但更可悲的是,还有很多前端同学压根就意识不到自己是Vue工程师

嗯,这说的就是我啊!

为什么学mybatis

最近小伙伴推了一个好用的前后台分离的管理系统:若依(Ruoyi),相信不少小伙伴都用过或者听过。

基础页面长这个样子:

image.png

之前生产环境用的是国产开源框架jfinal中集成的数据库访问组件,为了避免把自己绑死的一个框架上,多学点东西总是有益的。

若依中用的数据库组件是mybatis,也借着更文,系统的学习一下mybatis吧。

mybatis官方文档

目前是按照这一套官网文档学习。

若依中的mybatis架构

按照程序员的直觉和若依框架中的简单教程做了一个简单页面,执行了几个简单的sql,大致清楚了mybatis的框架。若依的框架大致有这样几部分:

controller

controller基本位于若依框架中的admin中:

image.png

service

service则根据功能版块拆分到各个子module中,如common, framework, system等,需要自己的业务版块可以新建新的modeul。 (这一点体现出jetbrain idea是真的好用)

mybatis架构

mybatis主要体现在若依的system版块中。System版块的项目截图如下图:

image.png

可以清晰的看到,其中分为domain,mapper,service文件夹,以及保存sql的xml文件包。

domain

domain,也就是通常说的DO, Domain Object,领域对象,网上查到的概念是:从现实世界中抽象出来的有形或无形的业务实体。

mapper

mybatis基础的通用mapper,通过配置可以自动生成单表的增删改查。这个是用来降低开发成本,减少程序员的工作量的。

service

这各基本和mybatis没什么关系了,是向上提供服务方法的,一般应该是一个完整的业务方法。

mybatis的配置

若依哪里配置了mybatis呢?

根据以往其他框架的经验,我们从spring的配置文件入手即可在admin版块中发现:

image.png

本文转载自: 掘金

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

MySQL:互联网公司常用分库分表方案汇总!

发表于 2021-11-05

一、数据库瓶颈

不管是IO瓶颈,还是CPU瓶颈,最终都会导致数据库的活跃连接数增加,进而逼近甚至达到数据库可承载活跃连接数的阈值。在业务Service来看就是,可用数据库连接少甚至无连接可用。接下来就可以想象了吧(并发量、吞吐量、崩溃)。

1、IO瓶颈

第一种:磁盘读IO瓶颈,热点数据太多,数据库缓存放不下,每次查询时会产生大量的IO,降低查询速度 -> 分库和垂直分表。第二种:网络IO瓶颈,请求的数据太多,网络带宽不够 -> 分库。

2、CPU瓶颈

第一种:SQL问题,如SQL中包含join,group by,order by,非索引字段条件查询等,增加CPU运算的操作 -> SQL优化,建立合适的索引,在业务Service层进行业务计算。第二种:单表数据量太大,查询时扫描的行太多,SQL效率低,CPU率先出现瓶颈 -> 水平分表。

二、分库分表

1、水平分库

image-20211105165829483

概念:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。结果:

  • 每个库的结构都一样;
  • 每个库的数据都不一样,没有交集;
  • 所有库的并集是全量数据;

场景:系统绝对并发量上来了,分表难以根本上解决问题,并且还没有明显的业务归属来垂直分库。分析:库多了,io和cpu的压力自然可以成倍缓解。

2、水平分表

image-20211105165852395

概念:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。

结果:

  • 每个表的结构都一样;
  • 每个表的数据都不一样,没有交集;
  • 所有表的并集是全量数据;

场景:系统绝对并发量并没有上来,只是单表的数据量太多,影响了SQL效率,加重了CPU负担,以至于成为瓶颈。推荐:一次SQL查询优化原理分析分析:表的数据量少了,单次SQL执行效率高,自然减轻了CPU的负担。

3、垂直分库

image-20211105165915065

概念:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。

结果:

  • 每个库的结构都不一样;
  • 每个库的数据也不一样,没有交集;
  • 所有库的并集是全量数据;

场景:系统绝对并发量上来了,并且可以抽象出单独的业务模块。分析:到这一步,基本上就可以服务化了。例如,随着业务的发展一些公用的配置表、字典表等越来越多,这时可以将这些表拆到单独的库中,甚至可以服务化。再有,随着业务的发展孵化出了一套业务模式,这时可以将相关的表拆到单独的库中,甚至可以服务化。

4、垂直分表

image-20211105165949107

概念:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。

结果:

  • 每个表的结构都不一样;
  • 每个表的数据也不一样,一般来说,每个表的字段至少有一列交集,一般是主键,用于关联数据;
  • 所有表的并集是全量数据;

场景:系统绝对并发量并没有上来,表的记录并不多,但是字段多,并且热点数据和非热点数据在一起,单行数据所需的存储空间较大。以至于数据库缓存的数据行减少,查询时会去读磁盘数据产生大量的随机读IO,产生IO瓶颈。分析:可以用列表页和详情页来帮助理解。垂直分表的拆分原则是将热点数据(可能会冗余经常一起查询的数据)放在一起作为主表,非热点数据放在一起作为扩展表。这样更多的热点数据就能被缓存下来,进而减少了随机读IO。拆了之后,要想获得全部数据就需要关联两个表来取数据。但记住,千万别用join,因为join不仅会增加CPU负担并且会讲两个表耦合在一起(必须在一个数据库实例上)。关联数据,应该在业务Service层做文章,分别获取主表和扩展表数据然后用关联字段关联得到全部数据。

三、分库分表工具

  • sharding-sphere:jar,前身是sharding-jdbc;
  • TDDL:jar,Taobao Distribute Data Layer;
  • Mycat:中间件。

注:工具的利弊,请自行调研,官网和社区优先。

四、分库分表步骤

根据容量(当前容量和增长量)评估分库或分表个数 -> 选key(均匀)-> 分表规则(hash或range等)-> 执行(一般双写)-> 扩容问题(尽量减少数据的移动)。

五、分库分表问题

1、非partition key的查询问题

基于水平分库分表,拆分策略为常用的hash法。端上除了partition key只有一个非partition key作为条件查询

映射法

image-20211105170045012

基因法

image-20211105170054628

注:写入时,基因法生成user_id,如图。关于xbit基因,例如要分8张表,23=8,故x取3,即3bit基因。根据user_id查询时可直接取模路由到对应的分库或分表。

根据user_name查询时,先通过user_name_code生成函数生成user_name_code再对其取模路由到对应的分库或分表。id生成常用snowflake算法。

端上除了partition key不止一个非partition key作为条件查询

映射法

image-20211105170133064

冗余法

image-20211105170202537

注:按照order_id或buyer_id查询时路由到db_o_buyer库中,按照seller_id查询时路由到db_o_seller库中。感觉有点本末倒置!有其他好的办法吗?改变技术栈呢?

后台除了partition key还有各种非partition key组合条件查询

NoSQL法

image-20211105170228931

冗余法

image-20211105170243211

2、非partition key跨库跨表分页查询问题

基于水平分库分表,拆分策略为常用的hash法。

注:用NoSQL法解决(ES等)。

3、扩容问题

基于水平分库分表,拆分策略为常用的hash法。

水平扩容库

(升级从库法)

image-20211105170406406

注:扩容是成倍的。

水平扩容表(双写迁移法)

image-20211105170416842

  • 第一步:(同步双写)修改应用配置和代码,加上双写,部署;
  • 第二步:(同步双写)将老库中的老数据复制到新库中;
  • 第三步:(同步双写)以老库为准校对新库中的老数据;
  • 第四步:(同步双写)修改应用配置和代码,去掉双写,部署;

注:双写是通用方案。

六、分库分表总结

  • 分库分表,首先得知道瓶颈在哪里,然后才能合理地拆分(分库还是分表?水平还是垂直?分几个?)。且不可为了分库分表而拆分。
  • 选key很重要,既要考虑到拆分均匀,也要考虑到非partition key的查询。
  • 只要能满足需求,拆分规则越简单越好。

本文转载自: 掘金

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

SpringBoot详细搭建过程并进行Nginx安装及配置详

发表于 2021-11-05

SpringBoot详细搭建过程并进行Nginx安装及配置详解

www.jianshu.com/p/03e77fefa…

Nginx安装及配置详解

nginx概述

1
arduino复制代码https://www.cnblogs.com/loong-hon/p/9060515.html

官方网站下载地址:

1
arduino复制代码https://nginx.org/en/download.html

image.png
启动nginx

1) 直接双击该目录下的nginx.exe,即可启动nginx服务器

2) 命令行计入该文件夹,执行nginx命令,也会直接启动nginx服务器

访问nginx

打开浏览器,输入地址:http://localhost,访问页面,出现如下页面表示访问成功

image.png
停止nginx

命令行进入nginx根目录,执行如下命令,停止服务器:

1
2
3
4
bash复制代码# 强制停止nginx服务器,如果有未处理的数据,丢弃 
D:/resp_application/nginx-1.13.5> nginx -s stop
# 优雅的停止nginx服务器,如果有未处理的数据,等待处理完成之后停止
D:/resp_application/nginx-1.13.5> nginx -s quit

本文转载自: 掘金

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

JVM-运行时数据区、类加载、GC详解

发表于 2021-11-05

前言:

Java 虚拟机是 Java 平台的基石。它是技术的组成部分,负责其硬件和操作系统的独立性、编译代码的小尺寸以及保护用户免受恶意程序侵害的能力。 该篇文章详细讲述JVM各个模块。


学习概览图:

image

运行时数据区

Java虚拟机定义了在程序执行期间使用的各种运行时数据区域。其中一些数据区域是在Java虚拟机启动时创建的,只有在Java虚拟机退出时才会被销毁。其他数据区域是每个线程的(线程私有)。每线程数据区域在线程创建时创建,在线程退出时销毁。

程序计数器

image
程序计数器是JVM运行时数据区中一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。(java -c xxx.class)命令

总结:

   1. 它是一块很小的内存空间,几乎可以忽略不计。且唯一一个在 JVM 规范中没有规定任何 OutOfMemoryError 情况的区域。

   2. 每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期一致。

   3. 程序计数器记录的是 JVM 字节码指令地址,如果是执行 native 方法,则是未指定值,程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

   4.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。


虚拟机栈

image
Java线程执行方法的内存模型,一个线程对应一个栈,每个方法在执行的同时都会创建一个栈帧(用于存储局部变量表,操作数栈,动态链接,方法出口等信息)不存在垃圾回收问题,只要线程一结束该栈就释放,生命周期和线程一致。

1.内部结构 栈帧(Stack Frame)

   1.1 局部变量表 栈上保存的本地变量

   1.2 操作数栈 参考上图的压栈和出栈

   1.3 动态链接 执行当前方法运行时常数池的引用 多态时指向子类

   1.4 方法返回地址 方法出口的地址

   1.5 其它附加信息

2.虚拟机栈详解
image
3. 异常 JVM对该区域规范了两种异常

   3.1 线程请求的栈深度大于虚拟机栈所允许的深度,将抛出StackOverFlowError异常

   3.2 若虚拟机栈可动态扩展,当无法申请到足够内存空间时将抛出OutOfMemoryError。

   3.3 参数-Xss来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度


堆

image
虚拟机启动时创建,用于存放对象实例,几乎所有的对象(包含常量池)都在堆上分配内存,当对象无法再该空间申请到内存时将抛出OutOfMemoryError异常。同时也是垃圾收集器管理的主要区域。可通过 -Xmx –Xms 参数来分别指定最大堆和最小堆。

新生区
类诞生、成长、消亡的区域,一个类在这里产生,应用,最后被垃圾回收器收集,结束生命。
新生区分为两部分: 伊甸区(Eden space)和幸存者区(Survivor pace) ,所有的类都是在伊甸区被new出来的。幸存区有两个: 0区(Survivor 0 space)和1区(Survivor 1 space)。当伊甸园的空间用完时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行垃圾回收(Minor GC),将伊甸园区中的不再被其他对象所引用的对象进行销毁。然后将伊甸园中的剩余对象移动到幸存 0区。若幸存 0区也满了,再对该区进行垃圾回收,然后移动到1区。

老年区

新生区经过多次GC仍然存活的对象移动到老年区。若老年区也满了,那么这个时候将产生MajorGC(FullGC),进行老年区的内存清理。若老年区执行了Full GC之后发现依然无法进行对象的保存,就会产生OOM异常“OutOfMemoryError”。

元数据区

元数据区取代了永久代(jdk1.8以前),本质和永久代类似,都是对JVM规范中方法区的实现,区别在于元数据区并不在虚拟机中,而是使用本地物理内存,永久代在虚拟机中,永久代逻辑结构上属于堆,但是物理上不属于堆,堆大小=新生代+老年代。元数据区也有可能发生OutOfMemory异常。

元数据区的动态扩展: 默认–XX:MetaspaceSize值为21MB的高水位线。一旦触及则Full GC将被触发并卸载没有用的类(类对应的类加载器不再存活),然后高水位线将会重置。新的高水位线的值取决于GC后释放的元空间。如果释放的空间少,这个高水位线则上升。如果释放空间过多,则高水位线下降。

移除永久代是为融合HotSpot JVM与 JRockit VM而做出的努力,因为JRockit没有永久代,不需要配置永久代。


方法区

类的所有字段和方法字节码,以及一些特殊方法如构造函数,接口代码也在此定义。简单说,所有定义的方法的信息都保存在该区域,静态变量+常量+类信息(构造方法/接口定义)+运行时常量池都存在方法区中,虽然Java虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 Non-Heap(非堆),目的应该是与 Java 堆区分开来。

解惑方法区、永生代、元数据区 方法区(method area)是JVM 规范中定义的一个概念,用于存储类信息、常量池、静态变量、JIT编译后的代码等数据,并没有规定如何去实现它,不同的厂商有不同的实现。而永久代(PermGen)是 Hotspot 虚拟机特有的概念, Java8 的时候又被元空间取代了,永久代和元空间都可以理解为方法区的落地实现。

jdk升级中方法区的变化

  1.Jdk1.6及之前: 有永久代, 常量池在方法区

  2.Jdk1.7: 有永久代,但已经逐步“去永久代”,常量池在堆

  3.Jdk1.8及之后: 无永久代,常量池在元空间


本地方法栈

登记native方法,在Execution Engine执行时加载本地方法库

什么地方用到

1.与 Java 环境外交互:有时 Java 应用需要与 Java外面的环境交互,这就是本地方法存在的原因。

2.与操作系统交互:JVM 支持 Java 语言本身和运行时库,但是有时仍需要依赖一些底层系统的支持。通过本地方法,我们可以实现用 Java 与实现了 jre 的底层系统交互, JVM 的一些部分就是 C语言写的。


栈+堆+方法区的交互关系

image
  1.HotSpot是使用指针的方式来访问对象

  2.Java堆中会存放访问类元数据的地址

  3.reference存储的就直接是对象的地址


类加载

image
类加载:类加载器将class文件加载到虚拟机的内存

  1. 加载:在硬盘上查找并通过IO读入字节码文件

  2. 连接:执行校验、准备、解析(可选)步骤

  3. 校验:校验字节码文件的正确性

  4. 准备:给类的静态变量分配内存,并赋予默认值

  5. 解析:类装载器装入类所引用的其他所有类

  6. 初始化:对类的静态变量初始化为指定的值,执行静态代码块


类加载器种类

   启动类加载器: 负责加载JRE的核心类库,如jre目标下的rt.jar,charsets.jar等

   扩展类加载器: 负责加载JRE扩展目录ext中JAR类包

   系统类加载器: 负责加载ClassPath路径下的类包

   用户自定义加载器: 负责加载用户自定义路径下的类包

1
2
3
4
5
6
7
8
9
java复制代码   public static void main(String[] args) {

System.out.println(ClassLoader.getSystemClassLoader().getClass().getName());
// 输出 sun.misc.Launcher$AppClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getClass().getName());
// 输出 sun.misc.Launcher$ExtClassLoader
System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
// 输出 null 原因 BootstrapLoader(引导类加载器)是用C语言实现的,找不到一个确定的返回父Loader的方式,于是就返回null。
}

类加载机制

image

全盘负责委托机制: 当一个ClassLoader加载一个类时,除非显示的使用另一个ClassLoader,该类所依赖和引用的类也由这个ClassLoader载入。

双亲委派机制: 指先委托父类加载器寻找目标类,在找不到的情况下在自己的路径中查找并载入目标类。

双亲委派模式优势
沙箱安全机制:自己写的String.class类不会被加载,这样便可以防止核心API库被随意篡改
避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再 加载一次。

动态加载类
JVM对class文件是按需加载(运行期间动态加载),非一次性加载。

类加载方式:

  1. 命令行启动应用时候由JVM初始化加载。

  2. 通过Class.forName()方法动态加载。

  3. 通过ClassLoader.loadClass()方法动态加载。


GC

1. 什么是GC

称为垃圾回收,是对内存管理的一种功能,用于释放无效对象并回收内存。

2. 如何判断是否可以回收

引用计数法: 给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。

可达性分析: 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 GC Roots: 本地方法栈中引用的对象、方法区中类静态属性引用的对象、方法区中的常量引用的对象、虚拟机栈中引用的对象。

3. 垃圾回收算法

Mark-Sweep: 标记清除。将存活的对象进行标记,然后清理掉未被标记的对象。最大的问题是空间碎片(清除垃圾之后剩下不连续的内存空间)。

Copying: 复制算法。将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。对于短命对象来说有用,否则需要复制大量的对象,效率低。如Java的新生代堆空间中就是使用了它(survivor空间的from和to区)。

Mark-Compact: 标记整理。让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。对于老年对象来说有用,无需复制,不会产生内存碎片。

4. 分代收集、单线程多线程并行串行收集

分代收集 由于对象的存活周期不同的特性,所以现在商用虚拟机一般采用分代收集算法, 不同的块用不同算法(新生代、老年代)。

  1. 新生代使用: 复制算法。

  2. 老年代使用: 标记 - 清除 或者 标记 - 整理 算法。

单线程多线程并行串行 为了平衡垃圾收集时的指标,使用多线程或者单线程运行

  1. 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程。

  2. 串行与并行: 串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

5. 垃圾收集器 (HotSpot虚拟机) 收集器之间是可以进行配合使用

image

Serial 收集器: 单线程的收集器。

ParNew 收集器: Serial 收集器的多线程版本 默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。

Parallel Scavenge 收集器: 多线程收集器 主要关注点是GC是尽可能的减少用户线程停顿时间 。

Serial Old 收集器: Serial 收集器的老年代版本 针对老年代;采用”标记-整理”算法(还有压缩,Mark-Sweep-Compact);单线程收集。

Parallel Old 收集器: Parallel Scavenge收集器的老年代版本 针对老年代;采用”标记-整理”算法;多线程收集。

CMS 收集器: 并发低停顿收集器 针对老年代;基于”标记-清除”算法(不进行压缩操作,产生内存碎片); 以获取最短回收停顿时间为目标;并发收集、低停顿;需要更多的内存。

G1 收集器: JDK7-u4才推出商用的收集器 在多CPU 和大内存的场景下有很好的性能 G1 可以直接对新生代和老年代一起回收。


qrcode_for_gh_928fd16f34e5_258.jpg

关注公众号 领取海量技术书籍、面试资料。利用碎片时间学习

本文转载自: 掘金

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

从零搭建vue工程(01)

发表于 2021-11-05

一. 安装node环境

1.下载地址:https://nodejs.org/en/

image.png

2、检查是否安装成功:node -v 如果输出版本号,说明我们安装node环境成功。

image.png

npm包管理器是集成在Node.js中了,所以在安装Node.js的时候就已经自带了npm。

image.png
输入npm -v可得到npm的版本。

由于资源的限制,安装npm依赖包的时候经常失败,建议使用npm的国内镜像cnpm 命令行工具代替默认的npm。国内npm镜像 npm.taobao.org/

3、国内npm镜像:npm install -g cnpm ––registry=registry.npm.taobao.org

检查是否安装成功:cnpm -v

image.png

二.搭建vue项目环境

1、cnpm 全局安装vue-cli,输入命令:cnpm install -g vue-cli 安装vue-cli;

  验证是否安装成功:vue,出来vue的信息,及说明安装成功;

image.png

2.打开命令行工具,然后跟着输入,进入项目目录vue-demo文件

3.接着输入:vue init webpack 然后出现以下后,输入 y(确认在当前目录下建了vue项目)

image.png

Failed to download repo vuejs-templates/webpack: connect ETIMEDOUT 20.205.243.166:443

出现此问题,在hosts文件中添加

140.82.112.3 github.com

199.232.69.194 github.global.ssl.fastly.net

4.按图片继续输入命令

image.png

5.进入项目目录,输入 npm install/ npm i 安装项目所需要的依赖包,

6.输入npm run dev,启动项目

image.png

三、vue项目目录讲解

image.png

 1、build:构建脚本目录

1)build.js ==> 生产环境构建脚本;

    2)check-versions.js ==> 检查npm,node.js版本;

    3)utils.js ==> 构建相关工具方法;

    4)vue-loader.conf.js ==> 配置了css加载器以及编译css之后自动添加前缀;

    5)webpack.base.conf.js ==> webpack基本配置;

    6)webpack.dev.conf.js ==> webpack开发环境配置;

    7)webpack.prod.conf.js ==> webpack生产环境配置;

  2、config:项目配置

    1)dev.env.js ==> 开发环境变量;

    2)index.js ==> 项目配置文件;

    3)prod.env.js ==> 生产环境变量;

  3、node_modules:npm 加载的项目依赖模块(依赖包都在这里)

  4、src:这里是我们要开发的目录,基本上要做的事情都在这个目录里。里面包含了几个目录及文件:

    1)assets:资源目录,放置一些图片或者公共js、公共css。这里的资源会被webpack构建;

    2)components:组件目录,我们写的组件就放在这个目录里面;

    3)router:前端路由,我们需要配置的路由路径写在index.js里面;

    4)App.vue:根组件;

    5)main.js:入口js文件;

  5、static:静态资源目录,如图片、字体等。不会被webpack构建

  6、index.html:首页入口文件,可以添加一些 meta 信息等

7、package.json:npm包配置文件,定义了项目的npm脚本,依赖包等信息

  8、README.md:项目的说明文档,markdown 格式

  9、.xxxx文件:这些是一些配置文件,包括语法配置,git配置等

本文转载自: 掘金

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

docker 部署logstash

发表于 2021-11-05

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

今天主要想介绍一下logstash 这个工具,真的很不错,最近在工作中使用到了,对于日志传输收集等都很友好,可以很方便的同步数据到elasticsearch 或者 kafka等工具均可以,今天主要介绍同步文件数据到elasticsearch中

1
复制代码docker pull logstash:6.4.0

km_log_pattern 文件:

1
perl复制代码STIME %{YEAR}-%{MONTHNUM}-%{MONTHDAY} %{HOUR}:?%{MINUTE}:?%{SECOND},?%{MSECONDS}

logstash.conf 配置参数:

读取 文件数据 并写入 elasticsearch

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
yaml复制代码input {

  file {

    path => ["/home/work/testVolume/test_map_log/*.log","/home/work/testVolume/test_map_log/*.log"]

    type => "test_map_new"

    start_position => "beginning"

  }

}

\


filter {

  grok {

    patterns_dir => ["/config-dir/cmap_log_pattern"]

    match => {

      "message" => [

          "\[%{YEAR:year}-%{MONTHNUM:month}-%{MONTHDAY:day} %{HOUR:hour}:%{MINUTE:minute}:%{SECOND:second},%{MSECONDS:mill_seconds}\]\[user_id:%{GREEDYDATA:user_id},mobile:%{GREEDYDATA:user_mobile},status:%{GREEDYDATA:user_status},real_name:%{GREEDYDATA:real_name},email:%{GREEDYDATA:user_email},city:%{GREEDYDATA:user_city},permission_info:%{GREEDYDATA:permission_info},b_stree_permission:%{GREEDYDATA:b_stree_permission},together_permission:%{GREEDYDATA:together_permission},is_admin:%{GREEDYDATA:is_admin}\]\[URL:%{GREEDYDATA:uri}\]\[params:%{GREEDYDATA:params_json_content}\]",

          "\[%{YEAR:year}-%{MONTHNUM:month}-%{MONTHDAY:day} %{HOUR:hour}:%{MINUTE:minute}:%{SECOND:second},%{MSECONDS:mill_seconds}\]\[user_id:%{GREEDYDATA:user_id},mobile:%{GREEDYDATA:mobile},platformCompany:%{GREEDYDATA:platformCompany},real_name:%{GREEDYDATA:real_name},email:%{GREEDYDATA:email},city:%{GREEDYDATA:city},role:%{GREEDYDATA:role},platformCompany:%{GREEDYDATA:platformCompany}\]\[URL:%{GREEDYDATA:uri}\]\[params:%{GREEDYDATA:params_json_content}\]",

          "\[%{YEAR:year}-%{MONTHNUM:month}-%{MONTHDAY:day} %{HOUR:hour}:%{MINUTE:minute}:%{SECOND:second},%{MSECONDS:mill_seconds}\]\[user_id:%{GREEDYDATA:user_id}\]\[URL:%{GREEDYDATA:uri}\]\[params:%{GREEDYDATA:params_json_content}\]"

      ]

    }

  }

  json {

    source => "params_json_content"

    target => "params_json"

    remove_field => ["paramsjson"]

  }

}

\


output {

  elasticsearch {

    hosts => ["127.0.0.1:9200"]

    index => "test_log"

    user => "test"

    password => "xxxxx"

  }

  stdout { codec => line }

}

读取kafka数据写入elasticsearch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ini复制代码input {
kafka {
bootstrap_servers => ["xxx.xxx.xxx.xxx:9092"]
auto_offset_reset => "latest"
consumer_threads => 5
decorate_events => true
group_id => "xxx"
topics => ["xxxxxxxxxx"]
type => "xxxxx"
}
}

output {
stdout {}
elasticsearch {
hosts => ["xxx.xxx.xxx.xxx:9200"]
index => "kafka-xxx-%{+YYYY.MM.dd}"
}
}

启动docker命令:

1
arduino复制代码docker run -d --name logstash_test  --log-opt max-size=10m --log-opt max-file=3  -v /config-dir:/config-dir -v /home/work/logstash_test/logstash:/home/work/logstash_test/logstash -v logstash -f /config-dir/logstash.conf

以上是通过读取文件然后写入elasticsearch 的方式去进行部署还有一种方式是通过部署logstash服务,其他服务进行服务调用去写入

相关logstash.conf 配置:

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
ini复制代码input {

  tcp {

      host => "0.0.0.0"

      port => "5044"

      codec => json

  }

}

filter{

  if [type] == "logstash" {

        ruby { 

            code => "event.set('timestamp', event.timestamp.time.localtime.strftime('%Y-%m-%d %H:%M:%S'))" 

        }

    }

}

output {

  elasticsearch { 

             hosts => ["xx.xx.xx.xx:9200","xx.xx.xx.xx:9200"] #可以配置多个机器 一般为集群

             user => "xxxxxx" 

             password => "xxxxxx" 

             index => "xxxxxx" 

             codec => "json"

  }

  

  stdout { codec => json }

}

启动命令:

1
ruby复制代码docker run -it -d -p 5044:5044--name logstash --net somenetwork -v /docker/logstash/logstash.yml:/usr/share/logstash/config/logstash.yml -v /docker/logstash/conf.d/:/usr/share/logstash/conf.d/ logstash:6.4.0

通过上面就可以搭建一个logstash的服务了,然后其他应用就可以直接调用 xx.xx.xx.xx:5044 传输日志文件进入elasticsearch 了

本文转载自: 掘金

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

1…412413414…956

开发者博客

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