Spring结合自定义注解实现 AOP 切面功能

Spring结合自定义注解实现 AOP 切面功能

  • Spring AOP 注解概述
  • @Aspect 快速入门
  • execution 切点表达式 拦截指定类的方法
  • @Pointcut("@annotation(xx)") 拦截拥有指定注解的方法
  • 环绕通知 实现开关目标方法
  • 案例1:自定义注解+切面实现统一日志处理
    • 1.自定义日志注解
    • 2声明日志切面组件
    • 3.控制层运行结果
    • 4.运行结果
  • 案例2:自定义注解与切面类
    • 1、创建自定义注解
    • 2、创建一个类,定义方法后使用自定义注解
    • 3、定义切面类进行,扫描自定义注解,并对切入点进行处理

Spring AOP 注解概述

1、Spring 的 AOP 功能除了在配置文件中配置一大堆的配置,比如切入点、表达式、通知等等以外,使用注解的方式更为方便快捷,特别是 Spring boot 出现以后,基本不再使用原先的 beans.xml 等配置文件了,而都推荐注解编程。

注解功能
@Aspect切面声明,标注在类、接口(包括注解类型)或枚举上。
@Pointcut切入点声明,即切入到哪些目标类的目标方法。既可以用 execution 切点表达式, 也可以是 annotation 指定拦截拥有指定注解的方法.value 属性指定切入点表达式,默认为 “”,用于被通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式
@Before前置通知, 在目标方法(切入点)执行之前执行。value 属性绑定通知的切入点表达式,可以关联切入点声明,也可以直接设置切入点表达式注意:如果在此回调方法中抛出异常,则目标方法不会再执行,会继续执行后置通知 -> 异常通知。
@After后置通知, 在目标方法(切入点)执行之后执行
@AfterReturning返回通知, 在目标方法(切入点)返回结果之后执行.pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 “”
@AfterThrowing异常通知, 在方法抛出异常之后执行, 意味着跳过返回通知pointcut 属性绑定通知的切入点表达式,优先级高于 value,默认为 ""注意:如果目标方法自己 try-catch 了异常,而没有继续往外抛,则不会进入此回调函数
@Around环绕通知:目标方法执行前后分别执行一些代码,类似拦截器,可以控制目标方法是否继续执行。通常用于统计方法耗时,参数校验等等操作。

正常流程:【环绕通知-前】-> 【前置通知】-> 【返回通知】-> 【后置通知】->【环绕通知-后】。

2、上面这些 AOP 注解都是位于如下所示的 aspectjweaver 依赖中:
在这里插入图片描述
3、对于习惯了 Spring 全家桶编程的人来说,并不是需要直接引入 aspectjweaver 依赖,因为 spring-boot-starter-aop 组件默认已经引用了 aspectjweaver 来实现 AOP 功能。换句话说 Spring 的 AOP 功能就是依赖的 aspectjweaver !

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-aop -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>2.1.4.RELEASE</version>
</dependency>

4、AOP 底层是通过 Spring 提供的的动态代理技术实现的,在运行期间动态生成代理对象,代理对象方法执行时进行增强功能的介入,再去调用目标对象的方法,从而完成功能的增强。主要使用 JDK 动态代理与 Cglib 动态代理。

5、所以如果目标类不是 Spring 组件,则无法拦截,如果是 类名.方法名 方式调用,也无法拦截。
在这里插入图片描述

@Aspect 快速入门

1、@Aspect 常见用于记录日志、异常集中处理、权限验证、Web 参数校验、事务处理等等
2、要想把一个类变成切面类,只需3步:

  • 在类上使用 @Aspect 注解使之成为切面类
  • 切面类需要交由 Sprign 容器管理,所以类上还需要有 @Service@Repository@Controller@Component 等注解
  • 在切面类中自定义方法接收通知

3、AOP 的含义就不再累述了,下面直接上示例:

/*** 切面注解 Aspect 使用入门* 1、@Aspect:声明本类为切面类* 2、@Component:将本类交由 Spring 容器管理* 3、@Order:指定切入执行顺序,数值越小,切面执行顺序越靠前,默认为 Integer.MAX_VALUE** @author wangMaoXiong* @version 1.0* @date 2020/8/20 19:22*/
@Aspect
@Order(value = 999)
@Component
public class AspectHelloWorld {private static final Logger LOG = LoggerFactory.getLogger(AspectHelloWorld.class);/*** @Pointcut :切入点声明,即切入到哪些目标方法。value 属性指定切入点表达式,默认为 ""。* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式* <p>* 切入点表达式常用格式举例如下:* - * com.wmx.aspect.EmpService.*(..)):表示 com.wmx.aspect.EmpService 类中的任意方法* - * com.wmx.aspect.*.*(..)):表示 com.wmx.aspect 包(不含子包)下任意类中的任意方法* - * com.wmx.aspect..*.*(..)):表示 com.wmx.aspect 包及其子包下任意类中的任意方法* </p>* value 的 execution 可以有多个,使用 || 隔开.*/@Pointcut(value ="execution(* com.wmx.hb.controller.DeptController.*(..)) " +"|| execution(* com.wmx.hb.controller.EmpController.*(..))")private void aspectPointcut() {}/*** 前置通知:目标方法执行之前执行以下方法体的内容。* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式* <br/>* * @param joinPoint:提供对连接点处可用状态和有关它的静态信息的反射访问<br/> <p>* * * Object[] getArgs():返回此连接点处(目标方法)的参数,目标方法无参数时,返回空数组* * * Signature getSignature():返回连接点处的签名。* * * Object getTarget():返回目标对象* * * Object getThis():返回当前正在执行的对象* * * StaticPart getStaticPart():返回一个封装此连接点的静态部分的对象。* * * SourceLocation getSourceLocation():返回与连接点对应的源位置* * * String toLongString():返回连接点的扩展字符串表示形式。* * * String toShortString():返回连接点的缩写字符串表示形式。* * * String getKind():返回表示连接点类型的字符串* * * </p>*/@Before(value = "aspectPointcut()")public void aspectBefore(JoinPoint joinPoint) {Object[] args = joinPoint.getArgs();Signature signature = joinPoint.getSignature();Object target = joinPoint.getTarget();Object aThis = joinPoint.getThis();JoinPoint.StaticPart staticPart = joinPoint.getStaticPart();SourceLocation sourceLocation = joinPoint.getSourceLocation();String longString = joinPoint.toLongString();String shortString = joinPoint.toShortString();LOG.debug("【前置通知】" +"args={},signature={},target={},aThis={},staticPart={}," +"sourceLocation={},longString={},shortString={}", Arrays.asList(args), signature, target, aThis, staticPart, sourceLocation, longString, shortString);}/*** 后置通知:目标方法执行之后执行以下方法体的内容,不管目标方法是否发生异常。* value:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式*/@After(value = "aspectPointcut()")public void aspectAfter(JoinPoint joinPoint) {LOG.debug("【后置通知】kind={}", joinPoint.getKind());}/*** 返回通知:目标方法返回后执行以下代码* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""* returning 属性:通知签名中要将返回值绑定到的参数的名称,默认为 ""** @param joinPoint :提供对连接点处可用状态和有关它的静态信息的反射访问* @param result    :目标方法返回的值,参数名称与 returning 属性值一致。无返回值时,这里 result 会为 null.*/@AfterReturning(pointcut = "aspectPointcut()", returning = "result")public void aspectAfterReturning(JoinPoint joinPoint, Object result) {LOG.debug("【返回通知】,shortString={},result=", joinPoint.toShortString(), result);}/*** 异常通知:目标方法发生异常的时候执行以下代码,此时返回通知不会再触发* value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式* pointcut 属性:绑定通知的切入点表达式,优先级高于 value,默认为 ""* throwing 属性:与方法中的异常参数名称一致,** @param ex:捕获的异常对象,名称与 throwing 属性值一致*/@AfterThrowing(pointcut = "aspectPointcut()", throwing = "ex")public void aspectAfterThrowing(JoinPoint jp, Exception ex) {String methodName = jp.getSignature().getName();if (ex instanceof ArithmeticException) {LOG.error("【异常通知】" + methodName + "方法算术异常(ArithmeticException):" + ex.getMessage());} else {LOG.error("【异常通知】" + methodName + "方法异常:" + ex.getMessage());}}/*** 环绕通知* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响目标方法的事务回滚* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发** @param joinPoint* @return* @throws Throwable*/@Around(value = "aspectPointcut()")public Object handleControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable {this.checkRequestParam(joinPoint);StopWatch stopWatch = StopWatch.createStarted();LOG.debug("【环绕通知】执行接口开始,方法={},参数={} ", joinPoint.getSignature(), Arrays.asList(joinPoint.getArgs()).toString());//继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.//如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走.Object proceed = joinPoint.proceed(joinPoint.getArgs());stopWatch.stop();long watchTime = stopWatch.getTime();LOG.debug("【环绕通知】执行接口结束,方法={}, 返回值={},耗时={} (毫秒)", joinPoint.getSignature(), proceed, watchTime);return proceed;}/*** 参数校验,防止 SQL 注入** @param joinPoint*/private void checkRequestParam(ProceedingJoinPoint joinPoint) {Object[] args = joinPoint.getArgs();if (args == null || args.length <= 0) {return;}String params = Arrays.toString(joinPoint.getArgs()).toUpperCase();String[] keywords = {"DELETE ", "UPDATE ", "SELECT ", "INSERT ", "SET ", "SUBSTR(", "COUNT(", "DROP ","TRUNCATE ", "INTO ", "DECLARE ", "EXEC ", "EXECUTE ", " AND ", " OR ", "--"};for (String keyword : keywords) {if (params.contains(keyword)) {LOG.warn("参数存在SQL注入风险,其中包含非法字符 {}.", keyword);throw new RuntimeException("参数存在SQL注入风险:params=" + params);}}}
}

在这里插入图片描述
如上所示在不修改原来业务层代码的基础上,就可以使用 AOP 功能,在目标方法执行前后或者异常时都能捕获然后执行。

execution 切点表达式 拦截指定类的方法

1、@Pointcut 切入点声明注解,以及所有的通知注解都可以通过 value 属性或者 pointcut 属性指定切入点表达式。
2、切入点表达式通过 execution 函数匹配连接点,语法:execution([方法修饰符] 返回类型 包名.类名.方法名(参数类型) [异常类型])

  • 访问修饰符可以省略;
  • 返回值类型、包名、类名、方法名可以使用星号*代表任意;
  • 包名与类名之间一个点.代表当前包下的类,两个点…表示当前包及其子包下的类;
  • 参数列表可以使用两个点…表示任意个数,任意类型的参数列表;
    3、切入点表达式的写法比较灵活,比如:* 号表示任意一个,… 表示任意多个,还可以使用 &&、||、! 进行逻辑运算,不过实际开发中通常用不到那么多花里胡哨的,掌握以下几种就基本够用了。
    4、特别注意:当明确指定了切入的类时,类必须存在,否则启动报错,此时可以在类名前后加上*号表示模糊包含。
    切入点表达式常用举例
标题内容
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(Integer))匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,且带有一个 Integer 类型参数。
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(*))匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,且带有一个任意类型参数。
execution(* com.wmx.aspect.EmpServiceImpl.findEmpById(…))匹配 com.wmx.aspect.EmpService 类中的 findEmpById 方法,参数不限。
execution(* grp.basic3.se.service.SEBasAgencyService3.editAgencyInfo(…)) || execution(*grp.basic3.se.service.SEBasAgencyService3.adjustAgencyInfo(…))匹配 editAgencyInfo 方法或者 adjustAgencyInfo 方法
@Pointcut(“(execution(* grp.basic3…Controller.(…)) && !execution( grp.basic3.BaseExceptionController*.*(…)))”)匹配 grp.basic3包及其子包下面名称包含 ‘Controller’ 类中的全部方法,但是排除掉其中的以 BaseExceptionController 开头的类。
execution(* com.wmx.aspect.EmpService.*(…))匹配 com.wmx.aspect.EmpService 类中的任意方法
execution(* com.wmx.aspect..(…))匹配 com.wmx.aspect 包(不含子包)下任意类中的任意方法
execution(* com.wmx.aspect….(…))匹配 com.wmx.aspect 包及其子包下任意类中的任意方法
execution(* grp.pm…Controller.(…))匹配 grp.pm 包下任意子孙包中以 “Controller” 结尾的类中的所有方法
* com.wmx…Controller.*(…))com.wmx 包及其子包下面类名包含’Controller’的任意类中的任意方法
* com.wmx..controller..*(…))第一二层包名为 com.wmx ,第三层包名任意,第4层包名为 controller 下面的任意类中的任意方法

@Pointcut(“@annotation(xx)”) 拦截拥有指定注解的方法

    /*** @Pointcut :切入点声明,即切入到哪些目标方法。* execution:可以用于指定具体类中的具体方法* annotation:匹配拥有指定注解的方法; 只匹配实现类中有注解的方法,不会匹配接口中的注解方法; 如果注解是在类上,而不是方法上,并不会匹配类中的全部方法.* 用于被下面的通知注解引用,这样通知注解只需要关联此切入点声明即可,无需再重复写切入点表达式* @annotation 中的路径表示拦截特定注解*/@Pointcut("@annotation(com.wmx.annotation.RedisLock)")public void redisLockPC() {}

环绕通知 实现开关目标方法

1、比如某个方法只有管理员才有权限执行,而普通用户是没有权限
2、比如不符合条件的时候,需要终止(跳过)目标方法的执行
3、比如一个组件(Component)专门用于做校验,里面的方法是否校验可以配置在数据库中,当配置为启用时,则继续校验,否则不校验。

    /*** 环绕通知* 1、@Around 的 value 属性:绑定通知的切入点表达式。可以关联切入点声明,也可以直接设置切入点表达式* 2、Object ProceedingJoinPoint.proceed(Object[] args) 方法:继续下一个通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed() 会抛异常.* 3、假如目标方法是控制层接口,则本方法的异常捕获与否都不会影响业务层方法的事务回滚* 4、假如目标方法是控制层接口,本方法 try-catch 了异常后没有继续往外抛,则全局异常处理 @RestControllerAdvice 中不会再触发*/@Around(value = "aspectPointcut()")public Object around(ProceedingJoinPoint joinPoint) throws Throwable {Signature signature = joinPoint.getSignature();Object target = joinPoint.getTarget();System.out.println("环绕通知=" + signature);System.out.println("环绕通知=" + target);// 是否继续校验boolean validation = true;if (validation) {// 校验通过后执行目标方法// 继续下一个切面通知或目标方法调用,返回处理结果,如果目标方法发生异常,则 proceed 会抛异常.// 如果在调用目标方法或者下一个切面通知前抛出异常,则不会再继续往后走return joinPoint.proceed(joinPoint.getArgs());} else {// 校验未通过时,不继续往后走,直接返回。// 可以返回提示信息,但是必须保证返回的参数类型与目标方法的返回值类型一致,否则类型转换异常。// 也可以直接抛异常。return null;}}

案例1:自定义注解+切面实现统一日志处理

1.自定义日志注解

/*** 自定义操作日志注解*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptLog {/*** 业务* @return*/String business();/*** 操作类型,增删改查* @return*/OptType optType();
}

2声明日志切面组件

import com.alibaba.fastjson.JSONObject;
import com.example.demo.annotation.OptLog;
import com.example.demo.annotation.OptType;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;@Aspect
@Component
public class OptLogAspect {private static final Logger LOG = LoggerFactory.getLogger(OptLogAspect.class);/*** 声明切入点,凡是使用该注解都经过拦截*/@Pointcut("@annotation(com.example.demo.annotation.OptLog)")public void OptLog() {}@Before("OptLog()")public void doOptLogBefore(JoinPoint proceedingJoinPoint) {LOG.info("前置通知, 在方法执行之前执行...");}@After("OptLog()")public void doOptLogAfter(JoinPoint proceedingJoinPoint) {LOG.info("后置通知, 在方法执行之后执行...");}@AfterReturning("OptLog()")public void doOptLogAfterReturning(JoinPoint proceedingJoinPoint) {LOG.info("返回通知, 在方法返回结果之后执行...");}@AfterThrowing("OptLog()")public void doOptLogAfterThrowing(JoinPoint proceedingJoinPoint) {LOG.info("异常通知, 在方法抛出异常之后执行...");}/*** 设置环绕通知,围绕着方法执行** @param proceedingJoinPoint* @return*/@Around("OptLog()")public Object optLogAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod();if (method == null) {return null;}// 获取方法名称String methodName = proceedingJoinPoint.getSignature().getName();LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();// 请求参数名称String[] parameterNames = discoverer.getParameterNames(method);// 请求参数值Object[] paramValues = proceedingJoinPoint.getArgs();OptLog optLog = method.getAnnotation(OptLog.class);this.handle(optLog.optType(), optLog.business(), methodName, parameterNames, paramValues);return proceedingJoinPoint.proceed();}/*** 日志处理** @param optType* @param business* @param methodName* @param parameterNames* @param paramValues*/public void handle(OptType optType, String business, String methodName, String[] parameterNames, Object[] paramValues) {JSONObject jsonObject = new JSONObject();if (parameterNames != null && parameterNames.length > 0) {for (int i = 0; i < parameterNames.length; i++) {jsonObject.put(parameterNames[i], paramValues[i]);}}LOG.info("optType:" + optType + ",business:" + business + ", methodName:" + methodName + ", params:" + jsonObject);}}

3.控制层运行结果

@RestController
@RequestMapping("/user/")
public class UserController {@OptLog(optType = OptType.CREATE,business = "用户信息")@RequestMapping("create")public String createUser(String userName,int age,String address) {System.out.println("方法执行中...");return "success";}
}

4.运行结果

15:32:49.494 [http-nio-8080-exec-2] INFO  c.e.d.a.OptLogAspect - [handle,91] - optType:CREATE,business:用户信息, methodName:createUser, params:{"address":"广州市","userName":"阿杰","age":18}
15:32:49.494 [http-nio-8080-exec-2] INFO  c.e.d.a.OptLogAspect - [doOptLogBefore,32] - 前置通知, 在方法执行之前执行...
方法执行中...
15:32:49.495 [http-nio-8080-exec-2] INFO  c.e.d.a.OptLogAspect - [doOptLogAfterReturning,42] - 返回通知, 在方法返回结果之后执行...
15:32:49.495 [http-nio-8080-exec-2] INFO  c.e.d.a.OptLogAspect - [doOptLogAfter,37] - 后置通知, 在方法执行之后执行...

案例2:自定义注解与切面类

1、创建自定义注解

import java.lang.annotation.*;@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TestAnnotation {String name() default "默认值";   // 允许注解有参数String age() default "15";   // 允许多个参数
}

2、创建一个类,定义方法后使用自定义注解

import com.yh.annotation.OperateLogAnnotation;
import com.yh.annotation.TestAnnotation;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class TestAOPController {@RequestMapping("/show3")@ResponseBody@TestAnnotation(name = "我把值传进去", age = "24")   // 加上自定义注解public String getById() { return "hello";}}

3、定义切面类进行,扫描自定义注解,并对切入点进行处理

import com.yh.annotation.TestAnnotation;
import com.yh.annotation.TestAnnotation;
//import javassist.bytecode.SignatureAttribute;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;@Aspect // FOR AOP
@Order(-99) // 控制多个Aspect的执行顺序,越小越先执行, 当然也可以不写这注解, 对于写和不写@order的两个切面, 有@order的优先于无@order的执行; 都有@order时, 越小越执先执行
@Component
public class TestAspect {// 可以参考若依的自定义注解。自定义注解一般使用@annotation// @Before可以有两种写法, @annotation(形参test),@Before("@annotation(test)")// 拦截被TestAnnotation注解的方法;如果你需要拦截指定package指定规则名称的方法,可以使用表达式execution(...)public void beforeTest(JoinPoint point, TestAnnotation test) throws Throwable {System.out.println("beforeTest:" + test.name());   // 直接获取注解参数//test.name()和test.age()}@After("@annotation(test)")public void afterTest(JoinPoint point, TestAnnotation test) {System.out.println("afterTest:" + test.name());  // 直接获取注解参数}// 可以控制方法运行, 同时修改入参和返回值@Around("@annotation(test)")   // test表示aroundTest方法中的test入参public Object aroundTest(ProceedingJoinPoint pjp, TestAnnotation test) throws Throwable {System.out.println("aroundTest:" + test.value());// 获取入参并修改Object[] args = pjp.getArgs();args[0] = "";// 传入修改后的参数, 并继续执行Object res = pjp.proceed(args);// 修改返回值return res.toString() + res.toString();}/* // 指定切面@Pointcut("@annotation(com.yh.annotation.TestAnnotation)")public void annotationPointCut() {}// @Before可以有两者写法, @annotation(函数名annotationPointCut)@Before("annotationPointCut()")public void before(JoinPoint joinPoint) {MethodSignature sign = (MethodSignature) joinPoint.getSignature();Method method = sign.getMethod();TestAnnotation annotation = method.getAnnotation(TestAnnotation.class);   // 获取指定注解实例System.out.println("打印:" + annotation.name() + " 前置日志1");   // 获取注解实例的参数}@After("annotationPointCut()")public void afterTTT(JoinPoint point) {MethodSignature sign = (MethodSignature) point.getSignature();Method method = sign.getMethod();TestAnnotation annotation = method.getAnnotation(TestAnnotation.class);  // 获取指定注解实例System.out.println("打印自带参数:" + annotation.age() + " 后置日志1");  // 获取注解实例的参数}
*/}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/90566.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

多线程总结(线程池 线程安全 常见锁)

本篇文章主要是对线程池进行详解。同时引出了单例模式的线程池&#xff0c;也对线程安全问题进行了解释。其中包含了智能指针、STL容器、饿汉模式的线程安全。也对常见的锁&#xff1a;悲观锁&#xff08;Pessimistic Locking&#xff09;、乐观锁&#xff08;Optimistic Locki…

brew 安装MySQL 5.7

写在前面&#xff1a;博主是一只经过实战开发历练后投身培训事业的“小山猪”&#xff0c;昵称取自动画片《狮子王》中的“彭彭”&#xff0c;总是以乐观、积极的心态对待周边的事物。本人的技术路线从Java全栈工程师一路奔向大数据开发、数据挖掘领域&#xff0c;如今终有小成…

openlayers-18-聚合显示补充(切换聚合与非聚合状态)

最近有一些网友问我&#xff0c;聚合显示怎么实现聚合与不聚合之间的切换&#xff0c;有很多方法能够实现&#xff0c;下面是一个示例作为参考。 <!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-…

Servlet开发-session和cookie理解案例-登录页面

项目展示 进入登录页面&#xff0c;输入正确的用户名和密码以后会自动跳到主页 登录成功以后打印用户名以及上次登录的时间&#xff0c;如果浏览器和客户端都保存有上次登录的信息&#xff0c;则不需要登录就可以进入主页 编码思路 1.首先提供一个登录的前端页面&…

DDS信号发生器Verilog波形发生器FPGA

名称&#xff1a;DDS信号发生器Verilog波形发生器 软件&#xff1a;Quartus 语言&#xff1a;Verilog 要求&#xff1a; 1.可产生正弦波&#xff0c;锯齿波&#xff0c;三角波&#xff0c;方波4种波形&#xff0c;频率可调 2.具有波形选择、起动、停止功能。 代码下载&…

QT信号槽

目录 信号槽的概念 按钮的常用信号 自定义槽函数 自定义信号函数 自定义槽和信号注意的事项 信号与槽的拓展 lambda表达式 信号槽的概念 信号槽是Qt框架引以为豪的机制之一。所谓信号槽&#xff0c;实际就是观察者模式。当某个事件发生之后&#xff0c;比如&#xff0c…

一篇博客学会系列(2)—— C语言中的自定义类型 :结构体、位段、枚举、联合体

目录 前言 1、结构体 1.1、结构体类型的声明 1.2、特殊的结构体类型声明 1.3、结构体的自引用 1.4、结构体的定义和初始化 1.5、结构体成员变量的调用 1.6、结构体内存对齐 1.6.1、offsetof 1.6.2、结构体大小的计算 1.6.3、为什么存在内存对齐&#xff1f; 1.7、…

Linux账户组管理及权限练习

1.使用id命令查看root账户信息 [rootserver ~]# id root 用户id0(root) 组id0(root) 组0(root) 2.使用id命令查看自己的普通账户信息 [rootserver ~]# id kxy 用户id1000(kxy) 组id1000(kxy) 组1000(kxy),10(wheel) 3.新建账户test1&#xff0c;并查看账户信息&#xff1a; [ro…

BASH shell脚本篇3——字符串处理

这篇文章介绍下BASH shell中的字符串处理的相关命令。之前有介绍过shell的其它命令&#xff0c;请参考&#xff1a; BASH shell脚本篇1——基本命令 BASH shell脚本篇2——条件命令 Bash字符串也是一种数据类型&#xff0c;它用于表示文本而不是数字&#xff0c;它是一组可能…

No150.精选前端面试题,享受每天的挑战和学习

🤍 前端开发工程师(主业)、技术博主(副业)、已过CET6 🍨 阿珊和她的猫_CSDN个人主页 🕠 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 🍚 蓝桥云课签约作者、已在蓝桥云课上架的前后端实战课程《Vue.js 和 Egg.js 开发企业级健康管理项目》、《带你从入…

后台管理系统: 商品管理

商品管理之三级联动静态组件 先做俩个卡片组件&#xff0c;分开距离 三级联动很多地方都用到了它&#xff0c;我们可以封装成一个组件 注册为一个全局组件 <div><el-form :inline"true" class"demo-form-inline"><el-form-item label&qu…

1200*A. Flipping Game(前缀和)

解析&#xff1a; 100数据量&#xff0c;两层遍历每个区间&#xff0c;然后前缀和计算1的个数&#xff0c;维护最大值即可。 #include<bits/stdc.h> using namespace std; #define int long long const int N110; int n,a[N],res,sum[N]; signed main(){scanf("%ll…

目标检测:FROD: Robust Object Detection for Free

论文作者&#xff1a;Muhammad,Awais,Weiming,Zhuang,Lingjuan,Lyu,Sung-Ho,Bae 作者单位&#xff1a;Sony AI; Kyung-Hee University 论文链接&#xff1a;http://arxiv.org/abs/2308.01888v1 内容简介&#xff1a; 1&#xff09;方向&#xff1a;目标检测 2&#xff09;…

3.6+铁死亡+WGCNA+机器学习

今天给同学们分享一篇3.6铁死亡WGCNA机器学习的生信文章“Identification of ferroptosis related biomarkers and immune infiltration in Parkinsons disease by integrated bioinformatic analysis”&#xff0c;这篇文章于2023年3月14日发表在BMC Med Genomics期刊上&#…

队列的使用以及模拟实现(C++版本)

&#x1f388;个人主页:&#x1f388; :✨✨✨初阶牛✨✨✨ &#x1f43b;强烈推荐优质专栏: &#x1f354;&#x1f35f;&#x1f32f;C的世界(持续更新中) &#x1f43b;推荐专栏1: &#x1f354;&#x1f35f;&#x1f32f;C语言初阶 &#x1f43b;推荐专栏2: &#x1f354;…

C运算符和控制语句

几乎每一个程序都需要进行运算&#xff0c;对数据进行加工处理&#xff0c;否则程序就没有意义了。要进行运算&#xff0c;就需规定可以使用的运算符。 C语言的运算符范围很宽&#xff0c;把除了控制语句和输人输出以外的几乎所有的基本操作都作为运算符处理。 运算符分类1 除…

likeadmin和fastapi的bug

以下内容写于2023年8月11日 bug 1 请求体 - 多个参数 - FastAPI (tiangolo.com)中“请求体中的单一值”处&#xff0c;选python3.6&#xff0c;接口示例代码是 from typing import Unionfrom fastapi import Body, FastAPI from pydantic import BaseModel from typing_exte…

Spring Boot中配置文件介绍及其使用教程

目录 一、配置文件介绍 二、配置简单数据 三、配置对象数据 四、配置集合数据 五、读取配置文件数据 六、占位符的使用 一、配置文件介绍 SpringBoot项目中&#xff0c;大部分配置都有默认值&#xff0c;但如果想替换默认配置的话&#xff0c;就可以使用application.prop…

从零手搓一个【消息队列】项目设计、需求分析、模块划分、目录结构

文章目录 一、需求分析1, 项目简介2, BrokerServer 核心概念3, BrokerServer 提供的核心 API4, 交换机类型5, 持久化存储6, 网络通信7, TCP 连接的复用8, 需求分析小结 二、模块划分三、目录结构 提示&#xff1a;是正在努力进步的小菜鸟一只&#xff0c;如有大佬发现文章欠佳之…

阿里云效自动构建python自动测试脚本

之前一直用的是jenkins自动构建自动化脚本&#xff0c;因为现在的公司统一在阿里云效的流水线上做代码的管理&#xff0c;构建&#xff0c;要求自动化测试也在上面自动构建&#xff0c;故而学习了一下。为自己做一个记录&#xff0c;也给有需要的朋友做一个参考。 1. 新建流水…