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日志并进行多维度分析。
相关文章
|
21天前
|
缓存 Java 开发工具
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
三级缓存是Spring框架里,一个经典的技术点,它很好地解决了循环依赖的问题,也是很多面试中会被问到的问题,本文从源码入手,详细剖析Spring三级缓存的来龙去脉。
Spring是如何解决循环依赖的?从底层源码入手,详细解读Spring框架的三级缓存
|
21天前
|
缓存 安全 Java
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
从底层源码入手,通过代码示例,追踪AnnotationConfigApplicationContext加载配置类、启动Spring容器的整个流程,并对IOC、BeanDefinition、PostProcesser等相关概念进行解释
Spring框架中Bean是如何加载的?从底层源码入手,详细解读Bean的创建流程
|
11天前
|
人工智能 开发框架 Java
重磅发布!AI 驱动的 Java 开发框架:Spring AI Alibaba
随着生成式 AI 的快速发展,基于 AI 开发框架构建 AI 应用的诉求迅速增长,涌现出了包括 LangChain、LlamaIndex 等开发框架,但大部分框架只提供了 Python 语言的实现。但这些开发框架对于国内习惯了 Spring 开发范式的 Java 开发者而言,并非十分友好和丝滑。因此,我们基于 Spring AI 发布并快速演进 Spring AI Alibaba,通过提供一种方便的 API 抽象,帮助 Java 开发者简化 AI 应用的开发。同时,提供了完整的开源配套,包括可观测、网关、消息队列、配置中心等。
554 6
|
9天前
|
XML 前端开发 Java
控制spring框架注解介绍
控制spring框架注解介绍
|
9天前
|
存储 NoSQL Java
Spring Session框架
Spring Session 是一个用于在分布式环境中管理会话的框架,旨在解决传统基于 Servlet 容器的会话管理在集群和云环境中的局限性。它通过将用户会话数据存储在外部介质(如数据库或 Redis)中,实现了会话数据的跨服务器共享,提高了应用的可扩展性和性能。Spring Session 提供了无缝集成 Spring 框架的 API,支持会话过期策略、并发控制等功能,使开发者能够轻松实现高可用的会话管理。
Spring Session框架
|
16天前
|
Java 应用服务中间件 开发者
深入探索并实践Spring Boot框架
深入探索并实践Spring Boot框架
27 2
|
16天前
|
机器学习/深度学习 数据采集 JavaScript
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
ADR药品不良反应监测系统是一款智能化工具,用于监测和分析药品不良反应。该系统通过收集和分析病历、处方及实验室数据,快速识别潜在不良反应,提升用药安全性。系统采用Java开发,基于SpringBoot框架,前端使用Vue,具备数据采集、清洗、分析等功能模块,并能生成监测报告辅助医务人员决策。通过集成多种数据源并运用机器学习算法,系统可自动预警药品不良反应,有效减少药害事故,保障公众健康。
ADR智能监测系统源码,系统采用Java开发,基于SpringBoot框架,前端使用Vue,可自动预警药品不良反应
|
25天前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
104 1
|
29天前
|
缓存 Java 应用服务中间件
随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架
【9月更文挑战第6天】随着微服务架构的兴起,Spring Boot凭借其快速开发和易部署的特点,成为构建RESTful API的首选框架。Nginx作为高性能的HTTP反向代理服务器,常用于前端负载均衡,提升应用的可用性和响应速度。本文详细介绍如何通过合理配置实现Spring Boot与Nginx的高效协同工作,包括负载均衡策略、静态资源缓存、数据压缩传输及Spring Boot内部优化(如线程池配置、缓存策略等)。通过这些方法,开发者可以显著提升系统的整体性能,打造高性能、高可用的Web应用。
58 2
|
1月前
|
Cloud Native 安全 Java
Micronaut对决Spring Boot:谁是微服务领域的王者?揭秘两者优劣,选对框架至关重要!
【9月更文挑战第5天】近年来,微服务架构备受关注,Micronaut和Spring Boot成为热门选择。Micronaut由OCI开发,基于注解的依赖注入,内置多种特性,轻量级且启动迅速;Spring Boot则简化了Spring应用开发,拥有丰富的生态支持。选择框架需考虑项目需求、团队经验、性能要求及社区支持等因素。希望本文能帮助您选择合适的微服务框架,助力您的软件开发项目取得成功!
99 2
下一篇
无影云桌面