一、前言

这一篇我们来说一下 Spring 中的 AOP 机制,为啥说完注解的原理然后又要说 AOP 机制呢?

1、标记日志打印的自定义注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PrintLog {
}

2、定义一个切面,在切面中对使用了 @PrintLog 自定义注解的方法进行环绕增强通知

@Component
@Aspect
@Slf4j
public class PrintLogAspect {@Around(value = "@annotation(com.riemann.core.annotation.PrintLog)")public Object handlerPrintLog(ProceedingJoinPoint joinPoint) throws Throwable {String clazzName = joinPoint.getSignature().getDeclaringTypeName();String methodName = joinPoint.getSignature().getName();Object[] args = joinPoint.getArgs();Map<String, Object> nameAndArgs = getFieldsName(this.getClass(), clazzName, methodName, args);log.info("Enter class[{}] method[{}] params[{}]", clazzName, methodName, nameAndArgs);Object object = null;try {object = joinPoint.proceed();} catch (Throwable throwable) {log.error("Process class[{}] method[{}] error", clazzName, methodName, throwable);}log.info("End class[{}] method[{}]", clazzName, methodName);return object;}private Map<String, Object> getFieldsName(Class clazz, String clazzName, String methodName, Object[] args) throws NotFoundException {Map<String, Object > map = new HashMap<>();ClassPool pool = ClassPool.getDefault();ClassClassPath classPath = new ClassClassPath(clazz);pool.insertClassPath(classPath);CtClass cc = pool.get(clazzName);CtMethod cm = cc.getDeclaredMethod(methodName);MethodInfo methodInfo = cm.getMethodInfo();CodeAttribute codeAttribute = methodInfo.getCodeAttribute();LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);if (attr == null) {// exception}int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;for (int i = 0; i < cm.getParameterTypes().length; i++) {map.put( attr.variableName(i + pos), args[i]);}return map;}
}

3、最后,在 Controller 中的方法上使用 @PrintLog 自定义注解即可;当某个方法上使用了自定义注解,那么这个方法就相当于一个切点,那么就会对这个方法做环绕(方法执行前和方法执行后)增强处理。

@RestController
public class Controller {@PrintLog@GetMapping(value = "/user/findUserNameById/{id}", produces = "application/json;charset=utf-8")public String findUserNameById(@PathVariable("id") int id) {// 模拟根据id查询用户名String userName = "公众号【老周聊架构】";return userName;}
}

了解完自定义注解的底层机制以后,我们来想一下,为啥在 Controller 类里的方法上添加 @PrintLog 就可以做到在这个方法前后打印相应的日志呢?这就是我们熟悉的 AOP 底层帮我们做完了这个事情。很多人知道是 AOP 完成的,但对于怎么完成的很多人还是不是很清楚。本文就来分析分析它背后的机制。

我们来看下 PrintLogAspect 这个增强类,它有 @Component、@Aspect、@Around 几个核心注解。

下面我们就可以来想一想,这几个核心注解是如何被 Spring 读取的,Spring 又是如何应用这些注解生成代理类,又是如何让它起到增强的作用。

二、Spring中的AOP机制

这里用一张时序图说一下整体机制

spring 在整个 Bean 初始化完成后,会执行后置处理器方法,调用各个 BeanPostProcessor,在各个 BeanPostProcessor 里,有一个 AnnotationAwareAspectJAutoProxyCreator,spring 会在该类的 postProcessBeforeInitialization 里进行 Advisor 的初始化。

可以这样理解,spring 在创建一个类之前,会看下有没有配置 AOP,如果有的话,会把配置给转换成一个个 advisor,然后缓存起来(这样后面需要生成代理类时候,就可以直接使用了)。

findCandidateAdvisors 的方式有两种,一种是上图第 7 步的 findAdvisorBeans 还有一种是第 8 步的 buildAspectJAdvisors。

1、findAdvisorBeans 方式

public List<Advisor> findAdvisorBeans() {String[] advisorNames = this.cachedAdvisorBeanNames;if (advisorNames == null) {advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Advisor.class, true, false);this.cachedAdvisorBeanNames = advisorNames;}if (advisorNames.length == 0) {return new ArrayList();} else {List<Advisor> advisors = new ArrayList();String[] var3 = advisorNames;int var4 = advisorNames.length;for(int var5 = 0; var5 < var4; ++var5) {String name = var3[var5];if (this.isEligibleBean(name)) {if (this.beanFactory.isCurrentlyInCreation(name)) {if (logger.isTraceEnabled()) {logger.trace("Skipping currently created advisor '" + name + "'");}} else {try {// 核心方法advisors.add(this.beanFactory.getBean(name, Advisor.class));} catch (BeanCreationException var11) {Throwable rootCause = var11.getMostSpecificCause();if (rootCause instanceof BeanCurrentlyInCreationException) {BeanCreationException bce = (BeanCreationException)rootCause;String bceBeanName = bce.getBeanName();if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) {if (logger.isTraceEnabled()) {logger.trace("Skipping advisor '" + name + "' with dependency on currently created bean: " + var11.getMessage());}continue;}}throw var11;}}}}return advisors;}
}

其实去掉那些读缓存的代码,就一句话:

advisors.add(this.beanFactory.getBean(name, Advisor.class));

找到实现了 Advisor 接口的类,并返回。

2、buildAspectJAdvisors 方式

public List<Advisor> buildAspectJAdvisors() {List<String> aspectNames = this.aspectBeanNames;if (aspectNames == null) {synchronized(this) {aspectNames = this.aspectBeanNames;if (aspectNames == null) {List<Advisor> advisors = new ArrayList();List<String> aspectNames = new ArrayList();// 找到所有的类(因为是Object所以基本上就是所有被spring管理的类)String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, Object.class, true, false);String[] var18 = beanNames;int var19 = beanNames.length;for(int var7 = 0; var7 < var19; ++var7) {String beanName = var18[var7];// 是否是Aspect(比如含有@Aspect注解)if (this.isEligibleBean(beanName)) {Class<?> beanType = this.beanFactory.getType(beanName);if (beanType != null && this.advisorFactory.isAspect(beanType)) {aspectNames.add(beanName);AspectMetadata amd = new AspectMetadata(beanType, beanName);if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);// 生成AdvisorList<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);if (this.beanFactory.isSingleton(beanName)) {this.advisorsCache.put(beanName, classAdvisors);} else {this.aspectFactoryCache.put(beanName, factory);}advisors.addAll(classAdvisors);} else {if (this.beanFactory.isSingleton(beanName)) {throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton");}MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName);this.aspectFactoryCache.put(beanName, factory);advisors.addAll(this.advisorFactory.getAdvisors(factory));}}}}this.aspectBeanNames = aspectNames;return advisors;}}}if (aspectNames.isEmpty()) {return Collections.emptyList();} else {List<Advisor> advisors = new ArrayList();Iterator var3 = aspectNames.iterator();while(var3.hasNext()) {String aspectName = (String)var3.next();List<Advisor> cachedAdvisors = (List)this.advisorsCache.get(aspectName);if (cachedAdvisors != null) {advisors.addAll(cachedAdvisors);} else {MetadataAwareAspectInstanceFactory factory = (MetadataAwareAspectInstanceFactory)this.aspectFactoryCache.get(aspectName);advisors.addAll(this.advisorFactory.getAdvisors(factory));}}return advisors;}
}

我们的 PrintLogAspect 例子里,并没有实现任何接口,只是使用了一个 @Aspect 注解。因此使用 buildAspectJAdvisors 方式,spring 会通过我们的 AspectJ 注解(比如@Around、@Pointcut、@Before、@After) 动态的生成各个 Advisor。

小结如下:

  • 找到所有被 spring 管理的类(父类是 Object 的类)

  • 如果类含有 @Aspect 注解,调用 advisorFactory.getAdvisors 方法生成对应的 advisor

  • 返回advisors

我们继续来看下最核心的,Advisor 的创建。

3、Advisor 的创建

org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvisors
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();this.validate(aspectClass);MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);List<Advisor> advisors = new ArrayList();Iterator var6 = this.getAdvisorMethods(aspectClass).iterator();// 遍历所有没有 @Pointcut 注解的方法while(var6.hasNext()) {Method method = (Method)var6.next();Advisor advisor = this.getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);if (advisor != null) {advisors.add(advisor);}}if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {Advisor instantiationAdvisor = new ReflectiveAspectJAdvisorFactory.SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);advisors.add(0, instantiationAdvisor);}Field[] var12 = aspectClass.getDeclaredFields();int var13 = var12.length;for(int var14 = 0; var14 < var13; ++var14) {Field field = var12[var14];Advisor advisor = this.getDeclareParentsAdvisor(field);if (advisor != null) {advisors.add(advisor);}}return advisors;
}

最核心的,就是遍历所有没有 Pointcut 注解的方法,调用 getAdvisor 生成对应的 Advisor。

@Nullable
public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) {this.validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());AspectJExpressionPointcut expressionPointcut = this.getPointcut(candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());return expressionPointcut == null ? null : new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}


也就是,生成的 Advisor 的实现类,其实是 InstantiationModelAwarePointcutAdvisorImpl。

4、不同类型的通知

使用过 AOP 的都知道,不同的注解,比如 @Before、@After、@Around 都是不一样的。InstantiationModelAwarePointcutAdvisorImpl 这个类,实际上,是对底层 Advisor 的包装,它记录了所对应 @AspectJ 的类、配置的方法、对应的切入点、以及最重要的通知,这个通知会在 InstantiationModelAwarePointcutAdvisorImpl 的构造函数中被初始化。

private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName);return advice != null ? advice : EMPTY_ADVICE;
}

org.springframework.aop.aspectj.annotation.ReflectiveAspectJAdvisorFactory#getAdvice

public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();this.validate(candidateAspectClass);AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);if (aspectJAnnotation == null) {return null;} else if (!this.isAspect(candidateAspectClass)) {throw new AopConfigException("Advice must be declared inside an aspect type: Offending method '" + candidateAdviceMethod + "' in class [" + candidateAspectClass.getName() + "]");} else {if (this.logger.isDebugEnabled()) {this.logger.debug("Found AspectJ method: " + candidateAdviceMethod);}Object springAdvice;switch(aspectJAnnotation.getAnnotationType()) {case AtPointcut:if (this.logger.isDebugEnabled()) {this.logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");}return null;case AtAround:springAdvice = new AspectJAroundAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;case AtBefore:springAdvice = new AspectJMethodBeforeAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;case AtAfter:springAdvice = new AspectJAfterAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);break;case AtAfterReturning:springAdvice = new AspectJAfterReturningAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);AfterReturning afterReturningAnnotation = (AfterReturning)aspectJAnnotation.getAnnotation();if (StringUtils.hasText(afterReturningAnnotation.returning())) {((AbstractAspectJAdvice)springAdvice).setReturningName(afterReturningAnnotation.returning());}break;case AtAfterThrowing:springAdvice = new AspectJAfterThrowingAdvice(candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);AfterThrowing afterThrowingAnnotation = (AfterThrowing)aspectJAnnotation.getAnnotation();if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {((AbstractAspectJAdvice)springAdvice).setThrowingName(afterThrowingAnnotation.throwing());}break;default:throw new UnsupportedOperationException("Unsupported advice type on method: " + candidateAdviceMethod);}((AbstractAspectJAdvice)springAdvice).setAspectName(aspectName);((AbstractAspectJAdvice)springAdvice).setDeclarationOrder(declarationOrder);String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);if (argNames != null) {((AbstractAspectJAdvice)springAdvice).setArgumentNamesFromStringArray(argNames);}((AbstractAspectJAdvice)springAdvice).calculateArgumentBindings();return (Advice)springAdvice;}
}

spring 会根据不同的注解的类型,生成对应的 Advice。

spring 会在真正创建一个类之前,根据我们带有 @Aspect 类的配置生成对应的 Advise 对象,这些对象会被缓存起来。在这之后,就是在 spring 创建完 bean 后,根据这个 bean 生成对应的代理对象,并替换掉(也就是说,实际调用方法时候调用的对象变为这个生成的代理对象) 代理对象的创建,代理对象的创建我们之前分析过了,下面补充一点生成代理的方法。

5、代理方法

虽然之前分析过,但这里老周还是提一下代理对象的创建。

AopProxy 接口类提供了 getProxy 方法来获取代理对象,其中有三个实现如下图。

public interface AopProxy {Object getProxy();Object getProxy(@Nullable ClassLoader var1);
}

这里以 JDK 动态代理来分析

public Object getProxy(@Nullable ClassLoader classLoader) {if (logger.isTraceEnabled()) {logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());}Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);this.findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

看到 Proxy.newProxyInstance 就非常熟悉了,JDK 的动态代理。

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Object oldProxy = null;boolean setProxyContext = false;TargetSource targetSource = this.advised.targetSource;Object target = null;Object retVal;try {// equals 方法if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {Boolean var18 = this.equals(args[0]);return var18;}// hashCode方法if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {Integer var17 = this.hashCode();return var17;}// 如果是 DecoratingProxy类if (method.getDeclaringClass() == DecoratingProxy.class) {Class var16 = AopProxyUtils.ultimateTargetClass(this.advised);return var16;}// 实现了Advised接口if (this.advised.opaque || !method.getDeclaringClass().isInterface() || !method.getDeclaringClass().isAssignableFrom(Advised.class)) {if (this.advised.exposeProxy) {// ThreadLocal里记录下当前被代理的对象oldProxy = AopContext.setCurrentProxy(proxy);setProxyContext = true;}target = targetSource.getTarget();Class<?> targetClass = target != null ? target.getClass() : null;// 核心方法,获取当前方法的拦截器List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);if (chain.isEmpty()) {Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);} else {MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);// 调用这些拦截器及方法retVal = invocation.proceed();}Class<?> returnType = method.getReturnType();if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {retVal = proxy;} else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {throw new AopInvocationException("Null return value from advice does not match primitive return type for: " + method);}Object var12 = retVal;return var12;}retVal = AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);} finally {if (target != null && !targetSource.isStatic()) {targetSource.releaseTarget(target);}if (setProxyContext) {AopContext.setCurrentProxy(oldProxy);}}return retVal;
}

小结如下:

  • hashCode、equals方法单独处理

  • 根据当前方法等,生成所需的方法拦截器

  • 调用方法及拦截器

6、ReflectiveMethodInvocation.proceed()

public Object proceed() throws Throwable {if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {return this.invokeJoinpoint(); // ①} else {// ②Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();} else {// ③return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);}}
}

6.1 代码①

这个地方的代码就是目标对象的实际执行的地方,也就是 findUserNameById 的实际执行的调用的地方。

6.2 代码②

这一块是一个递归调用

6.3 代码③

这一块就是我们的 @Before、@Around、@After 等注解注释的方法的执行调用的地方,这里 @Before、@Around、@After 等注解的方法都会被封装到不同的 MethodInterceptor 子类对象中去,也就是说 MethodInterceptor 子类对象里面会记录这些注解对应的方法的元数据信息,当调用 MethodInterceptor#invoke 的时候会根据这些元数据信息通过反射的方式调用实际对应的方法,也就是我们上面创建的 PrintLogAspect 这个类的被 @Around 标注的方法。

小结如下:

  • 会根据我们之前生成的各个 Advisor 对应的切入点,判断下当前的方法是否满足该切入点。如果满足,将其适配为 MethodInterceptor 接口并返回。

  • 核心调用逻辑,就是取出一个个拦截器,先判断下方法是否满足拦截器条件,如果满足就调用。

三、总结

  • spring 在创建一个类之前,会看下有没有配置 AOP(可能是xml、可能是注解),如果有的话,会把配置给转换成一个个 advisor,然后缓存起来(这样后面需要生成代理类时候,就可以直接使用了)。

  • 如果有继续看它的 PointCut 对应的规则,只要在创建 bean 的时候符合这个 PointCut 规则的,就用动态代理(JDK Proxy、CGLib)的方式创建代理对象作为 bean 放到容器中。

  • 当我们从 bean 容器中获取代理对象 bean 并调用它的方法的时候,因为这个bean是通过代理的方式创建的,所以必然会走org.springframework.aop.framework.CglibAopProxy.DynamicAdvisedInterceptor#intercept() 方法,而这个方法也必然会执行org.springframework.aop.framework.ReflectiveMethodInvocation#proceed() 这个方法,而这个方法就会根据上面说的执行过程依次执行不同的 MethodInterceptor 子类对象的 invoke() 方法,这个方法会根据元数据信息通过反射的方式调用代理对象对应的真正的对象的方法,例如我上面创建的 PrintLogAspect 这个类的被 @Around 标注的方法。

一文读懂Spring中的AOP机制相关推荐

  1. 一文读懂SpringBoot中的事件机制

    一文读懂SpringBoot中的事件机制?针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法. 要"监听"事件,我们总是 ...

  2. 一文读懂CV中的注意力机制

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 作者丨AdamLau@知乎 来源丨https://zhuanlan ...

  3. 一文读懂密码学中的证书

    一文读懂密码学中的证书 之前的文章中,我们讲到了数字签名,数字签名的作用就是防止篡改和伪装,并且能够防止否认.但是要正确运用数字签名技术还有一个非常大的前提,那就是用来验证签名的公钥必须真正的属于发送 ...

  4. 一文读懂机器学习中的模型偏差

    一文读懂机器学习中的模型偏差 http://blog.sina.com.cn/s/blog_cfa68e330102yz2c.html 在人工智能(AI)和机器学习(ML)领域,将预测模型参与决策过程 ...

  5. java中date类型如何赋值_一文读懂java中的Reference和引用类型

    简介 java中有值类型也有引用类型,引用类型一般是针对于java中对象来说的,今天介绍一下java中的引用类型.java为引用类型专门定义了一个类叫做Reference.Reference是跟jav ...

  6. 一文读懂Java中File类、字节流、字符流、转换流

    一文读懂Java中File类.字节流.字符流.转换流 第一章 递归:File类: 1.1:概述 java.io.File 类是文件和目录路径名的抽象表示,主要用于文件和目录的创建.查找和删除等操作. ...

  7. 前端面试必会 | 一文读懂 JavaScript 中的 this 关键字

    this 是一个令无数 JavaScript 编程者又爱又恨的知识点.它的重要性毋庸置疑,然而真正想掌握它却并非易事.希望本文可以帮助大家理解 this. JavaScript 中的 this Jav ...

  8. 带你一文读懂Javascript中ES6的Symbol

    带你一文读懂Javascript中ES6的Symbol 前言 基础类型 Symbol Symbol.for 与 Symbol.keyFor Symbol.iterator Symbol.search ...

  9. 一文读懂机器学习中奇异值分解SVD

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 目录: 矩阵分解 1.1 矩阵分解作用 1.2 矩阵分解的方法一文 ...

最新文章

  1. 基于FPGA的以太网开发
  2. Windows Virtual PC RC 发布
  3. origin数据平滑_独门绝技!Origin挑战绘制细胞分化轨迹热图
  4. 【数据平台】关于Hadoop集群namenode format安全事故
  5. runltp出现问题 [
  6. POJ NOI0105-41 数字统计
  7. oracle函数 NLS_INITCAP(x[,y])
  8. PCWorld测评的2012版世界级杀毒软件
  9. 无纸化办公软件app 快用这款科学处理办公事宜的便签
  10. linux 找出僵尸进程,linux 查看僵尸进程
  11. java基础—综合练习
  12. 智掌柜扫码点单,帮助店家解决开店烦恼
  13. 使用 arp-scan 快速扫描局域网 IP -> raspberry pi ssh vnc
  14. 【HBZ分享】数仓里面的概念-宽表-维度表-事实表概念讲解
  15. BUUCTF [SWPU2019]EasiestRe
  16. Java程序编写----个人所得税计算器编写
  17. 计算机制图公开课,信息工程大学公开课:地图文化(6集全)
  18. ip地址转换数字函数 iton_数字转IP地址函数
  19. 半导体TEC高低温实验设备-风冷温控平台
  20. 大数据相关英文名称解释

热门文章

  1. Go各时间字符串使用详解
  2. android 代码获取图片信息吗,Android 通过网络获取图片的代码
  3. C语言模拟实现库函数 atoi
  4. 关于学习Python的一点学习总结(27->关键字参数和默认值)
  5. HDU1880(map)
  6. 自定义表单mysql_自定义表单,计算答案然后更新mysql DB(Custom form, calculate answer then update mysql DB)...
  7. 开发缺点_成都嗨创科技:原生APP开发与混合APP开发的优缺点对比
  8. mysql三表查询数据重复_解决mybatis三表连接查询数据重复的问题
  9. python并发1000个http请求_php下api接口的并发http请求
  10. bind merge r 和join_R语言数据合并