Spring框架完全掌握(下)

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: Spring框架完全掌握(下)

接着上一篇文章的内容Spring框架完全掌握(上),我们继续深入了解Spring框架。

Spring_AOP

考虑到AOP在Spring中是非常重要的,很有必要拿出来单独说一说。所以本篇文章基本上讲述的就是关于Spring的AOP编程。

简介

先看一个例子:

package com.itcast.spring.bean.calc;

public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
   
   

    @Override
    public int add(int num1, int num2) {
   
   
        int result = num1 + num2;
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
   
   
        int result = num1 - num2;
        return result;
    }

    @Override
    public int mul(int num1, int num2) {
   
   
        int result = num1 * num2;
        return result;
    }

    @Override
    public int div(int num1, int num2) {
   
   
        int result = num1 / num2;
        return result;
    }
}

这是一个实现四则运算接口的实现类,能够进行两个数之间的加减乘除。而这个时候,我们有一个需求,就是在每个方法执行前后都必须输出日志信息,那么我们就得在每个方法中都加上日志信息:

...
@Override
    public int add(int num1, int num2) {
   
   
        System.out.println("add method start with[" + num1 + "," + num2 + "]");
        int result = num1 + num2;
        System.out.println("add method start with[" + num1 + "," + num2 + "]");
        return result;
}
...

这样所带来的问题是什么呢?

  1. 代码混乱:越来越多的非业务需求(例如日志、参数验证等)加入后,原有的业务方法急剧膨胀,每个方法在处理核心逻辑的同时还必须兼顾其它多个关注点。
  2. 代码分散:以日志需求为例,只是为了满足这个单一需求,就不得不在多个模块里多次重复相同的日志代码,如果日志需求发生变化,必须修改所有模块中的日志代码。

既然问题出现了,该如何解决呢?(使用动态代理)

public class ArithmeticCalculatorLoggingProxy {
   
   

    private ArithmeticCalculator target;

    public ArithmeticCalculator getLoggingProxy() {
   
   
        ArithmeticCalculator proxy = null;

        ClassLoader loader = target.getClass().getClassLoader();
        Class[] interfaces = new Class[] {
   
    ArithmeticCalculator.class };
        InvocationHandler h = new InvocationHandler() {
   
   

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   
   
                System.out.println(method.getName() + "method start with[ " + Arrays.asList(args) + "]");
                Object result = method.invoke(target, args);
                System.out.println(method.getName() + "method end with[ " + result + "]");
                return result;
            }
        };
        proxy = (ArithmeticCalculator) Proxy.newProxyInstance(loader, interfaces, h);
        return proxy;
    }
}

这样我们就可以去获取代理对象从而实现日志业务却不改变基本业务代码。
其实这样实现还是略显麻烦,但不用担心,Spring框架为我们提供了一种实现方式——AOP。
AOP(Aspect-Oriented Programming,面向切面编程):这是一种新的方法论,是对传统OOP(Object-Oriented Programming,面向对象编程)的补充,AOP的主要编程对象是切面。
在应用AOP编程时,仍然需要定义公共功能,但可以明确地定义这个功能在哪里,以什么方式应用,并且不必修改受影响的类,这样一来,横切关注点就被模块化到特殊的对象里。
好处:

  1. 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
  2. 业务模块更简洁,只包含核心业务代码

这样来看,AOP能够非常精准地解决我们遇到了问题。

前置通知

在Spring中,可以使用基于AspectJ注解或基于XML配置的AOP。AspectJ是Java社区里最完整最流行的AOP框架,所以我们以AspectJ注解方式为例进行讲解。
首先导入AOP框架的jar包:
在这里插入图片描述
然后我们在上面的案例中进行修改:

@Component
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
   
   

    @Override
    public int add(int num1, int num2) {
   
   
        int result = num1 + num2;
        return result;
    }

    @Override
    public int sub(int num1, int num2) {
   
   
        int result = num1 - num2;
        return result;

    }

    @Override
    public int mul(int num1, int num2) {
   
   
        int result = num1 * num2;
        return result;
    }

    @Override
    public int div(int num1, int num2) {
   
   
        int result = num1 / num2;
        return result;
    }
}

这里在实现类的开头加上了一个注解,目的是将该类交由Spring容器管理,其它代码不作改动。

//将该类声明为一个切面
@Aspect
@Component
public class LoggingAspect {
   
   

    // 声明该方法是一个前置通知:在目标方法开始之前执行
    @Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.add(int,int))")
    public void beforeMethd(JoinPoint joinPoint) {
   
   
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(methodName + " method start with" + args);
    }
}

接着我们将输出日志的业务看成一个切面,创建一个类,然后任意地定义一个方法,该方法要添加一个注解:Before。用于声明该方法是一个前置通知,前置通知方法会在目标方法开始之前执行。所以我们还需要在Before中声明目标方法。该方法可以添加一个参数为JoinPoint类型,执行方法的方法名和参数都封装在该对象中。其次,该类必须也交由Spring容器管理,所以添加注解@Component,且该类为一个切面,添加注解@Aspect。
然后要在配置文件中进行配置:

<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">

    <!-- 配置自动扫描的包 -->
    <context:component-scan
        base-package="com.itcast.aop.impl"></context:component-scan>

    <!-- 使AspjectJ注解起作用:自动为匹配的类生成代理对象 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

这样,框架会去自动寻找匹配的类并生成代理对象。
最后编写测试代码:

public static void main(String[] args) {
   
   
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
        int result = ac.add(1, 1);
        System.out.println("result:" + result);
    }

运行结果:

add method start with[1, 1]
result:2

但是当你调用其它的运算方法时发现日志信息又无法打印了,这是因为你在配置目标方法的时候配置的仅仅是add()方法,所以可以采用通配符的方式将类中的所有方法都配置进去。

@Before("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")

这里的exeution是执行的意思,也就是说,该属性的括号内填写的是目标方法,对于该目标方法,可以更加抽象地进行表示,例如权限修饰符、返回值等等都可以用通配符进行替换。
到这里,SpringAOP就轻松实现了我们开始遇到的问题。

后置通知

既然有前置通知,那肯定就会有后置通知,后置通知的实现方式和前置通知类似:

@After("execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
public void afterMetohd(JoinPoint joinPoint) {
   
   
    String methodName = joinPoint.getSignature().getName();
    List<Object> args = Arrays.asList(joinPoint.getArgs());
    System.out.println(methodName + " method ends with" + args);
}

运行测试代码,结果如下:

add method start with[1, 1]
add method ends with[1, 1]
result:2

后置通知是在目标方法执行后执行,但需要注意的是,后置通知不管目标方法是否成功执行,就算目标方法在执行过程中产生了异常,后置通知仍然会执行,而且在后置通知中无法访问到目标方法的执行结果。

返回通知

返回通知和后置通知类似,但是返回通知只在目标方法正确执行完成后才执行,如果目标方法在执行过程中产生了错误,返回通知将不起作用。所以返回通知能够获取目标方法的执行结果:

    // 声明该方法是一个返回通知:在方法正常执行结束后执行
    // 返回通知是可以访问到目标方法的返回值的
    @AfterReturning(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", returning = "result")
    public void afterReturning(JoinPoint joinPoint, Object result) {
   
   
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(methodName + " method ends with" + result);
    }

运行结果:

add method start with[1, 1]
add method ends with[1, 1]
add method ends with2
result:2

异常通知

异常通知是在目标方法执行过程中产生了异常后才会执行,异常通知能够获取到目标方法产生的异常信息:

    // 声明该方法是一个异常通知:在方法执行产生异常时执行
    // 异常通知可以获取到产生的异常信息
    @AfterThrowing(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
   
   
        String methodName = joinPoint.getSignature().getName();
        List<Object> args = Arrays.asList(joinPoint.getArgs());
        System.out.println(methodName + " method's exception is " + ex);
    }

我们人为产生一个异常来测试一下:

public static void main(String[] args) {
   
   
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);

        result = ac.div(10, 0);
        System.out.println("result:" + result);
    }

运行结果:

div method start with[10, 0]
div method ends with[10, 0]
div method's exception is java.lang.ArithmeticException: / by zero

环绕通知

对于环绕通知,这在所有通知中是功能最强大的通知,其实它并不常用,但是我们还是得了解一下它的用法:

    // 声明该方法是一个环绕通知,环绕通知需要携带ProceedingJoinPoint类型的参数
    // 环绕通知类似于动态代理的全过程
    // ProceedingJoinPoint类型的参数可以决定是否执行目标方法
    // 且环绕通知必须有返回值,返回的是目标方法的返回值
    @Around(value = "execution(public int com.itcast.aop.impl.ArithmeticCalculatorImpl.*(int,int))")
    public Object aroundMethod(ProceedingJoinPoint point) {
   
   
        Object result = null;
        String methodName = point.getSignature().getName();
        // 执行目标方法
        try {
   
   
            // 前置通知
            System.out.println(methodName + " method' start with" + Arrays.asList(point.getArgs()));
            result = point.proceed();
            // 返回通知
            System.out.println(methodName + " method' end with " + result);
        } catch (Throwable e) {
   
   
            // 异常通知
            System.out.println(methodName + " method's exception is " + e);
        }
        // 后置通知
        System.out.println(methodName + " method' end with");
        return result;
    }

环绕通知能够实现其它所有通知的功能,但是它有很多限制。

  • 必须要携带ProceedingJoinPoint类型的参数
  • 环绕通知必须有返回值,返回的是目标方法的返回值

测试代码:

public static void main(String[] args) {
   
   
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ArithmeticCalculator ac = ctx.getBean(ArithmeticCalculator.class);
        int result = ac.add(1, 1);
        System.out.println("result:" + result);
}

运行结果:

add method' start with[1, 1]
add method' end with 2
add method' end with
result:2

切面的优先级

在具有多个切面的项目中,我们可以指定切面的优先级,决定切面的先后执行顺序。使用@Order()注解来配置优先级(在类开头注解),括号里填入一个整数,值越小优先级越高。
例如:

@Order(1)
public class LoggingAspect {
   
   
......
......
}

关于SpringAOP的相关内容就说到这里,如有错误,欢迎指正。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
15天前
|
XML 安全 Java
|
19天前
|
缓存 NoSQL Java
什么是缓存?如何在 Spring Boot 中使用缓存框架
什么是缓存?如何在 Spring Boot 中使用缓存框架
27 0
|
1月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
2月前
|
Java API 数据库
构建RESTful API已经成为现代Web开发的标准做法之一。Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐。
【10月更文挑战第11天】本文介绍如何使用Spring Boot构建在线图书管理系统的RESTful API。通过创建Spring Boot项目,定义`Book`实体类、`BookRepository`接口和`BookService`服务类,最后实现`BookController`控制器来处理HTTP请求,展示了从基础环境搭建到API测试的完整过程。
58 4
|
2月前
|
JavaScript 安全 Java
如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架
本文介绍了如何使用 Spring Boot 和 Ant Design Pro Vue 实现动态路由和菜单功能,快速搭建前后端分离的应用框架。首先,确保开发环境已安装必要的工具,然后创建并配置 Spring Boot 项目,包括添加依赖和配置 Spring Security。接着,创建后端 API 和前端项目,配置动态路由和菜单。最后,运行项目并分享实践心得,包括版本兼容性、安全性、性能调优等方面。
173 1
|
2月前
|
Java API 数据库
Spring Boot框架因其简洁的配置、快速的启动特性及丰富的功能集而备受开发者青睐
本文通过在线图书管理系统案例,详细介绍如何使用Spring Boot构建RESTful API。从项目基础环境搭建、实体类与数据访问层定义,到业务逻辑实现和控制器编写,逐步展示了Spring Boot的简洁配置和强大功能。最后,通过Postman测试API,并介绍了如何添加安全性和异常处理,确保API的稳定性和安全性。
41 0
|
1天前
|
IDE Java 测试技术
互联网应用主流框架整合之Spring Boot开发
通过本文的介绍,我们详细探讨了Spring Boot开发的核心概念和实践方法,包括项目结构、数据访问层、服务层、控制层、配置管理、单元测试以及部署与运行。Spring Boot通过简化配置和强大的生态系统,使得互联网应用的开发更加高效和可靠。希望本文能够帮助开发者快速掌握Spring Boot,并在实际项目中灵活应用。
19 5
|
11天前
|
缓存 Java 数据库连接
Spring框架中的事件机制:深入理解与实践
Spring框架是一个广泛使用的Java企业级应用框架,提供了依赖注入、面向切面编程(AOP)、事务管理、Web应用程序开发等一系列功能。在Spring框架中,事件机制是一种重要的通信方式,它允许不同组件之间进行松耦合的通信,提高了应用程序的可维护性和可扩展性。本文将深入探讨Spring框架中的事件机制,包括不同类型的事件、底层原理、应用实践以及优缺点。
43 8
|
21天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
34 6
|
26天前
|
Java 数据库连接 数据库
不可不知道的Spring 框架七大模块
Spring框架是一个全面的Java企业级应用开发框架,其核心容器模块为其他模块提供基础支持,包括Beans、Core、Context和SpEL四大子模块;数据访问及集成模块支持数据库操作,涵盖JDBC、ORM、OXM、JMS和Transactions;Web模块则专注于Web应用,提供Servlet、WebSocket等功能;此外,还包括AOP、Aspects、Instrumentation、Messaging和Test等辅助模块,共同构建强大的企业级应用解决方案。
47 2