AOP介绍和使用

本文介绍了AOP(面向切面编程)的基本概念,包括什么是AOP、SpringAOP的作用,以及AOP在处理公共功能时的优势。详细阐述了AOP的组成部分、通知类型、切点表达式,并通过示例展示了如何在Spring中实现AOP,包括定义切面、切点和通知。此外,讨论了AOP的实现原理,如织入的时机,以及与拦截器的区别和联系。最后,文章探讨了AOP在用户登录验证、异常处理和统一数据返回等场景的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

目录

1. AOP的概念

1.1 什么是AOP?

1.2 什么是SpringAOP?

1.3 为什要⽤ AOP?

1.4 AOP的作用?

2. AOP的组成

3. 通知

4. AOP的实现

4.1  添加 Spring AOP 框架⽀持。

4.2  定义切面和切点。

4.3  定义通知。

4.3.1 切点表达式:

4.3.2 AOP演示

5.下面应用我们的AOP,怎么进行方法耗时的获取呢?

5.1 普通方法

 5.2 使用aop

6. AOP的实现原理

6.1 织⼊(Weaving):代理的⽣成时机

7. 拦截器

7.1 拦截器实现原理

7.2 拦截器代码实现

8. 使用AOP实现常用的统一功能处理

8.1 ⽤户统⼀登录验证的问题

8.2 统⼀异常处理

8.3 统一数据返回


1. AOP的概念

1.1 什么是AOP?

AOP(Aspect Oriented Programming):⾯向切⾯编程,它是⼀种思想,它是对某⼀类事情的 集中处理。

1.2 什么是SpringAOP?

⽽ AOP 是⼀种思想,⽽ Spring AOP 是⼀个框架,提供了⼀种对 AOP 思想的实现,它们的关系和 IoC 与 DI 类似。

1.3 为什要⽤ AOP?

在开发中,我们对于公共方法的处理分为三个阶段:

  1. 每个方法都去实现(初级)
  2. 抽取公共方法(中级)
  3. 采用AOP的方式,对代码进行无侵入是实现(高级)

因此,对于这种功能统⼀,且使⽤的地⽅较多的功能,在之前我们会抽取公共方法去实现,但是现在有更好的一个办法来处理这个问题,就是AOP。

也就是使⽤ AOP 可以扩充多个对象的某个能⼒,所以 AOP 可以说是 OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。

1.4 AOP的作用?

提供声明式事务;允许用户自定义切面

2. AOP的组成

AOP由下边四部分组成:

切面(Aspect)

由切点(Pointcut)和通知(Advice)组成,既包含了横切逻辑的定义,也包含了连接点的定义。

连接点(Join Point)

应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

切点(Pointcut)

Pointcut的作用就是提供一组规则 来匹配Join Point,给满足规则的Join Point 添加Advice.

通知(Advice)

切面也有目标-他必须要完成的工作。在AOP中,切面的工作被称为通知。

那么是不是感觉难以理解呢?

不着急我们简单说一下, AOP使用来对某一类事情进行集中处理的,那么处理事情,这个整体就是一个切面,处理事情的范围就是切点,范围中具体的事物就是连接点,怎么处理就是通知。

简单的体系图如下:

3. 通知

通知又名拦截器,它定义了切面是做什么以及何时使用,即在某个特定的连接点上执行的动作,它是切面的具体实现。以目标方法为参照点,根据放置位置的地方不同,通知分为如下5种类型通知:

  • 前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
  • 后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
  • 返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
  • 抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
  • 环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。

4. AOP的实现

实现步骤如下:

1. 添加 Spring AOP 框架⽀持。

2. 定义切⾯和切点。

3. 定义通知。

具体操作如下:

4.1  添加 Spring AOP 框架⽀持。

首先我们建立一个新的Springboot项目,在配置文件中添加AOP框架:

 <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

4.2  定义切面和切点。

4.3  定义通知。

定义一个切面:

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class LoginAspect {
//定义切点
    @Pointcut("execution(* com.example.springaopdemo.controller.UserController.*(..))")
    public void pointcut(){}
//定义通知
    @Before("pointcut()")
    public void before(){
        log.info("do before,,,,");
    }
    @After("pointcut()")
    public void after(){
        log.info("do after.....");
    }
    @AfterReturning("pointcut()")
    public void afterReturning(){
        log.info("do afterReturning....");
    }
    @AfterThrowing("pointcut()")
    public  void afterThrowing(){
        log.info("do afterThrowing....");
    }
//这里注意,环绕式通知必须自己写返回结果
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint){
        Object oj=null;
        log.info("之前");
        try{
            oj=joinPoint.proceed();//调用目标方法
        }catch(Throwable e){
            throw new RuntimeException(e);
        }
        log.info("之后");
        return oj;
    }

@Aspect是aop框架中提供的,表示是一个切面。切面类中最先定义的是切点,用@Pointcut注解表示是一个切点,括号里边是切点表达式。后边的是五大类型的通知。分别是用五大类名称进行注解。

4.3.1 切点表达式:

AspectJ ⽀持三种通配符

*匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
..匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
+表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的 所有⼦类包括本身

切点表达式由切点函数组成,execution是最常用的切点函数,语法为:

 修饰符和异常可以省略。

4.3.2 AOP演示

以上AOP部分的代码就已经完成,可以实现AOP的功能了,我们简单演示一下:

定义一个类来试一下它集中处理的实现:

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public String login(){
        log.info("login..");
        return "login...";
    }
    @RequestMapping("set")
    public String setup(){
        log.info("setup...");
        return "setup....";
    }
    @RequestMapping("/logout")
    public String logout(){
        log.info("logout...");
        return "logout.....";
    }
}

在浏览器执行过后就会发现执行结果:(任意执行一个方法,这里执行第三个方法)

 可以看到五类通知的执行顺序如上,但是还有一个AfterThrowing没有看到打印,为什么呢?

这是因为AfterThrowing是在出现异常后才会进行打印的,在diamagnetic中插入一条异常代码,我们就会看到他的打印了:

以上代码能看出来了,AOP的确实一类事情的集中处理,且处理的时候不会破坏原有的代码,且允许用户自定义切面。

5.下面应用我们的AOP,怎么进行方法耗时的获取呢?

首先我们先来看一下普通的获取实践的方法,对比一下就更体会aop的方便了。

5.1 普通方法

就是在每个方法内部去进行操作。

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public String login(){
//        log.info("login..");
//        return "login...";
        long start=System.currentTimeMillis();
        log.info("login..");
        long end=System.currentTimeMillis();
        log.info("login方法耗时:"+(end-start));
        return "login....";
    }
}

 结果显示(这里环绕通知关了):

 5.2 使用aop

定义一个环绕通知,在方法调用前记录,在方法调用完打印方法耗时。

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
public class LoginAspect {
  @Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        Object oj=null;
        long start=System.currentTimeMillis();
        try {
            oj=joinPoint.proceed();
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
//获取当前方法的名称并打印
        log.info(joinPoint.getSignature().toString()+"耗时:"+(System.currentTimeMillis()-start));
        return oj;
    }
}

结果显示:

使用aop只需写一次代码,要获取那个方法的耗时就调用就可以了,大大提高了效率,也可以明显看出aop的思想:是对某一类事情的集中处理。

注:这里的耗时可能每次获取的结果不是相同的,那是因为耗时受当前环境的影响,例如cpu,当前正在进行进程的多少。。所以不一样是正常的,并不是说结果就不正确。

6. AOP的实现原理

Spring AOP 是构建在动态代理代理模式是什么)基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。

6.1 织⼊(Weaving):代理的⽣成时机

织入就是什么时候把代理的代码放进运行的代码中。

在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:

  • 编译期(编译阶段):切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就 是以这种⽅式织⼊切⾯的。
  • 类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种⽅式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该⽬标类的字节码。AspectJ5的加载 时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
  • 运⾏期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为⽬标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。

7. 拦截器

那么,上边所学的AOP很方便,但是上边的AOP在我们实现复杂一点的功能的时候很难处理,我们没办法获取到 HttpSession 对象,还有时候我们要对⼀部分⽅法进⾏拦截,⽽另⼀部分⽅法不拦截,如注册⽅法和登录⽅法是不拦截的,这样的话排除⽅法的规则很难定义,甚⾄没办法定义。所以,引入拦截器。

7.1 拦截器实现原理

正常情况下的调用顺序

加入拦截器后的调用顺序

Spring 中的拦截器也是通过动态代理和环绕通知的思想实现的,大体流程:

7.2 拦截器代码实现

Spring 中提供了具体的实现拦截器:HandlerInterceptor,拦截器的实现分为以下两个步骤:

  • 创建⾃定义拦截器,实现 HandlerInterceptor 接⼝的 preHandle(执⾏具体⽅法之前的预处理)⽅ 法。
  • 将⾃定义拦截器加⼊ WebMvcConfigurer 的 addInterceptors ⽅法中。

自定义拦截器:(就是写什么拦截哪些内容并内容进行哪些操作)

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否登录
        HttpSession session= request.getSession(false);
        if(session!=null && session.getAttribute("username")!=null){
            //不拦截
            return true;
        }
        //拦截后进行的操作
        response.setStatus(401);
        return false;
    }
}

将自定义拦截器加入到系统中:

import com.example.springaopdemo.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class logConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/user/login");
    }
}

其中:

  • addPathPatterns:表示需要拦截的 URL,“**”表示拦截任意⽅法(也就是所有⽅法)。
  • excludePathPatterns:表示需要排除的 URL。

说明:以上拦截规则可以拦截此项⽬中的使⽤ URL,包括静态⽂件(图⽚⽂件、JS 和 CSS 等⽂件)

实现类还是我们上边的:

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public Boolean login(HttpServletRequest request, String username,String password){
        log.info("login....");
//        if(username!=null && "".equals(username) && password!=null && "".equals((password))){
//
//        }
        if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password)){
            return false;
        }
        if(!"admin".equals(username)||!"admin".equals(password)){
            return false;
        }
        HttpSession session= request.getSession(true);
        session.setAttribute("username",username);
        return true;

    }
    @RequestMapping("set")
    public String setup(){
        log.info("setup...");
        return "setup....";
    }
    @RequestMapping("/logout")
    public String logout(){
        log.info("logout...");
        return "logout.....";
    }
}

8. 使用AOP实现常用的统一功能处理

8.1 ⽤户统⼀登录验证的问题

实现就是上边的拦截器的例子,但是我们可以对登录类再进行优化。

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/user")
@RestController
public class UserController {
    @RequestMapping("/login")
    public Boolean login(HttpServletRequest request, String username,String password){
        log.info("login....");
//        if(username!=null && "".equals(username) && password!=null && "".equals((password))){
//
//        }
        if(!StringUtils.hasLength(username)||!StringUtils.hasLength(password)){
            return false;
        }
        if(!"admin".equals(username)||!"admin".equals(password)){
            return false;
        }
        HttpSession session= request.getSession(true);
        session.setAttribute("username",username);
        return true;
}

8.2 统⼀异常处理

统⼀异常处理使⽤的是 @ControllerAdvice + @ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器,两个结合表示当出现异常的时候执⾏某个通知, 也就是执⾏某个⽅法事件,具体实现代码如下:

实现类:

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ExceptionController {
    @RequestMapping("/test1")
    public boolean test1(){
        int a=10/0;
        return true;
    }
    @RequestMapping("/test2")
    public boolean test2(){
        String str=null;
        System.out.println(str.length());
        return  true;
    }
    @RequestMapping("/test3")
    public String test3(){
        throw new RuntimeException("test手动抛出异常");
    }
}

异常控制类:

import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
@ResponseBody
@ControllerAdvice//控制器通知
public class ErrorHandler {

    @ExceptionHandler
    public Object error1(Exception e){
        HashMap<String,Object> map=new HashMap<>();
        map.put("success",0);
        map.put("code",-1);
        map.put("msg","内部异常");
        return map;
    }

    @ExceptionHandler
    public Object error2(ArithmeticException e){
        HashMap<String,Object> map=new HashMap<>();
        map.put("success",0);
        map.put("code",2);
        map.put("msg","算数异常");
        return map;
    }

    @ExceptionHandler
    public Object error3(NullPointerException e){
        HashMap<String,Object> map=new HashMap<>();
        map.put("success",0);
        map.put("code",2);
        map.put("msg","算数异常");
        return map;
    }

}

统一异常处理,就是在异常控制类中,类注解@ControllerAdvice和方法注解@ExceptionHandler合在一起,表示在调用的实现类发生异常的时候,异常控制类就会去匹配异常的类型,然后去执行异常控制类中对应的异常类型的操作。

异常控制类里边我们对异常进行了类型划分,匹配的时候会根据他们的类型去自动执行相应的异常中的操作,但是环境是个很重要的问题,在我们单独是实现上边的时候,都可以自动匹配到正确的异常类型并返回,但是当多个环境相叠加的时候就可能会出现匹配错误的情况了。例如当我们叠加一个环绕通知的时候,返回的异常信息就不是正确的异常信息了。

 这是因为在环绕通知的时候,方法的调用在中间,后边我们会返回的异常去进行处理,所以,最后的异常结果就是环绕通知最后异常处理的结果。那么怎么解决呢?
我们在环绕通知中不对异常进行处理,再次抛出就好了。

8.3 统一数据返回

一般为了方便接口的处理,我们会返回对象,返回对象就需要统一格式,统一格式处理会包含一下几点,业务处理是否成功,成功的话执行结果是什么,失败的话失败的原因是什么。

统⼀数据返回格式的优点有很多,⽐如以下⼏个:

  1. ⽅便前端程序员更好的接收和解析后端数据接⼝返回的数据。
  2. 降低前端程序员和后端程序员的沟通成本,按照某个格式实现就⾏了,因为所有接⼝都是这样返回 的。
  3. 有利于项⽬统⼀数据的维护和修改。
  4. 有利于后端技术部⻔的统⼀规范的标准制定,不会出现稀奇古怪的返回内容。

代码实现,在返回结果错误的结果处理对应上边异常处理部分,这里我们只实现成功的时候的结果怎么进行统一格式返回,具体代码如下:

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.HashMap;
@ControllerAdvice
public class ResponseHandler implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    @SneakyThrows//帮我们实现了try-catch
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        HashMap<String,Object> map=new HashMap<>();
        map.put("success",1);
        map.put("date",body);
        map.put("errMsg","");
        //对字符串进行特殊处理
        if(body instanceof String){
            ObjectMapper mapper=new ObjectMapper();
            return mapper.writeValueAsString(map);
        }
        return map;
    }
}

注意:如果数据是String类型那么SpringMVC内部就会进行匹配,用hashmap的格式去匹配String类型,就会报错,所以在是String的时候我们不需要去匹配,直接输出就好了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值