Spring AOP 是 Java 面试的必考点,我们需要了解 AOP 的基本概念及原理。那么 Spring AOP 到底是啥,为什么面试官这么喜欢问它呢?本文先介绍 AOP 的基本概念,然后根据 AOP 原理,实现一个接口返回统一格式的小示例,方便大家理解 Spring AOP 到底如何用!

一、为什么要使用 AOP ?

在实际的开发过程中,我们的应用程序会被分为很多层。通常来讲一个 Java 的 Web 程序会拥有以下几个层次:

  • Web 层:主要是暴露一些 Restful API 供前端调用。
  • 业务层:主要是处理具体的业务逻辑。
  • 数据持久层:主要负责数据库的相关操作(增删改查)。

虽然看起来每一层都做着全然不同的事情,但是实际上总会有一些类似的代码,比如日志打印和异常处理等。如果我们选择在每一层都独立编写这部分代码,那么久而久之代码将变的很难维护。所以我们提供了另外的一种解决方案:AOP。这样可以保证这些通用的代码被聚合在一起维护,而且我们可以灵活的选择何处需要使用这些代码。

二、什么是 AOP ?

AOP(Aspect Oriented Programming,面向切面编程),可以说是 OOP(Object Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用来模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如权限管理、异常处理等也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而 AOP 技术则恰恰相反,它利用一种称为 “横切” 的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为 “Aspect” ,即切面。所谓“切面”,简单地说,就是将权限、事务、日志、异常等与业务逻辑相对独立的功能抽取封装,便于减少系统的重复代码,降低模块间的耦合度,增加代码的可维护性。AOP 代表的是一个横向的关系,如果说 “对象” 是一个空心的圆柱体,其中封装的是对象的属性和行为;那么面向切面编程,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息,然后又以巧夺天功的妙手将这些剖开的切面复原,不留痕迹。

切面理解:用刀将西瓜分成两瓣,切开的切口就是切面;炒菜、锅与炉子共同来完成炒菜,锅与炉子就是切面。Web 层级设计中,Controller 层、Service 层、Dao 层,每一层之间也是一个切面。编程中,对象与对象之间,方法与方法之间,模块与模块之间都是一个个切面。

推荐网上的一篇通俗易懂的 AOP 理解:https://blog.csdn.net/qukaiwei/article/details/50367761

三、AOP 使用场景

  • 权限控制
  • 日志存储
  • 统一异常处理
  • 缓存处理
  • 事务处理
  • ……

四、AOP 专业术语

AOP有很多专业术语,初看这么多术语,可能一下子不大理解,多读几遍,相信很快就会搞懂。

1、Advice(通知)

  • 前置通知(Before advice):在目标方法调用前执行通知
  • 环绕通知(Around advice):在目标方法调用前后均可执行自定义逻辑
  • 返回通知(After returning advice):在目标方法执行成功后,调用通知
  • 异常通知(After throwing advice):在目标方法抛出异常后,执行通知
  • 后置通知(After advice):在目标方法完成(不管是抛出异常还是执行成功)后执行通知

2、JoinPoint(连接点)

就是 Spring 允许你放通知(Advice)的地方,很多,基本每个方法的前、后(两者都有也行)或抛出异常时都可以是连接点,Spring 只支持方法连接点,和方法有关的前前后后都是连接点。

Tips:可以使用连接点获取执行的类名、方法名和参数名等。

3、Pointcut(切入点)

是在连接点的基础上来定义切入点。比如在一个类中,有 15 个方法,那么就会有几十个连接点,但只想让其中几个方法的前后或抛出异常时干点什么,那么就用切入点来定义这几个方法,让切入点来筛选连接点。

4、Aspect(切面)

是通知(Advice)和切入点(Pointcut)的结合,通知(Advice)说明了干什么和什么时候(通过@Before、@Around、@After、@AfterReturning、@AfterThrowing来定义执行时间点)干,切入点(Pointcut)说明了在哪(指定方法)干,这就是一个完整的切面定义。

5、AOP 代理

AOP Proxy:AOP 框架创建的对象,代理就是目标对象的加强。AOP 巧妙的例用动态代理优雅的解决了 OOP 力所不及的问题。Spring 中的 AOP 代理可以是 jdk 动态代理,也可以是 cglib 动态代理。前者基于接口,后者基于子类。

五、AOP 示例:实现 Spring 接口返回统一(正常/异常)格式

读完上面这么多抽象概念,如果不来一个 AOP 具体示例,吸收效果或者理解深度可能不是那么好。所以,请接着往下看:

1、定义返回格式

import lombok.Data;

@Data
public class Result<T> { // code 状态值:0 代表成功,其他数值代表失败
private Integer code; // msg 返回信息。如果code为0,msg则为success;如果code为1,msg则为error
private String msg; // data 返回结果集,使用泛型兼容不同的类型
private T data; }

2、定义一些已知异常

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor; @AllArgsConstructor
@NoArgsConstructor
public enum ExceptionEnum { UNKNOW_ERROR(-1, "未知错误"),
NULL_EXCEPTION(-2, "空指针异常:NullPointerException"),
INVALID_EXCEPTION(1146, "无效的数据访问资源使用异常:InvalidDataAccessResourceUsageException"); public Integer code; public String msg; }

3、异常类捕获并返回

//@ControllerAdvice
@Component
@Slf4j
public class ExceptionHandle { // @ExceptionHandler(value = Exception.class)
// @ResponseBody
public Result exceptionGet(Throwable t) {
log.error("异常信息:", t);
if (t instanceof InvalidDataAccessResourceUsageException) {
return ResultUtil.error(ExceptionEnum.INVALID_EXCEPTION);
} else if (t instanceof NullPointerException) {
return ResultUtil.error(ExceptionEnum.NULL_EXCEPTION);
}
return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR);
} }

制作一个结果返回工具类:

public class ResultUtil {

    /**
* @return com.study.spring.entity.Result
* @description 接口调用成功返回的数据格式
* @param: object
*/
public static Result success(Object object) {
Result result = new Result();
result.setCode(0);
result.setMsg("success");
result.setData(object);
return result;
} /**
* @return com.study.spring.entity.Result
* @description 接口调用失败返回的数据格式
* @param: code
* @param: msg
*/
public static Result error(Integer code, String msg) {
Result result = new Result();
result.setCode(code);
result.setMsg(msg);
result.setData(null);
return result;
} /**
* 返回异常信息,在已知的范围内
*
* @param exceptionEnum
* @return
*/
public static Result error(ExceptionEnum exceptionEnum) {
Result result = new Result();
result.setCode(exceptionEnum.code);
result.setMsg(exceptionEnum.msg);
result.setData(null);
return result;
} }

4、pom 依赖

必须要添加 spring aop 等相关依赖:

<!-- web 依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 用于日志切面中,以 json 格式打印出入参 -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- lombok 简化代码-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

5、自定义注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface HandleResult { String desc() default "create17"; }

上述代码内有些概念需要解释说明:

  • @Retention:定义注解的保留策略

    • @Retention(RetentionPolicy.SOURCE) :注解保留在源码中,当 Java 文件编译成 class 字节码文件的时候,注解被遗弃。
    • @Retention(RetentionPolicy.CLASS :默认的保留策略,注解会保留在 class 字节码文件中,但运行( jvm 加载 class 字节码文件)时会被遗弃。
    • @Retention(RetentionPolicy.RUNTIME) :注解保留在 class 字节码文件中,在运行时也可以通过反射获取到。
  • @Target:定义注解的作用目标,可多个,用逗号分隔。
    • @Target(ElementType.TYPE) :作用于接口、类、枚举、注解
    • @Target(ElementType.FIELD) :作用于字段、枚举的常量
    • @Target(ElementType.METHOD) :作用于方法,不包含构造方法
    • @Target(ElementType.PARAMETER) :作用于方法的参数
    • @Target(ElementType.CONSTRUCTOR) :作用于构造方法
    • @Target(ElementType.LOCAL_VARIABLE) :作用于本地变量
    • @Target(ElementType.ANNOTATION_TYPE) :作用于注解
    • @Target(ElementType.PACKAGE) :作用于包
  • @Document:说明该注解将被包含在javadoc中。
  • @Inherited:说明子类可以继承父类中的该注解。
  • @interface:声明自定义注解。
  • desc():定义一个属性,默认为 create17。具体使用为:@HandleResult(desc = "描述内容...")

到这里,一个完整的自定义注解就定义完成了。

6、切面实现

1)首先我们定义一个切面类 HandleResultAspect
  • 使用 @Aspect 注解来定义切面,将当前类标识为一个切面供容器管理,必不可少。
  • 使用 @Component 注解来定义组件,将当前类标识为一个组件供容器管理,也必不可少。
  • 使用 @Slf4j 注解来打印日志;
  • 使用 @Order(i) 注解来表示切面的顺序,后文会详细讲。
@Aspect
@Component
@Slf4j
@Order(100)
public class HandleResultAspect {
...
}
2)接下来,我们定义一个切点。

使用 @Pointcut 来定义一个切点。

@Pointcut("@annotation(com.study.spring.annotation.HandleResult)")
// @Pointcut("execution(* com.study.spring.controller..*.*(..))")
public void HandleResult() {
}

对于 execution 表达式,官网对 execution 表达式的介绍为:

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

除了返回类型模式、方法名模式和参数模式外,其它项都是可选的。这个解释可能有点难理解,下面我们通过一个具体的例子来了解一下。在 HandleResultAspect 中我们定义了一个切点,其 execution 表达式为:* com.study.spring.controller..*.*(..)),下表为该表达式比较通俗的解析:

标识符 含义
execution() 表达式的主体
第一个 * 符号 表示返回值的类型,* 代表所有返回类型
com.study.spring.controller AOP 所切的服务的包名,即需要进行横切的业务类
包名后面的 .. 表示当前包及子包
第二个 * 表示类名,* 表示所有类
最后的 .*(..) 第一个 . 表示任何方法名,括号内为参数类型,.. 代表任何类型参数

上述的 execution 表达式是把 com.study.spring.controller 下所有的方法当作一个切点。@Pointcut 除了可以使用 execution 表达式之外,还可用 @annotation 来指定注解切入,比如可指定上面创建的自定义注解 @HandleResult ,@HandleResult 在哪里被使用,哪里就是一个切点。

3)说一下 Advice(通知)有关的切面注解
  • @Before:修饰的方法会在进入切点之前执行。在这个部分,我们需要打印一个开始执行的日志,比如:类型、方法名、参数名等。
@Before(value = "HandleResult() && @annotation(t)", argNames = "joinPoint,t")
public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception {
// 类名
String className = joinPoint.getTarget().getClass().getName();
// 方法名
String methodName = joinPoint.getSignature().getName();
// 参数名
Object[] args = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
if (args != null && args.length > 0) {
for (Object arg : args) {
sb.append(arg).append(", ");
}
}
log.info("接口 {} 开始被调用, 类名: {}, 方法名: {}, 参数名为: {} .",
t.desc(), className, methodName, sb.toString());
}
  • @Around:修饰的方法会环绕整个切点,可以在切入点前后织入代码,并可以自由地控制何时执行切点。通俗点讲就是:在进入切点前执行一部分逻辑,然后进入切点执行业务逻辑(ProceedingJoinPoint.proceed() 方法可用来接收业务逻辑的返回信息),最后出切点执行另一部分逻辑。
@Around("HandleResult()")
public Result doAround(ProceedingJoinPoint point) {
long startTime = System.currentTimeMillis();
log.info("---HandleResultAspect--Around的前半部分----------------------------");
Object result;
try {
// 执行切点。point.proceed 为方法返回值
result = point.proceed();
// 打印出参
log.info("接口原输出内容: {}", new Gson().toJson(result));
// 执行耗时
log.info("执行耗时:{} ms", System.currentTimeMillis() - startTime);
return ResultUtil.success(result);
} catch (Throwable throwable) {
return exceptionHandle.exceptionGet(throwable);
}
}
  • @After:修饰的方法和 @Before 相对应,无论程序执行正常还是异常,均执行该方法。
@After("HandleResult()")
public void doAfter() {
log.info("doAfter...");
}
  • @AfterReturning:在切点正常执行后,执行该方法,一般用于对返回值做些加工处理的场景。

returning 可接收接口最终地返回信息。

@AfterReturning(pointcut = "@annotation(t)", returning = "res")
public void afterReturn(HandleResult t, Object res) {
log.info("接口 {} 被调用已结束, 最终返回结果为: {} .",
t.desc(), new Gson().toJson(res));
}
  • @AfterThrowing:在切点抛出异常后,执行该方法。

throwing 可用来获取异常信息。

@AfterThrowing(throwing = "throwable", pointcut = "HandleResult()")
public void afterThrowing(Throwable throwable) {
log.info("After throwing...", throwable);
}

关于这些通知的执行顺序如下图所示:

以下为切面实现的全部代码:

@Aspect
@Component
@Slf4j
@Order(100)
public class HandleResultAspect { @Autowired
private ExceptionHandle exceptionHandle; /**
* @return void
* @description 定义切点
*/
@Pointcut("@annotation(com.study.spring.annotation.HandleResult)")
// @Pointcut("execution(* com.study.spring.controller..*.*(..))")
public void HandleResult() {
} /**
* @return void
* @description 打印接口名、类名、方法名及参数名
* @param: joinPoint
* @param: t
*/
@Before(value = "@annotation(t)", argNames = "joinPoint,t")
public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception {
// 类名
String className = joinPoint.getTarget().getClass().getName();
// 方法名
String methodName = joinPoint.getSignature().getName();
// 参数名
Object[] args = joinPoint.getArgs();
StringBuilder sb = new StringBuilder();
if (args != null && args.length > 0) {
for (Object arg : args) {
sb.append(arg).append(", ");
}
}
log.info("接口 {} 开始被调用, 类名: {}, 方法名: {}, 参数名为: {} .",
t.desc(), className, methodName, sb.toString());
} /**
* @return java.lang.Object
* @description 定义@Around环绕,用于何时执行切点
* @param: proceedingJoinPoint
*/
@Around("HandleResult()")
public Result doAround(ProceedingJoinPoint point) {
long startTime = System.currentTimeMillis();
log.info("---HandleResultAspect--Around的前半部分----------------------------");
Object result;
try {
// 执行切点。point.proceed 为方法返回值
result = point.proceed();
// 打印出参
log.info("接口原输出内容: {}", new Gson().toJson(result));
// 执行耗时
log.info("执行耗时:{} ms", System.currentTimeMillis() - startTime);
return ResultUtil.success(result);
} catch (Throwable throwable) {
return exceptionHandle.exceptionGet(throwable);
}
} /**
* @return void
* @description 程序无论正常还是异常,均执行的方法
* @param:
*/
@After("HandleResult()")
public void doAfter() {
log.info("doAfter...");
} /**
* @return void
* @description 当程序运行正常,所执行的方法
* 以json格式打印接口执行结果
* @param: t
* @param: res
*/
@AfterReturning(pointcut = "@annotation(t)", returning = "res")
public void afterReturn(HandleResult t, Object res) {
log.info("接口 {} 被调用已结束, 接口最终返回结果为: {} .",
t.desc(), new Gson().toJson(res));
} /**
* @return void
* @description 当程序运行异常,所执行的方法
* 可用来打印异常
* @param: throwable
*/
@AfterThrowing(throwing = "throwable", pointcut = "HandleResult()")
public void afterThrowing(Throwable throwable) {
log.info("After throwing...", throwable);
} }

六、多切面的执行顺序

在生产中,我们的项目可能不止一个切面,那么在多切面的情况下,如何指定切面的优先级呢?

我们可以使用 @Order(i) 注解来定义切面的优先级,i 值越小,优先级越高。

比如我们再创建一个切面,代码示例如下:

@Aspect
@Component
@Order(50)
@Slf4j
public class TestAspect2 { @Pointcut("@annotation(com.study.spring.annotation.HandleResult)")
public void aa(){ } @Before("aa()")
public void bb(JoinPoint joinPoint){
log.info("我是 TestAspect2 的 Before 方法...");
} @Around("aa()")
public Object cc(ProceedingJoinPoint point){
log.info("我是 TestAspect2 的 Around 方法的前半部分...");
Object result = null;
try {
result = point.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
log.info("我是 TestAspect2 的 Around 方法的后半部分...");
return result;
} @After("aa()")
public void doAfter() {
log.info("我是 TestAspect2 的 After 方法...");
} @AfterReturning("aa()")
public void afterReturn() {
log.info("我是 TestAspect2 的 AfterReturning 方法...");
} @AfterThrowing("aa()")
public void afterThrowing() {
log.info("我是 TestAspect2 的 AfterThrowing 方法...");
}
}

切面 TestAspect2 为 @Order(50),之前的切面 HandleResultAspect 为 Order(100)。测试接口返回的日志如下图所示:

总结一下规律就是:

  • 在执行切点之前,@Order 从小到大被执行,也就是说 Order 越小的优先级越高;
  • 在执行切点之后,@Order 从大到小被执行,也就是说 Order 越大的优先级越高;

也就是:先进后出的原则。为了方便我们理解,我画了一个图,如下图所示:

七、如何设置在特定环境下使用AOP

一般在项目开发中,都会设置三个环境:开发、测试、生产。那么如果我只想在 开发 和 测试 环境下使用某切面该怎么办呢?我们只需要在指定的切面类上方加上注解 @Profile 就可以了,如下所示:

这样就指定了 HandleResultAspect 该切面只能在 dev(开发)环境、test(测试)环境下生效,prod(生产)环境不生效。当然,你需要创建相应的 application-${dev/test/prod}.yml 文件,最后在 application.yml 文件内指定 spring.profiles.active 属性为 dev 或 test 才可以生效。

八、总结

本文篇幅较长,但总算对 Spring AOP 有了一个简单的了解。从 AOP 的起源到概念、使用场景,然后深入了解其专业术语,利用 AOP 思想实现了示例,方便我们自己理解。读完这篇文章,相信大家可以基本不惧面试官对这个知识点的考核了!

本文所涉及的代码已上传至 github :

https://github.com/841809077/spring-boot-study/tree/master/src/main/java/com/study/spring/annotation

本文参考链接:


用心整理 | Spring AOP 干货文章,图文并茂,附带 AOP 示例 ~的更多相关文章

  1. Spring源码剖析7:AOP实现原理详解

    前言 前面写了六篇文章详细地分析了Spring Bean加载流程,这部分完了之后就要进入一个比较困难的部分了,就是AOP的实现原理分析.为了探究AOP实现原理,首先定义几个类,一个Dao接口: pub ...

  2. spring框架学习(六)AOP

    AOP(Aspect-OrientedProgramming)面向方面编程,与OOP完全不同,使用AOP编程系统被分为方面或关注点,而不是OOP中的对象. AOP的引入 在OOP面向对象的使用中,无可 ...

  3. 【转】Spring Boot干货系列:(一)优雅的入门篇

    转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...

  4. 【Spring】SpringMVC之详解AOP

    1,AOP简介 Aspect Oriented Programming  面向切面编程.AOP还是以OOP为基础,只不过将共同逻辑封装为组件,然后通过配置的方式将组件动态切入到原有组件中.这样做的有点 ...

  5. 【转】Spring Boot干货系列:(二)配置文件解析

    转自:Spring Boot干货系列:(二)配置文件解析 前言 上一篇介绍了Spring Boot的入门,知道了Spring Boot使用"习惯优于配置"(项目中存在大量的配置,此 ...

  6. 【转】Spring Boot干货系列:常用属性汇总

    转自Spring Boot干货系列:常用属性汇总 附录A.常用应用程序属性 摘自:http://docs.spring.io/spring-boot/docs/current/reference/ht ...

  7. Spring Boot干货系列:(五)开发Web应用JSP篇

    Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...

  8. Spring Boot干货系列:(二)配置文件解析

    Spring Boot干货系列:(二)配置文件解析 2017-02-28 嘟嘟MD 嘟爷java超神学堂   前言 上一篇介绍了Spring Boot的入门,知道了Spring Boot使用“习惯优于 ...

  9. Spring Boot干货系列:(一)优雅的入门篇

    Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂   前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...

随机推荐

  1. 看淡生死,不服就干(C语言指针)

    看淡生死,不服就干 emmmmm 其实今天蛮烦的 高等数学考的一塌糊涂 会的不会的都没写 真心没有高中轻松了啊 也不知道自己立的flag还能不能实现 既然选择了就一定坚持下去啊 下面还是放一段之前写的 ...

  2. opencv HSV找颜色,找轮廓用最小旋转矩形框出

    #include <opencv2/opencv.hpp> #include<iostream> #include<string> using namespace ...

  3. Python 之路 Day01 笔记-什么是变量,常量等

    变量 变量 是 为了存储 程序运算过程中的一些中间 结果,为了方便日后调用 变量的命名规则 1. 要具有描述性 2. 变量名只能'_','数字','字母'组成,不可以是空格或特殊字符(#?<., ...

  4. Ubuntu 16.04源码编译boost库 编写CMakeLists.txt | compile boost 1.66.0 from source on ubuntu 16.04

    本文首发于个人博客https://kezunlin.me/post/d5d4a460/,欢迎阅读! compile boost 1.66.0 from source on ubuntu 16.04 G ...

  5. 白话布隆过滤器BloomFilter

    通过本文将了解到以下内容: 查找问题的一般思路 布隆过滤器的基本原理 布隆过滤器的典型应用 布隆过滤器的工程实现 场景说明: 本文阐述的场景均为普通单机服务器.并非分布式大数据平台,因为在大数据平台下 ...

  6. vue—自定义指令

    今日分享—自定义指令 需要学习的点: modifiers属性的具体实例就是v-on:click.stop=”handClick” 一样,为指令添加一个修饰符. 全局指令:新建一个newDir.js i ...

  7. 读写分离很难吗?springboot结合aop简单就实现了

    目录 前言 环境部署 开始项目 注意 參考: 前言 入职新公司到现在也有一个月了,完成了手头的工作,前几天终于有时间研究下公司旧项目的代码.在研究代码的过程中,发现项目里用到了Spring Aop来实 ...

  8. 微信中使用popup等弹窗组件时点击输入框input键盘弹起导致IOS中按钮无效处理办法

    因为在IOS微信中在弹窗中使用input使键盘弹起,使弹窗的位置上移,当键盘关闭时页面还在上面,弹窗位移量也在上面,只有下拉才能回到原位,这样弹窗也消失了.我的处理办法就是在键盘弹起和消失的时候,让页 ...

  9. python CGI编程-----简单的本地使用(1)

    本章节需要安装python开发工具,window平台安装地址:https://www.python.org/downloads/windows/,linux安装地址:https://www.pytho ...

  10. 一篇文章搞定Python多进程(全)

    1.Python多进程模块 Python中的多进程是通过multiprocessing包来实现的,和多线程的threading.Thread差不多,它可以利用multiprocessing.Proce ...