欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Spring入门(十一):Spring AOP使用进阶

程序员文章站 2022-10-24 14:12:24
在上篇博客中,我们了解了什么是AOP以及在Spring中如何使用AOP,本篇博客继续深入讲解下AOP的高级用法。 1. 声明带参数的切点 假设我们有一个接口CompactDisc和它的实现类BlankDisc: 现在我们的需求是记录每首歌曲的播放次数,按照以往的做法,我们可能会修改BlankDisc ......

在上篇博客中,我们了解了什么是aop以及在spring中如何使用aop,本篇博客继续深入讲解下aop的高级用法。

1. 声明带参数的切点

假设我们有一个接口compactdisc和它的实现类blankdisc:

package chapter04.soundsystem;

/**
 * 光盘
 */
public interface compactdisc {
    void play();

    void play(int songnumber);
}
package chapter04.soundsystem;

import java.util.list;

/**
 * 空白光盘
 */
public class blankdisc implements compactdisc {
    /**
     * 唱片名称
     */
    private string title;

    /**
     * 艺术家
     */
    private string artist;

    /**
     * 唱片包含的歌曲集合
     */
    private list<string> songs;

    public blankdisc(string title, string artist, list<string> songs) {
        this.title = title;
        this.artist = artist;
        this.songs = songs;
    }

    @override
    public void play() {
        system.out.println("playing " + title + " by " + artist);
        for (string song : songs) {
            system.out.println("-song:" + song);
        }
    }

    /**
     * 播放某首歌曲
     *
     * @param songnumber
     */
    @override
    public void play(int songnumber) {
        system.out.println("play song:" + songs.get(songnumber - 1));
    }
}

现在我们的需求是记录每首歌曲的播放次数,按照以往的做法,我们可能会修改blankdisc类的逻辑,在播放每首歌曲的代码处增加记录播放次数的逻辑,但现在我们使用切面,在不修改blankdisc类的基础上,实现相同的功能。

首先,新建切面songcounter如下所示:

package chapter04.soundsystem;

import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.before;
import org.aspectj.lang.annotation.pointcut;

import java.util.hashmap;
import java.util.map;

@aspect
public class songcounter {
    private map<integer, integer> songcounts = new hashmap<>();

    /**
     * 可重用的切点
     *
     * @param songnumber
     */
    @pointcut("execution(* chapter04.soundsystem.compactdisc.play(int)) && args(songnumber)")
    public void songplayed(int songnumber) {
    }

    @before("songplayed(songnumber)")
    public void countsong(int songnumber) {
        system.out.println("播放歌曲计数:" + songnumber);
        int currentcount = getplaycount(songnumber);
        songcounts.put(songnumber, currentcount + 1);
    }

    /**
     * 获取歌曲播放次数
     *
     * @param songnumber
     * @return
     */
    public int getplaycount(int songnumber) {
        return songcounts.getordefault(songnumber, 0);
    }
}

重点关注下切点表达式execution(* chapter04.soundsystem.compactdisc.play(int)) && args(songnumber),其中int代表参数类型,songnumber代表参数名称。

新建配置类songcounterconfig:

package chapter04.soundsystem;

import org.springframework.context.annotation.bean;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.enableaspectjautoproxy;

import java.util.arraylist;
import java.util.list;

@configuration
@enableaspectjautoproxy
public class songcounterconfig {
    @bean
    public compactdisc yehuimei() {
        list<string> songs = new arraylist<>();
        songs.add("东风破");
        songs.add("以父之名");
        songs.add("晴天");
        songs.add("三年二班");
        songs.add("你听得到");

        blankdisc blankdisc = new blankdisc("叶惠美", "周杰伦", songs);
        return blankdisc;
    }

    @bean
    public songcounter songcounter() {
        return new songcounter();
    }
}

注意事项:

1)配置类要添加@enableaspectjautoproxy注解启用aspectj自动代理。

2)切面songcounter要被声明bean,否则切面不会生效。

最后,新建测试类songcountertest如下所示:

package chapter04.soundsystem;

import org.junit.test;
import org.junit.runner.runwith;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.test.context.contextconfiguration;
import org.springframework.test.context.junit4.springjunit4classrunner;

import static org.junit.assert.assertequals;

@runwith(springjunit4classrunner.class)
@contextconfiguration(classes = songcounterconfig.class)
public class songcountertest {
    @autowired
    private compactdisc compactdisc;

    @autowired
    private songcounter songcounter;

    @test
    public void testsongcounter() {
        compactdisc.play(1);

        compactdisc.play(2);

        compactdisc.play(3);
        compactdisc.play(3);
        compactdisc.play(3);
        compactdisc.play(3);

        compactdisc.play(5);
        compactdisc.play(5);

        assertequals(1, songcounter.getplaycount(1));
        assertequals(1, songcounter.getplaycount(2));

        assertequals(4, songcounter.getplaycount(3));

        assertequals(0, songcounter.getplaycount(4));

        assertequals(2, songcounter.getplaycount(5));
    }
}

运行测试方法testsongcounter(),测试通过,输出结果如下所示:

播放歌曲计数:1

play song:东风破

播放歌曲计数:2

play song:以父之名

播放歌曲计数:3

play song:晴天

播放歌曲计数:3

play song:晴天

播放歌曲计数:3

play song:晴天

播放歌曲计数:3

play song:晴天

播放歌曲计数:5

play song:你听得到

播放歌曲计数:5

play song:你听得到

2. 限定匹配带有指定注解的连接点

在之前我们声明的切点中,切点表达式都是使用全限定类名和方法名匹配到某个具体的方法,但有时候我们需要匹配到使用某个注解的所有方法,此时就可以在切点表达式使用@annotation来实现,注意和之前在切点表达式中使用execution的区别。

为了更好的理解,我们还是通过一个具体的例子来讲解。

首先,定义一个注解action:

package chapter04;

import java.lang.annotation.*;

@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface action {
    string name();
}

然后定义2个使用@action注解的方法:

package chapter04;

import org.springframework.stereotype.service;

@service
public class demoannotationservice {
    @action(name = "注解式拦截的add操作")
    public void add() {
        system.out.println("demoannotationservice.add()");
    }

    @action(name = "注解式拦截的plus操作")
    public void plus() {
        system.out.println("demoannotationservice.plus()");
    }
}

接着定义切面logaspect:

package chapter04;

import org.aspectj.lang.joinpoint;
import org.aspectj.lang.annotation.after;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.stereotype.component;

import java.lang.reflect.method;

@aspect
@component
public class logaspect {
    @pointcut("@annotation(chapter04.action)")
    public void annotationpointcut() {
    }

    @after("annotationpointcut()")
    public void after(joinpoint joinpoint) {
        methodsignature methodsignature = (methodsignature) joinpoint.getsignature();
        method method = methodsignature.getmethod();
        action action = method.getannotation(action.class);
        system.out.println("注解式拦截 " + action.name());
    }
}

注意事项:

1)切面使用了@component注解,以便spring能自动扫描到并创建为bean,如果这里不添加该注解,也可以通过java配置或者xml配置的方式将该切面声明为一个bean,否则切面不会生效。

2)@pointcut("@annotation(chapter04.action)"),这里我们在定义切点时使用了@annotation来指定某个注解,而不是之前使用execution来指定某些或某个方法。

我们之前使用的切面表达式是execution(* chapter04.concert.performance.perform(..))是匹配到某个具体的方法,如果想匹配到某些方法,可以修改为如下格式:

execution(* chapter04.concert.performance.*(..))

然后,定义配置类aopconfig:

package chapter04;

import org.springframework.context.annotation.componentscan;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.enableaspectjautoproxy;

@configuration
@componentscan
@enableaspectjautoproxy
public class aopconfig {
}

注意事项:配置类需要添加@enableaspectjautoproxy注解启用aspectj自动代理,否则切面不会生效。

最后新建main类,在其main()方法中添加如下测试代码:

package chapter04;

import org.springframework.context.annotation.annotationconfigapplicationcontext;

public class main {
    public static void main(string[] args) {
        annotationconfigapplicationcontext context = new annotationconfigapplicationcontext(aopconfig.class);

        demoannotationservice demoannotationservice = context.getbean(demoannotationservice.class);

        demoannotationservice.add();
        demoannotationservice.plus();

        context.close();
    }
}

输出结果如下所示:

demoannotationservice.add()

注解式拦截 注解式拦截的add操作

demoannotationservice.plus()

注解式拦截 注解式拦截的plus操作

可以看到使用@action注解的add()和plus()方法在执行完之后,都执行了切面中定义的after()方法。

如果再增加一个使用@action注解的subtract()方法,执行完之后,也会执行切面中定义的after()方法。

3. 项目中的实际使用

在实际的使用中,切面很适合用来记录日志,既满足了记录日志的需求又让日志代码和实际的业务逻辑隔离开了,

下面看下具体的实现方法。

首先,声明一个访问日志的注解accesslog:

package chapter04.log;

import java.lang.annotation.elementtype;
import java.lang.annotation.retention;
import java.lang.annotation.retentionpolicy;
import java.lang.annotation.target;

/**
 * 访问日志 注解
 */
@target(elementtype.method)
@retention(retentionpolicy.runtime)
public @interface accesslog {
    boolean recordlog() default true;
}

然后定义访问日志的切面accesslogaspectj:

package chapter04.log;

import com.alibaba.fastjson.json;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.around;
import org.aspectj.lang.annotation.aspect;
import org.aspectj.lang.annotation.pointcut;
import org.aspectj.lang.reflect.methodsignature;
import org.springframework.stereotype.component;

@aspect
@component
public class accesslogaspectj {
    @pointcut("@annotation(accesslog)")
    public void accesslog() {

    }

    @around("accesslog()")
    public void recordlog(proceedingjoinpoint proceedingjoinpoint) {
        try {
            object object = proceedingjoinpoint.proceed();

            accesslog accesslog = ((methodsignature) proceedingjoinpoint.getsignature()).getmethod().getannotation(accesslog.class);

            if (accesslog != null && accesslog.recordlog() && object != null) {
                // 这里只是打印出来,一般实际使用时都是记录到公司的日志中心
                system.out.println("方法名称:" + proceedingjoinpoint.getsignature().getname());
                system.out.println("入参:" + json.tojsonstring(proceedingjoinpoint.getargs()));
                system.out.println("出参:" + json.tojsonstring(object));
            }
        } catch (throwable throwable) {
            // 这里可以记录异常日志到公司的日志中心
            throwable.printstacktrace();
        }
    }
}

上面的代码需要在pom.xml中添加如下依赖:

<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupid>com.alibaba</groupid>
    <artifactid>fastjson</artifactid>
    <version>1.2.59</version>
</dependency>

然后定义配置类logconfig:

package chapter04.log;

import org.springframework.context.annotation.componentscan;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.enableaspectjautoproxy;

@configuration
@componentscan
@enableaspectjautoproxy
public class logconfig {
}

注意事项:不要忘记添加@enableaspectjautoproxy注解,否则切面不会生效。

然后,假设你的对外接口是下面这样的:

package chapter04.log;

import org.springframework.stereotype.service;

@service
public class mockservice {
    @accesslog
    public string mockmethodone(int index) {
        return index + "mockservice.mockmethodone";
    }

    @accesslog
    public string mockmethodtwo(int index) {
        return index + "mockservice.mockmethodtwo";
    }
}

因为要记录日志,所以每个方法都添加了@accesslog注解。

最后新建main类,在其main()方法中添加如下测试代码:

package chapter04.log;

import org.springframework.context.annotation.annotationconfigapplicationcontext;

public class main {
    public static void main(string[] args) {
        annotationconfigapplicationcontext context = new annotationconfigapplicationcontext(logconfig.class);

        mockservice mockservice = context.getbean(mockservice.class);

        mockservice.mockmethodone(1);
        mockservice.mockmethodtwo(2);

        context.close();
    }
}

输出日志如下所示:

方法名称:mockmethodone

入参:[1]

出参:"1mockservice.mockmethodone"

方法名称:mockmethodtwo

入参:[2]

出参:"2mockservice.mockmethodtwo"

如果某个方法不需要记录日志,可以不添加@accesslog注解:

public string mockmethodtwo(int index) {
    return index + "mockservice.mockmethodtwo";
}

也可以指定recordlog为false:

@accesslog(recordlog = false)
public string mockmethodtwo(int index) {
    return index + "mockservice.mockmethodtwo";
}

这里只是举了个简单的记录日志的例子,大家也可以把切面应用到记录接口耗时等更多的场景。

4. 源码及参考

源码地址:,欢迎下载。

craig walls 《spring实战(第4版)》

汪云飞《java ee开发的颠覆者:spring boot实战》

aop(面向切面编程)_百度百科

5. 最后

打个小广告,欢迎扫码关注微信公众号:「申城异乡人」,定期分享java技术干货,让我们一起进步。

Spring入门(十一):Spring AOP使用进阶