摘要:AOP在spring中又叫“面向切面编程”,是对传统我们面向对象编程的一个补充,主要操作对象就是“切面”,可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。

本文分享自华为云社区《一篇文搞懂《AOP面向切面编程》是一种什么样的体验?》,作者: 灰小猿。

一、什么是Spring的AOP?

AOP在spring中又叫“面向切面编程”,它可以说是对传统我们面向对象编程的一个补充,从字面上顾名思义就可以知道,它的主要操作对象就是“切面”,所以我们就可以简单的理解它是贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。相当于是将我们原本一条线执行的程序在中间切开加入了一些其他操作一样。

在应用AOP编程时,仍然需要定义公共功能,但可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常就称之为“切面”。

例如下面这个图就是一个AOP切面的模型图,是在某一个方法执行前后执行的一些操作,并且这些操作不会影响程序本身的运行。

AOP切面编程中有一个比较专业的术语,我给大家罗切出来了:

现在大概的了解了AOP切面编程的基本概念,接下来就是实际操作了。

二、AOP框架环境搭建

1、导入jar包

目前比较流行且常用的AOP框架是AspectJ,我们在做SSM开发时用到的也是AspectJ,使用该框架技术就需要导入它所支持的jar包,

  • aopalliance.jar
  • aspectj.weaver.jar
  • spring-aspects.jar

关于SSM开发所使用的所有jar包和相关配置文件我都已将帮大家准备好了!

点击链接下载就能用。【全网最全】SSM开发必备依赖-Jar包、参考文档、常用配置

2、引入AOP名称空间

使用AOP切面编程时是需要在容器中引入AOP名称空间的,

3、写配置

其实在做AOP切面编程时,最常使用也必备的一个标签就是,< aop:aspectj-autoproxy></aop:aspectj-autoproxy>,

我们在容器中需要添加这个元素,当Spring IOC容器侦测到bean配置文件中的< aop:aspectj-autoproxy>元素时,会自动为与AspectJ切面匹配的bean创建代理。
同时在现在的spring中使用AOP切面有两种方式,分别是AspectJ注解或基于XML配置的AOP,

下面我依次和大家介绍一下这两种方式的使用。

三、基于AspectJ注解的AOP开发

在上一篇文章中我也和大家将了关于spring中注解开发的强大,所以关于AOP开发我们同样也可以使用注解的形式来进行编写,下面我来和大家介绍一下如何使用注解方式书写AOP。

1、五种通知注解

首先要在Spring中声明AspectJ切面,只需要在IOC容器中将切面声明为bean实例。

当在Spring IOC容器中初始化AspectJ切面之后,Spring IOC容器就会为那些与 AspectJ切面相匹配的bean创建代理。

在AspectJ注解中,切面只是一个带有@Aspect注解的Java类,它往往要包含很多通知。通知是标注有某种注解的简单的Java方法。

AspectJ支持5种类型的通知注解:

  1. @Before:前置通知,在方法执行之前执行
  2. @After:后置通知,在方法执行之后执行
  3. @AfterRunning:返回通知,在方法返回结果之后执行
  4. @AfterThrowing:异常通知,在方法抛出异常之后执行
  5. @Around:环绕通知,围绕着方法执行

2、切入点表达式规范

这五种通知注解后面还可以跟特定的参数,来指定哪一个切面方法在哪一个方法执行时触发。那么具体操作是怎么样的呢?

这里就需要和大家介绍一个名词:“切入点表达式”,通过在注解中加入该表达式参数,我们就可以通过表达式的方式定位一个或多个具体的连接点,

切入点表达式的语法格式规范是:

execution([权限修饰符] [返回值类型] [简单类名/全类名] [方法名] ([参数列表]))

其中在表达式中有两个常用的特殊符号:

星号“ * ”代表所有的意思,星号还可以表示任意的数值类型

“.”号:“…”表示任意类型,或任意路径下的文件,

在这里举出几个例子:

表达式:

execution(* com.atguigu.spring.ArithmeticCalculator.*(…))

含义:

ArithmeticCalculator接口中声明的所有方法。第一个“”代表任意修饰符及任意返回值。第二个“”代表任意方法。“…”匹配任意数量、任意类型的参数。若目标类、接口与该切面类在同一个包中可以省略包名。

表达式:

execution(public * ArithmeticCalculator.*(…))

含义:

ArithmeticCalculator接口的所有公有方法

表达式:

execution(public double ArithmeticCalculator.*(…))

含义:

ArithmeticCalculator接口中返回double类型数值的方法

表达式:

execution(public double ArithmeticCalculator.*(double, …))

含义:

第一个参数为double类型的方法。“…” 匹配任意数量、任意类型的参数。

表达式:

execution(public double ArithmeticCalculator.*(double, double))

含义:

参数类型为double,double类型的方法

这里还有一个定位最模糊的表达式:

execution("* *(…)")

表示任意包下任意类的任意方法,但是这个表达式千万别写,哈哈,不然你每一个执行的方法都会有通知方法执行的!

同时,在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。

如:

execution (* .add(int,…)) || execution( *.sub(int,…))

表示任意类中第一个参数为int类型的add方法或sub方法

3、注解实践

现在我们已经知道了注解和切入点表达式的使用,那么接下来就是进行实践了,

对于切入点表达式,我们可以直接在注解中使用“”写在其中,还可以在@AfterReturning注解和@AfterThrowing注解中将切入点赋值给pointcut属性,但是在其他的注解中没有pointcut这个参数。

将切入点表达式应用到实际的切面类中如下:

@Aspect //切面注解
@Component //其他业务层
public class LogUtli {
//  方法执行开始,表示目标方法是com.spring.inpl包下的任意类的任意以两个int为参数,返回int类型参数的方法@Before("execution(public int com.spring.inpl.*.*(int, int))")public static void LogStart(JoinPoint joinPoint) {System.out.println("通知记录开始...");}
//  方法正常执行完之后/*** 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收* returning用来接收方法的返回值* */@AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")public static void LogReturn(JoinPoint joinPoint,Object result) {System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);}
}

以上只是一个最简单的通知方法,但是在实际的使用过程中我们可能会将多个通知方法切入到同一个目标方法上去,比如同一个目标方法上既有前置通知、又有异常通知和后置通知。

但是这样我们也只是在目标方法执行时切入了一些通知方法,那么我们能不能在通知方法中获取到执行的目标方法的一些信息呢?当然是可以的。

4、JoinPoint获取方法信息

在这里我们就可以使用JoinPoint接口来获取到目标方法的信息,如方法的返回值、方法名、参数类型等。

如我们在方法执行开始前,获取到该目标方法的方法名和输入的参数并输出。

//   方法执行开始@Before("execution(public int com.spring.inpl.*.*(int, int))")public static void LogStart(JoinPoint joinPoint) {Object[] args = joinPoint.getArgs();  //获取到参数信息Signature signature = joinPoint.getSignature(); //获取到方法签名String name = signature.getName();  //获取到方法名System.out.println("【" + name + "】记录开始...执行参数:" + Arrays.asList(args));}

5、接收方法的返回值和异常信息

对于有些目标方法在执行完之后可能会有返回值,或者方法中途异常抛出,那么对于这些情况,我们应该如何获取到这些信息呢?

首先我们来获取当方法执行完之后获取返回值,

在这里我们可以使用@AfterReturning注解,该注解表示的通知方法是在目标方法正常执行完之后执行的。

在返回通知中,只要将returning属性添加到@AfterReturning注解中,就可以访问连接点的返回值。

该属性的值即为用来传入返回值的参数名称,但是注意必须在通知方法的签名中添加一个同名参数。

在运行时Spring AOP会通过这个参数传递返回值,由于我们可能不知道返回值的类型,所以一般将返回值的类型设置为Object型。

与此同时,原始的切点表达式需要出现在pointcut属性中,如下所示:

//   方法正常执行完之后/*** 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收* returning用来接收方法的返回值* */@AfterReturning(pointcut="public int com.spring.inpl.*.*(int, int)",returning="result")public static void LogReturn(JoinPoint joinPoint,Object result) {System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);}

对于接收异常信息,方法其实是一样的。

我们需要将throwing属性添加到@AfterThrowing注解中,也可以访问连接点抛出的异常。Throwable是所有错误和异常类的顶级父类,所以在异常通知方法可以捕获到任何错误和异常。

如果只对某种特殊的异常类型感兴趣,可以将参数声明为其他异常的参数类型。然后通知就只在抛出这个类型及其子类的异常时才被执行。

实例如下:

//   异常抛出时/*** 在执行方法想要抛出异常的时候,可以使用throwing在注解中进行接收,* 其中value指明执行的全方法名* throwing指明返回的错误信息* */@AfterThrowing(pointcut="public int com.spring.inpl.*.*(int, int)",throwing="e")public static void LogThowing(JoinPoint joinPoint,Object e) {System.out.println("【" + joinPoint.getSignature().getName() +"】发现异常信息...,异常信息是:" + e);}

6、环绕通知

我们在上面介绍通知注解的时候,大家应该也看到了其实还有一个很重要的通知——环绕通知

环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。

对于环绕通知来说,连接点的参数类型必须是ProceedingJoinPoint。它是 JoinPoint的子接口,允许控制何时执行,是否执行连接点。

在环绕通知中需要明确调用ProceedingJoinPoint的proceed()方法来执行被代理的方法。如果忘记这样做就会导致通知被执行了,但目标方法没有被执行。这就意味着我们需要在方法中传入参数ProceedingJoinPoint来接收方法的各种信息。

注意:
环绕通知的方法需要返回目标方法执行之后的结果,即调用 joinPoint.proceed();的返回值,否则会出现空指针异常。

具体使用可以看下面这个实例:

/*** 环绕通知方法* 使用注解@Around()* 需要在方法中传入参数proceedingJoinPoint 来接收方法的各种信息* 使用环绕通知时需要使用proceed方法来执行方法* 同时需要将值进行返回,环绕方法会将需要执行的方法进行放行* ********************************************** @throws Throwable * */@Around("public int com.spring.inpl.*.*(int, int)")public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {//     获取到目标方法内部的参数Object[] args = pjp.getArgs();System.out.println("【方法执行前】");
//      获取到目标方法的签名Signature signature = pjp.getSignature();String name = signature.getName();Object proceed = null;try {
//          进行方法的执行proceed = pjp.proceed();System.out.println("方法返回时");} catch (Exception e) {System.out.println("方法异常时" + e);}finally{System.out.println("后置方法");}//将方法执行的返回值返回return proceed;}

7、通知注解的执行顺序

那么现在这五种通知注解的使用方法都已经介绍完了,我们来总结一下这几个通知注解都在同一个目标方法中时的一个执行顺序。

在正常情况下执行:

@Before(前置通知)—>@After(后置通知)---->@AfterReturning(返回通知)

在异常情况下执行:

@Before(前置通知)—>@After(后置通知)---->@AfterThrowing(异常通知)

当普通通知和环绕通知同时执行时:

执行顺序是:

环绕前置----普通前置----环绕返回/异常----环绕后置----普通后置----普通返回/异常

8、重用切入点定义

对于上面的通知注解,我们都是在每一个通知注解上都定义了一遍切入点表达式,

但是试想一个问题,如果我们不想给这个方法设置通知方法了,或者我们想要将这些通知方法切入到另一个目标方法,那么我们岂不是要一个一个的更改注解中的切入点表达式吗?这样也太麻烦了吧?

所以spring就想到了一个办法,重用切入点表达式

也就是说将这些会重复使用的切入点表达式用一个方法来表示,那么我们的通知注解只需要调用这个使用了该切入点表达式的方法即可实现和之前一样的效果,这样的话,我们即使想要更改切入点表达式的接入方法,也不用一个一个的去通知注解上修改了。

获取可重用的切入点表达式的方法是:

  1. 随便定义一个void的无实现的方法
  2. 为方法添加注解@Pointcut()
  3. 在注解中加入抽取出来的可重用的切入点表达式
  4. 使用value属性将方法加入到对应的切面函数的注解中

完整实例如下:

@Aspect //切面注解
@Component //其他业务层
public class LogUtli {/*** 定义切入点表达式的可重用方法* */@Pointcut("execution(public int com.spring.inpl.MyMathCalculator.*(int, int))")public void MyCanChongYong() {}//    方法执行开始@Before("MyCanChongYong()")public static void LogStart(JoinPoint joinPoint) {Object[] args = joinPoint.getArgs(); //获取到参数信息Signature signature = joinPoint.getSignature(); //获取到方法签名String name = signature.getName();  //获取到方法名System.out.println("【" + name + "】记录开始...执行参数:" + Arrays.asList(args));}
//  方法正常执行完之后/*** 在程序正常执行完之后如果有返回值,我们可以对这个返回值进行接收* returning用来接收方法的返回值* */@AfterReturning(value="MyCanChongYong()",returning="result")public static void LogReturn(JoinPoint joinPoint,Object result) {System.out.println("【" + joinPoint.getSignature().getName() + "】程序方法执行完毕了...结果是:" + result);}//  异常抛出时/*** 在执行方法想要抛出异常的时候,可以使用throwing在注解中进行接收,* 其中value指明执行的全方法名* throwing指明返回的错误信息* */@AfterThrowing(value="MyCanChongYong()",throwing="e")public static void LogThowing(JoinPoint joinPoint,Object e) {System.out.println("【" + joinPoint.getSignature().getName() +"】发现异常信息...,异常信息是:" + e);}//   结束得出结果@After(value = "execution(public int com.spring.inpl.MyMathCalculator.add(int, int))")public static void LogEnd(JoinPoint joinPoint) {System.out.println("【" + joinPoint.getSignature().getName() +"】执行结束");}/*** 环绕通知方法* @throws Throwable * */@Around("MyCanChongYong()")public Object MyAround(ProceedingJoinPoint pjp) throws Throwable {//     获取到目标方法内部的参数Object[] args = pjp.getArgs();System.out.println("【方法执行前】");
//      获取到目标方法的签名Signature signature = pjp.getSignature();String name = signature.getName();Object proceed = null;try {
//          进行方法的执行proceed = pjp.proceed();System.out.println("方法返回时");} catch (Exception e) {System.out.println("方法异常时" + e);}finally{System.out.println("后置方法");}//将方法执行的返回值返回return proceed;}
}

以上就是使用AspectJ注解实现AOP切面的全部过程了,

在这里还有一点特别有意思的规定提醒大家,就是当你有多个切面类时,切面类的执行顺序是按照类名的首字符先后来执行的(不区分大小写)。

接下来我来和大家讲解一下实现AOP切面编程的另一种方法——基于XML配置的AOP实现。

四、基于XML配置的AOP实现

基于XML配置的AOP切面顾名思义就是摒弃了注解的使用,转而在IOC容器中配置切面类,这种声明是基于aop名称空间中的XML元素来完成的,

在bean配置文件中,所有的Spring AOP配置都必须定义在< aop:config>元素内部。对于每个切面而言,都要创建一个< aop:aspect>元素来为具体的切面实现引用后端bean实例。

切面bean必须有一个标识符,供< aop:aspect>元素引用。

所以我们在bean的配置文件中首先应该先将所需切面类加入到IOC容器中去,之后在aop的元素标签中进行配置。我们在使用注解进行开发的时候,五种通知注解以及切入点表达式这些在xml文件中同样是可以配置出来的。

1、声明切入点

切入点使用

< aop:pointcut>元素声明。
切入点必须定义在< aop:aspect>元素下,或者直接定义在< aop:config>元素下。

定义在< aop:aspect>元素下:只对当前切面有效

定义在< aop:config>元素下:对所有切面都有效

基于XML的AOP配置不允许在切入点表达式中用名称引用其他切入点。

2、声明通知

在aop名称空间中,每种通知类型都对应一个特定的XML元素。

通知元素需要使用< pointcut-ref>来引用切入点,或用< pointcut>直接嵌入切入点表达式。
method属性指定切面类中通知方法的名称

具体使用可以看下面这里实例:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsdhttp://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"><!-- 通过配置文件实现切面 1、将目标类和切面类加入到容器中 @component2、声明哪个类是切面类,@Aspect3、在配置文件中配置五个通知方法,告诉切面类中的方法都何时运行4、开启基于注解的AOP功能--><!-- 将所需类加入到容器中 --><bean id="myCalculator" class="com.spring.inpl.MyMathCalculator"></bean><bean id="logUtil" class="com.spring.utils.LogUtli"></bean><bean id="SecondUtli" class="com.spring.utils.SecondUtli"></bean><!-- 进行基于AOP的配置 --><!-- 当有两个切面类和一个环绕方法时,方法的执行是按照配置文件中配置的先后顺序执行的配置在前的就会先执行,配置在后的就会后执行,但同时环绕方法进入之后就会先执行环绕方法--><aop:config><!-- 配置一个通用类 --><aop:pointcut expression="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" id="myPoint"/><!-- 配置某一个指定的切面类 --><aop:aspect id="logUtil_Aspect" ref="logUtil"><!-- 为具体的方法进行指定method指定具体的方法名pointcut指定具体要对应的方法--><aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.add(int, int))"/><aop:after-throwing method="LogThowing" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(int, int)))" throwing="e"/><aop:after-returning method="LogReturn" pointcut-ref="myPoint" returning="result"/><aop:after method="LogEnd" pointcut-ref="myPoint"/><!-- 定义一个环绕方法 --><aop:around method="MyAround" pointcut-ref="myPoint"/></aop:aspect><!-- 定义第二个切面类 --><aop:aspect ref="SecondUtli"><aop:before method="LogStart" pointcut="execution(public int com.spring.inpl.MyMathCalculator.*(..))"/><aop:after-throwing method="LogThowing" pointcut-ref="myPoint" throwing="e"/><aop:after method="LogEnd" pointcut-ref="myPoint"/></aop:aspect></aop:config>
</beans>

总结一下通过XML配置实现AOP切面编程的过程:

通过配置文件实现切面

  1. 将目标类和切面类加入到容器中 相当于注解@component
  2. 声明哪个类是切面类,相当于注解@Aspect
  3. 在配置文件中配置五个通知方法,告诉切面类中的方法都何时运行
  4. 开启基于注解的AOP功能

这里有一点还需要注意:

当有两个切面类和一个环绕方法时,方法的执行是按照配置文件中配置的先后顺序执行的,配置在前的就会先执行,配置在后的就会后执行,但同时环绕方法进入之后就会先执行环绕方法。

最后总结

至此通过AspectJ注解和XML配置两种方式来实现AOP切面编程的过程就和大家分享完了,

总体来说基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。

点击关注,第一时间了解华为云新鲜技术~

一文带你搞定AOP切面相关推荐

  1. 【RabbitMQ】一文带你搞定RabbitMQ延迟队列

    本文口味:鱼香肉丝   预计阅读:10分钟 0|1一.说明 在上一篇中,介绍了RabbitMQ中的死信队列是什么,何时使用以及如何使用RabbitMQ的死信队列.相信通过上一篇的学习,对于死信队列已经 ...

  2. 阿里、京东、字节跳动春招,Java岗offer不好拿?一文带你搞定

    前言 以下内容均为二月面试真题整理,面试内容均来自阿里.京东.腾讯.字节跳动等一线大厂,由网友集合反馈整理! 如有雷同,请在评论区提醒,全部内容GitHub可查阅. 由于篇幅原因,内容会比较杂乱,程序 ...

  3. 干货 | 一文带你搞定Python 数据可视化

    2019独角兽企业重金招聘Python工程师标准>>> 01前言 在之前的一篇文章<Python 数据可视化利器>中,我写了 Bokeh.pyecharts 的用法,但是 ...

  4. 一文带你搞定svg-icon的使用

    前置 有的时候,我们经常在业务中会需要使用字体图标的场景,比如同一个图标在不同的地方显示不同的颜色,这个时候我们使用字体图标就非常的合适, 这篇文章我们主要讲的是在Vue中显示的字体图标`svg-ic ...

  5. C语言学习:编程、源文件、源代码是什么?一文带你搞定它!

    编程:人通过某种方式命令计算机做一些动作,来得到人想要的结果,就叫编程. 比如开关灯,把灯看做计算机,按下按钮,灯就开了,松开按钮灯就关了,这样也就达到了人向计算机下达指令的需求. 在早期,计算机全是 ...

  6. 一文带你搞定抖音最近最火的情侣微信早报信息推送

    文章目录 一.引言 二.公众号配置 2.1 注册号申请 2.2 填写appID和appsecret 2.3 配置推送模板 2.4 扫码关注 三.API配置 3.1 账号申请 3.2 创建应用 3.3 ...

  7. 一文带你搞定线程池原理

    1.使用线程池的意义何在? 项目开发中,为了统一管理线程,并有效精准地进行排错,我们经常要求项目人员统一使用线程池去创建线程.因为我们是在受不了有些人动不动就去创建一个线程,使用的多了以后,一旦报错就 ...

  8. DDD进阶_一文带你搞定什么是前台、中台、后台

    DDD从入门到精通,系列文章传送地址,请点击本链接. 目录 一.中台和平台的关系 二.什么是中台? 三.数字化转型中台应该共享什么? 四.如何实现前中后台的协同? 1. 前台 2. 中台 3. 后台 ...

  9. 一文带你搞定JDBC

    前言:"只有自己强大,才不会被别人践踏." 你好,我是梦阳辰,让我们轻松玩编程,一起走进JDBC的世界吧!文章较长建议收藏再看! 文章目录 1.JDBC概述 2.JDBC编程六步( ...

  10. 并联谐振电路工作原理详解,案例+计算公式,几分钟带你搞定

    昨天给大家分享了关于串联谐振的文章,今天给大家分享关于并联谐振的文章.(私信我的那个朋友,记得准备来看) 错过了串联谐振的朋友,可以直接点击下方标题跳转. 串联谐振是怎么工作的?案例+公式,几分钟,一 ...

最新文章

  1. 2010.6.15 常用数据类型转换总结
  2. 中国队拿下口罩人脸识别世界第一!还将推出全球最大公开人脸数据集
  3. html设置div页面最底,使用css让大图片不超过网页宽度
  4. linux两个数字正则,正则表达式-Linux readelf显示具有不同数字系...
  5. mysql源码安装报错_mysql 的二进制和源码包 安装的报错总结
  6. php 常用函数 180,php 部分常用函数
  7. EdrawMax Crack,多合一的图表应用程序
  8. 万人血书的前端开发自学资料(书籍+教程),它来了~
  9. 南开100题C语言(021-030)
  10. 七牛云php回调,回调通知_开发指南_对象存储 - 七牛开发者中心
  11. Python爬虫方式抓取免费http代理IP
  12. PWM控制技术+Simulink仿真详解
  13. java爬虫系列(二)——爬取动态网页
  14. 黑龙江省双鸭山市谷歌高清卫星地图下载
  15. 堆栈内存两张图理解-来自珠峰猛男周啸天
  16. [TJOI2017]可乐
  17. ArchLinux 的 pacman 命令详解
  18. 实时显示时间(HTML+JS)
  19. ABBYY15免费照片识别文字识别软件
  20. 计算机技能标准包括哪些,(对口单招计算机技能考试标准.doc

热门文章

  1. idea社区版 html,利用IntelliJ IDEA社区版开发servlet
  2. 2.1 图像验证码(英文验证码、超级鹰)
  3. 软考中级软件设计师——数据结构与算法基础
  4. 浅谈登录服务器的方法
  5. 用ADB操纵手机实现连点器(折衷案)
  6. java 鼠标驱动模拟,dd虚拟键盘鼠标模拟软件
  7. 计算机酷我音乐文件夹,酷我音乐缓存文件在哪?打开酷我音乐缓存文件的方法...
  8. postman脚本文件存放的地址
  9. 微信小程序直播电脑端OBS推流直播教程
  10. Topaz Mask AI for mac(人工智能AI抠图软件) 最新版