Lesson9:Spring AOP

目录

一、Spring AOP的定义

1.1 AOP(Aspect Oriented Programming)面向切面编程

二、Spring AOP的作用

三、AOP组成

3.1 切面Aspect

3.2 连接点Join Point

3.3 切点PointCut

3.4 通知Advice

3.5在Spring切面类中,如何将一个方法设置为通知方法——加注解

四、Spring AOP的实现

4.1 添加Spring AOP框架支持

4.2 定义切面和切点

4.3 定义通知

五、Spring AOP实现原理(使用动态代理)

六、AOP实战

6.1 统一用户登录权限验证

6.2 统一数据返回格式

6.2.1 统一数据返回格式的原因

6.2.2 具体实现@ControllerAdvice+ResponseBodyAdvice

6.3 统一异常处理@ControllerAdvice+@ExceptionHandler

总结


一、Spring AOP的定义

1.1 AOP(Aspect Oriented Programming)面向切面编程

AOP是一种编程思想,他是指对一类事情进行集中处理。以博客系统为例,跳转到博客列表页、博客编辑页等页面,每个页面都需要判断用户是否登录,这样代码的冗余度较高。当我们学习了AOP,只需要进行配置一下,所有的页面都可以实现判断用户登录的功能。这样,我们的代码就更加简洁明了。

AOP是一种编程思想,SpringAOP是一个框架,是对AOP思想的具体实现

二、Spring AOP的作用

我们主要借助AOP实现功能统一,并且使用的地方较多的功能。AOP的常见实现功能有以下几种:

统一的用户判断登录、统一日志记录、统一方法执行时间统计、统一的返回格式设置、统一的异常处理、事务的开启和提交等

使用AOP可以扩充多个对象的某个能力,AOP是对OOP(Object Oriented Programming,⾯向对象编程)的补充和完善。

三、AOP组成

3.1 切面Aspect

切面是由切点和通知组成,它既包含了横切逻辑的定义,也包括了连接点的定义。

切面相当于包含了切点、通知和切面的类,相当于AOP实现的某个功能的集合。

3.2 连接点Join Point

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

连接点相当于需要被增强的某个AOP功能的所有方法。

3.3 切点PointCut

切点的作用是提供一组规则来匹配切面,给满足规则的连接点添加通知。

切点相当于保存了众多连接点的一个集合。

3.4 通知Advice

切面的工作叫做通知。

在通知中,需要定义切面是什么、切面什么时候适用,需要描述切面要完成的工作以及何时完成这个工作

3.5在Spring切面类中,如何将一个方法设置为通知方法——加注解

在方法上加上不同的注解,可以将其设置为通知,并且会在合适的时机调用这个通知方法。通知分为以下6种。

前置通知@Before:在目标方法调用前执行

后置通知@After:会在⽬标⽅法返回或者抛出异常后调⽤

返回之后通知@AfterReturning:在目标方法返回后调用

抛出异常后通知@AfterThrowing:在目标方法抛出异常后调用

环绕通知@Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

四、Spring AOP的实现

4.1 添加Spring AOP框架支持

在pom.xml添加以下配置:

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

4.2 定义切面和切点

切面:创建一个类,加上@Aspect注解,即可声明此类是一个切面

切点表达式的语法:

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

execution(<修饰符><返回类型><包.类.方法(参数)><异常>)

其中,修饰符和异常可以省略。

 其中:

* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)

.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。

+ :表示按照类型匹配指定类的所有类,必须跟在类名后⾯

接下来,举一个例子,拦截Controller.UserController类下的所有方法。

@Component
@Aspect  // 声明此类是一个切面
public class UserAspect {
    @Pointcut("execution(* com.example.demo.Controller.UserController.*(..)) ")
    public void pointcut(){}
    // pointcut()为空方法,不需要有方法体,只是声明方法,具体实现在通知里面写
}

pointcut ⽅法为空⽅法,它不需要有⽅法体,此⽅法名就是起到⼀个“标识”的作⽤,标识下⾯的通 知⽅法具体指的是哪个切点(因为切点可能有很多个)。

4.3 定义通知

通知中的内容是对切点方法的具体实现。举个例子:切点定义一个验证用户登录的空方法,通知中需要具体的实现这个方法要执行的任务。

通知分为前置通知、后置通知、返回之后通知、抛出异常之后通知、环绕通知五种。以下是五种通知的简单实现:

package com.example.demo.component;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component
@Aspect  // 声明此类是一个切面
public class UserAspect {
    @Pointcut("execution(* com.example.demo.Controller.UserController.*(..))")
    public void pointcut(){}
    // pointcut()为空方法,不需要有方法体,只是声明方法,具体实现在通知里面写

    //前置通知
    @Before("pointcut()")
    public void doBefore(){
        System.out.println("前置通知");
    }

    // 后置通知
    @After("pointcut()")
    public void doAfter(){
        System.out.println("后置通知");
    }

    // 返回之前通知
    @AfterReturning("pointcut()")
    public void afterReturn(){
        System.out.println("返回之前通知");
    }

    // 抛出异常之后通知
    @AfterThrowing("pointcut()")
    public void doAfterThrowing(){
        System.out.println("抛出异常之前通知");
    }

    // 环回通知
    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object obj = null;
        System.out.println("执行环绕通知的前置方法");
        try {
            obj = joinPoint.proceed(); // 执行拦截的方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        System.out.println("执行环绕通知的后置方法");
        return obj;
    }
}

注意:环绕通知比较麻烦,需要程序员手动的去触发

以下代码是UserController类的具体实现,

package com.example.demo.Controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@ResponseBody
@Controller
public class UserController {

    @RequestMapping("/hello")
    public String sayhi() throws InterruptedException {
        System.out.println("------hello-------");
        return "hello";
    }
}

小练习:适用环绕通知计算程序的运行时间

    @Around("pointcut()")
    public Object doAround(ProceedingJoinPoint joinPoint){
        Object obj = null;
        System.out.println("执行环绕通知的前置方法");
        long t1 = System.currentTimeMillis();
        // 在UserController中的方法被执行之前干的事情
        try {
            obj = joinPoint.proceed(); // 执行拦截的方法,即在UserController中的方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        // 在UserController中的方法被执行之后干的事情
        System.out.println("执行环绕通知的后置方法");
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("程序的执行时间:%d",t2-t1));
        return obj;
    }
    @RequestMapping("/runtime")
    public void runtime() throws InterruptedException {
        Thread thread = new Thread();
        thread.sleep(1000);
    }

五、Spring AOP实现原理(使用动态代理)

SpringAOP是通过动态代理的方式,在运行期将AOP代码织入到程序中的,他的实现方式有两种,JDK Proxy CGLIB。

Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的⽀持局限于⽅法级别的拦截。 Spring AOP ⽀持 JDK Proxy CGLIB ⽅式实现动态代理。

JDK Proxy:JDK自身的动态代理,在有接口的场景下会使用此方式。

CGLIB :使用字节码技术实现的动态代理,性能高,在普通类中会使用CGLIB

 调用者先调用代理类,代理类经过验证后,然后才能访问目标对象。

六、AOP实战

6.1 统一用户登录权限验证

博客系统中每个功能的实现都需要先判断用户是否登录,同样功能的代码写了多次,增加了程序的冗余度,同时增加了后期的修改成本和维修成本。我们现在可以使用SpringAOP的前置通知或者环绕通知来实现。在用户登录之后,会将用户的登录信息保存在session中,在实现其他的功能时,需要先读取session中的user信息。要想获取session,我们得先获得HttpSession对象。同时,并不是所有得方法都需要验证用户的登录信息,如登录和注册。将问题总结为以下两点:

①要想获取session,我们得先获得HttpSession对象。

②只需要拦截部分的方法,登录、注册这样的方法不用拦截。

我们可以使用Spring拦截器HandlerInterceptor,拦截器的实现分两个步骤:

①创建自定义拦截器,实现HandlerInterceptor接口的preHandle方法。

package com.example.demo.component;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
// 当前的类只是一个普通类,但是并没有加到Spring中。目前的情况就是:拦截器和拦截规则已经有了,但是还没有生效,要想让他生效,还需要配置一下
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(ApplicationVariable.USER_SESSION_KEY)!= null){
            // 如果是true,会访问当前你想要访问的目标方法
            log.info("--------------拦截器验证通过--------------");
            return true;
        }
        log.info("--------------拦截器验证未通过--------------");
        response.setStatus(401);
        // 如果是false。访问到此结束
        return false;
    }


}

②将自定义的拦截器加入WebMvcConfigure的addInterceptors方法中。

package com.example.demo.component;

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 AppConfig implements WebMvcConfigurer {
    // 添加拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor()) // 指明拦截器
                .addPathPatterns("/**") // 表示需要拦截的URL,**表示拦截任意方法
                .excludePathPatterns("/login");
//                .excludePathPatterns("/**/register") // 需要排除的URL和静态文件,如图片、js、css等文件
//                .excludePathPatterns("/**/*.js")
//                .excludePathPatterns("/**/*.css")
//                .excludePathPatterns("/**/*.jpg")
//                .excludePathPatterns("/**/*.html")
//                .excludePathPatterns("/**/login");
    }
}

为了验证拦截器代码的正确性,写一个login的伪代码,约定name=admin,password=1111即登录成功。登录成功后将用户信息保存在session中。

先建立一个user类存储用户信息

package com.example.demo.component;

import lombok.Data;
import org.springframework.stereotype.Component;

@Data
@Component
public class User {
    private String name;
    private String password;
}

再写login的伪代码:

@RequestMapping("/login")
    public String login(HttpServletRequest req, String name, String password){
        if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
                && "admin".equals(name) && "1111".equals(password)){
            HttpSession session = req.getSession();
            User user = new User();
            user.setName(name);
            user.setPassword(password);
            System.out.println(user);
            session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
            return "登录成功";
        }
        return "登录失败";
    }

将用户的session的可以设置为一个静态常量。

package com.example.demo.component;

public class ApplicationVariable {
    public final static String USER_SESSION_KEY = "USER_SESSION_KEY";
}

验证:

先写一个sayhi方法,不登陆直接访问,会返回一个401

    @RequestMapping("/hello")
    public String sayhi() throws InterruptedException {
        System.out.println("------hello-------");
        return "hello";
    }

,也会提示验证未通过。 

接下来,先登录再访问hello页面

6.2 统一数据返回格式

6.2.1 统一数据返回格式的原因

方便前端程序员更好的接收和解析后端数据接口返回的数据。

降低前端程序员和后端程序员的沟通成本,按照某个格式实现就行了,因为所有接口都是这样返回
的。

有利于项目统一数据的维护和修改。

有利于后端技术部门的统一规范的标准制定,不会出现奇奇怪怪的返回内容。

为了统一数据的返回格式,我们可以使用拼接字符串的形式:

    @RequestMapping("/login")
    public HashMap<String,Object> login(HttpServletRequest req, String name, String password){
        HashMap<String,Object> result = new HashMap<>();
        if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
                && "admin".equals(name) && "1111".equals(password)){
            HttpSession session = req.getSession();
            User user = new User();
            user.setName(name);
            user.setPassword(password);
            System.out.println(user);
            session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
            result.put("success",1);
            result.put("message","");
            result.put("data",true);
            return result;
        }
        result.put("success",-1);
        result.put("message","");
        result.put("data",false);
        return result;
    }

然而我们的理想代码是:

    @RequestMapping("/login2")
    public Boolean login2(HttpServletRequest req, String name, String password){
        if(StringUtils.hasLength(name) && StringUtils.hasLength(password)
                && "admin".equals(name) && "1111".equals(password)){
            HttpSession session = req.getSession();
            User user = new User();
            user.setName(name);
            user.setPassword(password);
            System.out.println(user);
            session.setAttribute(ApplicationVariable.USER_SESSION_KEY,user);
            return true;
        }
        return false;
    }

借助SpringAOP,我们可以只关注我们的业务逻辑,统一返回数据格式可以依靠SpringAOP。

6.2.2 具体实现@ControllerAdvice+ResponseBodyAdvice

package com.example.demo.common;

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 ResponseAdvice implements ResponseBodyAdvice {
    /**
     * 返回的内容是否需要重写
     * @return 返回true表示重写
     */
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }

    /**
     * 最终返回给前端封装的数据对象
     * @param body  方法返回的原始数据
     * @param returnType  方法的返回类型
     * @param selectedContentType  html标签、json
     * @param selectedConverterType  返回值的类的类型
     * @param request
     * @param response
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 构造统一的返回对象
        HashMap<String,Object> result = new HashMap<>();
        result.put("success",1);
        result.put("message","");
        result.put("data",body);
        return result;
    }
}

6.3 统一异常处理@ControllerAdvice+@ExceptionHandler

统一异常处理使用的是@ControllerAdvice和@ExceptionHandler来实现的,@ControllerAdvice表示控制器通知类,@ExceptionHandler是异常处理器,两个结合表示当出现异常的时候执行某个通知。

处理空指针的异常:

package com.example.demo.common;

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

import java.util.HashMap;

@ControllerAdvice
public class ErrorAdvice {

    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public HashMap<String,Object> nullPointerExceptionHandler(NullPointerException e){
        //如果出现了异常,就给前端返回一个HashMap的对象
        HashMap<String,Object> map = new HashMap<>();
        System.out.println("exception");
        map.put("success",0);
        map.put("status",1);
        map.put("msg",e.getMessage());
        System.out.println(map);
        return map;
    }
}

在UserController类中的sayhi方法中写一个空指针异常,代码如下

    @RequestMapping("/hello")
    public String sayhi() throws InterruptedException {
        int[] elem = null;
        int a = elem[0];
        return "hello";
    }

如果此时写一个算数异常:

    @RequestMapping("/hello")
    public String sayhi() throws InterruptedException {
        int a = 1/0;
        return "hello";
    }

刚刚写的方法不能处理算数异常,此时的解决方案有两个:

方案一,写一个函数啊,处理算数异常 

    @ExceptionHandler(ArithmeticException.class)
    @ResponseBody
    public HashMap<String,Object> nullPointerExceptionHandler(ArithmeticException e){
        //如果出现了异常,就给前端返回一个HashMap的对象
        HashMap<String,Object> map = new HashMap<>();
        System.out.println("算数exception");
        map.put("success",0);
        map.put("status",1);
        map.put("msg",e.getMessage());
        System.out.println(map);
        return map;
    }

 效果:

 方案2:写一个可以处理所有异常的函数

    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handler(Exception e){
        //如果出现了异常,就给前端返回一个HashMap的对象
        HashMap<String,Object> map = new HashMap<>();
        map.put("success",0);
        map.put("status",1);
        map.put("msg",e.getMessage());
        return map;
    }

由于无法将处理所有异常的方法都实现,因此,可以将处理Exception异常当作默认异常,不管出现啥异常,都可以将异常信息按照约定的格式返回给前端。

当处理算数异常和处理Exception的方法同时存在,发生算数异常时,调用的是前者。

总结

AOP是一种编程思想,他是指对一类事情进行集中处理。SpringAOP该思想的实现,是通过动态代理的方式,在运行期将AOP代码织入到程序中,实现方式有JDK Proxy 和 CGLIB两种。AOP由切面和连接点组成,切面由切点和通知组成。在Spring中,在类上加上@Aspect注解,即可声明此类是一个切面。切点由切点函数组成,常用的切点函数是execution()。切点函数是没有函数体,切点函数的具体实现由通知实现。最后通过统一用户登录权限验证、统一数据返回格式、统一异常处理介绍了AOP的应用。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

刘减减

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值