技术标签: aop java springBoot
AOP是面向切面编程的思想,而Spring AOP是这种思想的技术实现!
AOP采用"横切"的技术,剖解开封装的对象内部,将影响了多个类的公共行为封装到一个可重用模块。将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
AOP(Aspect Oriented Programming),即面向切面编程。众所周知,OOP(面向对象编程)通过的是继承、封装和多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP从纵向上区分出一个个的类来,而AOP则从横向上向对象中加入特定的代码。
1、面向切面编程使得每个关注点都集中于一个地方而不是分散在多处代码中,便于后期的统一维护管理。
2、服务模块更简洁,它们只包含主要关注点,而次要关注点的代码被转移到切面中了。
3、对原方法进行方法增强,且不影响原方法的正常使用。
4、使用简单可插拔的配置,在实际逻辑执行之前、之后或周围动态添加横切关注点。
viewpoint: 在运行时的某个或某些对象的某个或某些方法(如,method)的前面或后面,或是是这些方法抛出异常时,我要通过
AOP实现增加一段执行代码。
1)连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
viewpoint: 连接点就是哪些地方,我们可能去通过AOP插入一段执行代码。可以认为是类中的方法。
2) 切点(Pointcut)
切点定义了在何处工作,也就是真正被切入的地方,也就是在哪个方法应用通知。切点的定义会匹配通知所有要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
viewpoint: AOP通过正则匹配到的一个或多个链接点的集合。
3) 通知(Advice)
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
前置通知(Before):在目标方法被调用之前调用通知功能
后置通知(After):在目标方法完成之后调用通知,不关心方法的输出是什么。是“返回通知”和“异常通知”的并集。
返回通知(After-returning):在目标方法成功执行之后调用通知
异常通知(After-throwing):在目标方法抛出异常后调用通知
环绕通知(Around)通知包裹了被通知的方法,可同时定义前置通知和后置通知。
viewpoint: 对于那些匹配到的切点(方法)之前,之后,异常时 我们加入一些额外的动作,比如打印日志,性能判断等等。
4) 切面(Aspect)
切面是通知和切点的结合的集合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。比如事务管理是一个切面,权限管理也是一个切面。
问题1:实现过程分那几部?
问题2:5种类型的通知的顺序怎样?
step1:maven引入jar 坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
step2:创建个controller (此时是结合web接口的,这不是必须的,根据需求而定)
@RestController
public class HandlerTestController {
@RequestMapping(value="aopTest",method = RequestMethod.GET)
public String showMessage(HttpServletRequest request,String message) throws Exception {
// if(true){
// throw new Exception("抛出异常");
// }
return "message:"+message;
}
}
step3: 创建一个aspect切面类
package com.example.studyspringboot.studyboot.controller;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
//参考文档
//https://blog.csdn.net/qq_36582604/article/details/80502070?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2
/**
* web请求日志切面类---专门针对控制层,如谁被请求了,花了多少时间,请求发送的参数,返回得值等
*
* @author lps
*/
@Aspect // 表示一个切面bean
@Component // bean容器的组件注解。虽然放在contrller包里,但它不是控制器。如果注入service,但我们又没有放在service包里
@Order(3) // 有多个日志时,ORDER可以定义切面的执行顺序(数字越大,前置越后执行,后置越前执行)
public class WebLogAspect {
//定义切入点
/*1、execution 表达式主体
2、第1个* 表示返回值类型 *表示所有类型
3、包名 com.*.*.controller下
4、第4个* 类名,com.*.*.controller包下所有类
5、第5个* 方法名,com.*.*.controller包下所有类所有方法
6、(..) 表示方法参数,..表示任何参数
*/
@Pointcut("execution(public * com.*.*.*.controller.*.*(..))")
public void weblog() {
}
@Before("weblog()")
public void dobefore(JoinPoint joinPoint) { //方法里面注入连接点
System.out.println("---@Before---");
}
//环绕通知,环绕增强,相当于MethodInterceptor
@Around("weblog()")
public Object arround(ProceedingJoinPoint pjp) {
Object result = null;
try {
System.out.println("@Around before");
result = pjp.proceed();
System.out.print("执行结果");
System.out.println(result != null ? result : "");
System.out.println("@Around return");//执行结果
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around exception:" + e);
}
return result;
}
//方法的返回值注入给ret
@AfterReturning(returning = "ret", pointcut = "weblog()")
public void doafter(Object ret) {
System.out.println("---@AfterReturning---");
}
//后置异常通知
@AfterThrowing("weblog()")
public void throwss(JoinPoint jp) {
System.out.println("---@AfterThrowing---");
}
//后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
@After("weblog()")
public void after() throws Throwable {//JoinPoint jp
System.out.println("---@After---");
}
}
运行结果1---没有异常
@Around before
---@Before---
---@AfterReturning---
---@After---
执行结果message:yes
@Around return
运行结果2---存在异常
@Around before
---@Before---
java.lang.Exception: 抛出异常
---@AfterThrowing---
---@After---
@Around exception:java.lang.Exception: 抛出异常
增强注解的参数说明
除了@Around外,每个方法里都可以加或者不加参数JoinPoint,如果有用JoinPoint的地方就加,不加也可以,JoinPoint里包含了类名、被切面的方法名,参数等属性,可供读取使用。
@Around参数必须为ProceedingJoinPoint,pjp.proceed相应于执行被切面的方法。
@AfterReturning方法里,可以加returning = “XXX”,XXX即为在controller里方法的返回值。
@AfterThrowing方法里,可以加throwing = "XXX",供读取异常信息,
5个增强的注解是不是都要用上呢?
viewpoint: 我个人感觉没必要。视情况而定,一个@Around就可以概括所有(不能使用JoinPoint,
如果想获取相关信息不能只用它)。
如果只想在切点前面加增强直接用@Before;
如果只想在切点后面加增强直接用@After;兼顾@AfterThrowing和@After
如果只想在切点抛出异常后加增强直接用@AfterThrowing;
如果只想在切点(方法)执行成功后加增强直接用 @AfterReturning;
考虑到切点(方法)的返回值,则需要用到@AfterReturning或者@Around。
JoinPoint 中可以获取哪些信息呢?
参考文档: https://blog.csdn.net/u014683187/article/details/89395844
step4:对比step3 现在将日志功能加上去
package com.example.studyspringboot.studyboot.utils;
import org.apache.log4j.Logger;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
//参考文档
//https://blog.csdn.net/qq_36582604/article/details/80502070?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-2
/**
* web请求日志切面类---专门针对控制层,如谁被请求了,花了多少时间,请求发送的参数,返回得值等
* @author lps
*/
@Aspect // 表示一个切面bean
@Component // bean容器的组件注解。虽然放在contrller包里,但它不是控制器。如果注入service,但我们又没有放在service包里
@Order(3) // 有多个日志时,ORDER可以定义切面的执行顺序(数字越大,前置越后执行,后置越前执行)
public class WebLogAspect {
//定义日志记录器--获取log4j包下提供的logger
Logger logger = Logger.getLogger(Object.class);
ThreadLocal<Long> startTime = new ThreadLocal<>(); //线程副本类去记录各个线程的开始时间
//定义切入点
/*1、execution 表达式主体
2、第1个* 表示返回值类型 *表示所有类型
3、包名 com.*.*.controller下
4、第4个* 类名,com.*.*.controller包下所有类
5、第5个* 方法名,com.*.*.controller包下所有类所有方法
6、(..) 表示方法参数,..表示任何参数
*/
@Pointcut("execution(public * com.*.*.*.controller.*.*(..))")
public void weblog() {
}
@Before("weblog()")
public void dobefore(JoinPoint joinPoint) { //方法里面注入连接点
System.out.println("---@Before---"); //info ,debug ,warn ,erro四种级别,这里我们注入info级别
startTime.set(System.currentTimeMillis());
//获取servlet请求对象---因为这不是控制器,这里不能注入HttpServletRequest,但springMVC本身提供ServletRequestAttributes可以拿到
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("请求路径::" + request.getRequestURL().toString()); // 想那个url发的请求
logger.info("请求方式:" + request.getMethod());
logger.info("调用方法:" + joinPoint.getSignature().getDeclaringTypeName() + "."
+ joinPoint.getSignature().getName()); // 请求的是哪个类,哪种方法
logger.info("方法参数:" + Arrays.toString(joinPoint.getArgs())); // 方法本传了哪些参数
}
//环绕通知,环绕增强,相当于MethodInterceptor
@Around("weblog()")
public Object arround(ProceedingJoinPoint pjp) {
Object result=null;
try {
System.out.println("@Around before");
result = pjp.proceed();
System.out.print("执行结果");
System.out.println(result!=null?result:"");
System.out.println("@Around return");//执行结果
} catch (Throwable e) {
e.printStackTrace();
System.out.println("@Around exception:"+e);
}
return result;
}
//方法的返回值注入给ret
@AfterReturning(returning = "ret", pointcut = "weblog()")
public void doafter(Object ret) {
System.out.println("---@AfterReturning---");
logger.info("相应内容:" + ret); // 响应的内容---方法的返回值responseEntity
logger.info("执行时间(ms):" + ( System.currentTimeMillis()-startTime.get() ));
}
//后置异常通知
@AfterThrowing("weblog()")
public void throwss(JoinPoint jp){
System.out.println("---@AfterThrowing---");
}
//后置最终通知,final增强,不管是抛出异常或者正常退出都会执行
@After("weblog()")
public void after() throws Throwable {//JoinPoint jp
//Object result = pjp.proceed();
System.out.println("---@After---");
}
}
Spring AOP的底层实现方式:一种是JDK动态代理,另一种是CGLib的方式。
JDK动态代理有一个限制,就是它只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,如何创建动态代理实例哪?答案就是CGLib。
CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑
JDK动态代理是面向接口的。
CGLib动态代理是通过字节码底层继承要代理类来实现(如果被代理类被final关键字所修饰,那么抱歉会失败)。
使用注意:
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
Spring boot 项目的默认代理方式。
Spring 5.x 中 AOP 默认依旧使用 JDK 动态代理。
SpringBoot 2.x 开始,为了解决使用 JDK 动态代理可能导致的类型转化异常而默认使用 CGLIB。
在 SpringBoot 2.x 中,如果需要默认使用 JDK 动态代理可以通过(properties配置文件)配置项spring.aop.proxy-target-class=false
来进行修改,proxyTargetClass
配置已无效(启动类上加@EnableAspectJAutoProxy(proxyTargetClass = false))。
查看当前spring boot 项目的 spring 和spring boot版本
@Test
public void method01() {
String version = SpringVersion.getVersion();
String version1 = SpringBootVersion.getVersion();
System.out.println("SpringVersion==>"+version);
System.out.println("SpringBootVersion==>"+version1);
}
spring.aop.proxy-target-class=false
则会出现一些麻烦 假设,我们有一个UserServiceImpl
和UserService
类,此时需要在UserContoller
中使用UserService
。在 Spring 中通常都习惯这样写代码:
@Autowired
UserService userService;
在这种情况下,无论是使用 JDK 动态代理,还是 CGLIB 都不会出现问题。
但是,如果你的代码是这样的呢:
@Autowired
UserServiceImpl userService;
这个时候,如果我们是使用 JDK 动态代理,那在启动时就会报错:
因为 JDK 动态代理是基于接口的,代理生成的对象只能赋值给接口变量。
而 CGLIB 就不存在这个问题。因为 CGLIB 是通过生成子类来实现的,代理对象无论是赋值给接口还是实现类这两者都是代理对象的父类。
SpringBoot 正是出于这种考虑,于是在 2.x 版本中,将 AOP 默认实现改为了 CGLIB。
实际应用的思考
因此要解决几个方面的问题
item1:需要了解切点的更多匹配方式:
参考文档:https://blog.csdn.net/qq_23167527/article/details/78623639
@Pointcut("execution(public * com.*.*.*.controller.*.*(..))")
public void weblog() {
}
相关切点匹配的详细信息请参考:https://www.cnblogs.com/zhangxufeng/p/9160869.html
item1.1 通配符
如下示例表示返回值为任意类型,在com.spring.service.BusinessObject类中,并且参数个数为零的方法:
execution(* com.spring.service.BusinessObject.*())
下述示例表示返回值为任意类型,在com.spring.service包中,以Business为前缀的类,并且是类中参数个数为零方法:
execution(* com.spring.service.Business*.*())
如下示例表示匹配返回值为任意类型,并且是com.spring.service包及其子包下的任意类的名称为businessService的方法,而且该方法不能有任何参数:
execution(* com.spring.service..*.businessService())
这里需要说明的是,包路径service..*.businessService()中的..应该理解为延续前面的service路径,表示到service路径为止,或者继续延续service路径,从而包括其子包路径;后面的*.businessService(),这里的*表示匹配一个单词,因为是在方法名前,因而表示匹配任意的类。
如下示例是使用..表示任意个数的参数的示例,需要注意,表示参数的时候可以在括号中事先指定某些类型的参数,而其余的参数则由..进行匹配:
execution(* com.spring.service.BusinessObject.businessService(java.lang.String,..))
item1.2 组合切入点表达式
AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式。
item1.3 切点匹配的实际使用
参考文档 :https://blog.csdn.net/qq_23167527/article/details/78623639
1.3.1 +通配符的使用
@Pointcut("within(com.example.studyspringboot.studyboot.service.impl.ProxyTestImpl+)")
private void weblog() {
}
接口为ProxyTest 实现类为ProxyTestImpl,另个类ProxyTestImpl1继承ProxyTestImpl。此时用两个类分别调用自己的方法都可被拦截,然后增强,包括ProxyTestImpl1的自己的非接口实现方法也可以被增强。
如果只是within(com.example.studyspringboot.studyboot.service.impl.ProxyTestImpl1) 则只会增强ProxyTestImpl1的自己的非接口实现方法,集成自ProxyTestImpl的方法不会增强。
如果只是within(com.example.studyspringboot.studyboot.service.impl.ProxyTestImpl)则只增强ProxyTestImpl的所有方法。而子类ProxyTestImpl1中的方法都不会增强。
注意:new 出来的对象不会被拦截(匹配),所以需要注解@Autowired或者@Resource.关于多个实现类的注入方法参考一下文档:
https://www.cnblogs.com/leeego-123/p/10882069.html1.3.2within:使用“within(类型表达式)”匹配指定类型内的方法执行;
within(cn.javass..*) cn.javass包及子包下的任何方法执行
within(cn.javass..IPointcutService+) cn.javass包或所有子包下IPointcutService类型及子类型的任何方法
within(@cn.javass..Secure *) 持有cn.javass..Secure注解的任何类型的任何方法。必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
1.3.3:@DeclareParents 想代理类中添加方法
@DeclareParents
@DeclareParents也称为Introduction(引入),表示为指定的目标类引入新的属性和方法。非常类似让目标了去实现一个没有抽象方法,只有默认方法或属性的接口。
关于@DeclareParents的原理其实比较好理解,因为无论是Jdk代理还是Cglib代理,想要引入新的方法,只需要通过一定的方式将新声明的方法织入到代理类中即可,因为代理类都是新生成的类,因而织入过程也比较方便。如下是@DeclareParents的使用语法:
step1:
@Aspect
@Component
public class IntroductionAspect {
@DeclareParents(value = "com.example.studyspringboot.studyboot.service.impl.AspectServiceImpl", defaultImpl = DescriptImpl.class)
private IDescript iDescript;
}
现在想把DescriptImpl类(属性,方法)植入到类AspectServiceImpl中去
step2:DescriptImpl的接口IDescript (必要)
public interface IDescript {
String name="xiaoming";
String tell(String msg);
}
step3:DescriptImpl (必要)
public class DescriptImpl implements IDescript {
@Override
public String tell(String msg) {
String res = msg!=null? msg:"hello world";
return res;
}
}
step4.调用
@RequestMapping("introduction")
public String intro(){
IDescript iDescript = (IDescript) aspectServiceImpl;
String res = iDescript.tell("this is introduction");
System.out.println(iDescript.name);
return res;
}
引出的问题:如何为植入的方法进行增强? 使用this()切点表达式匹配
1.3.4 this(M)与args(M)没测试出来有什么区别?下面以this为例
如 this(cn.javass.spring.chapter6.service.IPointcutService)
注意:M不能是通配符。
M可以使类,普通接口,引入接口(如IDescript)。
匹配的范围。
比如接口AspectService 实现类AspectServiceImpl 继承类AspectServiceImpl1
this(AspectServiceImpl) 则匹配了AspectServiceImpl 的对象和AspectServiceImpl1的对象(包括非实现方法)以及引入的aspectServiceImpl中的方法都会被增强。
这说明this(M)匹配了M、M的子类,以及M的引入接口(对象)。
1.3.5 args(M)
args:使用“args(参数类型列表)”匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
args (java.io.Serializable,..)
任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的。
例如:
类型M
@Component 交给spring容器
public class Apple {
public String getMessage(String msg){
return msg;
}
}
M为参数的方法所在的类
@Component 交给spring容器
public class OptionApple {
public void optionApple(Apple apple,String op){
System.out.println(apple.getMessage("xxyyzz"));
}
public void other(String str){
System.out.println(str);
}
}
调用
//测试args optionApple对象和apple对象 通过注解@Autowired 注入,不要去new
@RequestMapping("args")
public Object argsTest(){
optionApple.optionApple(apple,"abc");
return "args";
}
切点
@Aspect
@Component
@Order(100)
public class CutPointTest02 {
@Pointcut("args(com.example.studyspringboot.studyboot.utils.aspect.args.Apple,..)&&within(com..*)")
private void advicedemo(){
}
@Before("advicedemo()")
private void doBefore(){
System.out.println("---this is before---");
}
}
注意 @Pointcut("args(com.example.studyspringboot.studyboot.utils.aspect.args.Apple,..)&&within(com..*)")后面要加限定,不然启动报错。
1.3.6@within(M):使用“@within(注解类型)”匹配所以持有指定注解类型中的方法;
M是注解的全路径
注解类型也必须是全限定类型名;
@within cn.javass.spring.chapter6.Secure)
任何目标对象对应的类型持有Secure注解的类方法;
必须是在目标对象上声明这个注解,在接口上声明的对它不起作用.
step1自定义注解
//测试注解用于标记类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoWithin {
}
step2标记目标类
@Component //交给spring容器
@AnnoWithin
public class AnnotationWithin {
public String getMessage(){
return "test @Within";
}
}
step3切点
@Pointcut("@within(com.example.studyspringboot.studyboot.utils.aspect.selfAnnotation.AnnoWithin)")
step4调用
//@within 测试
@RequestMapping("withiin")
public Object annotationWithin(){
annotationWithin.getMessage();//annotationWithin 为@Autowired注入spring中AnnotationWithin的对象
return "@withiin";
}
1.3.7 @target(M)和@within(M)一样也是通过注解匹配类对象,匹配表达式后要加限制,不然报错。
@Pointcut("@target(com.example.studyspringboot.studyboot.utils.aspect.selfAnnotation.AnnoWithin)&&within(com..*)")
注解类型M也必须是全限定类型名;
任何目标对象持有AnnoWithin注解的类方法;
必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
1.3.8@args(M):使用“@args(注解列表)”匹配当前执行的方法传入的参数持有指定注解的执行;
注解类型M也必须是全限定类型名;
args(M,..) 中的M是类型,@args(M,..)中的M是注解,但是这个注解M会去标记一些类,当某个方法中的参数对应含有该类型就会被匹配。例如,当M标注在类People上 ,则@args(M,..) 会匹配到method(People people,String sign)。
1.3.9@annotation(M):使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;
注解类型M也必须是全限定类型名;
@annotation(cn.javass.spring.chapter6.Secure ) 当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配
用法很简单,自定义一个注解放在某个方法上,然后切点匹配,就可以实现增强。
1.3.10 bean(M) 根据Bean id 和名字通配符来匹配java bean 中的所有方法
使用“bean(Bean id或名字通配符)”匹配特定名称的Bean对象的执行方法;Spring ASP扩展的,在AspectJ中无相应概念;
模式 |
描述 |
bean(*Service) |
匹配所有以Service命名(id或name)结尾的Bean |
step1 普通的类
@Component("beanPeople")//不加的话默认peopleClass 即类名首字母小写
public class PeopleClass {
public String getMessage(){
return "test bean匹配";
}
}
step2 切点
@Pointcut("bean(beanPeople)")
step3 调用
public class AspectController02 {
@Qualifier("beanPeople")
@Autowired private PeopleClass peopleClass;
// bean 测试
@RequestMapping("bean")
public Object beanTest(){
String res = peopleClass.getMessage();
return res;
}
}
item1.4 获取当前代理对象
有一个这样的问题?
目标对象 Subje 被代理了,代理对象假设为ProxySubject,则目标对象中的方法A,和方法B都被增强,Controller曾可以通过注解注入Subje的代理对象,然后调用方法A,发现A被增强了。调用方法B,发现B也被增强了。但是如果,方法A中调用了方法B,则这个调用过程中只有A被增强,B的调用没有倍增强。
原因是:A调用B,其实B的前面有一个默认的this指向的是目标对象,而不是代理对象。有一种解决方案,就是此时A中B 的调用改为当前代理对象的调用,当然这种方式并不好。
怎样获取当前方法的代理对象?
step1.在启动类上,先暴露代理对象,通过加注解@EnableAspectJAutoProxy(exposeProxy = true)
step2.
public String showNumber(){
((AspectServiceImpl) AopContext.currentProxy()).calculate(12);
return null;
}
其中calculate(12) 方法和showNumber()在同一个类中,并且该类的对象被代理。((AspectServiceImpl) AopContext.currentProxy())可以获取代理对象。AspectServiceImpl为当前类名。
关于log4j的相关知识参考:https://blog.csdn.net/qq_40331861/article/details/106815436
学习参考文档:
文章浏览阅读4.2k次。打开src目录下的AppDelegate.cpp文件,若无修改则在第45行处找到全局声明的Size变量,修改`designResolutionSize`中的大小即可。_cocos2dx设置窗口大小
文章浏览阅读1.6k次。测试代码:@PostMapping() public void test(@RequestBody Student student){ System.out.println(student.getLover().name()); }class Student{ private Lover lover; public Lover getLover() { return lover; } public void setLover_springboot get请求怎么接收前端传递的枚举数字
文章浏览阅读1.5w次,点赞24次,收藏120次。简单来说就是去量纲后的回归(因为你要比较不同变量之间的显著性的大小,那么带着量纲怎么比,所以先把量纲去掉,然后再比较)官话:为了更为精准的研究影响评价量的重要因素(去除量纲的影响),我们可考虑使用标准化回归系数。_stata两个虚拟变量的交互项
文章浏览阅读203次。有时候安装mysql后使用mysql命令时报错 Can't connect to MySQL server on localhost (10061),或者用net start mysql 时报服务名无效,一般是因为mysql服务没有启动。这时候可以用管理身份运行cmd.exe(注意必须是管理..._c:\program files\mysql\mysql server 5.6\bin>mysqld --install install/remove
文章浏览阅读6.2k次,点赞3次,收藏44次。亚信联创科技校园招聘B 卷考试时间60_分钟 _考试方式(闭)卷(本试卷满分 100 分,答案请写在答题卡上)请不要在问卷上答题或涂改,笔试结束后请务必交回试卷部分内容分值备注一、计算机基础40分C/C++语言基础40分技能部分二、二选一JAVA 语言基础40分三、数据库20分总分100 分第一部分——计算机基础一、选择题(每题 2 分,总分 40分)1.CPU 状态分为目态和管态两种..._亚信科技java实习笔试题
文章浏览阅读1.3k次。3年对一个程序员来说是非常重要的。像我自己本身就是做程序员的,目前的薪资待遇是13K左右,虽然在我所在的公司不是最高的,但在所在的这个城市的消费水平来说,除了日常的开支,包括房租、水电、伙食、人际交往等费用之外,还能留下一部分闲钱自己存起来。不同城市的薪资待遇是不一样的,这主要是由于当地的消费水平和经济发展水平不同,所以如果你想要更高的薪资待遇,就要考虑在一线城市或者经济发达的城市工作。一个有着丰富工作经验的程序员,他的技能水平、经验和能力都比没有经验的程序员更加出色,所以他们的薪资待遇也会更高一些。_三线城市学java
文章浏览阅读418次。标签PostgreSQL , 标签 , 推荐系统 , 实时圈人 , 数组 , gin , gist , 索引 , rum , tsvector , tsquery , 万亿 , user , tag , 淘宝背景我们仅用了PostgreSQL的两个小特性,却解决了业务困扰已久的大问题。推荐系统是广告营销平台的奶牛,其核心是精准、实时、..._实时圈人
文章浏览阅读430次。软件测试风险追踪表风险追踪表 项目名称: 填制人: 编号 风险描述 影响 风险等级 发生的可能性 应对策略 状态 责任人 备注 ..._软件测试风险管理表格
文章浏览阅读1.2k次。一、AAC音频格式种类有哪些AAC音频格式是一种由MPEG-4标准定义的有损音频压缩格式。AAC包含两种格式 ADIF(Audio Data Interchange Format音频数据交换格式)和ADTS(Audio Data transport Stream音频数据传输流)。ADIF特点:可以确定的找到音视频数据的开始,不需要进行在音视频数据流中间开始的解码,它的解码必须在明确的定义开始。应用场景:常用在磁盘文件中。ADTS特点:具有同步字的比特流,解码可以在这个流中任何位置开始。类似于mp_aac adts
文章浏览阅读213次。像要使用Resouce类,必须创建一个 Resouce 文件夹,然后把需要的资源放进去,才可以在代码中设置路径进行访问_unity基本概念
文章浏览阅读2.4k次。指定自定义 CI/CD 配置文件,顾名思义就是在项目中指定文件来代替默认的.gitlab-ci.yml文件的方式来运行流水线。以往我们在使用流水线的时候,都是默认将.gitlab-ci.yml文件存在在项目的跟路径下,但是我们也可以指定备用文件名路径,或者不想在每个项目中来维护这个yml文件,那么通过自定义 CI/CD 配置文件便可以实现。_gitlab配置cicd
文章浏览阅读1w次。出现这个表示如果设置了自动增长,字段类型应该设置为int整型。_sql 错误 [1063] [42000]: incorrect column specifier for column 'id' incorrec