点击上方 好好学java ,选择 星标 公众号

重磅资讯、干货,第一时间送达
今日推荐:干掉 Navicat:这个 IDEA 的兄弟真香!个人原创100W+访问量博客:点击前往,查看更多

作者:马佩

juejin.im/post/5bf4fc84f265da611b57f906

概述:

最近在开发中遇到了一个刚好可以用AOP实现的例子,就顺便研究了AOP的实现原理,把学习到的东西进行一个总结。文章中用到的编程语言为kotlin,需要的可以在IDEA中直接转为java。

这篇文章将会按照如下目录展开:

  • AOP简介

  • 代码中实现举例

  • AOP实现原理

  • 部分源码解析

1. AOP简介

附上我历时三个月总结的 Java 面试 + Java 后端技术学习指南,笔者这几年及春招的总结,github 1.1k star,拿去不谢!

下载方式

1. 首先扫描下方二维码

2. 后台回复「Java面试」即可获取

相信大家或多或少的了解过AOP,都知道它是面向切面编程,在网上搜索可以找到很多的解释。这里我用一句话来总结:AOP是能够让我们在不影响原有功能的前提下,为软件横向扩展功能。

那么横向扩展怎么理解呢,我们在WEB项目开发中,通常都遵守三层原则,包括控制层(Controller)->业务层(Service)->数据层(dao),那么从这个结构下来的为纵向,它具体的某一层就是我们所说的横向。我们的AOP就是可以作用于这某一个横向模块当中的所有方法。

我们在来看一下AOP和OOP的区别:AOP是OOP的补充,当我们需要为多个对象引入一个公共行为,比如日志,操作记录等,就需要在每个对象中引用公共行为,这样程序就产生了大量的重复代码,使用AOP可以完美解决这个问题。

接下来介绍一下提到AOP就必须要了解的知识点:

切面:拦截器类,其中会定义切点以及通知

切点:具体拦截的某个业务点。

通知:切面当中的方法,声明通知方法在目标业务层的执行位置,通知类型如下:

  • 前置通知:@Before 在目标业务方法执行之前执行

  • 后置通知:@After 在目标业务方法执行之后执行

  • 返回通知:@AfterReturning 在目标业务方法返回结果之后执行

  • 异常通知:@AfterThrowing 在目标业务方法抛出异常之后

  • 环绕通知:@Around 功能强大,可代替以上四种通知,还可以控制目标业务方法是否执行以及何时执行

2. 代码中实现举例

上面已经大概的介绍了AOP中需要了解的基本知识,也知道了AOP的好处,那怎么在代码中实现呢?给大家举个例子:我们现在有个学校管理系统,已经实现了对老师和学生的增删改,又新来个需求,说是对老师和学生的每次增删改做一个记录,到时候校长可以查看记录的列表。

那么问题来了,怎么样处理是最好的解决办法呢?这里我罗列了三种解决办法,我们来看下他的优缺点。

最简单的就是第一种方法,我们直接在每次的增删改的函数当中直接实现这个记录的方法,这样代码的重复度太高,耦合性太强,不建议使用。

其次就是我们最长使用的,将记录这个方法抽离出来,其他的增删改调用这个记录函数即可,显然代码重复度降低,但是这样的调用还是没有降低耦合性。

这个时候我们想一下AOP的定义,再想想我们的场景,其实我们就是要在不改变原来增删改的方法,给这个系统增加记录的方法,而且作用的也是一个层面的方法。这个时候我们就可以采用AOP来实现了。

我们来看下代码的具体实现:

1,首先我定义了一个自定义注解作为切点

@Target(AnnotationTarget.FUNCTION)
//注解作用的范围,这里声明为函数
@Order(Ordered.HIGHEST_PRECEDENCE)
//声明注解的优先级为最高,假设有多个注解,先执行这个
annotation class Hanler(val handler: HandlerType)
//自定义注解类,HandlerType是一个枚举类型,里面定义的就是学生和老师的增删改操作,在这里就不展示具体内容了

2,接下来就是要定义切面类了

@Aspect   //该注解声明这个类为一个切面类
@Component
class HandlerAspect{@Autowiredprivate lateinit var handlerService: HandlerService@AfterReturning("@annotation(handler)")   //当有函数注释了注解,将会在函数正常返回后在执行我们定义的方法
fun hanler(hanler: Hanler) {handlerService.add(handler.operate.value)   //这里是真正执行记录的方法
}
}

3,最后就是我们本来的业务方法了

/**
* 删除学生方法
*/
@Handler(operate= Handler.STUDENT_DELETE)   //当执行到删除学生方法时,切面类就会起作用了,当学生正常删除后就会执行记录方法,我们就可以看到记录方法生成的数据
fun delete(id:String) {studentService.delete(id)
}

3. AOP实现原理

我们现在了解了代码中如何实现,那么AOP实现的原理是什么呢?之前看了一个博客说到,提到AOP大家都知道他的实现原理是动态代理,显然我之前就是不知道的,哈哈,但是相信阅读文章的你们一定是知道的。

讲到动态代理就不得不说代理模式了,代理模式的定义:给某一个对象提供一个代理,并由代理对象控制对原对象的引用。

代理模式包含如下角色:

  • subject:抽象主题角色,是一个接口。该接口是对象和它的代理共用的接口;

  • RealSubject:真实主题角色,是实现抽象主题接口的类;

  • Proxy:代理角色,内部含有对真实对象RealSubject的引用,从而可以操作真实对象。

代理对象提供与真实对象相同的接口,以便代替真实对象。同时,代理对象可以在执行真实对象操作时,附加其他的操作,相当于对真实对象进行封装。如下图所示:

那么代理又分为静态代理和动态代理,这里写两个小的demo,动态代理采用的就是JDK代理。举个例子就是现在一个班上的学生需要交作业,现在由班长代理交作业,那么班长就是代理,学生就是被代理的对象。

3.1 静态代理

首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有交作业的行为。这样,学生交作业就可以让班长来代理执行。

/*** 创建person接口*/
public interface Person {//交作业void giveTask();
}

Student类实现Person接口,Student可以具体实施交作业这个行为。

public class Student implements Person {private String name;public Student(String name) {this.name = name;}public void giveTask() {System.out.println(name + "交语文作业");}
}

StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象,那么他可以代理学生类对象执行交作业的行为。

/*** 学生代理类,也实现了Person接口,保存一个学生实体,这样就可以代理学生产生行为*/
public class StudentsProxy implements Person{//被代理的学生Student stu;public StudentsProxy(Person stu) {// 只代理学生对象if(stu.getClass() == Student.class) {this.stu = (Student)stu;}}//代理交作业,调用被代理学生的交作业的行为public void giveTask() {stu.giveTask();}
}

下面测试一下,看代理模式如何使用:

public class StaticProxyTest {public static void main(String[] args) {//被代理的学生林浅,他的作业上交有代理对象monitor完成Person linqian = new Student("林浅");//生成代理对象,并将林浅传给代理对象Person monitor = new StudentsProxy(linqian);//班长代理交作业monitor.giveTask();}
}

运行结果:

这里并没有直接通过林浅(被代理对象)来执行交作业的行为,而是通过班长(代理对象)来代理执行了。这就是代理模式。

代理模式就是在访问实际对象时引入一定程度的间接性,这里的间接性就是指不直接调用实际对象的方法,那么我们在代理过程中就可以加上一些其他用途。

比如班长在帮林浅交作业的时候想告诉老师最近林浅的进步很大,就可以轻松的通过代理模式办到。在代理类的交作业之前加入方法即可。这个优点就可以运用在spring中的AOP,我们能在一个切点之前执行一些操作,在一个切点之后执行一些操作,这个切点就是一个个方法。这些方法所在类肯定就是被代理了,在代理过程中切入了一些其他操作。

3.2 动态代理

动态代理和静态代理的区别是,静态代理的的代理类是我们自己定义好的,在程序运行之前就已经变异完成,但是动态代理的代理类是在程序运行时创建的。

相比于静态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。比如我们想在每个代理方法之前都加一个处理方法,我们上面的例子中只有一个代理方法,如果还有很多的代理方法,就太麻烦了,我们来看下动态代理是怎么去实现的。

首先还是定义一个Person接口:

/*** 创建person接口*/
public interface Person {//交作业void giveTask();
}

接下来是创建需要被代理的实际类,也就是学生类:

public class Student implements Person {private String name;public Student(String name) {this.name = name;}public void giveTask() {System.out.println(name + "交语文作业");}
}

创建StuInvocationHandler类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。

public class StuInvocationHandler<T> implements InvocationHandler {//invocationHandler持有的被代理对象T target;public StuInvocationHandler(T target) {this.target = target;}/*** proxy:代表动态代理对象* method:代表正在执行的方法* args:代表调用目标方法时传入的实参*/public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("代理执行" +method.getName() + "方法");Object result = method.invoke(target, args);return result;}
}

那么接下来我们就可以具体的创建代理对象了。

/*** 代理类*/
public class ProxyTest {public static void main(String[] args) {//创建一个实例对象,这个对象是被代理的对象Person linqian = new Student("林浅");//创建一个与代理对象相关联的InvocationHandlerInvocationHandler stuHandler = new StuInvocationHandler<Person>(linqian);//创建一个代理对象stuProxy来代理linqian,代理对象的每个执行方法都会替换执行Invocation中的invoke方法Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);//代理执行交作业的方法stuProxy.giveTask();}
}

我们执行代理测试类,首先我们创建了一个需要被代理的学生林浅,将林浅传入stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数,那么所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。所以在看到下面的运行结果也就理所当然了。

那么到这里问题就来了,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行,带着这个问题,我们需要看一下动态代理的源码,对他进行简单的分析。更多:动态代理解析

上面我们使用Proxy类的newProxyInstance方法创建了一个动态代理对象,看一下他的源码:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)throws IllegalArgumentException{Objects.requireNonNull(h);final Class<?>[] intfs = interfaces.clone();final SecurityManager sm = System.getSecurityManager();if (sm != null) {checkProxyAccess(Reflection.getCallerClass(), loader, intfs);}/** Look up or generate the designated proxy class.*/Class<?> cl = getProxyClass0(loader, intfs);/** Invoke its constructor with the designated invocation handler.*/try {if (sm != null) {checkNewProxyPermission(Reflection.getCallerClass(), cl);}final Constructor<?> cons = cl.getConstructor(constructorParams);final InvocationHandler ih = h;if (!Modifier.isPublic(cl.getModifiers())) {AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {cons.setAccessible(true);return null;}});}return cons.newInstance(new Object[]{h});} catch (IllegalAccessException|InstantiationException e) {throw new InternalError(e.toString(), e);} catch (InvocationTargetException e) {Throwable t = e.getCause();if (t instanceof RuntimeException) {throw (RuntimeException) t;} else {throw new InternalError(t.toString(), t);}} catch (NoSuchMethodException e) {throw new InternalError(e.toString(), e);}}

然后,我们需要重点关注Class<?> cl = getProxyClass0(loader, intfs)这句代码,这里产生了代理类,这个类就是动态代理的关键,由于是动态生成的类文件,我们将这个类文件打印到文件中。

       byte[] classFile = ProxyGenerator.generateProxyClass("$Proxy0", Student.class.getInterfaces());String path = "/Users/mapei/Desktop/okay/65707.class";try{FileOutputStream fos = new FileOutputStream(path);fos.write(classFile);fos.flush();System.out.println("代理类class文件写入成功");}catch (Exception e) {System.out.println("写文件错误");}

对这个class文件进行反编译,我们看看jdk为我们生成了什么样的内容:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import proxy.Person;public final class $Proxy0 extends Proxy implements Person
{private static Method m1;private static Method m2;private static Method m3;private static Method m0;/***注意这里是生成代理类的构造方法,方法参数为InvocationHandler类型,看到这,是不是就有点明白*为何代理对象调用方法都是执行InvocationHandler中的invoke方法,而InvocationHandler又持有一个*被代理对象的实例,就可以去调用真正的对象实例。*/public $Proxy0(InvocationHandler paramInvocationHandler)throws {super(paramInvocationHandler);}//这个静态块本来是在最后的,我把它拿到前面来,方便描述static{try{//看看这儿静态块儿里面的住giveTask通过反射得到的名字m3,其他的先不管m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);m3 = Class.forName("proxy.Person").getMethod("giveTask", new Class[0]);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);return;}catch (NoSuchMethodException localNoSuchMethodException){throw new NoSuchMethodError(localNoSuchMethodException.getMessage());}catch (ClassNotFoundException localClassNotFoundException){throw new NoClassDefFoundError(localClassNotFoundException.getMessage());}}/*** *这里调用代理对象的giveMoney方法,直接就调用了InvocationHandler中的invoke方法,并把m3传了进去。*this.h.invoke(this, m3, null);我们可以对将InvocationHandler看做一个中介类,中介类持有一个被代理对象,在invoke方法中调用了被代理对象的相应方法。通过聚合方式持有被代理对象的引用,把外部对invoke的调用最终都转为对被代理对象的调用。*/public final void giveTask()throws {try{this.h.invoke(this, m3, null);return;}catch (Error|RuntimeException localError){throw localError;}catch (Throwable localThrowable){throw new UndeclaredThrowableException(localThrowable);}}}

看完了动态代理的源码,我们接下来就要看一下Spring中AOP实现的源码是怎样的?

4. 部分源码解析

aop创建代理的源码分析

1,看一下bean如何被包装为proxy

           protected Object createProxy(Class<?> beanClass, String beanName, Object[] specificInterceptors, TargetSource targetSource) {if (this.beanFactory instanceof ConfigurableListableBeanFactory) {AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);}// 1.创建proxyFactory,proxy的生产主要就是在proxyFactory做的ProxyFactory proxyFactory = new ProxyFactory();proxyFactory.copyFrom(this);if (!proxyFactory.isProxyTargetClass()) {if (shouldProxyTargetClass(beanClass, beanName)) {proxyFactory.setProxyTargetClass(true);}else {evaluateProxyInterfaces(beanClass, proxyFactory);}}// 2.将当前bean适合的advice,重新封装下,封装为Advisor类,然后添加到ProxyFactory中Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);for (Advisor advisor : advisors) {proxyFactory.addAdvisor(advisor);}proxyFactory.setTargetSource(targetSource);customizeProxyFactory(proxyFactory);proxyFactory.setFrozen(this.freezeProxy);if (advisorsPreFiltered()) {proxyFactory.setPreFiltered(true);}// 3.调用getProxy获取bean对应的proxyreturn proxyFactory.getProxy(getProxyClassLoader());}

2,创建何种类型的Proxy?JDKProxy还是CGLIBProxy?

    public Object getProxy(ClassLoader classLoader) {return createAopProxy().getProxy(classLoader);}// createAopProxy()方法就是决定究竟创建何种类型的proxyprotected final synchronized AopProxy createAopProxy() {if (!this.active) {activate();}// 关键方法createAopProxy()return getAopProxyFactory().createAopProxy(this);}// createAopProxy()public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {// 1.config.isOptimize()是否使用优化的代理策略,目前使用与CGLIB// config.isProxyTargetClass() 是否目标类本身被代理而不是目标类的接口// hasNoUserSuppliedProxyInterfaces()是否存在代理接口if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {Class<?> targetClass = config.getTargetClass();if (targetClass == null) {throw new AopConfigException("TargetSource cannot determine target class: " +"Either an interface or a target is required for proxy creation.");}// 2.如果目标类是接口类(目标对象实现了接口),则直接使用JDKproxyif (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {return new JdkDynamicAopProxy(config);}// 3.其他情况则使用CGLIBproxyreturn new ObjenesisCglibAopProxy(config);}else {return new JdkDynamicAopProxy(config);}}

3,getProxy()方法

   final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable// JdkDynamicAopProxy类结构,由此可知,其实现了InvocationHandler,则必定有invoke方法,来被调用,也就是用户调用bean相关方法时,此invoke()被真正调用// getProxy()public Object getProxy(ClassLoader classLoader) {if (logger.isDebugEnabled()) {logger.debug("Creating JDK dynamic proxy: target source is " + this.advised.getTargetSource());}Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);// JDK proxy 动态代理的标准用法return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);}

4,invoke()方法法

    //使用了JDK动态代理模式,真正的方法执行在invoke()方法里,看到这里在想一下上面动态代理的例子,是不是就完全明白Spring源码实现动态代理的原理了。public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {MethodInvocation invocation;Object oldProxy = null;boolean setProxyContext = false;TargetSource targetSource = this.advised.targetSource;Class<?> targetClass = null;Object target = null;try {// 1.以下的几个判断,主要是为了判断method是否为equals、hashCode等Object的方法if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {// The target does not implement the equals(Object) method itself.return equals(args[0]);}else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {// The target does not implement the hashCode() method itself.return hashCode();}else if (method.getDeclaringClass() == DecoratingProxy.class) {// There is only getDecoratedClass() declared -> dispatch to proxy config.return AopProxyUtils.ultimateTargetClass(this.advised);}else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&method.getDeclaringClass().isAssignableFrom(Advised.class)) {// Service invocations on ProxyConfig with the proxy config...return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);}Object retVal;if (this.advised.exposeProxy) {// Make invocation available if necessary.oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}// May be null. Get as late as possible to minimize the time we "own" the target,// in case it comes from a pool.target = targetSource.getTarget();if (target != null) {targetClass = target.getClass();}// 2.获取当前bean被拦截方法链表List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);// 3.如果为空,则直接调用target的methodif (chain.isEmpty()) {Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);}// 4.不为空,则逐一调用chain中的每一个拦截方法的proceed,这里的一系列执行的原因以及proceed执行的内容,我 在这里就不详细讲了,大家感兴趣可以自己去研读哈else {// We need to create a method invocation...invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);// Proceed to the joinpoint through the interceptor chain.retVal = invocation.proceed();}...return retVal;}}}

那么到了这里,我要讲的内容就差不多结束了,如果有什么不对的,或者有什么疑惑,欢迎大家指点

最后,再附上我历时三个月总结的 Java 面试 + Java 后端技术学习指南,笔者这几年及春招的总结,github 1.1k star,拿去不谢!

下载方式

1. 首先扫描下方二维码

2. 后台回复「Java面试」即可获取

Java:由浅入深揭开 AOP 实现原理相关推荐

  1. aop实现原理_Java:由浅入深揭开 AOP 实现原理

    点击上方"Java专栏",选择"置顶或者星标" 第一时间阅读精彩文章! 1.☞ 程序员进阶必备资源免费送「21种技术方向!」 点击查看☜ 2.☞ <Jav ...

  2. aop实现原理_从宏观的实现原理和设计本质入手,带你理解 AOP 框架的原理

    点击上方"Java知音",选择"置顶公众号" 技术文章第一时间送达! 作者:FeelsChaotic juejin.im/post/5c57b2d5e51d45 ...

  3. Spring AOP 实现原理与 CGLIB 应用--转

    AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理.安全检查.缓存.对象池管理等.AOP 实现的关键就在于 ...

  4. Spring AOP 实现原理与 CGLIB 应用

    WeiboGoogle+用电子邮件发送本页面 10 AOP(Aspect Orient Programming),作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如事务管理.安 ...

  5. Spring 学习二-----AOP的原理与简单实践

    一.Spring  AOP的原理 AOP全名Aspect-Oriented Programming,中文直译为面向切面(方面)编程.何为切面,就比如说我们系统中的权限管理,日志,事务等我们都可以将其看 ...

  6. 一行代码揭开CPU执行原理

    参考:一行代码,揭开CPU执行原理! 作者:嵌入式ARM 网址:https://mp.weixin.qq.com/s/qFnKhWqBGRCFAnp_KC1dmw 目录 1.高级语言 2.编译链接 3 ...

  7. [Spring5]AOP底层原理

    AOP底层原理 1.AOP底层使用动态代理 (1)有两种情况动态代理 第一种 有接口的情况,使用JDK动态代理 a.创建接口实现类代理对象,增强类的方法 第二种 没有接口的情况,使用CGLIB动态代理 ...

  8. 深入源码分析Java线程池的实现原理

    转载自   深入源码分析Java线程池的实现原理 程序的运行,其本质上,是对系统资源(CPU.内存.磁盘.网络等等)的使用.如何高效的使用这些资源是我们编程优化演进的一个方向.今天说的线程池就是一种对 ...

  9. Spring AOP实现原理,从代理说起

    前言 为了理解Spring AOP,我们先来了解一下Java的代理模式 什么是代理? 举个例子来说明代理的作用: 假设我们想邀请一位明星,那么并不是直接联系明星,而是联系明星的经纪人,来达到同样的目的 ...

最新文章

  1. Design Pattern - Proxy(C#)
  2. C语言中函数的参数列表为空和void的区别
  3. java中timer和timertask_使用Java中的Timer和TimerTask
  4. python怎么写接口自动化_python接口自动化 小结
  5. javascript中 __proto__与prorotype的理解
  6. rocketmq 顺序消费_RocketMQ核心概念扫盲
  7. JEPLUS之特殊字段类型的使用——JEPLUS软件快速开发平台
  8. MyBatis sql查询字段问题
  9. 安装 Dynamics AX 2012 Data Migration Framework
  10. 婚纱摄影、影楼、照相馆流量制造工具预约系统之种草社区
  11. 90个面试问题(带答案)
  12. matlab音乐信号处理,数字信号处理课程设计---基于 MATLAB 的音乐信号处理和分析...
  13. 2000字精华总结,安利一个超好用的 Python 数据分析神器
  14. 关于csdn里面如何设置字体和背景的问题
  15. scp:Mac使用方法(文件上传与下载服务器)
  16. 什么是Type-c口?Type-c口有什么优势?
  17. 北风网课程开放下载第一季
  18. suma++[代碼分析一]: 主入口visualizer.cpp
  19. HTML+CSS制作DNA双螺旋结构
  20. IDEA的好用小工具Test RESTful web Service

热门文章

  1. 标题栏外区域拖动窗体
  2. VC学习笔记 -单选按钮控件(Ridio Button)的使用
  3. ili9341屏幕在断电一段时间后首次上电白屏问题
  4. 初等数论--同余--WILSON定理
  5. python—sql语句参数化
  6. 多级cache之间的替换(缓存)策略
  7. [ARM异常]-ARM体系中是否支持中断嵌套
  8. [HOW TO]-官网拉取Trusty-TEE的代码
  9. [gic]-ARM gicv2和gicv3的中断模型总结
  10. KPROCESS 结构体属性介绍