2

背景介绍

本篇文章会以实际的项目代码作为示例,讲解Spring框架中的控制反转(IOC)和面向切面编程(AOP)的应用思想和开发方式。这里主要是讲解应用设计层面的,具体的Coding部分在整体结构中的占比随缘。

技术背景

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了。这篇文章我们除了讲解它们的概念和思想以外,还会通过代码,来体现在实际企业开发中的应用。

项目背景

文章中会以之前做过的一个小项目的代码作为示例--给食堂的微信小程序提供后台接口,使用SSM架构(Spring+SpringMVC+Mybatis)。
该项目在启动之初,只是考虑用Mybatis实现后台接口。但后来考虑到每次手动初始化各种类的Bean很麻烦,除了代码结构难看以外还有系统的性能问题。后来在了解到Spring的IOC特性后,才决定使用SSM架构。

控制反转(IOC)

基础介绍

控制反转(IOC)是一种软件设计模式,它告诉你应该如何做,来解除相互依赖模块的耦合。控制反转(IOC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取。依赖注入(DI)则是实现IOC的一种方法。
我在网上见到了下面的张图,我觉得很能简单描述IOC的这种思想:

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里面,而且结构清晰。

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;
    }

}

1、增加pom中的aop的maven依赖;2、在配置文件中增加对切面文件的扫描(项目切面文件路径com.smec.lgt.ct.aspect);3、写切面文件路径。

  • @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;
    }

}

  

备注说明,后续讨论的问题

1、在使用AOP时,在pom.xml中添加aop的jar依赖时,要大致保证aop的jar包version和springframework的version一致,如果有较大的差距,在加载时会报错:

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

2、在示例写LoggerAspect.java方法,做接口的日志记录时实际上会有一个“坑”。我们最重要的是要记录接口的request和response的参数。
对于POST请求接口,request只能通过HttpServletRequest中获取InputStream,再获取请求的JSON格式字符串。但我们知道InputStream只能读一次,不能多次读取。
如果我们在AOP的切面端获取过一次POST请求的参数,那在Controller接口层就获取不到POST请求的参数了。
该问题的解决步骤比较多,这次忽略,下次在“实践篇”中另立篇幅讲解。


KerryWu
641 声望159 粉丝

保持饥饿