稻草问答-Kafka-AOP

稻草问答-Kafka-AOP

1 用Kafka优化网站性能

1.1 发表问题的性能问题

上述案例中已经完成是搜索功能,我们将问题数据从MySQL复制迁移到Elasticsearch中,然后在Elasticsearch中进行全文搜索功能。然而目前稻草问答的新问题提交功能是直接保存到数据库中,没有保存到Elasticsearch,这样就不能被全文搜索到。显然要解决这个问题就需要在提交新问题时候,就需要将问题同时保存到Elasticsearch中。这个问题的解决方案可以利用Ribbon实现,也就是在保存问题时候,利用Ribbon将问题数据同时保存到Elasticsearch中:

如何解决重构以后带来的性能延迟问题呢,在互联网架构的解决方案就是采用缓存队列技术。常用的缓存队列有Kafka、RabbitMQ等。

如何概要的描述优化呢?可以这样说︰在互联网架构中,一个主业务流程包含耗时的分支处理流程时候,可以考虑将分支的耗时处理流程交给队列进行异步处理,这样可以提高主流程的处理性能,减少主流程处理延迟。但是同时也会带来数据延迟处理造成的短暂不一致的问题。要容忍这种短暂的不一致情况。如果不能容忍数据不一致,就采用同步处理。
下面就先研究一下Kafka,然后再使用Kafka优化问题保存流程。

1.2 异步数据处理方式

经过上述开发《稻草问答》已经初具规模,每次客户端发起的请求,一般在服务端需要针对这些请求做一些数据处理,当这些事情其实用户并不关心或者用户不需要立即拿到这些事情的处理结果,这种情况就比较适合用异步的方式处理这些事情。

异步处理的优点:

  • 缩短接口响应时间,使用户的请求快速返回,用户体验更好。

  • 避免线程长时间处于运行状态,这样会引起服务线程池的可用线程长时间不够用,进而引起线程池任务队列长度增大,从而阻塞更多请求任务,使得更多请求得不到技术处理。

  • 线程长时间处于运行状态,可能还会引起系统Load、CPU使用率、机器整体性能下降等一系列问题,甚至引发雪崩。异步的思路可以在不增加机器数和CPU数的情况下,有效解决这个问题。

    注意!异步处理方式不能及时得到处理结果,对于及时得到处理结果的请求,请使用直接请求处理方式。

异步队列产品很多:Kafka,RabbitMQ,ZeroMQ、RocketMQ等等。我们课程采用的是Kafka

1.3 Kafka

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。该项目的目标是为处理实时数据提供一个统一、高吞吐、低延迟的平台。Kafka最初是由Linkedln开发,并随后于2011年初开源。

Kafka架构的主要术语包括Topic(话题)、Record(记录)和Broker(中间商)。

  • Broker(中间商)用来实现数据存储的主机服务器,集群时候也负责复制消息记录;
  • Topic(话题)用来对消息进行分类,每个进入到Kafka的信息Record都会被放到一个Topic下;
  • Record(记录) 就是被队列传送处理的消息记录;
  • Producer (生产者)消息的生产者,由使用者编写,利用Kafka API将Record(消息记录)发送到Topic(话题)中,也就是生产了数据;
  • Consumer (消费者)消息的消费者,由使用者编写,利用Kafka API收取Topic(话题)中Record(消息记录),然后进行处理,消耗掉数据。
安装Kafka

与Tomcat等服务器软件类似,第一步就是下载安装软件,可以从Apache官方网站下载,也可以doc.canglaoshi.org提供的连接下载:

下载后安装并不麻烦,Kafka本 身是基于Java的,只需要释放到文件夹就安装好了,还要创建一个数据文件夹,用于存储kafka的数据文件:

启动Kafka服务之前需要更改配置文件/kafka_2.13-2.4.1/config/zookeeper. properties:
Windows系统配置(参考) :

# dataDir=/tmp/zookeeper
dataDir=D:/opt/kafka/zookeeper

苹果Mac系统配置(参考):

# dataDir=/tmp/zookeeper
dataDir=/Users/liucangsong/Documents/kafka/zookeeper

然后利用命令启动Kafka服务,由于Kafka利用了Zookeeper,所以要先启动Zookeeper服务,再启动Kafka服务:

Windows系统启动Zookeeper服务命令(参考) :

D:
cd kafka_2.13-2.4.1\bin\windows
zokeeper-server-start.bat ..\..\config\zookeeper.properties

Mac系统启动Kafka服务命令(参考) :

#进入Kafka文件夹
cd Documents/kafka_2.13-2.4.1/bin/
#动Zookeeper服务
./zookeeper-server-start.sh -daemon ../config/zookeeper.properties
#启动Kafka服务
./kafka-server-start.sh-daemon ../config/server.properties

1.4 使用SpringBoot测试Kafka

SpringBoot对Kafka提供了支持,只需要添加相关的依赖就可以使用Kafka服务了。具体步骤如下:

创建一个单独的模块项目,测试Kafka。

创建SpringBoot模块

添加模块名称

跳过选择依赖组件

确认模块的文件夹位置

修改pom.xml,设置项目继承于straw:

<parent>
    <groupId>cn.tedu</groupId>
    <artifactId>straw</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>straw-kafka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>straw-kafka</name>
<description>kafka 测试项目</description>

在父级项目straw的pom文件中添加模块:

<modules>
    <module>straw-portal</module>
    <module>straw-generator</module>
    <module>straw-resource</module>
    <module>straw-eureka</module>
    <module>straw-gateway</module>
    <module>straw-sys</module>
    <module>straw-commons</module>
    <module>straw-faq</module>
    <module>straw-search</module>
    <module>straw-kafka</module>
</modules>

为项目添加Kafka依赖,由于在传送Kafka消息时候需要利用Google JSON API对消息进行JSON编码,所以添加了gson API:

<dependencies>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.kafka</groupId>
        <artifactId>spring-kafka</artifactId>
    </dependency>
</dependencies>

在application.properties添加Kafka服务器配置,这样SpringBoot就可以连接到Kafka服务器了。这一点与使用MySQL服务器类似。

server.port=8082
spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=straw

logging.level.cn.tedu.straw.kafka=debug

首先编写一个类,封装需要发送的数据:

package cn.tedu.straw.kafka.vo;

import lombok.Data;
import lombok.experimental.Accessors;

@Data
@Accessors(chain=true)
public class DemoMessage {
    private String content;
    private Integer id;
    private Long time;
}

然后编写数据生产者负责发送数据,这个数据发送端利用了Spring提供的定时计划功每秒中利用kafkaTempalte向Kafka中发送一个数据 。kafka默认采用字符串序列化数据,数据发送时候利用Gson将Java对象转换为JSON格式发送。

package cn.tedu.straw.kafka.service;

import cn.tedu.straw.kafka.vo.DemoMessage;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
/**
 * 消息的生产者,就是消息的发送者
 */
@Component
@Slf4j
public class DemoProducer {
    @Resource
    private KafkaTemplate<String, String> kafkaTemplate;

    private Gson gson=new Gson();

    /**
     * 利用Spring安排了一个计划执行功能
     * 每间隔十秒钟执行一次sendMessage方法
     */
    @Scheduled(fixedRate = 1000*10)
    public void sendMessage(){
        DemoMessage message=new DemoMessage()
                .setContent("您好")
                .setId(100)
                .setTime(System.currentTimeMillis());
        log.debug("发送数据{}", message);
        //将需要发送的数据转换为JSON 格式进行发送
        String json=gson.toJson(message);
        kafkaTemplate.send("MyTopic",json);
    }
}

为了启动定时计划功能,需要在SpringBoot启动类上添加开启定时计划注解@EnableScheduling,为了使用Kafka也需要添加注解@EnableKafka:

@SpringBootApplication
@EnableScheduling
@EnableKafka
public class StrawKafkaApplication {

    public static void main(String[] args) {
        SpringApplication.run(StrawKafkaApplication.class, args);
    }
}

接收消息的案例也不麻烦,标志@KafkaListener注解的方法会自动收到Kafka中的消息,接收后会注入给ConsumerRecord参数。需要在@KafkaListener注解的参数上指定Topic (话题) 这样就可以收到指定Topic的消息了。要注意的是发送时候的Topic和接收的Topic必须一样。

package cn.tedu.straw.kafka.service;

import cn.tedu.straw.kafka.vo.DemoMessage;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
 * 监听接收kafka 的数据
 */
@Component
@Slf4j
public class DemoConsumer {
    private Gson gson=new Gson();

    @KafkaListener(topics = "MyTopic")
    public void receive(ConsumerRecord<String , String> record){
        /*
        record 代表从Kafka中收到数据消息,其中value值就是收到json数据
         */
        String json=record.value();
        log.debug("收到:{}",json);
        //利用Gson API将消息转换为消息对象
        DemoMessage message=gson.fromJson(json, DemoMessage.class);
        log.debug("收到对象:{}",message);
    }
}

测试......

2 利用Kafka重构问题保存功能

2.1 straw-faq项目向Kafka发送问题数据

首先为了约定发送端和接受端采用相同Topic名称,故在straw-commons项目中创建Topics类,声明Topic名称:

package cn.tedu.straw.commons.kafka;

public class Topics {
    /**
     * 从starw-faq向starw-search传输问题数据
     */
    public final static String QUESTIONS="search.questions";
}

然后在straw-faq项目中导入Kafka包:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

配置Kafka服务器位置,application.properties:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=straw

创建数据消费者类KafkaProducer,将问题数据发送到Kafka:

package cn.tedu.straw.faq.kafka;

import cn.tedu.straw.commons.kafka.Topics;
import cn.tedu.straw.commons.model.Question;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 负责向Kafka发送数据
 */
@Component
@Slf4j
public class KafkaProducer {
    @Resource
    private KafkaTemplate<String,String> kafkaTemplate;
    private Gson gson=new Gson();

    public void sendQuestion(Question question){
        String json=gson.toJson(question);
        log.debug("发送问题数据:{}", json);
        kafkaTemplate.send(Topics.QUESTIONS, json);
    }
}

重构保存数据到业务方法,在保存数据到最后一步将数据发送到Kafka:

@Resource
private KafkaProducer kafkaProducer;
@Override
@Transactional //声明式事务,当前方法中的SQL作为整体执行,执行失败会回滚到开始状态
public void saveQuestion(String username,QuestionVo questionVo) {
    log.debug("问题信息{}",questionVo);
    //获取当前用户
    //String username=userService.currentUsername();
    //获取用户全部信息
    //User username=userMapper.findUserByUsername(username);
    //String url="http://sys-service/v1/auth/user?username={1}";
    //User user =restTemplate.getForObject(url,User.class, username);
    User user=ribbonClient.getUser(username);
    log.debug("当前用户{}",user);


    //根据标签名数组创建标签名列表
    StringBuilder buf=new StringBuilder();
    for(String tagName:questionVo.getTagNames()){
        buf.append(tagName).append(",");
    }
    String tagNames=buf.deleteCharAt(buf.length()-1).toString();
    log.debug("标签名列表{}",tagNames);

    Question question=new Question()
        .setTitle(questionVo.getTitle())
        .setContent(questionVo.getContent())
        .setStatus(0)//0表示刚创建的问题还没有老师回复
        .setPublicStatus(0)//0表示只能学员自己查看到问题
        .setDeleteStatus(0)//0表示没有删除,1表示已经删除
        .setPageViews(0)//问题被查看到次数
        .setCreatetime(LocalDateTime.now())//问题创建时候,默认当前时间
        .setModifytime(LocalDateTime.now())//问题修改时间
        .setUserNickName(user.getNickname())//发布问题昵称,就是当前用户
        .setUserId(user.getId())
        .setTagNames(tagNames); //问题相关的标签名列表
    log.debug("问题信息{}",question);
    int rows=questionMapper.insert(question);
    if(rows !=1){
        throw new ServiceException("数据库繁忙,请稍后再试!");
    }
    log.debug("问题信息{}",question);

    //保存问题和标签的关系 questionVo.getTagNames();
    //根据标签名找到标签ID,问题ID和标签ID存储到 questionTag表
    Map<String, Tag> name2TagMap=tagService.getName2TagMap();

    for (String tagName:questionVo.getTagNames()){
        //根据标签名,查找到其对应到标签信息
        Tag tag=name2TagMap.get(tagName);
        if (tag==null){
            throw ServiceException.unprocesabelEntiry("标签名错误!");
        }
        QuestionTag questionTag=new QuestionTag()
            .setQuestionId(question.getId())
            .setTagId(tag.getId());
        //将标签和问题的对应关系插入到数据库
        rows= questionTagMapper.insert(questionTag);
        if(rows !=1){
            throw new ServiceException("数据库繁忙,请稍后再试!");
        }
    }

    //保存问题和答疑老师关系
    //Map<String, User> masterMap=userService.getMasterMap();
    //利用Ribbon从Straw-sys服务中获取全部回答问题的老师
    //已进行重构封装
    User[] users=ribbonClient.masters();
    Map<String,User> masterMap=new HashMap<>();
    for(User u:users){
        masterMap.put(u.getNickname(),u);
    }

    for(String nickname:questionVo.getTeacherNicknames()){
        User master=masterMap.get(nickname);
        if (master==null){
            throw ServiceException.unprocesabelEntiry("讲师名错误!");
        }
        UserQuestion userQuestion=new UserQuestion()
            .setQuestionId(question.getId())
            .setUserId(master.getId())
            .setCreatetime(LocalDateTime.now());
        rows=userQuestionMapper.insert(userQuestion);
        if(rows !=1){
            throw new ServiceException("数据库繁忙,请稍后再试!");
        }
    }

    //问题保存完成,就将问题发送到Kafka
    kafkaProducer.sendQuestion(question);
}

2.2 在straw-search中接收数据

首先在straw-search模块中导入Kafka客户端的包:

<dependency>
    <groupId>org.springframework.kafka</groupId>
    <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
</dependency>

配置Kafka服务器位置,application.properties:

spring.kafka.bootstrap-servers=localhost:9092
spring.kafka.consumer.group-id=straw

在业务层lQuestionService中声明保存问题数据的方法:

void saveQuestion(QuestionVo questionVo);

然后在实现类中QuestionServicelmpl中实现保存数据的方法:

@Override
public void saveQuestion(QuestionVo questionVo) {
    questionRepository.save(questionVo);
}

声明KafkaConsumer将收到的问题数据保存到Elasticsearch中:

package cn.tedu.straw.search.kafka;

import cn.tedu.straw.commons.kafka.Topics;
import cn.tedu.straw.search.service.IQuestionService;
import cn.tedu.straw.search.vo.QuestionVo;
import com.google.gson.Gson;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
@Component
@Slf4j
public class KafkaConsumer {

    private Gson gson=new Gson();

    @Resource
    private IQuestionService questionService;

    @KafkaListener(topics = Topics.QUESTIONS)
    public void receiveQuestion(ConsumerRecord<String, String> record){
        String json=record.value();
        log.debug("收到数据:{}",json);
        QuestionVo questionVo=gson.fromJson(json, QuestionVo.class);
        log.debug("转换为:{}",questionVo);
        questionService.saveQuestion(questionVo);
    }
}

3 Kafka 概念总结

  • Kafka是消息队列服务器,其核心目的实现业务系统之间进行异步消息通讯;
  • 利用Kafka可以将业务层过程中的分支流程优化为异步处理,提升软件的性能;
  • Kafka数据发送端称为:消息生产者、或者消息发布者;
  • Kafka数据接收端称为:消息消费者、或者消息订阅者;
  • 队列服务器可以解决数据的“生产者和消费者”问题;
  • 队列服务器也有人称为发布于订阅模型;
  • “生产者和消费者”问题:是指数据生产和数据消费速度不一致会造成数据堆积的矛盾,这种矛盾的解决常用方式就是采用队列进行缓存,然后再进行异步处理。也称为“削峰去谷”,也就是把数据高峰缓存到队列中,在一步一步处理平滑处理。

4 面向切面AOP

4.1 什么是AOP

面向切面的程序设计(Aspect-oriented programming, AOP, 又译作剖面导向程序设计)是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离, 以提高程序代码的模块化程度。通过在现有代码基础上增加额外的通知(Advice) 机制,能够对被声明为“切点(Pointcut) ” 的代码块进行统一管理与扩展。

日志功能即是横切关注点的一个典型案例,因为日志功能往往横跨系统中的每个业务模块,即“横切”所有有日志需求的类及方法体.

软件中的常规功能都是从界面到业务层再到持久层的纵向执行过程。而AOP是目的就是拦截纵向功能,插入橫向的扩展功能。比如检查每个业务方法的耗时情况。

AOP术语
  • 连接点(join point) :对应的是具体被拦截的对象,因为Spring只支持方法, 所以被拦截的对象往往就是指特定的方法。

  • 切点(point cut) : 有时候,我们的切面不单单应用于单个方法,也可以是多个类的不同方法,这时,可以通过正则表达式和指示器的规则去定义。

  • 通知(advice):

    • 前置通知(before advice)
    • 后置通知(after advice)
    • 环绕通知(around advice)
    • 事后返回通知(afterReturming advice)
    • 异常通知(afterThrowingadvice)
  • 目标对象(target) :即被代理对象。

  • 引入(introduction) : 是指引入新的类和方法,增强现有Bean的功能。

  • 织入(weaving) :它是-一个通过动态代理技术,为原有服务对象生成动态对象,然后将与切点定义匹配的连接点拦截,并按约定将各类通知织入约定流程的过程。

  • 切面(aspect) :是一个可以定义切点、各类通知和引入的内容.

4.2 AOP HelloWorld

使用AOP编程不需要特别的设置,SpringBoot项目已经提供好的相应的配置。首先在straw-gateway上声明一个被AOP切入的控制器:

package cn.tedu.straw.gateway.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
@RequestMapping("/test")
public class TestController {

    @GetMapping
    public String test(String username){
        log.debug("Hello {}",username);
        return "Hello World";
    }
}

然后声明切面组件:

  • @Aspect注解将一个java类定义为切面类
  • @Pointcut用于约定“切入点”,也就是AOP代码执行的位置
  • @Before用于约定“之前通知”,表示AOP方法会在切入点位置之前执行:
package cn.tedu.straw.gateway.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class DemoAspect {
    /**
     * 声明切入点
     * 表示AOP组件方法切入到TestController.test(..)),在调用test方法时候执行
     */
    @Pointcut("execution(public * cn.tedu.straw.gateway.controller.TestController.test(..))")
    public void pointCut(){}
        /**
         * 在。。。之前
         */
        @Before("pointCut()")
                public void before(){
            log.debug("Before test()");
    }
}

用启动项目,用浏览器测试http://localhost:9000/test

4.3 通知

通知就是根据需要在切入点不同位置的切入内容

  • 使用@Before在切入点开始处切入内容
  • 使用@After在切入点结尾处切入内容
  • 使用@AfterRetuming在切入点return内容之后切入内容
  • 使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
  • 使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑

Spring AOP提供使用org.aspectj.lang.JoinPoint类型获取连接点数据,任何通知方法的第一个参数都可以是JoinPoint(环绕通知是ProceedingJoinPoint, JoinPoint子类)。

  • JoinPoint: 提供访问当前被通知方法的目标对象、代理对象、方法参数等数据

  • ProceedingJoinPoint: 只用于环绕通知,使用proceed()方法来执行目标方法

    案例,测试AOP通知:

package cn.tedu.straw.gateway.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import javax.xml.crypto.dsig.SignatureMethod;

@Aspect
@Component
@Slf4j
public class DemoAdvice {

    @Pointcut("execution(public * cn.tedu.straw.gateway.controller.TestController.test(..))")
    public void pointCut(){}

    @Before("pointCut()")
    public void before(JoinPoint joinPoint){
        Signature method=joinPoint.getSignature();
        log.debug("在{}方法之前执行",method);
    }

    @After("pointCut()")
    public void after(JoinPoint joinPoint){
        Signature method=joinPoint.getSignature();
        log.debug("在{}方法之后执行",method);
    }

    @AfterReturning("pointCut()")
    public void afterReturning(JoinPoint joinPoint){
        Signature method=joinPoint.getSignature();
        log.debug("在{}正常返回之后执行",method);
    }

    @AfterThrowing("pointCut()")
    public void afterThrowing(JoinPoint joinPoint){
        Signature method=joinPoint.getSignature();
        log.debug("在{}有异常出现后执行",method);
    }

    @Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint)throws Throwable{
        Signature method=joinPoint.getSignature();
        log.debug("Around 在方法{}之前执行",method);
        //只有执行了proceed() 才会执行被切入的最终方法
        log.debug("Around 在方法{}之后执行",method);
        Object val=joinPoint.proceed();
        return val;
    }
}

4.4 切入点

PointCut的定义包括两个部分: Pointcut表示式(expression)和Pointcut签名(signature)。

//Pointcut表达式
@Pointcut("execution(public * cn.tedu.straw.gateway.controller.TestController.test(..))")
//Pointcut签名
public void pointCut(){}

execution表示式的格式:

execution(
    modifier-pattern?
    ret-type-pattern
    declaring-type-pattern?
    name-pattern(param-pattern)
    throws-pattern?)

括号中各个pattern分别表示

  1. 修饰符匹配(modifier-pattern?)
  2. 返回值匹配(ret-type-pattern)
  3. 类路径匹配(declaring-type-pattern?)
  4. 方法名匹配(name-pattern)
  5. 参数匹配((param-patten)
  6. 异常类型匹配(throws- -pattern?), 其中后面跟着“?”的是可选项。

例:

  1. execution(**(..) ) :表示匹配所有方法

  2. execution(public * com.test.TestContrller.. :表示匹配com.test.TestController类中所有的公有方法

  3. execution(* com.test.. * .*(..)) :表示匹配com.test包中所有的方法

4.5 利用AOP记录性能

项目上线以后如果出现软件响应速度慢卡顿就需要进行测试性能,找出慢速功能,然后针对慢速功能进行优化,这样就可以针对性的提升软件整体性能。软件功能就是我们软件的一个一个业务层方法, 这些方法的运行性能可以用方法执行的耗时来衡量。如果在每个业务方法的地方使用代码标注时间戳测量运行时间显然是一个非常费时费力的工作。并且需要修改每个调用业务方法的代码,如果业务方法很多,修改量也很大。

此时就可以采用AOP技术进行解决,只需利用Around通知切入到业务层的每个方法前后,利用时间戳计算出方法耗时情况,就可以不修改代码情况下将性能测试组件织入现有程序中。使问题轻松解决。

在straw-faq中使用AOP测量业务方法执行性能:

package cn.tedu.straw.faq.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
public class PerformanceAspect {
    @Pointcut("execution(* cn.tedu.straw.faq.service.*Service.*(..))")
    public void servicePointcut(){}
    @Around("servicePointcut()")
    public Object log(ProceedingJoinPoint joinPoint)throws Throwable{
        long t1=System.nanoTime();
        Object value=joinPoint.proceed();

        long t2=System.nanoTime();
        Signature method=joinPoint.getSignature();
        log.info("{}方法执行耗时{}",method,(t2-t1));
        return value;
    }
}
posted @ 2022-04-19 22:05  指尖上的未来  阅读(15)  评论(0)    收藏  举报