引言
访问后端服务器时,有时交互时间很长,因此想找出跟后端交互问题出在哪个方法上。
方案:获取controller
中每个方法的执行时间,大于一定时间的抓出来,后期再进行优化。
正文
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在application.properties中加入配置
spring.aop.auto=true
实现具体代码
@Component
@Aspect
@Slf4j
public class HttpTimeAspect{
String methodName; // 方法名
long startTime; // 开始时间
/**
* 定义一个切入点.
* 解释下:
*
* ~ 第一个 * 代表任意修饰符及任意返回值.
* ~ 第二个 * 定义在web包或者子包
* ~ 第三个 * 任意方法
* ~ .. 匹配任意数量的参数.
*/
@Pointcut("execution( public * com.ifuwu.order.demo.controller..*.*(..))")
public void aopPointCut() {
}
@Before("aopPointCut()")
public void doBefore(JoinPoint joinPoint) {
methodName = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
startTime = System.currentTimeMillis();
}
@After("aopPointCut()")
public void doAfter() {
long E_time = System.currentTimeMillis() - startTime;
log.info("执行 " + methodName + " 耗时为:" + E_time + "ms");
}
@AfterReturning(returning = "object", pointcut = "aopPointCut()")
public void doAfterReturning(Object object) {
log.info("response={}", object.toString());
}
@Around("aopPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long end = System.currentTimeMillis();
log.error("+++++around " + joinPoint + "\tUse time : " + (end - start) + " ms!");
return result;
} catch (Throwable e) {
long end = System.currentTimeMillis();
log.error("+++++around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : " + e.getMessage());
throw e;
}
}
}
注意问题:
Spring
的文档中这么写的:Spring AOP
部分使用JDK
动态代理或者CGLIB
来为目标对象创建代理。如果被代理的目标实现了至少一个接口,则会使用JDK
动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB
代理。
默认是JDK
动态代理,可更改为cglib
总结
AOP开发流程
-
spring-boot-starter-aop
,加入依赖,默认就开启了AOP
的支持 - 写一个
Aspect
,封装横切关注点(日志,监控等等),需要配置通知(前置通知、后置通知等等)和 切入点(哪些包的哪些类的哪些方法等等) - 这个
Aspect
需要纳入到spring
容器管理,并且需要加入@Aspect
可以使用spring.aop.auto
配置决定是否启用AOP,默认启用。
application.properties
配置如下属性,则aop
失效
spring.aop.auto=false
看了AopAutoConfiguration
源码发现我们可以指定是JDK
的动态代理还是cglib
的动态代理,spring.aop.proxy-target-class
这个属性。
默认是使用基于JDK
的动态代理来实现AOP
,spring.aop.proxy-target-class=false
或者不配置,表示使用JDK
的动态代理,pring.aop.proxy-target-class=true
表示使用cglib
,如果配置了spring.aop.proxy-target-class=false
,但是代理类没有实现接口,则依然使用cglib
。
也可以使用
@EnableAspectJAutoProxy(proxyTargetClass=true,exposeProxy=true)
这个注解,注解表示启用AOP
,当然默认也是启用AOP
的,因为springboot
的自动装配机制,第一个参数表示是使用JDK
的动态代理还是Cglib
的代理,第二个参数表示可以使用AopContext
这个类进行一些操作。
注释之前所有的application.properties
中的配置,在启动类上加上@EnableAspectJAutoProxy(proxyTargetClass=true,exposeProxy=true)
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass=true,exposeProxy=true)
public class Application {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(Application.class,args);
String userServiceClassname = context.getBean("userService").getClass().getName();
System.out.println("userServiceClassname==="+userServiceClassname);
String orderServiceclassname = context.getBean("orderService").getClass().getName();
System.out.println("orderServiceclassname===="+orderServiceclassname);
}
}
表示自己启动cglib
代理,并且exposeProxy
配置为true
表示可以横切关注点中使用AopContext
这个类,修改一下上面的LogAspect
的log
方法
@Before("execution(* com.zhihao.miao.service..*.*(..))")
public void log(){
logger.info("before method log done"+ AopContext.currentProxy().getClass());
//logger.info("before method log done");
}
启动并测试
可以通过AopContext
得到当前的代理对象,更一步得到一些方法签名,方法的参数等一些具体信息。
延申
上面提及动态代理的选择,可使用JDK
或是cglib
动态代理的方式,我们来看看这两者本身有啥区别。
一、 JDK和CGLIB动态代理原理
1、JDK动态代理
利用拦截器(拦截器必须实现InvocationHanlder
)加上反射机制生成一个实现代理接口的匿名类,
在调用具体方法前调用InvokeHandler
来处理。
2、CGLIB动态代理
利用ASM
开源包,对代理对象类的class
文件加载进来,通过修改其字节码生成子类来处理。
3、何时使用JDK还是CGLIB?
1)如果目标对象实现了接口,默认情况下会采用JDK
的动态代理实现AOP
。
2)如果目标对象实现了接口,可以强制使用CGLIB
实现AOP
。
3)如果目标对象没有实现了接口,必须采用CGLIB
库,Spring
会自动在JDK
动态代理和CGLIB
之间转换。
4、如何强制使用CGLIB实现AOP?
1)添加CGLIB库(aspectjrt-xxx.jar、aspectjweaver-xxx.jar、cglib-nodep-xxx.jar)
2)在Spring配置文件中加入<aop:aspectj-autoproxy proxy-target-class="true"/>
5、JDK动态代理和CGLIB字节码生成的区别?
1)JDK动态代理只能对实现了接口的类生成代理,而不能针对类。
2)CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,
并覆盖其中方法实现增强,但是因为采用的是继承,所以该类或方法最好不要声明成`final`,
对于`final`类或方法,是无法继承的。
6、CGlib比JDK快?
1)使用CGLib
实现动态代理,CGLib
底层采用ASM
字节码生成框架,使用字节码技术生成代理类,
在jdk6
之前比使用Java反射效率要高。唯一需要注意的是,CGLib
不能对声明为final
的方法进行代理,
因为CGLib
原理是动态生成被代理类的子类。
2)在jdk6
、jdk7
、jdk8
逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK
代理效率高于CGLIB
代理效率
只有当进行大量调用的时候,jdk6
和jdk7
比CGLIB
代理效率低一点,但是到jdk8的时候,jdk
代理效率高于CGLIB
代理,
总之,每一次jdk
版本升级,jdk
代理效率都得到提升,而CGLIB
代理消息确有点跟不上步伐。
7、Spring如何选择用JDK还是CGLIB?
1)当Bean实现接口时,Spring
就会用JDK
的动态代理。
2)当Bean没有实现接口时,Spring
使用CGlib
是实现。
3)可以强制使用CGlib
(在spring配置中加入<aop:aspectj-autoproxy proxy-target-class="true"/>)。
二、代码实例
接口:
/**
* 用户管理接口(真实主题和代理主题的共同接口,这样在任何可以使用真实主题的地方都可以使用代理主题代理。)
* --被代理接口定义
*/
public interface IUserManager {
void addUser(String id, String password);
}
实现类:
/**
* 用户管理接口实现(被代理的实现类)
*/
public class UserManagerImpl implements IUserManager {
@Override
public void addUser(String id, String password) {
System.out.println("======调用了UserManagerImpl.addUser()方法======");
}
}
JDK代理实现:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* JDK动态代理类
*/
public class JDKProxy implements InvocationHandler {
/** 需要代理的目标对象 */
private Object targetObject;
/**
* 将目标对象传入进行代理
*/
public Object newProxy(Object targetObject) {
this.targetObject = targetObject;
//返回代理对象
return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(),
targetObject.getClass().getInterfaces(), this);
}
/**
* invoke方法
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 一般我们进行逻辑处理的函数比如这个地方是模拟检查权限
checkPopedom();
// 设置方法的返回值
Object ret = null;
// 调用invoke方法,ret存储该方法的返回值
ret = method.invoke(targetObject, args);
return ret;
}
/**
* 模拟检查权限的例子
*/
private void checkPopedom() {
System.out.println("======检查权限checkPopedom()======");
}
}
CGLIB代理实现:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* CGLibProxy动态代理类
*/
public class CGLibProxy implements MethodInterceptor {
/** CGLib需要代理的目标对象 */
private Object targetObject;
public Object createProxyObject(Object obj) {
this.targetObject = obj;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass());
enhancer.setCallback(this);
Object proxyObj = enhancer.create();
// 返回代理对象
return proxyObj;
}
@Override
public Object intercept(Object proxy, Method method, Object[] args,
MethodProxy methodProxy) throws Throwable {
Object obj = null;
// 过滤方法
if ("addUser".equals(method.getName())) {
// 检查权限
checkPopedom();
}
obj = method.invoke(targetObject, args);
return obj;
}
private void checkPopedom() {
System.out.println("======检查权限checkPopedom()======");
}
}
客户端测试类:
/**
* 代理模式[[ 客户端--》代理对象--》目标对象 ]]
*/
public class Client {
public static void main(String[] args) {
System.out.println("**********************CGLibProxy**********************");
CGLibProxy cgLibProxy = new CGLibProxy();
IUserManager userManager = (IUserManager) cgLibProxy.createProxyObject(new UserManagerImpl());
userManager.addUser("lanhuigu", "123456");
System.out.println("**********************JDKProxy**********************");
JDKProxy jdkPrpxy = new JDKProxy();
IUserManager userManagerJDK = (IUserManager) jdkPrpxy.newProxy(new UserManagerImpl());
userManagerJDK.addUser("lanhuigu", "123456");
}
}
三、JDK和CGLIB动态代理总结
JDK
代理是不需要第三方库支持,只需要JDK
环境就可以进行代理,使用条件:
1)实现InvocationHandler
2)使用Proxy.newProxyInstance
产生代理对象
3)被代理的对象必须要实现接口
CGLib
必须依赖于CGLib
的类库,但是它需要类来实现任何接口代理的是指定的类生成一个子类,覆盖其中的方法,是一种继承,但是针对接口编程的环境下推荐使用JDK
的代理
回顾切入点表达式
execution———用来匹配执行方法的连接点
语法结构:
execution( 方法修饰符 方法返回值 方法所属类 匹配方法名 ( 方法中的形参表 ) 方法申明抛出的异常 )
各部分都支持通配符 *
来匹配全部。
方法返回值、匹配方法名、方法中的形参表不能省略
比较特殊的为形参表部分,其支持两种通配符
*
:代表一个任意类型的参数;
..
:代表零个或多个任意类型的参数。
例如:
-
()匹配一个无参方法
-
(..)匹配一个可接受任意数量参数和类型的方法
-
(*)匹配一个接受一个任意类型参数的方法
-
(*,Integer)匹配一个接受两个参数的方法,第一个可以为任意类型,第二个必须为Integer。
下面举一些execution
的使用实例:
分类 | 示例 | 描述 |
---|---|---|
通过方法签名定义切入点 | execution(public * * (..)) | 匹配所有目标类的public方法,第一个* 为返回类型,第二个* 为方法名 |
execution(* save* (..)) | 匹配所有目标类以save开头的方法,第一个* 代表返回类型 |
|
execution(**product(*,String)) |
匹配目标类所有以product结尾的方法,并且其方法的参数表第一个参数可为任意类型,第二个参数必须为String | |
通过类定义切入点 | execution(* aop_part.Demo1.service.*(..)) | 匹配service接口及其实现子类中的所有方法 |
通过包定义切入点 | execution(* aop_part.*(..)) | 匹配aop_part包下的所有类的所有方法,但不包括子包 |
execution(* aop_part..*(..)) | 匹配aop_part包下的所有类的所有方法,包括子包。(当".."出现在类名中时,后面必须跟* ,表示包、子孙包下的所有类) |
|
execution(* aop_part..*.*service.find*(..)) |
匹配aop_part包及其子包下的所有后缀名为service的类中,所有方法名必须以find为前缀的方法 | |
通过方法形参定义切入点 | execution(*foo(String,int)) | 匹配所有方法名为foo,且有两个参数,其中,第一个的类型为String,第二个的类型为int |
execution(* foo(String,..)) | 匹配所有方法名为foo,且至少含有一个参数,并且第一个参数为String的方法(后面可以有任意个类型不限的形参) |
网友评论