本篇文章会以实际的项目代码做为示例,讲解Spring框架中的控制反转(IOC)和面向切面编程(AOP)的应用思想和开发方式。这里主要是讲解应用设计层面的,具体的Coding部分在总体结构中的占比随缘。html
Spring框架做为目前市场上做为火热的框架,分析起来它的框架主要有下面几点:前端
对于后端开发人员来讲,核心要学习的就是 IOC/DI 和 AOP了。这篇文章咱们除了讲解它们的概念和思想之外,还会经过代码,来体如今实际企业开发中的应用。java
文章中会以以前作过的一个小项目的代码做为示例--给食堂的微信小程序提供后台接口,使用SSM架构(Spring+SpringMVC+Mybatis)。
该项目在启动之初,只是考虑用Mybatis实现后台接口。但后来考虑到每次手动初始化各类类的Bean很麻烦,除了代码结构难看之外还有系统的性能问题。后来在了解到Spring的IOC特性后,才决定使用SSM架构。web
控制反转(IOC)是一种软件设计模式,它告诉你应该如何作,来解除相互依赖模块的耦合。控制反转(IOC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的得到交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接经过new来获取。依赖注入(DI)则是实现IOC的一种方法。
我在网上见到了下面的张图,我以为很能简单描述IOC的这种思想:spring
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
根据咱们以前的图,在读取配置文件时,就是将所需的元数据组装成Bean加载到容器中。component-scan标签在默认状况下会自动扫描指定路径下的包(含全部子包),将带有@Component、@Repository、@Service、@Controller标签的类自动注册到spring容器。
咱们首先须要了解一些经常使用到的注解:@Controller、@Service、@Resource等小程序
简单讲解一下代码结构:后端
注解@Controller、@Service等,是为了在初始化装载到容器。而当咱们须要依赖下一层类的某个方法时,能够经过@Resource来引用。而具体类的实例方式则是交给容器微信小程序
AOP叫面向切面编程,咱们大学的时候学过“面向过程编程”、“面向对象编程”,那么这个“面向切面编程”是否是同一个演变的思路呢?
其实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的主要编程对象是切面(aopect),而切面模块化横切关注点。咱们理解下面几个点:
咱们能够经过如下步骤,新增内容改进:
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);三、写切面文件路径。
包括咱们在作接口日志记录时,也很是合适面向切面编程,代码以下:
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请求的参数了。该问题的解决步骤比较多,此次忽略,下次在“实践篇”中另立篇幅讲解。