写在前面的话

相关背景及资源:

曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享

曹工说Spring Boot源码(2)-- Bean Definition到底是什么,咱们对着接口,逐个方法讲解

曹工说Spring Boot源码(3)-- 手动注册Bean Definition不比游戏好玩吗,我们来试一下

曹工说Spring Boot源码(4)-- 我是怎么自定义ApplicationContext,从json文件读取bean definition的?

曹工说Spring Boot源码(5)-- 怎么从properties文件读取bean

曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的

曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)

曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中得到了什么(util命名空间)

曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

曹工说Spring Boot源码(11)-- context:component-scan,你真的会用吗(这次来说说它的奇技淫巧)

曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

曹工说Spring Boot源码(13)-- AspectJ的运行时织入(Load-Time-Weaving),基本内容是讲清楚了(附源码)

曹工说Spring Boot源码(14)-- AspectJ的Load-Time-Weaving的两种实现方式细细讲解,以及怎么和Spring Instrumentation集成

曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

曹工说Spring Boot源码(17)-- Spring从xml文件里到底得到了什么(aop:config完整解析【中】)

曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

曹工说Spring Boot源码(19)-- Spring 带给我们的工具利器,创建代理不用愁(ProxyFactory)

曹工说Spring Boot源码(20)-- 码网恢恢,疏而不漏,如何记录Spring RedisTemplate每次操作日志

曹工说Spring Boot源码(21)-- 为了让大家理解Spring Aop利器ProxyFactory,我已经拼了

曹工说Spring Boot源码(22)-- 你说我Spring Aop依赖AspectJ,我依赖它什么了

曹工说Spring Boot源码(23)-- ASM又立功了,Spring原来是这么递归获取注解的元注解的

曹工说Spring Boot源码(24)-- Spring注解扫描的瑞士军刀,asm技术实战(上)

工程代码地址 思维导图地址

工程结构图:

概要

上一篇,我们讲了ASM基本的使用方法,具体包括:复制一个class、修改class版本号、增加一个field、去掉一个field/method等等;同时,我们也知道了怎么才能生成一个全新的class。

但是,仅凭这点粗浅的知识,我们依然不太理解能干嘛,本篇会带大家实现简单的AOP功能,当然了,学完了之后,可能你像我一样,更困惑了,那说明你变强了。

本篇的核心是,在JVM加载class的时候,去修改class,修改class的时候,加入我们的aop逻辑。JVM加载class的时候,去修改class,这项技术就是load-time-weaver,实现load-time-weaver有两种方式,这两种方式,核心差别在于修改class的时机不同。

  • 第一种,定制classloader,在把字节码交给JVM去defineClass之前,去织入切面逻辑
  • 第二种,利用Java 官方提供的instrumentation机制,注册一个类转换器到 JVM。JVM在加载class的时候,就会传入class的原始字节码数组,回调我们的类转换器,我们类转换器中可以修改原始字节码,并将修改后的字节码数组返回回去,JVM就会用我们修改后的字节码去defineClass了

在直接开始前,声明本篇文章,是基于下面这篇文章中的代码demo,我自己稍做了修改,并附上源码(原文是贴了代码,但是没有直接提供代码地址,不贴心啊)。

初探 Java agent

要达成的目标

目标就是给下面的测试类,加上一点点切面功能。

package org.xunche.app;
public class HelloXunChe {
public static void main(String[] args) throws InterruptedException {
HelloXunChe helloXunChe = new HelloXunChe();
helloXunChe.sayHi();
}
public void sayHi() throws InterruptedException {
System.out.println("hi, xunche");
sleep();
}
public void sleep() throws InterruptedException {
Thread.sleep((long) (Math.random() * 200));
}
}

我们希望,class在执行的时候,能够打印方法执行的耗时,也就是,最终的class,需要是下面这样的。

package org.xunche.app;
import org.xunche.agent.TimeHolder;
public class HelloXunChe {
public HelloXunChe() {
}
public static void main(String[] args) throws InterruptedException {
TimeHolder.start(args.getClass().getName() + "." + "main");
// 业务逻辑开始
HelloXunChe helloXunChe = new HelloXunChe();
helloXunChe.sayHi();
//业务逻辑结束
HelloXunChe helloXunChe = args.getClass().getName() + "." + "main";
System.out.println(helloXunChe + ": " + TimeHolder.cost(helloXunChe));
}
public void sayHi() throws InterruptedException {
TimeHolder.start(this.getClass().getName() + "." + "sayHi");
System.out.println("hi, xunche");
// 业务逻辑开始
this.sleep();
//业务逻辑结束
String var1 = this.getClass().getName() + "." + "sayHi";
System.out.println(var1 + ": " + TimeHolder.cost(var1));
}
public void sleep() throws InterruptedException {
TimeHolder.start(this.getClass().getName() + "." + "sleep");
// 业务逻辑开始
Thread.sleep((long)(Math.random() * 200.0D));
//业务逻辑结束
String var1 = this.getClass().getName() + "." + "sleep";
System.out.println(var1 + ": " + TimeHolder.cost(var1));
}
}

所以,我们大概就是,要做下面的这样一个切面:

@Override
protected void onMethodEnter() {
//在方法入口处植入
String className = getClass().getName();
String s = className + "." + methodName;
TimeHolder.start(s);
}
@Override
protected void onMethodExit(int i) {
//在方法出口植入
String className = getClass().getName();
String s = className + "." + methodName;
long cost = TimeHolder.cost(s);
System.out.println(s + ": " + cost);
}

但是,习惯了动态代理的我们,看上面的代码可能会有点误解。上面的代码,不是在执行目标方法前,调用切面;而是:直接把切面代码嵌入了目标方法。

想必大家都明确了要达成的目标了,下面说,怎么做。

java agent/instrumentation机制

这部分,大家可以结合开头那个链接一起学习。

首先,我请大家看看java命令行的选项。直接在cmd里敲java,出现如下:

看了和没看一样,那我们再看一张图,在大家破解某些java编写的软件时,可能会涉及到jar包破解,比如:

大家可以使用jad这类反编译软件,打开jar包看下,看看里面是啥:

可以发现,里面有一个MANIFEST.MF文件,里面指定了Premain-Class这个key-value,从这个名字,大家可能知道了,我们平时运行java程序,都是运行main方法,这里来个premain,那这意思,就是在main方法前面插个队呗?

你说的没有错,确实是插队了,拿上面的破解jar包举例,里面的Premain-Class方法,对应的Agent类,反编译后的代码如下:

核心代码就是图里那一行:

java.lang.instrument.Instrumentation
public interface Instrumentation {
/**
* Registers the supplied transformer. All future class definitions
* will be seen by the transformer, except definitions of classes upon which any
* registered transformer is dependent.
* The transformer is called when classes are loaded, when they are
* {@linkplain #redefineClasses redefined}. and if <code>canRetransform</code> is true,
* when they are {@linkplain #retransformClasses retransformed}.
* See {@link java.lang.instrument.ClassFileTransformer#transform
* ClassFileTransformer.transform} for the order
* of transform calls.
* If a transformer throws
* an exception during execution, the JVM will still call the other registered
* transformers in order. The same transformer may be added more than once,
* but it is strongly discouraged -- avoid this by creating a new instance of
* transformer class.
* <P>
* This method is intended for use in instrumentation, as described in the
* {@linkplain Instrumentation class specification}.
*
* @param transformer the transformer to register
* @param canRetransform can this transformer's transformations be retransformed
* @throws java.lang.NullPointerException if passed a <code>null</code> transformer
* @throws java.lang.UnsupportedOperationException if <code>canRetransform</code>
* is true and the current configuration of the JVM does not allow
* retransformation ({@link #isRetransformClassesSupported} is false)
* @since 1.6
*/
void
addTransformer(ClassFileTransformer transformer, boolean canRetransform); ...
}

这个类,就是官方jdk提供的类,官方的本意呢,肯定是让大家,在加载class的时候,给大家提供一个机会,去修改class,比如,某个第三方jar包,我们需要修改,但是没有源码,就可以这么干;或者是一些要统一处理,不方便在应用中耦合的功能:比如埋点、性能监控、日志记录、安全监测等。

说回这个方法,参数为ClassFileTransformer,这个接口,就一个方法,大家看看注释:

	/**
* ...
*
* @param classfileBuffer the input byte buffer in class file format - must not be modified
*
* @throws IllegalClassFormatException if the input does not represent a well-formed class file
* @return a well-formed class file buffer (the result of the transform),
or <code>null</code> if no transform is performed.
* @see Instrumentation#redefineClasses
*/
byte[]
transform( ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException;
  • classfileBuffer,就是原始class字节码数组,官方注释说:一定不能修改它
  • 返回的byte[]数组,注释:一个格式正确的class文件数组,或者null,表示没有进行转换

别的也不多说了,反正就是:jvm给你原始class,你自己修改,还jvm一个改后的class。

所以,大家估计也能猜到破解的原理了,但我还是希望大家:有能力支持正版的话,还是要支持。

接下来,我们回到我们的目标的实现上。

agent模块开发

完整代码:https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo

增加类转换器

package org.xunche.agent;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter; import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain; public class TimeAgentByJava {
public static void premain(String args, Instrumentation instrumentation) {
instrumentation.addTransformer(new TimeClassFileTransformer());
}
}

类转换器的详细代码如下:

private static class TimeClassFileTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
if (className.startsWith("java") || className.startsWith("jdk") || className.startsWith("javax") || className.startsWith("sun") || className.startsWith("com/sun")|| className.startsWith("org/xunche/agent")) {
//return null或者执行异常会执行原来的字节码
return null;
}
// 1
System.out.println("loaded class: " + className);
ClassReader reader = new ClassReader(classfileBuffer);
// 2
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
// 3
reader.accept(new TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES);
// 4
return writer.toByteArray();
}
}
  • 1处,将原始的类字节码加载到classReader中

    ClassReader reader = new ClassReader(classfileBuffer);

  • 2处,将reader传给ClassWriter,这个我们没讲过,大概就是使用classreader中的东西,来构造ClassWriter;可以差不多理解为复制classreader的东西到ClassWriter中。

    大家可以看如下代码:

    public ClassWriter(final ClassReader classReader, final int flags) {
    super(Opcodes.ASM6);
    symbolTable = new SymbolTable(this, classReader);
    ...
    }

    这里new了一个对象,SymbolTable。

    SymbolTable(final ClassWriter classWriter, final ClassReader classReader) {
    this.classWriter = classWriter;
    this.sourceClassReader = classReader; // Copy the constant pool binary content.
    byte[] inputBytes = classReader.b;
    int constantPoolOffset = classReader.getItem(1) - 1;
    int constantPoolLength = classReader.header - constantPoolOffset;
    constantPoolCount = classReader.getItemCount();
    constantPool = new ByteVector(constantPoolLength);
    constantPool.putByteArray(inputBytes, constantPoolOffset, constantPoolLength);
    ...
    }

    大家直接看上面的注释吧,Copy the constant pool binary content。反正吧,基本可以理解为,classwriter拷贝了classreader中的一部分东西,应该不是全部。

    为什么不是全部,因为我试了下:

        public static void main(String[] args) throws IOException {
    ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe");
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
    byte[] bytes = writer.toByteArray(); File file = new File(
    "F:\\gitee-ckl\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunChe.class");
    FileOutputStream fos = new FileOutputStream(file);
    fos.write(bytes);
    fos.close();
    }

    上面这样,出来的class文件,是破损的,格式不正确的,无法反编译。

  • 3处,使用TimeClassVisitor作为writer的中间商,此时,顺序变成了:

    classreader --> TimeClassVisitor --> classWriter

  • 4处,返回writer的字节码,给jvm;jvm使用该字节码,去redefine一个class出来

类转换器的具体实现

public static class TimeClassVisitor extends ClassVisitor {
public TimeClassVisitor(ClassVisitor classVisitor) {
super(Opcodes.ASM6, classVisitor);
}
// 1
@Override
public MethodVisitor visitMethod(int methodAccess, String methodName, String methodDesc, String signature, String[] exceptions) {
MethodVisitor methodVisitor = cv.visitMethod(methodAccess, methodName, methodDesc, signature, exceptions);
// 2
return new TimeAdviceAdapter(Opcodes.ASM6, methodVisitor, methodAccess, methodName, methodDesc);
}
}
}
  • 1处,visitMethod方法,会返回一个MethodVisitor,ASM会拿着我们返回的methodVisitor,去访问当前这个方法
  • 2处,new了一个适配器,TimeAdviceAdapter。

我们这里的TimeAdviceAdapter,主要是希望在方法执行前后做点事,类似于切面,所以继承了一个AdviceAdapter,这个AdviceAdaper,帮我们实现了MethodVisitor的全部方法,我们只需要覆写我们想要覆盖的方法即可。

比如,AdviceAdaper,因为继承了MethodVisitor,其visitCode方法,会在访问方法体时被回调:

@Override
public void visitCode() {
super.visitCode();
// 1
onMethodEnter();
}
//2
protected void onMethodEnter() {}
  • 1处,回调本来的onMethodEnter,是一个空实现,就是留给子类去重写的。
  • 2处,可以看到,空实现。

所以,我们最终的TimeAdviceAdaper,代码如下:

public static class TimeAdviceAdapter extends AdviceAdapter {
private String methodName;
protected TimeAdviceAdapter(int api, MethodVisitor methodVisitor, int methodAccess, String methodName, String methodDesc) {
super(api, methodVisitor, methodAccess, methodName, methodDesc);
this.methodName = methodName;
}
@Override
protected void onMethodEnter() {
//在方法入口处植入
if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) {
return;
}
String className = getClass().getName();
String s = className + "." + methodName;
TimeHolder.start(s);
}
@Override
protected void onMethodExit(int i) {
//在方法出口植入
if ("<init>".equals(methodName) || "<clinit>".equals(methodName)) {
return;
}
String className = getClass().getName();
String s = className + "." + methodName;
long cost = TimeHolder.cost(s);
System.out.println(s + ": " + cost);
}
}

这份代码看着可还行?可惜啊,是假的,是错误的!写asm这么简单的话,那我要从梦里笑醒。

为啥是假的,因为:真正的代码,是长下面这样的:

看到这里,是不是想溜了,这都啥玩意,看不懂啊,不过不要着急,办法总比困难多。

类转换器的真正实现方法

我们先装个idea插件,叫:asm-bytecode-outline。这个插件的作用,简而言之,就是帮你把java代码翻译成ASM的写法。在线装不了的,可以离线装:

asm-bytecode-outline

装好插件后,只要在我们的TimeAdviceAdapter类,点右键:

就会生成我们需要的ASM代码,然后拷贝:

什么时候拷贝结束呢?

基本上,这样就可以了。

填坑指南

作为一个常年掉坑的人,我在这个坑里也摸爬了整整一天。

大家可以看到,我们的java写的方法里,是这样的:

        @Override
protected void onMethodEnter() {
//在方法入口处植入
if ("<init>".equals(methodName)|| "<clinit>".equals(methodName)) {
return;
}
String className = getClass().getName();
// 1.
String s = className + "." + methodName;
TimeHolder.start(s);
}
  • 1处,访问了本地field,methodName

所以,asm也帮我们贴心地生成了这样的语句:

mv.visitFieldInsn(Opcodes.GETFIELD, "org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter", "methodName", "Ljava/lang/String;");

看起来就像是说,访问org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter类的methodName字段。

但是,这是有问题的。因为,这段代码,最终aop切面会被插入到target:

public class HelloXunChe {
private String methodName = "abc";
public static void main(String[] args) throws InterruptedException {
HelloXunChe helloXunChe = new HelloXunChe();
helloXunChe.sayHi();
}
public void sayHi() throws InterruptedException {
System.out.println("hi, xunche");
sleep();
} public void sleep() throws InterruptedException {
Thread.sleep((long) (Math.random() * 200));
} }

我实话跟你说,这个target类里,压根访问不到org/xunche/agent/TimeAgentByJava$TimeAdviceAdapter类的methodName字段。

我是怎么发现这个问题的,之前一直报错,直到我在target后来加了这么一行:

public class HelloXunChe {
private String methodName = "abc";
...
}

哎,没个大佬带我,真的难。

当然,我是通过这个确认了上述问题,最终解决的思路呢,就是:把你生成的class,反编译出来看看,看看是不是你想要的。

所以,我专门写了个main测试类,来测试改后的class是否符合预期。

public class SaveGeneratedClassWithOriginAgentTest {

    public static void main(String[] args) throws IOException {
//1
ClassReader reader = new ClassReader("org.xunche.app.HelloXunChe");
ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
reader.accept(new TimeAgentByJava.TimeClassVisitor(writer), ClassReader.EXPAND_FRAMES);
byte[] bytes = writer.toByteArray(); // 2
File file = new File(
"F:\\ownprojects\\all-simple-demo-in-work\\java-agent-premain-demo\\test-agent\\src\\main\\java\\org\\xunche\\app\\HelloXunCheCopy2.class");
FileOutputStream fos = new FileOutputStream(file);
fos.write(bytes);
fos.close();
}
}
  • 1处这段代码,就是模拟在classTransformer中的那段。
  • 2处,将最终要返回给jvm的那段class字节码,写到一个文件里,然后我们就可以反编译,看看有问题没。

所以,上面那段asm,大家如果看:

初探 Java agent

会发现,访问methodname那句代码,是这么写的:

mv.visitLdcInsn(methodName);

这就是,相当于直接把methodName写死到最终的class里去了;最终的class就会是想要的样子:

public void sayHi() throws InterruptedException {
//1
TimeHolder.start(this.getClass().getName() + "." + "sayHi");
System.out.println("hi, xunche");
this.sleep();
// 2
String var1 = this.getClass().getName() + "." + "sayHi";
System.out.println(var1 + ": " + TimeHolder.cost(var1));
}
  • 1/2处,直接把sayHi写死到target了,而不是此时再去访问field。

maven插件配置premain-class

插件中,配置Premain-Class

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
</manifest>
<manifestEntries>
<Premain-Class>
org.xunche.agent.TimeAgent
</Premain-Class>
</manifestEntries>
</archive>
</configuration>
</plugin>

测试模块开发

测试模块,没啥开发的,就只有那个target那个类。

运行

最终我是这么运行的:

java -javaagent:agent.jar -classpath lib/*;java-agent-premain-demo.jar org/xunche/app/He
lloXunChe

这里指定了lib目录,主要是agent模块需要的jar包:

简单的运行效果如下:

loaded class: org/xunche/app/HelloXunChe
methodName = 0 <init>
methodName = 0 main
methodName = 0 sayHi
methodName = 0 sleep
hi, xunche
org.xunche.app.HelloXunChe.abc: 129
org.xunche.app.HelloXunChe.abc: 129

总结

ASM这个东西,想要不熟悉字节码就去像我上面这样傻瓜操作,坑还是比较多的,比较难趟。回头有空再介绍字节码吧。我也是半桶水,大家一起学习吧。

本节源码:

https://gitee.com/ckl111/all-simple-demo-in-work/tree/master/java-agent-premain-demo

曹工说Spring Boot源码(25)-- Spring注解扫描的瑞士军刀,ASM + Java Instrumentation,顺便提提Jar包破解的更多相关文章

  1. 精尽Spring Boot源码分析 - @ConfigurationProperties 注解的实现

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  2. 精尽Spring Boot源码分析 - 文章导读

    该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Sprin ...

  3. 曹工说Spring Boot源码系列开讲了(1)-- Bean Definition到底是什么,附spring思维导图分享

    写在前面的话&&About me 网上写spring的文章多如牛毛,为什么还要写呢,因为,很简单,那是人家写的:网上都鼓励你不要造轮子,为什么你还要造呢,因为,那不是你造的. 我不是要 ...

  4. 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎

    曹工说Spring Boot源码(26)-- 学习字节码也太难了,实在不能忍受了,写了个小小的字节码执行引擎 写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean De ...

  6. 曹工说Spring Boot源码(27)-- Spring的component-scan,光是include-filter属性的各种配置方式,就够玩半天了.md

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(28)-- Spring的component-scan机制,让你自己来进行简单实现,怎么办

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(30)-- ConfigurationClassPostProcessor 实在太硬核了,为了了解它,我可能debug了快一天

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. javaWeb简单登录实现验证数据库

    用户登录案例需求: 1.编写login.html登录页面 username & password 两个输入框 2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表 ...

  2. numpy的基础计算2

    import numpy as np A = np.arange(14,2,-1).reshape((3,4)) #平均值 print(np.mean(A)) print(A.mean()) prin ...

  3. Contour等高线图代码

    import matplotlib.pyplot as plt import numpy as np def f(x,y): # the height function return (1 - x / ...

  4. JAVA学习大纲

    1.第一节 JAVA概念与JDK的安装 (1)JDK的安装和环境变量的设置: (2)相关基本概念:JDK.SDK.JRE.JVM.J2SE.J2EE.J2ME.java API.JAVA语言特点: ( ...

  5. mongodb 4.0配置认证模块

    use admin db.createUser({user:"root",pwd:"xxx",roles:[{role:"root",db: ...

  6. Redis: userd_memory使用超出maxmemory

    Redis:userd_memory使用超出maxmemory 一.问题现象 2018.12.30 19:26分,收到Redis实例内存使用告警“内存使用率299%>=80%”,检查实例info ...

  7. iPhone5se难逃“酱油”命运?

    苹果春季新品发布会即将举行,按照惯例,只会有一些不痛不痒的产品出现,最起码,不会有革命性的爆点,今年大抵相似,最大的亮点莫过于,苹果有可能会推出一款名叫"iPhone5se"的手机 ...

  8. CPU踩点图

    CPU占比探测用js来检查当前系统cpu的占用比例,通过 setTimeout 的方式探测 CPU 的大小,这样可以实现网页游戏中动画等耗时操作的自动调节.这个原理是很多人都知道的,就是用JS来踩点. ...

  9. 代工黑马,纬创如何强吞iPhone?

    ​ 现在,智能手机市场非常得意兴阑珊,以苹果为首的最强大脑似乎再也想不出什么好的创意,iPhone7也只不过是旧机种的翻新款式,看上去跟一块板砖.一块镜子差不多:软体方面则出现了大批的"过度 ...

  10. 《深入理解 Java 虚拟机》读书笔记:类文件结构

    正文 一.无关性的基石 1.两种无关性 平台无关性: Java 程序的运行不受计算机平台的限制,"一次编写,到处运行". 语言无关性: Java 虚拟机只与 Class 文件关联, ...