总结
三个工作日收到了offer,头条面试体验还是很棒的,这次的头条面试好像每面技术都问了我算法,然后就是中间件、MySQL、Redis、Kafka、网络等等。
- 第一个是算法
关于算法,我觉得最好的是刷题,作死的刷的,多做多练习,加上自己的理解,还是比较容易拿下的。
而且,我貌似是将《算法刷题LeetCode中文版》、《算法的乐趣》大概都过了一遍,尤其是这本
《算法刷题LeetCode中文版》总共有15个章节:编程技巧、线性表、字符串、栈和队列、树、排序、查找、暴力枚举法、广度优先搜索、深度优先搜索、分治法、贪心法、动态规划、图、细节实现题
《算法的乐趣》共有23个章节:
- 第二个是Redis、MySQL、kafka(给大家看下我都有哪些复习笔记)
基本上都是面试真题解析、笔记和学习大纲图,感觉复习也就需要这些吧(个人意见)
- 第三个是网络(给大家看一本我之前得到的《JAVA核心知识整理》包括30个章节分类,这本283页的JAVA核心知识整理还是很不错的,一次性总结了30个分享的大知识点)
true
com.cn.agent.MyPreMainAgent
true
true
true
配置的打包参数中,通过manifestEntries的方式添加属性到MANIFEST.MF文件中,解释一下里面的几个参数:
Premain-Class:包含premain方法的类,需要配置为类的全路径
-
Can-Redefine-Classes:为true时表示能够重新定义class
-
Can-Retransform-Classes:为true时表示能够重新转换class,实现字节码替换
-
Can-Set-Native-Method-Prefix:为true时表示能够设置native方法的前缀
其中Premain-Class为必须配置,其余几项是非必须选项,默认情况下都为false,通常也建议加入,这几个功能我们会在后面具体介绍。在配置完成后,使用mvn命令打包:
mvn clean package
打包完成后生成myAgent-1.0.jar文件,我们可以解压jar文件,看一下生成的MANIFEST.MF文件:
可以看到,添加的属性已经被加入到了文件中。到这里,agent代理部分就完成了,因为代理不能够直接运行,需要附着于其他程序,所以下面新建一个工程来实现主程序。
主程序
在主程序的工程中,只需要一个能够执行的main方法的入口就可以了。
public class AgentTest {
public static void main(String[] args) {
System.out.println(“main project start”);
}
}
在主程序完成后,要考虑的就是应该如何将主程序与agent工程连接起来。这里可以通过-javaagent参数来指定运行的代理,命令格式如下:
java -javaagent:myAgent.jar -jar AgentTest.jar
并且,可以指定的代理的数量是没有限制的,会根据指定的顺序先后依次执行各个代理,如果要同时运行两个代理,就可以按照下面的命令执行:
java -javaagent:myAgent1.jar -javaagent:myAgent2.jar -jar AgentTest.jar
以我们在idea中执行程序为例,在VM options中加入添加启动参数:
-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Hydra
-javaagent:F:\Workspace\MyAgent\target\myAgent-1.0.jar=Trunks
执行main方法,查看输出结果:
根据执行结果的打印语句可以看出,在执行主程序前,依次执行了两次我们的agent代理。可以通过下面的图来表示执行代理与主程序的执行顺序。
缺陷
在提供便利的同时,premain模式也有一些缺陷,例如如果agent在运行过程中出现异常,那么也会导致主程序的启动失败。我们对上面例子中agent的代码进行一下改造,手动抛出一个异常。
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println(“premain start”);
System.out.println(“args:”+agentArgs);
throw new RuntimeException(“error”);
}
再次运行主程序:
可以看到,在agent抛出异常后主程序也没有启动。针对premain模式的一些缺陷,在jdk1.6之后引入了agentmain模式。
2、Agentmain模式
agentmain模式可以说是premain的升级版本,它允许代理的目标主程序的jvm先行启动,再通过attach机制连接两个jvm,下面我们分3个部分实现。
agent
agent部分和上面一样,实现简单的打印功能:
public class MyAgentMain {
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println(“agent main start”);
System.out.println(“args:”+agentArgs);
}
}
修改maven插件配置,指定Agent-Class:
org.apache.maven.plugins
maven-jar-plugin
3.1.0
true
com.cn.agent.MyAgentMain
true
true
主程序
这里我们直接启动主程序等待代理被载入,在主程序中使用了System.in进行阻塞,防止主进程提前结束。
public class AgentmainTest {
public static void main(String[] args) throws IOException {
System.in.read();
}
}
attach机制
和premain模式不同,我们不能再通过添加启动参数的方式来连接agent和主程序了,这里需要借助com.sun.tools.attach包下的VirtualMachine工具类,需要注意该类不是jvm标准规范,是由Sun公司自己实现的,使用前需要引入依赖:
com.sun
tools
1.8
system
${JAVA_HOME}\lib\tools.jar
VirtualMachine代表了一个要被附着的java虚拟机,也就是程序中需要监控的目标虚拟机,外部进程可以使用VirtualMachine的实例将agent加载到目标虚拟机中。先看一下它的静态方法attach:
public static VirtualMachine attach(String var0);
通过attach方法可以获取一个jvm的对象实例,这里传入的参数是目标虚拟机运行时的进程号pid。也就是说,我们在使用attach前,需要先获取刚才启动的主程序的pid,使用jps命令查看线程pid:
11140
16372 RemoteMavenServer36
16392 AgentmainTest
20204 Jps
2460 Launcher
获取到主程序AgentmainTest运行时pid是16392,将它应用于虚拟机的连接。
public class AttachTest {
public static void main(String[] args) {
try {
VirtualMachine vm= VirtualMachine.attach(“16392”);
vm.loadAgent(“F:\Workspace\MyAgent\target\myAgent-1.0.jar”,“param”);
} catch (Exception e) {
e.printStackTrace();
}
}
}
在获取到VirtualMachine实例后,就可以通过loadAgent方法可以实现注入agent代理类的操作,方法的第一个参数是代理的本地路径,第二个参数是传给代理的参数。执行AttachTest,再回到主程序AgentmainTest的控制台,可以看到执行了了agent中的代码:
这样,一个简单的agentMain模式代理就实现完成了,可以通过下面这张图再梳理一下三个模块之间的关系。
3、应用
到这里,我们就已经简单地了解了两种模式的实现方法,但是作为高质量程序员,我们肯定不能满足于只用代理单纯地打印语句,下面我们再来看看能怎么利用Java Agent搞点实用的东西。
在上面的两种模式中,agent部分的逻辑分别是在premain方法和agentmain方法中实现的,并且,这两个方法在签名上对参数有严格的要求,premain方法允许以下面两种方式定义:
public static void premain(String agentArgs)
public static void premain(String agentArgs, Instrumentation inst)
agentmain方法允许以下面两种方式定义:
public static void agentmain(String agentArgs)
public static void agentmain(String agentArgs, Instrumentation inst)
如果在agent中同时存在两种签名的方法,带有Instrumentation参数的方法优先级更高,会被jvm优先加载,它的实例inst会由jvm自动注入,下面我们就看看能通过Instrumentation实现什么功能。
Instrumentation
先大体介绍一下Instrumentation接口,其中的方法允许在运行时操作java程序,提供了诸如改变字节码,新增jar包,替换class等功能,而通过这些功能使Java具有了更强的动态控制和解释能力。在我们编写agent代理的过程中,Instrumentation中下面3个方法比较重要和常用,我们来着重看一下。
addTransformer
addTransformer方法允许我们在类加载之前,重新定义Class,先看一下方法的定义:
void addTransformer(ClassFileTransformer transformer);
ClassFileTransformer是一个接口,只有一个transform方法,它在主程序的main方法执行前,装载的每个类都要经过transform执行一次,可以将它称为转换器。我们可以实现这个方法来重新定义Class,下面就通过一个例子看看具体如何使用。
首先,在主程序工程创建一个Fruit类:
public class Fruit {
public void getFruit(){
System.out.println(“banana”);
}
}
编译完成后复制一份class文件,并将其重命名为Fruit2.class,再修改Fruit中的方法为:
public void getFruit(){
System.out.println(“apple”);
}
创建主程序,在主程序中创建了一个Fruit对象并调用了其getFruit方法:
public class TransformMain {
public static void main(String[] args) {
new Fruit().getFruit();
}
}
这时执行结果会打印apple,接下来开始实现premain代理部分。
在代理的premain方法中,使用Instrumentation的addTransformer方法拦截类的加载:
public class TransformAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new FruitTransformer());
}
}
FruitTransformer类实现了ClassFileTransformer接口,转换class部分的逻辑都在transform方法中:
public class FruitTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer){
if (!className.equals(“com/cn/hydra/test/Fruit”))
return classfileBuffer;
String fileName=“F:\Workspace\agent-test\target\classes\com\cn\hydra\test\Fruit2.class”;
return getClassBytes(fileName);
}
public static byte[] getClassBytes(String fileName){
File file = new File(fileName);
try(InputStream is = new FileInputStream(file);
ByteArrayOutputStream bs = new ByteArrayOutputStream()){
long length = file.length();
byte[] bytes = new byte[(int) length];
int n;
while ((n = is.read(bytes)) != -1) {
bs.write(bytes, 0, n);
}
return bytes;
}catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
在transform方法中,主要做了两件事:
因为addTransformer方法不能指明需要转换的类,所以需要通过className判断当前加载的class是否我们要拦截的目标class,对于非目标class直接返回原字节数组,注意className的格式,需要将类全限定名中的.替换为/
- 读取我们之前复制出来的class文件,读入二进制字符流,替换原有classfileBuffer字节数组并返回,完成class定义的替换
将agent部分打包完成后,在主程序添加启动参数:
-javaagent:F:\Workspace\MyAgent\target\transformAgent-1.0.jar
再次执行主程序,结果打印:
banana
这样,就实现了在main方法执行前class的替换。
redefineClasses
我们可以直观地从方法的名字上来理解它的作用,重定义class,通俗点来讲的话就是实现指定类的替换。方法定义如下:
void redefineClasses(ClassDefinition… definitions) throws ClassNotFoundException, UnmodifiableClassException;
它的参数是可变长的ClassDefinition数组,再看一下ClassDefinition的构造方法:
public ClassDefinition(Class<?> theClass,byte[] theClassFile) {…}
ClassDefinition中指定了的Class对象和修改后的字节码数组,简单来说,就是使用提供的类文件字节,替换了原有的类。并且,在redefineClasses方法重定义的过程中,传入的是ClassDefinition的数组,它会按照这个数组顺序进行加载,以便满足在类之间相互依赖的情况下进行更改。
下面通过一个例子来看一下它的生效过程,premain代理部分:
public class RedefineAgent {
public static void premain(String agentArgs, Instrumentation inst)
throws UnmodifiableClassException, ClassNotFoundException {
String fileName=“F:\Workspace\agent-test\target\classes\com\cn\hydra\test\Fruit2.class”;
ClassDefinition def=new ClassDefinition(Fruit.class,
FruitTransformer.getClassBytes(fileName));
inst.redefineClasses(new ClassDefinition[]{def});
}
}
主程序可以直接复用上面的,执行后打印:
总结
机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
nition(Fruit.class,
FruitTransformer.getClassBytes(fileName));
inst.redefineClasses(new ClassDefinition[]{def});
}
}
主程序可以直接复用上面的,执行后打印:
总结
机会是留给有准备的人,大家在求职之前应该要明确自己的态度,熟悉求职流程,做好充分的准备,把一些可预见的事情做好。
对于应届毕业生来说,校招更适合你们,因为绝大部分都不会有工作经验,企业也不会有工作经验的需求。同时,你也不需要伪造高大上的实战经验,以此让自己的简历能够脱颖而出,反倒会让面试官有所怀疑。
你在大学时期应该明确自己的发展方向,如果你在大一就确定你以后想成为Java工程师,那就不要花太多的时间去学习其他的技术语言,高数之类的,不如好好想着如何夯实Java基础。下图涵盖了应届生乃至转行过来的小白要学习的Java内容:
请转发本文支持一下
[外链图片转存中…(img-RgGFdXo0-1715594730258)]
[外链图片转存中…(img-QZtseusF-1715594730258)]