前言
首先说一下什么是AOP?
AOP就是面向切面编程,它是一个思想,通过切面,我们可以将那些反复出现的代码抽取出来,放在一个地方统一处理,提高代码的复用性。AOP的一大好处就是解耦。以下几种方式实现AOP:
1自定义注解+@Aspect
2拦截器
3过滤器
4.JDK动态代理和CGlib
5.设计模型--静态代理
*.基于非侵入式运行时AOP方案(篇幅问题,不细说,感兴趣的朋友可以自行百度阿里开源的jvm-Sandbox)
自定义注解+@Aspect 实现日志记录1.首先你需要先引入pom依赖。(springboot2.x默认使用的代理是cglib代理)
<dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-aopartifactId>dependency><dependency> <groupId>com.google.code.gsongroupId> <artifactId>gsonartifactId> <version>2.8.5version>dependency>
注意:
在application.properties中也不需要添加spring.aop.auto=true,这个默认就是true,值为true就是启用@EnableAspectJAutoProxy注解了。
你不需要手工添加在启动类上添加 @EnableAspectJAutoProxy 注解。
当你需要使用CGLIB来实现AOP的时候,需要配置spring.aop.proxy-target-class=true,这个默认值是false,不然默认使用的是标准Java的实现(JDK动态代理基于接口代理)。
2.自定义日志注解(使用Java元注解,Java5.0定义了4个标准的meta-annotation类型)
@Retention(RetentionPolicy.RUNTIME) //定义为运行时使用注解@Target({ElementType.METHOD})//在方法上使用注解@Documented//注解将包含javaDoc中public @interface WebLog { /** * 日志描述信息 * @return */ //定义一个属性,默认作为空字符串 String description() default "";}
3.配置AOP切面类
@Aspect@Component //将这个类交给Spring管理public class WebLogAspect { private final static Logger logger = LoggerFactory.getLogger(WebLogAspect.class); /** 换行符 */ private static final String LINE_SEPARATOR = System.lineSeparator(); /** 以自定义 @WebLog 注解为切点 */ @Pointcut("@annotation(site.exception.aspect.WebLog)") //<=全路径 public void webLog() {} /** * 在切点之前织入 * @param joinPoint * @throws Throwable */ @Before("webLog()") public void doBefore(JoinPoint joinPoint) throws Throwable { // 开始打印请求日志 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); // 获取 @WebLog 注解的描述信息 String methodDescription = getAspectLogDescription(joinPoint); // 打印请求相关参数 logger.info("========================================== Start =========================================="); // 打印请求 url logger.info("URL: {}", request.getRequestURL().toString()); // 打印描述信息 logger.info("Description : {}", methodDescription); // 打印 Http method logger.info("HTTP Method : {}", request.getMethod()); // 打印调用 controller 的全路径以及执行方法 logger.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName()); // 打印请求的 IP logger.info("IP : {}", request.getRemoteAddr()); // 打印请求入参 logger.info("Request Args : {}", new Gson().toJson(joinPoint.getArgs())); } /** * 在切点之后织入 * @throws Throwable */ @After("webLog()") public void doAfter() throws Throwable { // 接口结束后换行,方便分割查看 logger.info("=========================================== End ===========================================" + LINE_SEPARATOR); } /** * 环绕 * @param proceedingJoinPoint * @return * @throws Throwable */ @Around("webLog()") public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long startTime = System.currentTimeMillis(); //执行切点后,会去依次调用 @Before -> 接口逻辑代码(之后,执行完doAround方法) -> @After -> @AfterReturning; Object result = proceedingJoinPoint.proceed(); // 打印出参 logger.info("Response Args : {}", new Gson().toJson(result)); // 执行耗时 logger.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime); return result; } /** * 获取切面注解的描述 * * @param joinPoint 切点 * @return 描述信息 * @throws Exception */ public String getAspectLogDescription(JoinPoint joinPoint) throws Exception { String targetName = joinPoint.getTarget().getClass().getName(); String methodName = joinPoint.getSignature().getName(); Object[] arguments = joinPoint.getArgs(); Class targetClass = Class.forName(targetName); Method[] methods = targetClass.getMethods(); StringBuilder description = new StringBuilder(""); for (Method method : methods) { if (method.getName().equals(methodName)) { Class[] clazzs = method.getParameterTypes(); if (clazzs.length == arguments.length) { description.append(method.getAnnotation(WebLog.class).description()); break; } } } return description.toString(); }}
4.使用注解
@PostMapping("/user")@WebLog(description="用户请求接口")public User userLogin(@RequestBody User user){ logger.info("user login ..."); return user; }
特别说明
多切面如何指定优先级?
假设说我们的服务中不止定义了一个切面,比如说我们针对 Web 层的接口,不止要打印日志,还要校验 token 等。要如何指定切面的优先级呢?也就是如何指定切面的执行顺序?
我们可以通过 @Order(i)注解来指定优先级,注意:i 值越小,优先级则越高。
假设说我们定义上面这个日志切面的优先级为 @Order(10), 然后我们还有个校验 token 的切面 CheckTokenAspect.java,我们定义为了 @Order(11), 那么它们之间的执行顺序如下
spring借鉴了AspectJ的切面,以提供注解驱动的AOP,本质上它依然是Spring基于代理的AOP,只是编程模型与AspectJ完全一致,这种风格的好处就是不需要使用XML进行配置。
通过拦截器实现
拦截器拦截的是URL
拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。Spring中拦截器有三个方法:preHandle,postHandle,afterCompletion。
@Configurationpublic class HomeOpenHandlerConfigration extends WebMvcConfigurerAdapter { //关键,将拦截器作为bean写入配置中 @Bean public HomeOpenInterceptor myInterceptor(){ return new HomeOpenInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptor()).addPathPatterns("/api/open/portal/**") .excludePathPatterns("/api/open/footerInfo").excludePathPatterns("/api/open/portal/template/default"); super.addInterceptors(registry); }}
/** * 首页外放拦截器 * */@Componentpublic class HomeOpenInterceptor extends HandlerInterceptorAdapter { @Autowired private PortalCommonService portalCommonService; @Autowired private ApplicationProperties applicationProperties; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断是否需要拦截 Boolean flag = false; if(flag){ //判断是否允许不登录的情况下 访问主页 //如果不允许匿名访问返回401 throw new UnauthenticatedException(); } //否则允许直接放过,不进行任何拦截 return true; }}
过滤器的实现
过滤器拦截的是URL
Spring中自定义过滤器(Filter)一般只有一个方法,返回值是void,当请求到达web容器时,会探测当前请求地址是否配置有过滤器,有则调用该过滤器的方法(可能会有多个过滤器),然后才调用真实的业务逻辑,至此过滤器任务完成。过滤器并没有定义业务逻辑执行前、后等,仅仅是请求到达就执行。
特别注意:过滤器方法的入参有request,response,FilterChain,其中FilterChain是过滤器链,使用比较简单,而request,response则关联到请求流程,因此可以对请求参数做过滤和修改,同时FilterChain过滤链执行完,并且完成业务流程后,会返回到过滤器,此时也可以对请求的返回数据做处理。
@Component@Order(1) //注解,配合 @WebFilter 注解使用,用于多个过滤器时定义执行顺序,值越小越先执行。@WebFilter(urlPatterns = "/*", filterName = "test")public class TestFilter implements Filter { @Override public void init(FilterConfig arg0) throws ServletException { System.out.println("过滤器初始化"); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.printf("过滤器实现"); System.out.println(((HttpServletRequest) servletRequest).getRequestURI()); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { System.out.println("过滤器销毁了"); } }
3.JDK动态代理
@SuppressWarnings("restriction")public class JavaProxyTest { public static void main(String[] args) throws Exception { JavaProxyInterface javaProxyInterface = new ConcreteClass(); JavaProxyInterface newJavaProxyInterface = (JavaProxyInterface) Proxy.newProxyInstance( JavaProxyTest.class.getClassLoader(), new Class[] { JavaProxyInterface.class }, new MyInvocationHandler(javaProxyInterface)); //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和oneDayFinal()的区别。oneDayFinal的方法aopMethod执行1次,oneDay的aopMethod执行1次 newJavaProxyInterface.gotoSchool(); newJavaProxyInterface.gotoWork(); newJavaProxyInterface.oneDayFinal(); newJavaProxyInterface.oneDay(); }}/*** InvocationHandler 的一个实现,实际上处理代理的逻辑在这里*/class MyInvocationHandler implements InvocationHandler { JavaProxyInterface javaProxy; public MyInvocationHandler(JavaProxyInterface javaProxy) { this.javaProxy = javaProxy; } private void aopMethod() { System.out.println("before method"); } //继承方法,代理时实际执行的犯法,如果要实现原方法,则需要调用method.invoke(javaProxy, args),这里还调用了一个aopMethod(),可以类比于Spring中的切面before注解。 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { aopMethod(); return method.invoke(javaProxy, args); }}/*** 需要一个最顶层接口,必须*/interface JavaProxyInterface { void gotoSchool(); void gotoWork(); void oneDay(); void oneDayFinal();}/*** 需要被代理的类,实现了顶层接口,非必须*/class ConcreteClass implements JavaProxyInterface { @Override public void gotoSchool() { System.out.println("gotoSchool"); } @Override public void gotoWork() { System.out.println("gotoWork"); } @Override public void oneDay() { gotoSchool(); gotoWork(); } @Override public final void oneDayFinal() { gotoSchool(); gotoWork(); }}
底层实现部分代码:
// proxyName 为类名,interfaces为顶层接口Classbyte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces); File file = new File("D:/testProxy/Ddd.class");FileOutputStream fileOutputStream = new FileOutputStream(file);fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();
CGlib动态代理
public class CglibProxyTest { public static void main(String[] args) throws Exception { CglibTestSon CglibTestSon = new CglibTestSon(); Enhancer enhancer = new Enhancer(); Callback s = new MthdInvoker(CglibTestSon); enhancer.setSuperclass(CglibTestSon.class); Callback callbacks[] = new Callback[] { s }; enhancer.setCallbacks(callbacks); CglibTestSon CglibTestSon2 = (CglibTestSon) enhancer.create(); CglibTestSon2.gotoHome(); CglibTestSon2.gotoSchool(); //这里可以看到这个类以及被代理,在执行方法前会执行aopMethod()。这里需要注意的是oneDay()方法和onedayFinal()的区别。onedayFinal的方法aopMethod执行2次,oneDay的aopMethod执行1次 ,注意这里和jdk的代理的区别 CglibTestSon2.oneday(); CglibTestSon2.onedayFinal(); }}/*** 需要被代理的类,不需要实现顶层接口*/class CglibTestSon { public CglibTestSon() { } public void gotoHome() { System.out.println("============gotoHome============"); } public void gotoSchool() { System.out.println("===========gotoSchool============"); } public void oneday() { gotoHome(); gotoSchool(); } public final void onedayFinal() { gotoHome(); gotoSchool(); }}/*** 可以类比于jdk动态代理中的InvocationHandler ,实际上被代理后重要的类,实际上后续执行的就是intercept里的方法,如果需要执行原来的方法,则调用 method.invoke(s, args);这里也加了一个aopMethod();*/class MthdInvoker implements MethodInterceptor { private CglibTestSon s; public MthdInvoker(CglibTestSon s) { this.s = s; } private void aopMethod() { System.out.println("i am aopMethod"); } public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { aopMethod(); Object a = method.invoke(s, args); return a; }}
CGlib底层实现部分代码:
byte[] bs = DefaultGeneratorStrategy.INSTANCE.generate(enhancer);FileOutputStream fileOutputStream = new FileOutputStream("D:/testProxy/Cc.class");fileOutputStream.write(bs);fileOutputStream.flush();fileOutputStream.close();
简单来看就是先生成新的class文件,然后加载到jvm中,然后使用反射,先用class取得他的构造方法,然后使用构造方法反射得到他的一个实例。
标红的是最复杂的。然后cglib的实现原理基本一致,唯一的区别在于生成新的class文件方式和结果不一样。
4.静态代理模式的实现AOP
Font.java
package com.java.proxy; import lombok.Data; @Datapublic class Font { private String name;}
FontProvider.java
package com.java.proxy; public interface FontProvider { Font getFont(String name); void printName(String name);}
代理类CachedFontProvider.java
/** * 给FontProvider的getFont添加缓存功能,用静态代理来实现 * */public class CachedFontProvider implements FontProvider { private FontProvider fontProvider; private Map cached = new HashMap(); public CachedFontProvider() { fontProvider = new FontProviderFromDisk(); } @Override public Font getFont(String name) { System.out.println("静态代理getFont()"); Font font = cached.get(name); if(font == null) { font = fontProvider.getFont(name); cached.put(name, font); } return font; } @Override public void printName(String name) { System.out.println("静态代理printName()"); fontProvider.printName(name);; } }
工厂类ProviderFactory.java
/** * 每个字体都增加了缓存功能,其实工厂就是用的缓存字体提供器,跟io一样 * 使用代理(静态),已经避免了再去修改每个字体提供器(这违反了开闭原则,而且工作量很大,容易出错;而且如果要增加别的功能 * 比如日志打印,权限检查,异常处理,每个都要去修改,代码重复,而且很麻烦) * * ② 然而为什么要用动态代理? *考虑以下各种情况,有多个提供类,每个类都有getXxx(String name)方法, *每个类都要加入缓存功能,使用静态代理虽然也能实现,但是也是略显繁琐,需要手动一一创建代理类。 */public class ProviderFactory { public static FontProvider getFontProvider() { return new CachedFontProvider(); } }
测试类;
public class Business { public static void main(String[] args) { FontProvider fontProvider = ProviderFactory.getFontProvider(); Font font = fontProvider.getFont("微软雅黑"); System.out.println(font); fontProvider.printName("代理模式实现AOP"); } }
总结三者功能类似,但各有优势,从过滤器--》拦截器--》切面,拦截规则越来越细致,执行顺序依次是过滤器、拦截器、切面。一般情况下数据被过滤的时机越早对服务的性能影响越小,因此我们在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。比如权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。
点个“在看”表示朕
已阅