深入浅出Spring AOP:面向切面编程的实战与解析-程序员宅基地

技术标签: spring  java  开发语言  

导语

Spring AOP(面向切面编程)作为Spring框架的核心特性之一,提供了强大的横切关注点处理能力,使得开发者能够更好地解耦系统架构,将非功能性需求(如日志记录、事务管理、权限控制等)从主业务逻辑中抽离出来,实现模块化的交叉关注点处理。本文将带你逐步探索Spring AOP的关键技术要点及其实战应用。

 

一、AOP基础概念

在Spring AOP中,有几个基础概念对于理解和使用AOP至关重要。以下是对这些概念的详细解释,并配合Java示例代码说明:


1.切面(Aspect)

  • 定义:切面是关注点的模块化,包含了通知(Advice)和切入点(Pointcut)的定义。它是AOP的核心部分,代表了应用中的某个特定关注点,比如事务管理、日志记录等。
   @Aspect
   public class LoggingAspect {

       // 这个类就是一个切面
   }
   

2.连接点(Join Point)

  • 定义:连接点是在程序执行过程中明确的一个点,例如一个方法调用、字段访问等。在Spring AOP中,仅支持方法级别的连接点。
   // 比如,在整个应用中有成千上万个方法调用,每一个都是潜在的连接点
   public class UserService {
       public void addUser(User user) {...}
   }
   

3.切入点(Pointcut)

  • 定义:切入点是一个或多个连接点的集合,定义了哪些连接点将被执行增强。在Spring AOP中,使用 AspectJ 表达式来指定切入点。
   @Pointcut("execution(* com.example.service.*.*(..))")
   public void anyServiceMethod() {}

   // 上述表达式表示所有位于com.example.service包及其子包下的任何公共方法
   

4.通知(Advice)

  • 定义:通知是在特定连接点上执行的操作,它可以是方法级别的拦截器,根据不同时机有不同的类型:
    • 前置通知(Before Advice):在方法执行前执行。
     @Before("anyServiceMethod()")
     public void logBefore(JoinPoint joinPoint) {
         System.out.println("Before executing: " + joinPoint.getSignature().getName());
     }
     
  • 后置通知(After Advice):无论方法是否抛出异常都会执行,但在方法返回结果之后。
    • 返回通知(AfterReturning Advice):在方法成功执行并返回结果后执行。
    • 异常通知(AfterThrowing Advice):在方法抛出异常后执行。
    • 环绕通知(Around Advice):最强大的一种通知,可以控制方法的执行流程,决定方法是否执行,何时执行以及如何执行。
     @Around("anyServiceMethod()")
     public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
         long start = System.currentTimeMillis();
         try {
             Object result = pjp.proceed(); // 执行目标方法
             System.out.println("Method executed in: " + (System.currentTimeMillis() - start) + "ms");
             return result;
         } catch (Throwable ex) {
             System.err.println("Exception caught: " + ex.getMessage());
             throw ex;
         }
     }
     

二、Spring AOP实现机制

Spring AOP 实现机制主要是基于代理模式来实现的。它有两种主要的代理实现方式:JDK动态代理和CGLIB代理。


JDK动态代理
JDK动态代理通过实现java.lang.reflect.Proxy接口来创建代理对象。当目标类实现了至少一个接口时,Spring AOP会优先使用JDK动态代理。


示例代码:

public interface MyService {
    void doSomething();
}

@Service
public class MyServiceImpl implements MyService {
    @Override
    public void doSomething() {
        // 主业务逻辑
    }
}

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.MyService.doSomething(..))")
    public void logBefore() {
        System.out.println("Before method execution");
    }
}

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = false) // 默认情况下使用JDK代理
public class AppConfig {
    // ...
}

在这个例子中,MyServiceImpl类实现了MyService接口,Spring AOP通过JDK动态代理为MyService接口创建一个代理对象。当调用doSomething方法时,代理对象会在调用真实方法之前执行LoggingAspect切面中的logBefore方法。

CGLIB代理
当目标类没有实现任何接口时,Spring AOP会选择CGLIB库来生成一个代理子类,扩展自目标类并在其中插入横切逻辑。


示例代码(CGLIB代理需要显式指定):

@Service
public class NonInterfaceService {
    public void doSomething() {
        // 主业务逻辑
    }
}

// 由于NonInterfaceService没有实现接口,Spring AOP将使用CGLIB代理
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
    // ...
}

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.NonInterfaceService.doSomething(..))")
    public void logBefore() {
        System.out.println("Before method execution");
    }
}

在CGLIB代理的例子中,尽管NonInterfaceService类没有实现任何接口,Spring AOP依然可以通过CGLIB生成它的子类代理,在调用doSomething方法时插入切面逻辑。


工作原理
无论是JDK动态代理还是CGLIB代理,Spring AOP都是通过在代理对象的方法调用时,插入切面逻辑来实现横切关注点的处理。代理对象在运行时“拦截”方法调用,执行对应的切面通知,然后再调用实际的目标方法。这样就达到了在不修改原有业务逻辑代码的情况下,添加通用处理逻辑的目的。

三、Spring AOP API

Spring AOP API主要包括一系列注解和接口,用于定义切面、切入点、通知等。以下是关键API元素的示例代码和详细讲解:


1. 定义切面(Aspect) - 使用@Aspect注解

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class LoggingAspect {

    // 切面内部包含各种通知方法
}

@Aspect注解标记了一个Java类作为切面,表示这个类中包含了一系列与横切关注点相关的通知。


2. 定义切入点(Pointcut) - 使用@Pointcut注解

@Pointcut("execution(* com.example.service.*.*(..))")
public void businessServiceOperation() {}

@Pointcut注解定义了一个切入点表达式,该表达式标识了那些方法调用会被切面影响。上面的表达式匹配了com.example.service包及其子包下所有类的所有方法。


3. 定义通知(Advice) - 使用不同类型的注解

  • 前置通知(Before Advice):在目标方法执行前调用。
@Before("businessServiceOperation()")
public void beforeAdvice(JoinPoint joinPoint) {
    System.out.println("Executing Before advice on method: " + joinPoint.getSignature().getName());
}
  • 后置通知(After Advice):在目标方法执行完后(无论是否有异常)调用,无法访问到方法的返回值。
@After("businessServiceOperation()")
public void afterAdvice(JoinPoint joinPoint) {
    System.out.println("Executing After advice on method: " + joinPoint.getSignature().getName());
}
  • 返回后通知(AfterReturning Advice):在目标方法成功执行并返回后调用,可以访问到方法的返回值。
@AfterReturning(pointcut = "businessServiceOperation()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
    System.out.println("Executing AfterReturning advice on method: " + joinPoint.getSignature().getName() + ", Result: " + result);
}
  • 异常抛出通知(AfterThrowing Advice):在目标方法抛出异常后调用,可以访问到抛出的异常。
@AfterThrowing(pointcut = "businessServiceOperation()", throwing = "exception")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception exception) {
    System.out.println("Executing AfterThrowing advice on method: " + joinPoint.getSignature().getName() + ", Exception: " + exception.getMessage());
}
  • 环绕通知(Around Advice):最强大的通知类型,可以完全控制目标方法的执行,可以选择是否执行目标方法,也可以修改方法的返回值。
@Around("businessServiceOperation()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    System.out.println("Starting Around advice on method: " + joinPoint.getSignature().getName());

    try {
        // 前置逻辑
        Object result = joinPoint.proceed(); // 调用目标方法
        
        // 后置逻辑
        System.out.println("Completed Around advice on method, Result: " + result);

        return result;
    } catch (Throwable throwable) {
        // 处理异常逻辑
        System.out.println("Exception thrown from method: " + joinPoint.getSignature().getName());
        throw throwable;
    }
}

4. Spring AOP自动代理
为了使以上切面生效,你需要将此切面类加入Spring容器,并启用AOP代理。在Spring Boot中,通常通过@EnableAspectJAutoProxy注解开启自动代理:

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // ...
}

这样,Spring容器在创建bean时,会为符合条件的bean生成代理对象,当调用代理对象的方法时,就会触发相应的切面通知。

四、配置AOP

Spring AOP可以通过Java注解和XML配置两种方式进行配置。这里我们分别介绍这两种配置方式的示例代码和详细讲解。


1. Java注解方式配置AOP


a. 创建切面类

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.service.*.*(..))")
    public void businessServiceMethods() {}

    @Before("businessServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        // 前置通知逻辑
    }

    @AfterReturning(pointcut = "businessServiceMethods()", returning = "result")
    public void logAfterReturning(Object result) {
        // 后置通知逻辑
    }

    // 其他通知类型的实现...
}
  • @Aspect 注解表示这是一个切面类。
  • @Component 注解使切面类成为Spring容器中的一个bean。
  • @Pointcut 定义了一个切入点表达式,标识了所有在com.example.service包下定义的方法。
  • @Before 和 @AfterReturning 分别定义了在方法执行前和执行后返回后的通知方法。

 

b. 配置Spring扫描并启用AOP
在Spring Boot应用中,通常只需要添加@EnableAspectJAutoProxy注解在启动类上即可自动扫描带有@Aspect注解的类并启用AOP。

@SpringBootApplication
@EnableAspectJAutoProxy
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2. XML配置方式配置AOP


a. 配置切面类

<aop:config>
    <aop:aspect id="loggingAspect" ref="loggingAspectBean">

        <!-- 定义切入点 -->
        <aop:pointcut id="businessServiceMethods"
                      expression="execution(* com.example.service.*.*(..))"/>

        <!-- 前置通知 -->
        <aop:before method="logBefore" pointcut-ref="businessServiceMethods"/>

        <!-- 后置通知 -->
        <aop:after-returning method="logAfterReturning"
                             pointcut-ref="businessServiceMethods"
                             returning="result"/>

        <!-- 其他通知类型的配置... -->
    </aop:aspect>
</aop:config>

<!-- 配置切面类 bean -->
<bean id="loggingAspectBean" class="com.example.aspect.LoggingAspect"/>
  • <aop:config> 标签开始定义AOP配置区域。
  • <aop:aspect> 标签定义一个切面,id属性为其命名,ref属性引用切面类的bean。
  • <aop:pointcut> 定义一个切入点,expression属性内填写切入点表达式。
  • <aop:before> 和 <aop:after-returning> 分别定义前置通知和后置通知,method属性指向切面类中的通知方法,pointcut-ref属性引用前面定义的切入点。

 

b. 配置Spring扫描并启用AOP
在Spring的XML配置文件中,你需要启用AOP名称空间,并确保Spring能够扫描到包含切面类的包。

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 开启AOP自动代理 -->
    <aop:aspectj-autoproxy/>

    <!-- 包扫描配置 -->
    <context:component-scan base-package="com.example"/>

    <!-- 其他配置... -->
</beans>

在这里,<aop:aspectj-autoproxy/> 开启了AOP自动代理功能,而 <context:component-scan> 标签用于指定Spring应该扫描哪些包以查找标注了@Component、@Service等注解的bean,这其中包括我们的切面类。

五、实战应用

Spring AOP 在实战中主要用于处理横切关注点,比如日志记录、事务管理、权限验证、性能统计等。下面给出一个使用Spring AOP进行日志记录的实战应用示例,并详细讲解。
假设有一个简单的服务接口UserService,以及其实现类UserServiceImpl,现在希望在调用服务方法时自动记录日志。


定义服务接口和服务实现

package com.example.service;

public interface UserService {
    void addUser(User user);
    void updateUser(User user);
    void deleteUser(Long id);
}

@Service
public class UserServiceImpl implements UserService {
    // 实现具体的业务逻辑
    @Override
    public void addUser(User user) {
        // 添加用户的逻辑...
    }

    @Override
    public void updateUser(User user) {
        // 更新用户的逻辑...
    }

    @Override
    public void deleteUser(Long id) {
        // 删除用户的逻辑...
    }
}

 

创建日志切面

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    private final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    // 定义切入点,这里是所有`UserService`接口方法的执行
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {
    }

    // 定义前置通知,在执行方法前记录日志
    @Before("userServiceMethods()")
    public void logBefore(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        logger.info("方法名:{},准备执行...", signature.getName());
        // 可以进一步获取方法参数等信息并加入日志
    }
}

在上面的代码中:

  • @Aspect 表明LoggingAspect是一个切面类。
  • @Component 将这个切面类注册为Spring Bean,以便Spring AOP能自动发现和管理。
  • @Pointcut 定义了一个切入点表达式,匹配UserService接口的所有方法。
  • @Before 注解的方法会在符合userServiceMethods切入点条件的方法执行前调用,负责记录日志。

 

通过这种方式,每当调用UserService接口中的任何一个方法时,都会先执行logBefore方法记录日志,然后才执行实际的服务方法。这种做法极大地提高了代码复用性和可维护性,同时降低了侵入性,让业务代码更加清晰简洁。

六、Spring Boot整合AOP

在Spring Boot项目中整合Spring AOP非常简单,因为Spring Boot已经内置了对AOP的支持。下面是一个Spring Boot整合Spring AOP的完整示例,包括创建切面类、定义切入点和通知,以及启动Spring Boot项目时自动启用AOP代理。


Step 1: 添加依赖
在pom.xml文件中,如果使用Maven构建项目,确保已经引入了Spring AOP相关的起步依赖:

<dependencies>
    <!-- Spring Boot Starter Web 或其他组件可能已经包含了此依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
</dependencies>

 

Step 2: 创建切面类

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class LoggingAspect {

    @Around("execution(* com.example.service..*.*(..))")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        // 方法执行前的逻辑
        System.out.println("方法 " + joinPoint.getSignature().getName() + " 开始执行,时间:" + start);
        
        try {
            // 调用方法并获取返回值
            Object result = joinPoint.proceed();

            // 方法执行后的逻辑
            long end = System.currentTimeMillis();
            System.out.println("方法 " + joinPoint.getSignature().getName() + " 执行结束,耗时:" + (end - start) + " ms");

            return result;
        } catch (IllegalArgumentException e) {
            // 处理自定义异常
            System.err.println("非法参数异常:" + e.getMessage());
            throw e;
        }
    }
}

在上述代码中:

  • 使用@Aspect注解声明LoggingAspect是一个切面类。
  • 使用@Component注解将其注入Spring容器,以便Spring管理其生命周期。
  • @Around注解的方法是一个环绕通知,其表达式execution(* com.example.service..*.*(..))表示切入点,即在com.example.service及其子包下所有类的所有方法执行时触发此通知。


Step 3: 启用AOP代理
在Spring Boot项目中,默认已启用AOP代理,所以通常不需要额外的配置。但如果你在某些特殊情况下需要禁用或定制AOP配置,可以在主配置类或者配置文件中进行调整。
例如,在application.properties中,你可以设置:

spring.aop.auto=true # 默认为true,表示启用AOP代理

或者在主配置类上使用@EnableAspectJAutoProxy注解:

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy // 启用AOP代理
public class AppConfig {
    // ...
}

通过以上步骤,当你运行Spring Boot应用并调用com.example.service包下某个服务类的方法时,将会触发LoggingAspect切面类中的logAround方法,在方法执行前后打印日志。这就是Spring Boot整合Spring AOP的基本过程。

七、Spring AOP限制

Spring AOP存在一些内在的技术限制,了解这些限制有助于我们在实际开发中合理地设计和使用AOP。以下是Spring AOP的一些主要限制及相应示例说明:


1.不能拦截final方法:

Spring AOP基于代理机制实现,对于final方法,由于Java语言特性,子类不能覆盖final方法,因此Spring AOP也无法通过代理的方式在其前后增加额外的行为。
示例:

   public final class FinalClass {
       public final void finalMethod() {
           // 主要业务逻辑
       }
   }
   

上述FinalClass中的finalMethod方法无法被Spring AOP所拦截。


2.不能直接代理静态方法:

Spring AOP是基于代理(JDK动态代理或CGLIB)的方式来实现代理功能的,静态方法属于类级别的方法,而非对象实例方法,因此不能通过代理的方式对其进行增强。
示例:

   public class StaticService {
       public static void staticMethod() {
           // 主要业务逻辑
       }
   }
   

上述StaticService中的staticMethod方法无法被Spring AOP所拦截。


3.代理对象内部方法调用的问题:

如果在一个类的内部方法中调用了同一个类的另一个方法,而不是通过代理对象去调用,那么AOP将不会生效,因为此时调用的是实际对象而非代理对象的方法。
示例:

   @Service
   public class SelfCallService {
       
       public void publicMethod() {
           internalMethod(); // 直接内部调用,AOP将不会对此内部调用进行拦截
       }
       
       private void internalMethod() {
           // 主要业务逻辑
       }
   }
   

在此示例中,如果只对publicMethod进行了AOP增强,那么internalMethod的调用将不会受到AOP通知的影响。


4.只能代理Spring管理的bean:

Spring AOP仅能增强那些由Spring IoC容器管理的对象。这意味着非Spring管理的实例,或者通过new关键字直接创建的对象,其方法不会被AOP拦截器处理。


5.CGLIB代理与final类和方法:

虽然CGLIB代理可以代理没有实现接口的类,但它仍然不能代理final类和final方法。


6.构造器注入问题:

由于AOP代理是动态生成的,所以在构造器注入时,注入的将是原始类型而非代理类型。为避免这个问题,推荐使用setter或field注入。


总的来说,Spring AOP在大多数常规应用场景下是非常有效的,但在遇到上述限制时,可能需要考虑更强大的AOP框架如AspectJ,或者重新审视设计,确保业务逻辑适合使用AOP的代理模式。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/2301_78813969/article/details/137111174

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签