首頁 > 軟體

SpringBoot使用AOP實現統計全域性介面存取次數詳解

2022-06-16 14:01:01

AOP是什麼

AOP(Aspect Oriented Programming),也就是面向切面程式設計,是通過預編譯方式和執行期間動態代理實現程式功能的傳統已維護的一種技術。

AOP的作用和優勢

作用:在程式執行期間,在不修改原始碼的情況下對某些方法進行功能增強

優勢:減少重複程式碼,提高開發效率,並且便於維護

常見的動態代理技術

jdk代理:基於介面的動態代理技術

cglib代理:基於父類別的動態代理技術

AOP相關概念

  • List item- Target(目標物件):代理的目標物件
  • Proxy(代理):一個類被AOP織入增強後,就產生一個結果代理類
  • Joinpoint(連線點):連線點是指那些被攔截到的點。在Spring中,這些點指的是方法,因為Spring只支援方法型別的連線點(可以被增強的方法叫連線點)
  • PointCut(切入點):切入點是指我們要對哪些Joinpoint進行攔截的定義
  • Advice(通知/增強):通知是指攔截到Joinpoint之後所要做的事情就是通知
  • Aspect(切面):是切入點和通知的結合
  • Weaving(織入):把增強應用到目標物件來建立新的代理物件的過程。Spring採用動態代理織入,而AspectJ採用編譯器織入和類裝載器織入

實現

我在這裡採用基於註解形式的的AOP開發。

開發步驟

加入依賴

		<!--引入AOP依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--AOP-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.12</version>
        </dependency>

建立目標介面和目標類(內部有切點)

建立切面類(內部有增強方法)

將目標類和切面類的物件建立權交給Spirng

在切面類中使用註解設定織入關係

在組態檔中開啟元件掃描和AOP自動代理

因為我的專案是SpringBoot Web專案,在這裡開啟註解就好了。

下面的程式碼為使用到的原子計數類。

import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter {
    private static final AtomicCounter atomicCounter = new AtomicCounter();
    /**
     * 單例,不允許外界主動範例化
     */
    private AtomicCounter() {
    }
    public static AtomicCounter getInstance() {
        return atomicCounter;
    }
    private static AtomicInteger counter = new AtomicInteger();
    public int getValue() {
        return counter.get();
    }
    public int increase() {
        return counter.incrementAndGet();
    }
    public int decrease() {
        return counter.decrementAndGet();
    }
    // 清零
    public void toZero(){
        counter.set(0);
    }
}

下面的程式碼為實現的全域性介面監控。

我在專案中簡單使用了Redis作快取,所有你可以看到有Redis相關的操作。

使用 @Before 用於設定前置通知。指定增強的方法在切入點方法之前執行。

使用@ @AfterReturning 用於設定後置通知。指定增強的方法在切入點方法之後執行。

使用@ @AfterThrowing 用於設定異常丟擲通知。指定增強的方法在出現異常時執行。

@Component
@Aspect
public class GlobalActuator {
    private static final Logger log = LoggerFactory.getLogger(GlobalActuator.class);
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    ThreadLocal<Long> startTime = new ThreadLocal<>();
    ConcurrentHashMap<Object, Object> countMap = new ConcurrentHashMap<Object, Object>();
    /**
     * 匹配控制層層通知 這裡監控controller下的所有介面
     */
    @Pointcut("execution(* com.sf.controller.*Controller.*(..))")
    private void controllerPt() {
    }
    /**
     * 在介面原有的方法執行前,將會首先執行此處的程式碼
     */
    @Before("com.sf.actuator.GlobalActuator.controllerPt()")
    public void doBefore(JoinPoint joinPoint) throws Throwable {
        startTime.set(System.currentTimeMillis());
        //獲取傳入目標方法的引數
        Object[] args = joinPoint.getArgs();
    }
    /**
     * 只有正常返回才會執行此方法
     * 如果程式執行失敗,則不執行此方法
     */
    @AfterReturning(returning = "returnVal", pointcut = "com.sf.actuator.GlobalActuator.controllerPt()")
    public void doAfterReturning(JoinPoint joinPoint, Object returnVal) throws Throwable {
        Signature signature = joinPoint.getSignature();
        String declaringName = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        String mapKey = declaringName + methodName;
        // 執行成功則計數加一
        int increase = AtomicCounter.getInstance().increase();
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        synchronized (this) {
        //在專案啟動時,需要在Redis中讀取原有的介面請求次數
            if (countMap.size() == 0) {
                JSONObject jsonObject = RedisUtils.objFromRedis(StringConst.INTERFACE_ACTUATOR);
                if (jsonObject != null) {
                    Set<String> strings = jsonObject.keySet();
                    for (String string : strings) {
                        Object o = jsonObject.get(string);
                        countMap.putIfAbsent(string, o);
                    }
                }
            }
        }
        // 如果此次存取的介面不在countMap,放入countMap
        countMap.putIfAbsent(mapKey, 0);
        countMap.compute(mapKey, (key, value) -> (Integer) value + 1);
        synchronized (this) {
            // 記憶體計數達到30 更新redis
            if (increase == 30) {
                RedisUtils.objToRedis(StringConst.INTERFACE_ACTUATOR, countMap, Constants.AVA_REDIS_TIMEOUT);
                //刪除過期時間
                stringRedisTemplate.persist(StringConst.INTERFACE_ACTUATOR);
                //計數器置為0
                AtomicCounter.getInstance().toZero();
            }
        }
        //log.info("方法執行次數:" + mapKey + "------>" + countMap.get(mapKey));
        //log.info("URI:[{}], 耗費時間:[{}] ms", request.getRequestURI(), System.currentTimeMillis() - startTime.get());
    }
    /**
     * 當介面報錯時執行此方法
     */
    @AfterThrowing(pointcut = "com.sf.actuator.GlobalActuator.controllerPt()")
    public void doAfterThrowing(JoinPoint joinPoint) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.info("介面存取失敗,URI:[{}], 耗費時間:[{}] ms", request.getRequestURI(), System.currentTimeMillis() - startTime.get());
    }
}

這裡再給出Controller層程式碼。

    @GetMapping("/interface/{intCount}")
    @ApiOperation(value = "查詢介面成功存取次數(預設倒序)")
    public Result<List<InterfaceDto>> findInterfaceCount(
            @ApiParam(name = "intCount", value = "需要的介面數") @PathVariable Integer intCount
    ) {
        HashMap<String, Integer> hashMap = new HashMap<>();
        JSONObject jsonObject = RedisUtils.objFromRedis(StringConst.INTERFACE_ACTUATOR);
        if (jsonObject != null) {
            Set<String> strings = jsonObject.keySet();
            for (String string : strings) {
                Integer o = (Integer) jsonObject.get(string);
                hashMap.putIfAbsent(string, o);
            }
        }
        //根據value倒序
        Map<String, Integer> sortedMap = hashMap.entrySet().stream().sorted(Collections.reverseOrder(Map.Entry.comparingByValue()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new));
        //返回列表
        List<InterfaceDto> resultList = new ArrayList<>();
        //排序後中的map中所有的key
        Object[] objects = sortedMap.keySet().toArray();
        for (int i = 0; i < intCount; i++) {
            InterfaceDto interfaceDto = new InterfaceDto();
            interfaceDto.setName((String) objects[i]);
            interfaceDto.setCount(sortedMap.get((String) objects[i]));
            resultList.add(interfaceDto);
        }
        return Result.success(resultList);
    }

專案執行一段時間後,在Redis中可以看到介面的請求次數。

Web最終效果圖如下:

到此這篇關於SpringBoot使用AOP實現統計全域性介面存取次數詳解的文章就介紹到這了,更多相關SpringBoot AOP內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com