当前位置: 移动技术网 > IT编程>开发语言>Java > Spring入门(十一):Spring AOP使用进阶

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

2019年08月28日  | 移动技术网IT编程  | 我要评论

在上篇博客中,我们了解了什么是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技术干货,让我们一起进步。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网