JAVA MVC RSET

来源:互联网 发布:sql联合主键怎么设置 编辑:程序博客网 时间:2024/06/05 10:48
摘要:MVC模式早在上个世纪70年代就诞生了,直到今天它依然存在,可见生命力相当之强。MVC模式最早用于Smalltalk语言中,最后在其它许多开发语言中都得到了很好的应用,例如,Java中的Struts、Spring MVC等框架。

1. 理解MVC

MVC是一种经典的设计模式,全名为Model-View-Controller,即模型-视图-控制器。

其中,模型是用于封装数据的载体,例如,在Java中一般通过一个简单的POJO(Plain Ordinary Java Object)来表示,其本质是一个普通的Java Bean,包含一系列的成员变量及其getter/setter方法。对于视图而言,它更加偏重于展现,也就是说,视图决定了界面到底长什么样子,在Java中可通过JSP来充当视图,或者通过纯HTML的方式进行展现,而后者才是目前的主流。模型和视图需要通过控制器来进行粘合,例如,用户发送一个HTTP请求,此时该请求首先会进入控制器,然后控制器去获取数据并将其封装为模型,最后将模型传递到视图中进行展现。

综上所述,MVC的交互过程如图1所示。

2. MVC模式的优点与不足

MVC模式早在上个世纪70年代就诞生了,直到今天它依然存在,可见生命力相当之强。MVC模式最早用于Smalltalk语言中,最后在其它许多开发语言中都得到了很好的应用,例如,Java中的Struts、Spring MVC等框架。正是因为这些MVC框架的出现,才让MVC模式真正落地,让开发更加高效,让代码耦合度尽量减小,让应用程序各部分的职责更加清晰。

既然MVC模式这么好,难道它就没有不足的地方吗?我认为MVC至少有以下三点不足:

  1. 每次请求必须经过“控制器->模型->视图”这个流程,用户才能看到最终的展现的界面,这个过程似乎有些复杂。
  2. 实际上视图是依赖于模型的,换句话说,如果没有模型,视图也无法呈现出最终的效果。
  3. 渲染视图的过程是在服务端来完成的,最终呈现给浏览器的是带有模型的视图页面,性能无法得到很好的优化。

为了使数据展现过程更加直接,并且提供更好的用户体验,我们有必要对MVC模式进行改进。不妨这样来尝试,首先从浏览器发送AJAX请求,然后服务端接受该请求并返回JSON数据返回给浏览器,最后在浏览器中进行界面渲染。

改进后的MVC模式如图2所示。

 

也就是说,我们输入的是AJAX请求,输出的是JSON数据,市面上有这样的技术来实现这个功能吗?答案是REST。

REST全称是Representational State Transfer(表述性状态转移),它是Roy Fielding博士在2000年写的一篇关于软件架构风格的论文,此文一出,威震四方!国内外许多知名互联网公司纷纷开始采用这种轻量级的Web服务,大家习惯将其称为RESTful Web Services,或简称REST服务。]

如果将浏览器这一端视为前端,而服务器那一端视为后端的话,可以将以上改进后的MVC模式简化为以下前后端分离模式,如图3所示。

 

可见,有了REST服务,前端关注界面展现,后端关注业务逻辑,分工明确,职责清晰。那么,如何使用REST服务将应用程序进行前后端分离呢?我们接下来继续探讨,首先我们需要认识REST。

3. 认识REST

REST本质上是使用URL来访问资源种方式。众所周知,URL就是我们平常使用的请求地址了,其中包括两部分:请求方式与请求路径,比较常见的请求方式是GET与POST,但在REST中又提出了几种其它类型的请求方式,汇总起来有六种:GET、POST、PUT、DELETE、HEAD、OPTIONS。尤其是前四种,正好与CRUD(Create-Retrieve-Update-Delete,增删改查)四种操作相对应,例如,GET(查)、POST(增)、PUT(改)、DELETE(删),这正是REST与CRUD的异曲同工之妙!需要强调的是,REST是“面向资源”的,这里提到的资源,实际上就是我们常说的领域对象,在系统设计过程中,我们经常通过领域对象来进行数据建模。

REST是一个“无状态”的架构模式,因为在任何时候都可以由客户端发出请求到服务端,最终返回自己想要的数据,当前请求不会受到上次请求的影响。也就是说,服务端将内部资源发布REST服务,客户端通过URL来访问这些资源,这不就是SOA所提倡的“面向服务”的思想吗?所以,REST也被人们看做是一种“轻量级”的SOA实现技术,因此在企业级应用与互联网应用中都得到了广泛应用。

下面我们举几个例子对REST请求进行简单描述:

 

可见,请求路径相同,但请求方式不同,所代表的业务操作也不同,例如,/advertiser/1这个请求,带有GET、PUT、DELETE三种不同的请求方式,对应三种不同的业务操作。

虽然REST看起来还是很简单的,实际上我们往往需要提供一个REST框架,让其实现前后端分离架构,让开发人员将精力集中在业务上,而并非那些具体的技术细节。下面我们将使用Java技术来实现这个REST框架,整体框架会基于Spring进行开发。

4. 实现REST框架

4.1 统一响应结构

使用REST框架实现前后端分离架构,我们需要首先确定返回的JSON响应结构是统一的,也就是说,每个REST请求将返回相同结构的JSON响应结构。不妨定义一个相对通用的JSON响应结构,其中包含两部分:元数据与返回值,其中,元数据表示操作是否成功与返回值消息等,返回值对应服务端方法所返回的数据。该JSON响应结构如下:

{"meta": {"success": true,"message": "ok"},"data": ...}
为了在框架中映射以上JSON响应结构,我们需要编写一个Response类与其对应:

[java] view plaincopy
  1. public class Response {  
  2.   
  3.     private static final String OK = "ok";  
  4.     private static final String ERROR = "error";  
  5.   
  6.     private Meta meta;  
  7.     private Object data;  
  8.   
  9.     public Response success() {  
  10.         this.meta = new Meta(true, OK);  
  11.         return this;  
  12.     }  
  13.   
  14.     public Response success(Object data) {  
  15.         this.meta = new Meta(true, OK);  
  16.         this.data = data;  
  17.         return this;  
  18.     }  
  19.   
  20.     public Response failure() {  
  21.         this.meta = new Meta(false, ERROR);  
  22.         return this;  
  23.     }  
  24.   
  25.     public Response failure(String message) {  
  26.         this.meta = new Meta(false, message);  
  27.         return this;  
  28.     }  
  29.   
  30.     public Meta getMeta() {  
  31.         return meta;  
  32.     }  
  33.   
  34.     public Object getData() {  
  35.         return data;  
  36.     }  
  37.   
  38.     public class Meta {  
  39.   
  40.         private boolean success;  
  41.         private String message;  
  42.   
  43.         public Meta(boolean success) {  
  44.             this.success = success;  
  45.         }  
  46.   
  47.         public Meta(boolean success, String message) {  
  48.             this.success = success;  
  49.             this.message = message;  
  50.         }  
  51.   
  52.         public boolean isSuccess() {  
  53.             return success;  
  54.         }  
  55.   
  56.         public String getMessage() {  
  57.             return message;  
  58.         }  
  59.     }  
  60. }  
以上Response类包括两类通用返回值消息:ok与error,还包括两个常用的操作方法:success( )与failure( ),通过一个内部类来展现元数据结构,我们在下文中多次会使用该Response类。

实现该REST框架需要考虑许多问题,首当其冲的就是对象序列化问题。

4.2 实现对象序列化

想要解释什么是对象序列化?不妨通过一些例子进行说明。比如,通过浏览器发送了一个普通的HTTP请求,该请求携带了一个JSON格式的参数,在服务端需要将该JSON参数转换为普通的Java对象,这个转换过程称为序列化。再比如,在服务端获取了数据,此时该数据是一个普通的Java对象,然后需要将这个Java对象转换为JSON字符串,并将其返回到浏览器中进行渲染,这个转换过程称为反序列化。不管是序列化还是反序列化,我们一般都称为序列化。

实际上,Spring MVC已经为我们提供了这类序列化特性,只需在Controller的方法参数中使用@RequestBody注解定义需要反序列化的参数即可,如以下代码片段:

[java] view plaincopy
  1. @Controller  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
若需要对Controller的方法返回值进行序列化,则需要在该返回值上使用@ResponseBody注解来定义,如以下代码片段:

[java] view plaincopy
  1. @Controller  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)  
  5.     public @ResponseBody Response getAdvertiser(@PathVariable("id") String advertiserId) {  
  6.         ...  
  7.     }  
  8. }  
当然,@ResponseBody注解也可以定义在类上,这样所有的方法都继承了该特性。由于经常会使用到@ResponseBody注解,所以Spring提供了一个名为@RestController的注解来取代以上的@Controller注解,这样我们就可以省略返回值前面的@ResponseBody注解了,但参数前面的@RequestBody注解是无法省略的。实际上,看看Spring中对应@RestController注解的源码便可知晓:

[java] view plaincopy
  1. @Target({ElementType.TYPE})  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. @Documented  
  4. @Controller  
  5. @ResponseBody  
  6. public @interface RestController {  
  7.   
  8.     String value() default "";  
  9. }  
可见,@RestController注解已经被@Controller与@ResponseBody注解定义过了,Spring框架会识别这类注解。需要注意的是,该特性在Spring 4.0中才引入。

因此,我们可将以上代码进行如下改写:

[java] view plaincopy
  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8.   
  9.     @RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)  
  10.     public Response getAdvertiser(@PathVariable("id") String advertiserId) {  
  11.         ...  
  12.     }  
  13. }  

除了使用注解来定义序列化行为以外,我们还需要使用Jackson来提供JSON的序列化操作,在Spring配置文件中只需添加以下配置即可:

[xml] view plaincopy
  1. <mvc:annotation-driven>  
  2.     <mvc:message-converters>  
  3.         <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>  
  4.     </mvc:message-converters>  
  5. </mvc:annotation-driven>  
若需要对Jackson的序列化行为进行定制,比如,排除值为空属性、进行缩进输出、将驼峰转为下划线、进行日期格式化等,这又如何实现呢?

首先,我们需要扩展Jackson提供的ObjectMapper类,代码如下:

[java] view plaincopy
  1. public class CustomObjectMapper extends ObjectMapper {  
  2.   
  3.     private boolean camelCaseToLowerCaseWithUnderscores = false;  
  4.     private String dateFormatPattern;  
  5.   
  6.     public void setCamelCaseToLowerCaseWithUnderscores(boolean camelCaseToLowerCaseWithUnderscores) {  
  7.         this.camelCaseToLowerCaseWithUnderscores = camelCaseToLowerCaseWithUnderscores;  
  8.     }  
  9.   
  10.     public void setDateFormatPattern(String dateFormatPattern) {  
  11.         this.dateFormatPattern = dateFormatPattern;  
  12.     }  
  13.   
  14.     public void init() {  
  15.         // 排除值为空属性  
  16.         setSerializationInclusion(JsonInclude.Include.NON_NULL);  
  17.         // 进行缩进输出  
  18.         configure(SerializationFeature.INDENT_OUTPUT, true);  
  19.         // 将驼峰转为下划线  
  20.         if (camelCaseToLowerCaseWithUnderscores) {  
  21.             setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);  
  22.         }  
  23.         // 进行日期格式化  
  24.         if (StringUtil.isNotEmpty(dateFormatPattern)) {  
  25.             DateFormat dateFormat = new SimpleDateFormat(dateFormatPattern);  
  26.             setDateFormat(dateFormat);  
  27.         }  
  28.     }  
  29. }  
然后,将CustomObjectMapper注入到MappingJackson2HttpMessageConverter中,Spring配置如下:

[xml] view plaincopy
  1. <bean id="objectMapper" class="com.xxx.api.json.CustomObjectMapper" init-method="init">  
  2.     <property name="camelCaseToLowerCaseWithUnderscores" value="true"/>  
  3.     <property name="dateFormatPattern" value="yyyy-MM-dd HH:mm:ss"/>  
  4. </bean>  
  5.   
  6. <mvc:annotation-driven>  
  7.     <mvc:message-converters>  
  8.         <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">  
  9.             <property name="objectMapper" ref="objectMapper"/>  
  10.         </bean>  
  11.     </mvc:message-converters>  
  12. </mvc:annotation-driven>  
通过以上过程,我们已经完成了一个基于Spring MVC的REST框架,只不过该框架还非常单薄,还缺乏很多关键性特性,尤其是异常处理。

4.3 处理异常行为

在Spring MVC中,我们可以使用AOP技术,编写一个全局的异常处理切面类,用它来统一处理所有的异常行为,在Spring 3.2中才开始提供。使用法很简单,只需定义一个类,并通过@ControllerAdvice注解将其标注即可,同时需要使用@ResponseBody注解表示返回值可序列化为JSON字符串。代码如下:

[java] view plaincopy
  1. @ControllerAdvice  
  2. @ResponseBody  
  3. public class ExceptionAdvice {  
  4.   
  5.     /** 
  6.      * 400 - Bad Request 
  7.      */  
  8.     @ResponseStatus(HttpStatus.BAD_REQUEST)  
  9.     @ExceptionHandler(HttpMessageNotReadableException.class)  
  10.     public Response handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {  
  11.         logger.error("参数解析失败", e);  
  12.         return new Response().failure("could_not_read_json");  
  13.     }  
  14.   
  15.     /** 
  16.      * 405 - Method Not Allowed 
  17.      */  
  18.     @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)  
  19.     @ExceptionHandler(HttpRequestMethodNotSupportedException.class)  
  20.     public Response handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {  
  21.         logger.error("不支持当前请求方法", e);  
  22.         return new Response().failure("request_method_not_supported");  
  23.     }  
  24.   
  25.     /** 
  26.      * 415 - Unsupported Media Type 
  27.      */  
  28.     @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)  
  29.     @ExceptionHandler(HttpMediaTypeNotSupportedException.class)  
  30.     public Response handleHttpMediaTypeNotSupportedException(Exception e) {  
  31.         logger.error("不支持当前媒体类型", e);  
  32.         return new Response().failure("content_type_not_supported");  
  33.     }  
  34.   
  35.     /** 
  36.      * 500 - Internal Server Error 
  37.      */  
  38.     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)  
  39.     @ExceptionHandler(Exception.class)  
  40.     public Response handleException(Exception e) {  
  41.         logger.error("服务运行异常", e);  
  42.         return new Response().failure(e.getMessage());  
  43.     }  
  44. }  
可见,在ExceptionAdvice类中包含一系列的异常处理方法,每个方法都通过@ResponseStatus注解定义了响应状态码,此外还通过@ExceptionHandler注解指定了具体需要拦截的异常类。以上过程只是包含了一部分的异常情况,若需处理其它异常,可添加方法具体的方法。需要注意的是,在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与@ExceptionHandler注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经JSON序列化后的Response对象。

4.4 支持参数验证

我们回到上文所提到的示例,这里处理一个普通的POST请求,代码如下:

[java] view plaincopy
  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
其中,AdvertiserParam参数包含若干属性,通过以下类结构可见,它是一个传统的POJO:

[java] view plaincopy
  1. public class AdvertiserParam {  
  2.   
  3.     private String advertiserName;  
  4.       
  5.     private String description;  
  6.   
  7.     // 省略 getter/setter 方法  
  8. }  
如果业务上需要确保AdvertiserParam对象的advertiserName属性必填,如何实现呢?

若将这类参数验证的代码写死在Controller中,势必会与正常的业务逻辑搅在一起,导致责任不够单一,违背于“单一责任原则”。建议将其参数验证行为从Controller中剥离出来,放到另外的类中,这里仅提供一个@Valid注解来定义AdvertiserParam参数,并在AdvertiserParam类中通过@NotEmpty注解来定义advertiserName属性,就像下面这样:

[java] view plaincopy
  1. @RestController  
  2. public class AdvertiserController {  
  3.   
  4.     @RequestMapping(value = "/advertiser", method = RequestMethod.POST)  
  5.     public Response createAdvertiser(@RequestBody @Valid AdvertiserParam advertiserParam) {  
  6.         ...  
  7.     }  
  8. }  
  9.   
  10. public class AdvertiserParam {  
  11.   
  12.     @NotEmpty  
  13.     private String advertiserName;  
  14.       
  15.     private String description;  
  16.   
  17.     // 省略 getter/setter 方法  
  18. }  
这里的@Valid注解实际上是Validation Bean规范提供的注解,该规范已由Hibernate Validator框架实现,因此需要添加以下Maven依赖到pom.xml文件中:

[xml] view plaincopy
  1. <dependency>  
  2.     <groupId>org.hibernate</groupId>  
  3.     <artifactId>hibernate-validator</artifactId>  
  4.     <version>${hibernate-validator.version}</version>  
  5. </dependency>  
需要注意的是,Hibernate Validator与Hibernate没有任何依赖关系,唯一有联系的只是都属于JBoss公司的开源项目而已。

要实现@NotEmpty注解的功能,我们需要做以下几件事情。

首先,定义一个@NotEmpty注解类,代码如下:

[java] view plaincopy
  1. @Documented  
  2. @Target({ElementType.FIELD, ElementType.PARAMETER})  
  3. @Retention(RetentionPolicy.RUNTIME)  
  4. @Constraint(validatedBy = NotEmptyValidator.class)  
  5. public @interface NotEmpty {  
  6.   
  7.     String message() default "not_empty";  
  8.   
  9.     Class<?>[] groups() default {};  
  10.   
  11.     Class<? extends Payload>[] payload() default {};  
  12. }  
以上注解类必须包含message、groups、payload三个属性,因为这是规范所要求的,此外,需要通过@Constraint注解指定一个验证器类,这里对应的是NotEmptyValidator,其代码如下:

[java] view plaincopy
  1. public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {  
  2.   
  3.     @Override  
  4.     public void initialize(NotEmpty constraintAnnotation) {  
  5.     }  
  6.   
  7.     @Override  
  8.     public boolean isValid(String value, ConstraintValidatorContext context) {  
  9.         return StringUtil.isNotEmpty(value);  
  10.     }  
  11. }  
以上验证器类实现了ConstraintValidator接口,并在该接口的isValid( )方法中完成了具体的参数验证逻辑。需要注意的是,实现接口时需要指定泛型,第一个参数表示验证注解类型(NotEmpty),第二个参数表示需要验证的参数类型(String)。

然后,我们需要在Spring配置文件中开启该特性,需添加如下配置:

[xml] view plaincopy
  1. <bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>  

最后,需要在全局异常处理类中添加参数验证处理方法,代码如下:

[java] view plaincopy
  1. @ControllerAdvice  
  2. @ResponseBody  
  3. public class ExceptionAdvice {  
  4.   
  5.     /** 
  6.      * 400 - Bad Request 
  7.      */  
  8.     @ResponseStatus(HttpStatus.BAD_REQUEST)  
  9.     @ExceptionHandler(ValidationException.class)  
  10.     public Response handleValidationException(ValidationException e) {  
  11.         logger.error("参数验证失败", e);  
  12.         return new Response().failure("validation_exception");  
  13.     }  
  14. }  
至此,REST框架已集成了Bean Validation特性,我们可以使用各种注解来完成所需的参数验证行为了。

看似该框架可以在本地成功跑起来,整个架构包含两个应用,前端应用提供纯静态的HTML页面,后端应用发布REST API,前端需要通过AJAX调用后端发布的REST API,然而AJAX是不支持跨域访问的,也就是说,前后端两个应用必须在同一个域名下才能访问。这是非常严重的技术障碍,一定需要找到解决方案。

4.5 解决跨域问题

比如,前端应用为静态站点且部署在http://web.xxx.com域下,后端应用发布REST API并部署在http://api.xxx.com域下,如何使前端应用通过AJAX跨域访问后端应用呢?这需要使用到CORS技术来实现,这也是目前最好的解决方案了。

[CORS全称为Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出AJAX跨域请求。]

CORS技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(IE8浏览器也支持了),服务端可通过任何编程语言来实现,只要能将CORS响应头写入response对象中即可。

下面我们继续扩展REST框架,通过CORS技术实现AJAX跨域访问。

首先,我们需要编写一个Filter,用于过滤所有的HTTP请求,并将CORS响应头写入response对象中,代码如下:

[java] view plaincopy
  1. public class CorsFilter implements Filter {  
  2.   
  3.     private String allowOrigin;  
  4.     private String allowMethods;  
  5.     private String allowCredentials;  
  6.     private String allowHeaders;  
  7.     private String exposeHeaders;  
  8.   
  9.     @Override  
  10.     public void init(FilterConfig filterConfig) throws ServletException {  
  11.         allowOrigin = filterConfig.getInitParameter("allowOrigin");  
  12.         allowMethods = filterConfig.getInitParameter("allowMethods");  
  13.         allowCredentials = filterConfig.getInitParameter("allowCredentials");  
  14.         allowHeaders = filterConfig.getInitParameter("allowHeaders");  
  15.         exposeHeaders = filterConfig.getInitParameter("exposeHeaders");  
  16.     }  
  17.   
  18.     @Override  
  19.     public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {  
  20.         HttpServletRequest request = (HttpServletRequest) req;  
  21.         HttpServletResponse response = (HttpServletResponse) res;  
  22.         if (StringUtil.isNotEmpty(allowOrigin)) {  
  23.             List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));  
  24.             if (CollectionUtil.isNotEmpty(allowOriginList)) {  
  25.                 String currentOrigin = request.getHeader("Origin");  
  26.                 if (allowOriginList.contains(currentOrigin)) {  
  27.                     response.setHeader("Access-Control-Allow-Origin", currentOrigin);  
  28.                 }  
  29.             }  
  30.         }  
  31.         if (StringUtil.isNotEmpty(allowMethods)) {  
  32.             response.setHeader("Access-Control-Allow-Methods", allowMethods);  
  33.         }  
  34.         if (StringUtil.isNotEmpty(allowCredentials)) {  
  35.             response.setHeader("Access-Control-Allow-Credentials", allowCredentials);  
  36.         }  
  37.         if (StringUtil.isNotEmpty(allowHeaders)) {  
  38.             response.setHeader("Access-Control-Allow-Headers", allowHeaders);  
  39.         }  
  40.         if (StringUtil.isNotEmpty(exposeHeaders)) {  
  41.             response.setHeader("Access-Control-Expose-Headers", exposeHeaders);  
  42.         }  
  43.         chain.doFilter(req, res);  
  44.     }  
  45.   
  46.     @Override  
  47.     public void destroy() {  
  48.     }  
  49. }  
以上CorsFilter将从web.xml中读取相关Filter初始化参数,并将在处理HTTP请求时将这些参数写入对应的CORS响应头中,下面大致描述一下这些CORS响应头的意义:

  • Access-Control-Allow-Origin:允许访问的客户端域名,例如:http://web.xxx.com,若为*,则表示从任意域都能访问,即不做任何限制。
  • Access-Control-Allow-Methods:允许访问的方法名,多个方法名用逗号分割,例如:GET,POST,PUT,DELETE,OPTIONS。
  • Access-Control-Allow-Credentials:是否允许请求带有验证信息,若要获取客户端域下的cookie时,需要将其设置为true。
  • Access-Control-Allow-Headers:允许服务端访问的客户端请求头,多个请求头用逗号分割,例如:Content-Type。
  • Access-Control-Expose-Headers:允许客户端访问的服务端响应头,多个响应头用逗号分割。

需要注意的是,CORS规范中定义Access-Control-Allow-Origin只允许两种取值,要么为*,要么为具体的域名,也就是说,不支持同时配置多个域名。为了解决跨多个域的问题,需要在代码中做一些处理,这里将Filter初始化参数作为一个域名的集合(用逗号分隔),只需从当前请求中获取Origin请求头,就知道是从哪个域中发出的请求,若该请求在以上允许的域名集合中,则将其放入Access-Control-Allow-Origin响应头,这样跨多个域的问题就轻松解决了。

以下是web.xml中配置CorsFilter的方法:

[xml] view plaincopy
  1. <filter>  
  2.     <filter-name>corsFilter</filter-name>  
  3.     <filter-class>com.xxx.api.cors.CorsFilter</filter-class>  
  4.     <init-param>  
  5.         <param-name>allowOrigin</param-name>  
  6.         <param-value>http://web.xxx.com</param-value>  
  7.     </init-param>  
  8.     <init-param>  
  9.         <param-name>allowMethods</param-name>  
  10.         <param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>  
  11.     </init-param>  
  12.     <init-param>  
  13.         <param-name>allowCredentials</param-name>  
  14.         <param-value>true</param-value>  
  15.     </init-param>  
  16.     <init-param>  
  17.         <param-name>allowHeaders</param-name>  
  18.         <param-value>Content-Type</param-value>  
  19.     </init-param>  
  20. </filter>  
  21. <filter-mapping>  
  22.     <filter-name>corsFilter</filter-name>  
  23.     <url-pattern>/*</url-pattern>  
  24. </filter-mapping>  
完成以上过程即可实现AJAX跨域功能了,但似乎还存在另外一个问题,由于REST是无状态的,后端应用发布的REST API可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为REST请求提供安全机制。

4.6 提供安全机制

解决REST安全调用问题,可以做得很复杂,也可以做得特简单,可按照以下过程提供REST安全机制:

  1. 当用户登录成功后,在服务端生成一个token,并将其放入内存中(可放入JVM或Redis中),同时将该token返回到客户端。
  2. 在客户端中将返回的token写入cookie中,并且每次请求时都将token随请求头一起发送到服务端。
  3. 提供一个AOP切面,用于拦截所有的Controller方法,在切面中判断token的有效性。
  4. 当登出时,只需清理掉cookie中的token即可,服务端token可设置过期时间,使其自行移除。

首先,我们需要定义一个用于管理token的接口,包括创建token与检查token有效性的功能。代码如下:

[java] view plaincopy
  1. public interface TokenManager {  
  2.   
  3.     String createToken(String username);  
  4.   
  5.     boolean checkToken(String token);  
  6. }  
然后,我们可提供一个简单的TokenManager实现类,将token存储到JVM内存中。代码如下:

[java] view plaincopy
  1. public class DefaultTokenManager implements TokenManager {  
  2.   
  3.     private static Map<String, String> tokenMap = new ConcurrentHashMap<>();  
  4.   
  5.     @Override  
  6.     public String createToken(String username) {  
  7.         String token = CodecUtil.createUUID();  
  8.         tokenMap.put(token, username);  
  9.         return token;  
  10.     }  
  11.   
  12.     @Override  
  13.     public boolean checkToken(String token) {  
  14.         return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);  
  15.     }  
  16. }  
需要注意的是,如果需要做到分布式集群,建议基于Redis提供一个实现类,将token存储到Redis中,并利用Redis与生俱来的特性,做到token的分布式一致性。

然后,我们可以基于Spring AOP写一个切面类,用于拦截Controller类的方法,并从请求头中获取token,最后对token有效性进行判断。代码如下:

[java] view plaincopy
  1. public class SecurityAspect {  
  2.   
  3.     private static final String DEFAULT_TOKEN_NAME = "X-Token";  
  4.   
  5.     private TokenManager tokenManager;  
  6.     private String tokenName;  
  7.   
  8.     public void setTokenManager(TokenManager tokenManager) {  
  9.         this.tokenManager = tokenManager;  
  10.     }  
  11.   
  12.     public void setTokenName(String tokenName) {  
  13.         if (StringUtil.isEmpty(tokenName)) {  
  14.             tokenName = DEFAULT_TOKEN_NAME;  
  15.         }  
  16.         this.tokenName = tokenName;  
  17.     }  
  18.   
  19.     public Object execute(ProceedingJoinPoint pjp) throws Throwable {  
  20.         // 从切点上获取目标方法  
  21.         MethodSignature methodSignature = (MethodSignature) pjp.getSignature();  
  22.         Method method = methodSignature.getMethod();  
  23.         // 若目标方法忽略了安全性检查,则直接调用目标方法  
  24.         if (method.isAnnotationPresent(IgnoreSecurity.class)) {  
  25.             return pjp.proceed();  
  26.         }  
  27.         // 从 request header 中获取当前 token  
  28.         String token = WebContext.getRequest().getHeader(tokenName);  
  29.         // 检查 token 有效性  
  30.         if (!tokenManager.checkToken(token)) {  
  31.             String message = String.format("token [%s] is invalid", token);  
  32.             throw new TokenException(message);  
  33.         }  
  34.         // 调用目标方法  
  35.         return pjp.proceed();  
  36.     }  
  37. }  
若要使SecurityAspect生效,则需要添加如下Spring 配置:

[xml] view plaincopy
  1. <bean id="securityAspect" class="com.xxx.api.security.SecurityAspect">  
  2.     <property name="tokenManager" ref="tokenManager"/>  
  3.     <property name="tokenName" value="X-Token"/>  
  4. </bean>  
  5.   
  6. <aop:config>  
  7.     <aop:aspect ref="securityAspect">  
  8.         <aop:around method="execute" pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)"/>  
  9.     </aop:aspect>  
  10. </aop:config>  
最后,别忘了在web.xml中添加允许的X-Token响应头,配置如下:

[xml] view plaincopy
  1. <init-param>  
  2.     <param-name>allowHeaders</param-name>  
  3.     <param-value>Content-Type,X-Token</param-value>  
  4. </init-param>  

总结

本文从经典的MVC模式开始,对MVC模式是什么以及该模式存在的不足进行了简述。然后引出了如何对MVC模式的改良,让其转变为前后端分离架构,以及解释了为何要进行前后端分离。最后通过REST服务将前后端进行解耦,并提供了一款基于Java的REST框架的主要实现过程,尤其是需要注意的核心技术问题及其解决方案。希望本文对正在探索前后端分离的读者们有所帮助,期待与大家共同探讨。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 wifi登录密码忘记了怎么办 电脑登录密码忘记了怎么办 笔记本电脑登录密码忘记了怎么办 信用卡登录密码忘记了怎么办 华为p9后置摄像头模糊怎么办 手机图片文件夹删了怎么办 发票系统导出的xml怎么办 微信支付被限制怎么办 跨行三天不到账怎么办 测速正常但实际很慢怎么办 善领wifi上网慢怎么办 文件太大拷贝不到u盘怎么办 电脑的视频文件不显示缩略图怎么办 无线网连接不上怎么办 路由器wifi密码忘了怎么办 路由器账号密码忘了怎么办 刷卡刷多了钱怎么办 刷卡刷了两次钱怎么办 小米手机强刷后一联网就被锁怎么办 苹果刷机出现的问题怎么办 小米手机解不开图案锁怎么办 小米6无限重启怎么办 小米5一直显示mi怎么办 小米手机电池进入休眠状态怎么办 小米3s开不开机怎么办 小米n充电关机开机不了怎么办? 关机后强制刷机怎么办 红米手机开机画面怎么办 红米note1无法清理数据怎么办 红米2a密码忘记怎么办 线刷也不成功该怎么办 红米2开不了机怎么办 魅族无限重启怎么办 坚果pro无法双清怎么办 usb外置网卡网速慢怎么办? 无线路由器被改密码怎么办 电脑打不开flv格式的视频怎么办 电脑打不开pdf格式的文件怎么办 pdf格式在电脑上打不开怎么办 zip压缩的时候空间不足怎么办 电子发票填抬头错了怎么办