java 切面_实用|AOP切面编程手段大汇总

c6a23ee22cb7b62d57397ffc4aa14c7b.gif

点击上方"欧学长的架构成长之路" 关注我

前言

首先说一下什么是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; }

特别说明

d55161d23e7307445dd361ad50d87250.png

多切面如何指定优先级?

假设说我们的服务中不止定义了一个切面,比如说我们针对 Web 层的接口,不止要打印日志,还要校验 token 等。要如何指定切面的优先级呢?也就是如何指定切面的执行顺序?

我们可以通过 @Order(i)注解来指定优先级,注意:i 值越小,优先级则越高。

假设说我们定义上面这个日志切面的优先级为 @Order(10), 然后我们还有个校验 token 的切面 CheckTokenAspect.java,我们定义为了 @Order(11), 那么它们之间的执行顺序如下

a68365db00910c8b6441965e6e74c155.png

         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可以针对代码的方法级别做拦截,很适合日志功能。

点个“在看”表示朕

已阅

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值