Spring系列-基礎篇(2)-IOC和AOP的應用淺談

背景介紹

本篇文章會以實際的項目代碼做爲示例,講解Spring框架中的控制反轉(IOC)和麪向切面編程(AOP)的應用思想和開發方式。這裏主要是講解應用設計層面的,具體的Coding部分在總體結構中的佔比隨緣。html

技術背景

Spring框架做爲目前市場上做爲火熱的框架,分析起來它的框架主要有下面幾點:前端

  1. 核心容器:主要的功能是實現了控制反轉(IOC)與依賴注入(DI)、Bean配置、加載以及生命週期的管理。
  2. AOP模塊:負責Spring的全部AOP(面向切面)的功能。
  3. Web模塊:擴展了Spring的Web功能。使其符合MVC的設計規範,最重要的是提供了Spring MVC的容器。
  4. Data模塊:提供了一些數據相關的組件:包括JDBC、orm(對象關係映射)、事務操做、oxm(對象xml映射)、Jms(Java消息服務)。

對於後端開發人員來講,核心要學習的就是 IOC/DI 和 AOP了。這篇文章咱們除了講解它們的概念和思想之外,還會經過代碼,來體如今實際企業開發中的應用。java

項目背景

文章中會以以前作過的一個小項目的代碼做爲示例--給食堂的微信小程序提供後臺接口,使用SSM架構(Spring+SpringMVC+Mybatis)。
該項目在啓動之初,只是考慮用Mybatis實現後臺接口。但後來考慮到每次手動初始化各類類的Bean很麻煩,除了代碼結構難看之外還有系統的性能問題。後來在瞭解到Spring的IOC特性後,才決定使用SSM架構。web

控制反轉(IOC)

基礎介紹

控制反轉(IOC)是一種軟件設計模式,它告訴你應該如何作,來解除相互依賴模塊的耦合。控制反轉(IOC),它爲相互依賴的組件提供抽象,將依賴(低層模塊)對象的得到交給第三方(系統)來控制,即依賴對象不在被依賴模塊的類中直接經過new來獲取。依賴注入(DI)則是實現IOC的一種方法。
我在網上見到了下面的張圖,我以爲很能簡單描述IOC的這種思想:spring

clipboard.png

clipboard.png

xml配置文件

Spring MVC中的配置文件仍是挺多的,通常會有spring-mvc.xml、spring-service.xml,若是要整合Mybatis,還會有spring-mybatis.xml。這些配置文件的目的,是爲了定義須要自動加載初始化的Bean、以及依賴關係,一般都是配合Java註解使用的。
這裏簡單拿一個配置文件的代碼示例:spring-mvc.xml編程

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                            http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
                            http://www.springframework.org/schema/context
                            http://www.springframework.org/schema/context/spring-context-4.0.xsd
                            http://www.springframework.org/schema/mvc
                            http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--避免IE執行AJAX時,返回JSON出現下載文件 -->
    <bean id="mappingJacksonHttpMessageConverter"
          class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter">
        <property name="supportedMediaTypes">
            <list>
                <value>text/html;charset=UTF-8</value>
            </list>
        </property>
    </bean>

    <!-- 啓動SpringMVC的註解功能,完成請求和註解model的映射 -->
    <bean
            class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
        <property name="messageConverters">
            <list>
                <ref bean="mappingJacksonHttpMessageConverter" />    <!-- JSON轉換器 -->
            </list>
        </property>
    </bean>

    <context:component-scan base-package="com.smec.lgt.ct.aspect" />
    <!--*************** 支持aop **************** -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

    <!-- 自動掃描該包,使SpringMVC認爲包下用了@controller註解的類是控制器 -->
    <mvc:default-servlet-handler/>
    <context:annotation-config/>
    <context:component-scan base-package="com.smec.lgt.ct.controller" />

    <!-- 添加註解驅動 -->
    <mvc:annotation-driven enable-matrix-variables="true" />
    <!-- 容許對靜態資源文件的訪問 -->
    <mvc:default-servlet-handler />
    <!-- 定義跳轉的文件的先後綴 ,視圖模式配置 -->
    <bean
            class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!-- 這裏的配置個人理解是自動給後面action的方法return的字符串加上前綴和後綴,變成一個 可用的url地址 -->
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!-- 設置默認編碼 -->
        <property name="defaultEncoding" value="utf-8"></property>
        <!-- 上傳圖片最大大小5M-->
        <property name="maxUploadSize" value="5242440"></property>
    </bean>

</beans>

咱們會發現,Spring MVC中配置文件太多了,在管理上面就沒那麼方便了。這時候Spring boot就應用而生,它的不少配置數據都只寫在一個配置文件application.properties裏面,並且結構清晰。json

Java註解

根據咱們以前的圖,在讀取配置文件時,就是將所需的元數據組裝成Bean加載到容器中。component-scan標籤在默認狀況下會自動掃描指定路徑下的包(含全部子包),將帶有@Component、@Repository、@Service、@Controller標籤的類自動註冊到spring容器。
咱們首先須要瞭解一些經常使用到的註解:@Controller、@Service、@Resource等小程序

clipboard.png

clipboard.png

簡單講解一下代碼結構:後端

  1. model:對應數據表結構的Bean
  2. mapper:因爲整合Mybatis,經過namespace綁定對應的xml配置文件,映射Dao層的接口
  3. service:具體實現提供給前端的接口
  4. controller:service的Impl

註解@Controller、@Service等,是爲了在初始化裝載到容器。而當咱們須要依賴下一層類的某個方法時,能夠經過@Resource來引用。而具體類的實例方式則是交給容器微信小程序

面向切面編程(AOP)

基礎介紹

AOP叫面向切面編程,咱們大學的時候學過「面向過程編程」、「面向對象編程」,那麼這個「面向切面編程」是否是同一個演變的思路呢?
其實AOP就是做爲面向對象的一種補充,用於處理系統中分佈於各個模塊的橫切關注點,好比事務管理、日誌、緩存等等。

clipboard.png

咱們看上面這張圖,咱們有三個接口,但其實其中每一個接口都有「登陸權限認證」和「日誌記錄」這些模塊。它們的實現邏輯是共同的,在代碼上面看是重複冗餘的。
對於面向切面編程最直觀的理解就是;我很想用刀把這些接口這些模塊,水平的「切」下來單獨編程。

爲何須要AOP

咱們這個項目是開發微信小程序的接口,那麼對於企業應用的接口來講,就免不了要有權限驗證。
咱們先看一下包含權限驗證的圖表模塊的接口類--ChartController.java

package com.smec.lgt.ct.controller;

import com.smec.lgt.ct.service.ChartService;
import com.smec.lgt.ct.util.JwtUtil;
import com.smec.lgt.ct.util.Response;
import com.smec.lgt.ct.util.ServiceUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * 圖表模塊
 */
@Controller
@RequestMapping(value = "/lgt/ct/chart")
public class ChartController {
    @Resource
    private ChartService chartService;

    /**
     * 圖表彙總接口
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/getChartSummary", method = RequestMethod.GET)
    public Response getChartSummary(@RequestHeader("DF_KEY")String header) {
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }
        return chartService.getChartSummary();
    }

    /**
     * 獲取菜品種類列表接口
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/getFoodSortList", method = RequestMethod.GET)
    public Response getFoodSortList(@RequestHeader("DF_KEY")String header) {
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }
        System.out.println(tokenMap.get("userCode"));
        return chartService.getFoodSortList();
    }

    /**
     * 已維護菜品列表接口
     * @param request
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/getMaintainedDishList",method = RequestMethod.POST)
    public Response getMaintainedDishList(HttpServletRequest request,@RequestHeader("DF_KEY")String header){
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }
        StringBuffer requestJson = ServiceUtil.getJsonByRequest(request);
        return chartService.getMaintainedDishList(requestJson.toString(),tokenMap);
    }
    /**
     * 未維護菜品列表接口
     * @param request
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/getUnmaintainedDishList",method = RequestMethod.POST)
    public Response getUnmaintainedDishList(HttpServletRequest request,@RequestHeader("DF_KEY")String header){
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }
        StringBuffer requestJson = ServiceUtil.getJsonByRequest(request);
        return chartService.getUnmaintainedDishList(requestJson.toString(),tokenMap);
    }

    /**
     * 未分配菜品列表接口
     * @param request
     * @param header
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/getUnassignDishList",method = RequestMethod.POST)
    public Response getUnassignDishList(HttpServletRequest request,@RequestHeader("DF_KEY")String header){
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }
        StringBuffer requestJson = ServiceUtil.getJsonByRequest(request);
        return chartService.getUnassignDishList(requestJson.toString());
    }
}

這個類的代碼中有四個接口,每一個接口都須要權限認證。我使用的是JWT的驗證方式,封裝了一個JwtUtil的類。移動端在登陸的時候會獲取token,後續調用全部其餘的接口都須要將該token放在Header中,後端經過獲取每一個接口請求的token來驗證權限。
因此,一共二十多個接口,除了登陸接口之外的全部接口都免不了如下重複的代碼:

Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            return  Response.fail("登陸token驗證失敗!");
        }

全部除了登陸之外接口在Controller這一層,都在作token驗證這同一件事。咱們是但願將token驗證這件事從全部接口的這一層分離開來。

實現AOP(權限驗證、日誌記錄爲例)

AOP的主要編程對象是切面(aopect),而切面模塊化橫切關注點。咱們理解下面幾個點:

  1. 切面(Aepect):橫切關注點(跨越應用程序多個模塊的功能)被模塊化的對象
  2. 通知(Advice):切面必需要完成的工做
  3. 目標(Target):被通知的對象
  4. 代理(Proxy):像目標對象應用通知以後建立的對象
  5. 鏈接點(Joinpoint):程序執行的某個特殊位置,如類某個方法調用前、調用後、方法拋出異常後等。鏈接點由兩個信息肯定:方法表示的程序執行點;想對點表示的方位
  6. 切點(pointcut):每一個類都擁有多個鏈接點,即鏈接點是程序類中客觀存在的事務

咱們能夠經過如下步驟,新增內容改進:
1.pom.xml(Spring MVC)

<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.0.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.0</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjrt</artifactId>
            <version>1.9.0</version>
        </dependency>

1.pom.xml(Spring boot)

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>

2.spring-mvc.xml

<context:component-scan base-package="com.smec.lgt.ct.aspect" />
    <!--*************** 支持aop **************** -->
    <aop:aspectj-autoproxy proxy-target-class="true" />

3.TokenAspect.java(com.smec.lgt.ct.aspect)

package com.smec.lgt.ct.aspect;

import com.smec.lgt.ct.util.JwtUtil;
import com.smec.lgt.ct.util.Response;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

@Aspect
@Order(1)
@Component
public class TokenAspect {

    /**
     * AssignController、ChartController、DishController、MaintenController、StuffController
     */
   @Pointcut("execution(public * com.smec.lgt.ct.controller.*.*(..))&& !execution(public * com.smec.lgt.ct.controller.UtilController.login(*))")
    public void tokenPointcut() {
    }

    @Around("tokenPointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable{
        Object result=null;
        RequestAttributes requestAttributes= RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes=(ServletRequestAttributes)requestAttributes;
        HttpServletRequest httpServletRequest=servletRequestAttributes.getRequest();
       String header= httpServletRequest.getHeader("DF_KEY");
        Map<String,String> tokenMap= JwtUtil.getTokenResult(header);
        if(Response.FAILED.equals(tokenMap.get("code"))){
            result=  Response.fail("登陸token驗證失敗!");
        }else{
            result=point.proceed();
        }
        return result;
    }

}

一、增長pom中的aop的maven依賴;二、在配置文件中增長對切面文件的掃描(項目切面文件路徑com.smec.lgt.ct.aspect);三、寫切面文件路徑。

  • @Aspect:申明爲一個切面
  • @Order:切面的執行順序(當有多個切面時)
  • @Component:申明交給容器管理
  • @Pointcut:定義切點:代碼中定義爲com.smec.lgt.ct.controller下除了UtilController.login的登陸接口
  • @Around:相似的有@Before 和 @After等

包括咱們在作接口日誌記錄時,也很是合適面向切面編程,代碼以下:
LoggerAspect.java(com.smec.lgt.ct.aspect)

package com.smec.lgt.ct.aspect;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.smec.lgt.ct.model.LoggerBean;
import com.smec.lgt.ct.util.JwtUtil;
import com.smec.lgt.ct.util.Response;
import com.smec.lgt.ct.util.ServiceUtil;
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.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;


@Aspect
@Order(2)
@Component
public class LoggerAspect {
    private static final String RESPONSE_CHARSET = "UTF-8";
    
    @Pointcut("execution(public * com.smec.lgt.ct.controller.*.*(..))")
    public void loggerPointcut() {
    }

    @Around("loggerPointcut()")
    public Object doAround(ProceedingJoinPoint point) throws Throwable {
        Object result = null;
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
        HttpServletRequest httpServletRequest = servletRequestAttributes.getRequest();
        String header = httpServletRequest.getHeader("DF_KEY");
        String user = JwtUtil.getTokenResult(header).get("userCode");
        //
        StringBuffer requestJsonBuffer = ServiceUtil.getJsonByRequest(httpServletRequest);
        String requestJson=requestJsonBuffer.toString();

        String method = httpServletRequest.getMethod();
        String url = httpServletRequest.getRequestURL().toString();
        Date date = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String currentTime = sdf.format(date);
        try {
            result = point.proceed();
        }catch (Exception e){
            e.printStackTrace();
        }
        String responseJson = JSON.toJSONString(result);
        LoggerBean loggerBean = new LoggerBean(requestJson, user, url, method, currentTime, responseJson);
        System.out.println(JSON.toJSONString(loggerBean));
        return result;
    }

}

  

備註說明,後續討論的問題

一、在使用AOP時,在pom.xml中添加aop的jar依賴時,要大體保證aop的jar包version和springframework的version一致,若是有較大的差距,在加載時會報錯:

java.lang.NoSuchMethodError: org.springframework.beans.factory.config.ConfigurableBeanFactory.getSingletonMutex()Ljava/lang/Object

二、在示例寫LoggerAspect.java方法,作接口的日誌記錄時實際上會有一個「坑」。咱們最重要的是要記錄接口的request和response的參數。對於POST請求接口,request只能經過HttpServletRequest中獲取InputStream,再獲取請求的JSON格式字符串。但咱們知道InputStream只能讀一次,不能屢次讀取。若是咱們在AOP的切面端獲取過一次POST請求的參數,那在Controller接口層就獲取不到POST請求的參數了。該問題的解決步驟比較多,此次忽略,下次在「實踐篇」中另立篇幅講解。

相關文章
相關標籤/搜索