Java安全相关基础知识总结

JVM运行机制

java作为当今最热门的语言之一,具有一次编译 随处运行的特点。而Java拥有的有这种跨平台特性离不开Java特有的JVM

JVM,全称Java virtual machine,即Java虚拟机,它是模拟出的一个虚拟计算机,用于解析Java程序编译产生的class文件,而JVM针对每个操作系统都有其对应的解释器,故Java能够做到一次编译 随处运行的跨平台特性

JVM结构

img

对于各阶段的详解可以看这篇文章 传送门

然后再来了解一下Java生命周期

Java程序生命周期

image-20210302213851316
  • Class Loader:从jarzip或网络等途径获取类的二进制字节流数据,加载进JVM内存,然后从内存中生成代表该类的对象java.lang.Class
  • 链接
    • 验证:确保Class文件的字节流信息不会危害虚拟机自身安全
    • 准备:给类变量分配内存,并赋值为默认值
    • 解析:将常量池内的符号引用替换为直接引用的过程
  • 初始化:真正执行Java代码
  • 使用:使用Java类所提供的功能
  • 卸载:释放内存,启动GC(垃圾回收机制)

Class loader机制

参考详细深入分析 Java ClassLoader 工作机制

Java程序在运行前需要先编译成字节码文件,而Java类在初始化时会调用java.lang.ClassLoader来加载类字节码到内存中,只有class文件加载到内存中才能被其他class文件调用

Java程序在启动时不会将所有class文件加载进内存,而是根据程序运行时的需要通过Class loader来将某个类加载进内存

作用

  • class文件加载到JVM
  • 解析类字节码二进制内容,然后解析为对象
  • 控制类加载的先后顺序(父类优先)

类加载器

先看下Java生命程序周期(上面有,这里就不重复了)

可以看到进入JVM前需要通过Class loader来加载字节码文件,而在Java中默认提供了三个这样的类加载器(又叫类加载子系统)

1、BootStrap ClassLoader :称之为启动类加载器,是最顶层的类加载器,负责加载JDK中的核心类库,如 rt.jar、resources.jar、charsets.jar等

2、Extension ClassLoader:称之为扩展类加载器,负责加载Java的扩展类库,默认加载$JAVA_HOMEjre/lib/*.jar-Djava.ext.dirs指定目录下的jar包。

3、App ClassLoader:称之为系统类加载器,负责加载应用程序classpath目录下所有jar和class文件。

Java默认提供的这三个Class Loader外,我们还可以自己编写Class Loader,但自定义的Class loader必须继承java.lang.ClassLoader

ClassLoader常用方法

  • ClassLoader
  • defineClass(byte[], int, int)把字节数组 b中的内容转换成 Java 类,返回的结果是java.lang.Class类的实
    例。这个方法被声明为final的
  • findClass(String name)查找名称为 name的类,返回的结果是java.lang.Class类的实例
  • loadClass(String name)加载名称为 name的类,返回的结果是java.lang.Class类的实例
  • resolveClass(Class<?>)链接指定的 Java 类
  • getParent() 返回该类加载器的父类加载器

双亲委派模型

ClassLoader使用双亲委派模型来搜索类,那什么叫双亲委派模型呢?

image-20210304180110086

加载类时,会优先从父类去加载,即最先交给BootStrap ClassLoader加载器,当BootStrap ClassLoader无法加载时,就会向下到Extension ClassLoader加载,然后是App ClassLoader。如果三个加载都未加载成功,则就会返回到指定文件系统或网络中进行加载。

若经历上面加载还没加载成功,就会抛出ClassNotFoundException

这种加载方式提高了类加载的安全性,确保了一些原生类不能通过自定义的ClassLoader修改,而不会被中间人修改

因为如String这种类只能通过BootStrap ClassLoader加载,而不能通过自定义的ClassLoader加载

通过也提高了加载效率,避免重复加载(当父亲已经加载了该类的时候,ClassLoader就不会再加载一次)

只有两个class的类名相同且由同一加载器加载时才会认为这两个class相等

显/隐式加载

隐式加载:程序在运行过程中碰到new 一个类的实例时,会先检查此类是否已经加载到内存中,如果没有,则会隐式调用类装载器加载对应的类到JVM中。

显式加载:Java也提供了一些接口用于手动加载类,即通过class.forname()this.getClass.getClassLoader().loadClass()等方法显式加载需要的类,或者我们自己实现的 ClassLoaderfindlass() 方法。

Class.forname

默认会初始化类方法

Class.forName(String name,boolean initialize, ClassLoader loader)

name表示类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。

Class.forName(String className),相当于Class.forName(String className,true, this.getClass().getClassLoader())

this.getClass.getClassLoader().loadClass()

this.getClass().getClassLoader().loadClass(String classname)

默认不初始化类方法

自定义ClassLoader

来自园长,加载远程jar来命令执行

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
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

/**
* Creator: yz
* Date: 2019/12/18
*/
public class TestURLClassLoader {

public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("https://javaweb.org/tools/cmd.jar");

// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});

// 定义需要执行的系统命令
String cmd = "ls";

// 通过URLClassLoader加载远程jar包中的CMD类
Class cmdClass = ucl.loadClass("CMD");

// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

// 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;

// 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}

// 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}

}

cmd.jar里就只有CMD.class文件

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.io.IOException;

/**
* Creator: yz
* Date: 2019/12/18
*/
public class CMD {

public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}

}

泛型

这应该算Java基础???可能因为之前学得不熟,还是写一下

参考:java 泛型详解

先看一个简单的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.ArrayList;
import java.util.List;

public class Main{
public static void main(String[] args){
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);

for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
System.out.println("item = " + item);
}
}
}

上面代码将字符串和整数存储进了List,而在输出的时候统一与字符串拼接进行了输出,故输出报错

1
class java.lang.Integer cannot be cast to class java.lang.String

而为了解决这类问题,泛型应运而生

泛型类

基本写法

1
2
3
4
5
6
class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{
private 泛型标识 /*(成员变量类型)*/ var;
.....

}
}

例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class test <T>{
private T var;
public test(T var){
this.var = var;
}

public T getVar(){
return this.var;
}
}


public class Main{
public static void main(String[] args){
test<Integer> genericInteger = new test<Integer>(123456);

test<String> genericString = new test<String>("key_vlaue");
System.out.println("key is " + ((test<?>) genericInteger).getVar());
System.out.println("key is " + ((test<?>) genericString).getVar());
}
}

泛型接口

泛型接口和泛型类的定义基本一致

定义

1
2
3
4
//定义一个泛型接口
public interface Generator<T> {
public T next();
}

当实现泛型接口的类而未传入泛型实参时,声明类需将泛型的声明也一起加到类中

1
2
3
4
5
6
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}

当实现泛型接口的类,传入泛型实参时,所有使用泛型的地方都要替换成传入的实参类型,即:Generator<T>,public T next();中的的T都要替换成传入的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 传入泛型实参时:
* 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T>
* 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。
*/
public class FruitGenerator implements Generator<String> {
private String[] fruits = new String[]{"Apple", "Banana", "Pear"};

@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}

泛型通配符

1
2
3
public void showKeyValue1(Generic<?> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

注意:此处的?不是形参,而是实参,可以看作NumberIntegerString

通配符可以解决具体类型不确定的情况

泛型方法

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型,只有public与返回值中间存在如<T>的这种方法才能称为泛型方法,其余方法最多只能叫做使用了泛型的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 泛型方法的基本介绍
* @param tClass 传入的泛型实参
* @return T 返回值为T类型
* 说明:
* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public <T> T genericMethod(Class<T> tClass)throws InstantiationException,IllegalAccessException{
T instance = tClass.newInstance();
return instance;
}

注意与泛型类中的方法进行区分

eg:

1
2
3
4
5
6
public <T> T showKeyName(Generic<T> container){
System.out.println("container key :" + container.getKey());
//当然这个例子举的不太合适,只是为了说明泛型方法的特性。
T test = container.getKey();
return test;
}

泛型方法与可变参数

再看一个泛型方法和可变参数的例子:

1
2
3
4
5
6
public <T> void printMsg( T... args){
for(T t : args){
System.out.println("t is " + t);
}
}
printMsg("111",222,"aaaa","2323.4",55.55);

泛型上下边界

在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

1
2
3
public void showKeyValue1(Generic<? extends Number> obj){
Log.d("泛型测试","key value is " + obj.getKey());
}

如上面代码要求传入参数只能是Number类的子类

泛型数组

在java中是”不能创建一个确切的泛型类型的数组”的,如下

1
2
3
List<String>[] ls = new ArrayList<String>[10];  //报错
List<String>[] ls = new ArrayList[10]; //正确
List<?>[] ls = new ArrayList<?>[10]; //正确

序列化和反序列化

之前总结过,见这篇 传送门

反射

之前总结过,见这篇 传送门

Java代理

Java代理是一种程序的设计模式,它提供了一个代理对象,我们可以通过代理对象访问目标对象进行相关操作而不去直接修改原对象

通过代理对象我们可以扩展对目标对象的访问功能,简化访问方式

代理又分为了静态和动态两种代理方式

  • 静态代理:即在程序运行前就已经确定了代理类与目标对象的关系,存在一个实质性的字节码文件
  • 动态代理:根据运行时的情况确定代理情况,动态生成字节码(通过反射、泛型等),然后加载到JVM

静态代理

静态代理一般需要代理对象和目标对象实现一样的接口

缺点:

  • 因为其在运行前就已经确定了代理类和对象,所以一旦增加功能就只能修改源代码,不易优化维护
  • 需要代理多个类时,如果只设置一个代理类,则该代理类过于庞大。如果设置多个代理类,则就产生了过多的代理类

优点:

  • 运行前以及存在编译好的class文件,运行速度快

实例:返回用户名的静态代理

接口

1
2
3
4
public interface IUsername {
public void setName(String name);
public void getName();
}

目标类

1
2
3
4
5
6
7
8
9
10
11
12
13
class Username implements IUsername{
private String name;

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

@Override
public void getName() {
System.out.println(this.name);
}
}

代理类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class UsernameProxy implements IUsername{
private IUsername target;
public UsernameProxy(IUsername target) {
this.target = target;
}

@Override
public void setName(String name) {
target.setName(name);
}

@Override
public void getName() {
target.getName();
}
}

运行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Main{
public static void main(String[] args){
IUsername target = new Username();
//代理对象
UsernameProxy proxy = new UsernameProxy(target);
proxy.setName("test");
proxy.getName();
}
}

/*result
test
*/

JDK原生动态代理

JDK原生动态代理对象不需要实现接口,但目标对象需要实现接口

JDK原生动态代理用到了JDK原生的API(主要是java.lang.reflect.Proxyjava.lang.reflect.InvocationHandler),在Java程序运行过程中动态地生成代理对象。

优点

  • 减少代理类的数量,更易维护优化

缺点

  • 比较消耗系统性能

代理类Proxy的使用

创建代理对象

1
newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) 
  • loader:类加载器
  • interfaces:泛型类数组,目标对象实现的接口的类型
  • h:调用处理器(目标对象)
1
static InvocationHandler getInvocationHandler(Object proxy)

获取代理对象所关联的调用处理器

1
static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

返回指定接口的代理类

1
tatic Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法

1
static boolean isProxyClass(Class<?> cl)

返回 cl 是否为一个代理类

代理类InvocationHandler的使用

1
Object invoke(Object proxy, Method method, Object[] args)

定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用

JDK原生动态代理实例

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
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;


//利用InvocationHandler创建处理器
class TraceHandler implements InvocationHandler {
private Object target;

public TraceHandler(Object target) {
this.target = target;
}


//自动打印出代理对象调用的方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.print(target);
System.out.print("." + method.getName() + "(");

if (args != null) {
for (int i = 0; i < args.length; i++) {
System.out.print(args[i]);
if (i<args.length - 1){
System.out.print(", ");
}
}
}

System.out.println(")");

return method.invoke(target, args);
}
}


public class Main {
public static void main(String[] agrs) {
Object[] elements = new Object[1000];

for (int i = 0; i < elements.length; i++) {
Integer value = i + 1;
//创建处理器对象
InvocationHandler handler = new TraceHandler(value);

//动态创建代理对象
Object proxy = Proxy.newProxyInstance(null, new Class[]{Comparable.class}, handler);
elements[i] = proxy;
}

Integer key = new Random().nextInt(elements.length) + 1;

//二分法查找元素
int result = Arrays.binarySearch(elements, key);

if (result > 0) {
System.out.println(elements[result]);
}
}
}

cglib动态代理

cglib是一个第三方代码库,运行时会在内存动态生成一个子类对象从而实现对目标对象功能的扩展。

特点

  • JDK的动态代理有一个限制,就是使用动态代理的对象必须实现一个或多个接口。
    如果想代理没有实现接口的类,就可以使用CGLIB实现。
  • CGLIB是一个强大的高性能的代码生成包,它可以在运行期扩展Java类与实现Java接口。
    它广泛的被许多AOP的框架使用,例如Spring AOP和dynaop,为他们提供方法的interception(拦截)。
  • CGLIB包的底层是通过使用一个小而快的字节码处理框架ASM,来转换字节码并生成新的类。
    不鼓励直接使用ASM,因为它需要你对JVM内部结构包括class文件的格式和指令集都很熟悉。

简而言之,JDK动态代理是实现接口的方式,cglib则是继承类的方式

优点

  • 可以实现无接口侵入式动态代理
  • 通过生成类字节码实现代理,比反射稍快

缺点

  • 目标对象不能为final类,因为cglib会继承目标对象,需要重写方法。

可以通过maven安装cglib

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>


<groupId>org.example</groupId>
<artifactId>项目名称</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>
</dependencies>

</project>

示例

自己没写了,搬的这里

  • 目标对象:UserDao
1
2
3
4
5
6
7
8
package com.cglib;

public class UserDao{

public void save() {
System.out.println("保存数据");
}
}
  • 代理对象:ProxyFactory
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
package com.cglib;

import java.lang.reflect.Method;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

public class ProxyFactory implements MethodInterceptor{

private Object target;//维护一个目标对象
public ProxyFactory(Object target) {
this.target = target;
}

//为目标对象生成代理对象
public Object getProxyInstance() {
//工具类
Enhancer en = new Enhancer();
//设置父类
en.setSuperclass(target.getClass());
//设置回调函数
en.setCallback(this);
//创建子类对象代理
return en.create();
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("开启事务");
// 执行目标对象的方法
Object returnValue = method.invoke(target, args);
System.out.println("关闭事务");
return null;
}
}
  • 测试类:TestProxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.cglib;

import org.junit.Test;

public class TestProxy {

@Test
public void testCglibProxy(){
//目标对象
UserDao target = new UserDao();
System.out.println(target.getClass());
//代理对象
UserDao proxy = (UserDao) new ProxyFactory(target).getProxyInstance();
System.out.println(proxy.getClass());
//执行代理对象方法
proxy.save();
}
}
  • 输出结果
1
2
3
4
5
class com.cglib.UserDao
class com.cglib.UserDao$$EnhancerByCGLIB$$552188b6
开启事务
保存数据
关闭事务

参考

https://laijianfeng.org/2018/12/Java-%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86%E8%AF%A6%E8%A7%A3/

https://segmentfault.com/a/1190000011291179

评论