想要解释什么是 对象序列化?不妨通过一些例子进行说明。比如,在服务端从数据库中获取了数据,此时该数据是一个普通的 Java 对象,然后需要将这个 Java 对象转换为 JSON 字符串,并将其返回到浏览器中进行渲染,这个转换过程称为序列化;再比如,通过浏览器发送了一个普通的 HTTP 请求,该请求携带了一个 JSON 格式的参数,在服务端需要将该 JSON 参数转换为普通的 Java 对象,这个转换过程称为反序列化。不管是序列化还是反序列化,我们一般都称为序列化。
实际上,Spring MVC 已经为我们提供了这类序列化特性,只需在 Controller 的方法参数中使用 @RequestBody 注解定义需要反序列化的参数即可,如以下代码片段:
@Controller
public class AdvertiserController {
@RequestMapping(value = "/advertiser", method = RequestMethod.POST)
public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {
...
}
}
若需要对 Controller 的方法返回值进行序列化,则需要在该返回值上使用 @ResponseBody 注解来定义,如以下代码片段:
@Controller
public class AdvertiserController {
@RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)
public @ResponseBody Response getAdvertiser(@PathVariable("id") String advertiserId) {
...
}
}
当然,@ResponseBody 注解也可以定义在类上,这样所有的方法都继承了该特性。由于经常会使用到 @ResponseBody 注解,所以 Spring 提供了一个名为@RestController 的注解来取代以上的 @Controller 注解,这样我们就可以省略返回值前面的 @ResponseBody 注解了,但参数前面的 @RequestBody 注解是无法省略的。实际上,看看 Spring 中对应 @RestController 注解的源码便可知晓:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
String value() default "";
}
可见,@RestController 注解已经被 @Controller 与 @ResponseBody 注解定义过了,Spring 框架会识别这类注解。需要注意的是,该特性在 Spring 4.0 中才引入。因此,我们可将以上代码进行如下改写:
@RestController
public class AdvertiserController {
@RequestMapping(value = "/advertiser", method = RequestMethod.POST)
public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {
...
}
@RequestMapping(value = "/advertiser/{id}", method = RequestMethod.GET)
public Response getAdvertiser(@PathVariable("id") String advertiserId) {
...
}
}
除了使用注解来定义序列化行为以外,我们还需要使用 Jackson 来提供 JSON 的序列化操作,在 Spring 配置文件中只需添加以下配置即可:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"/>
</mvc:message-converters>
</mvc:annotation-driven>
若需要对 Jackson 的序列化行为进行定制,比如,排除值为空属性、进行缩进输出、将驼峰转为下划线、进行日期格式化等,这又如何实现呢?
首先,我们需要扩展 Jackson 提供的 ObjectMapper 类,代码如下:
public class CustomObjectMapper extends ObjectMapper {
private boolean camelCaseToLowerCaseWithUnderscores = false;
private String dateFormatPattern;
public void setCamelCaseToLowerCaseWithUnderscores(boolean camelCaseToLowerCaseWithUnderscores) {
this.camelCaseToLowerCaseWithUnderscores = camelCaseToLowerCaseWithUnderscores;
}
public void setDateFormatPattern(String dateFormatPattern) {
this.dateFormatPattern = dateFormatPattern;
}
public void init() {
// 排除值为空属性
setSerializationInclusion(JsonInclude.Include.NON_NULL);
// 进行缩进输出
configure(SerializationFeature.INDENT_OUTPUT, true);
// 将驼峰转为下划线
if (camelCaseToLowerCaseWithUnderscores) {
setPropertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
}
// 进行日期格式化
if (StringUtil.isNotEmpty(dateFormatPattern)) {
DateFormat dateFormat = new SimpleDateFormat(dateFormatPattern);
setDateFormat(dateFormat);
}
}
}
然后,将 CustomObjectMapper 注入到 MappingJackson2HttpMessageConverter 中,Spring 配置如下:
<bean id="objectMapper" class="com.xxx.api.json.CustomObjectMapper" init-method="init">
<property name="camelCaseToLowerCaseWithUnderscores" value="true"/>
<property name="dateFormatPattern" value="yyyy-MM-dd HH:mm:ss"/>
</bean>
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
<property name="objectMapper" ref="objectMapper"/>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
通过以上过程,我们已经完成了一个基于 Spring MVC 的 REST 框架,只不过该框架还非常单薄,还缺乏很多关键性特性,尤其是异常处理。
4.3、处理异常行为
在 Spring MVC 中,我们可以使用 AOP 技术,编写一个全局的异常处理切面类,用它来统一处理所有的异常行为,在 Spring 3.2 中才开始提供。使用法很简单,只需定义一个类,并通过@ControllerAdvice 注解将其标注即可,同时需要使用 @ResponseBody 注解表示返回值可序列化为 JSON 字符串。代码如下:
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public Response handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
logger.error("参数解析失败", e);
return new Response().failure("could_not_read_json");
}
/**
* 405 - Method Not Allowed
*/
@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public Response handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
logger.error("不支持当前请求方法", e);
return new Response().failure("request_method_not_supported");
}
/**
* 415 - Unsupported Media Type
*/
@ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public Response handleHttpMediaTypeNotSupportedException(Exception e) {
logger.error("不支持当前媒体类型", e);
return new Response().failure("content_type_not_supported");
}
/**
* 500 - Internal Server Error
*/
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public Response handleException(Exception e) {
logger.error("服务运行异常", e);
return new Response().failure(e.getMessage());
}
}
可见,在 ExceptionAdvice 类中包含一系列的异常处理方法,每个方法都通过 @ResponseStatus 注解定义了响应状态码,此外还通过@ExceptionHandler 注解指定了具体需要拦截的异常类。以上过程只是包含了一部分的异常情况,若需处理其它异常,可添加方法具体的方法。需要注意的是,在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与 @ExceptionHandler 注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经 JSON 序列化后的 Response 对象。
4.4、支持参数验证
我们回到上文所提到的示例,这里处理一个普通的 POST 请求,代码如下:
@RestController
public class AdvertiserController {
@RequestMapping(value = "/advertiser", method = RequestMethod.POST)
public Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {
...
}
}
其中,AdvertiserParam 参数包含若干属性,通过以下类结构可见,它是一个传统的 POJO:
public class AdvertiserParam {
private String advertiserName;
private String description;
// 省略 getter/setter 方法
}
如果业务上需要确保 AdvertiserParam 对象的 advertiserName 属性必填,如何实现呢?
若将这类参数验证的代码写死在 Controller 中,势必会与正常的业务逻辑搅在一起,导致责任不够单一,违背于“单一责任原则”。建议将其参数验证行为从 Controller 中剥离出来,放到另外的类中,这里仅提供一个@Valid 注解来定义 AdvertiserParam 参数,并在 AdvertiserParam 类中通过 @NotEmpty 注解来定义 advertiserName 属性,就像下面这样:
@RestController
public class AdvertiserController {
@RequestMapping(value = "/advertiser", method = RequestMethod.POST)
public Response createAdvertiser(@RequestBody @Valid AdvertiserParam advertiserParam) {
...
}
}
public class AdvertiserParam {
@NotEmpty
private String advertiserName;
private String description;
// 省略 getter/setter 方法
}
这里的 @Valid 注解实际上是 Validation Bean 规范提供的注解,该规范已由 Hibernate Validator 框架实现,因此需要添加以下 Maven 依赖到 pom.xml 文件中:
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator.version}</version>
</dependency>
需要注意的是,Hibernate Validator 与 Hibernate 没有任何依赖关系,唯一有联系的只是都属于 JBoss 公司的开源项目而已。
要实现 @NotEmpty 注解的功能,我们需要做以下几件事情。
首先,定义一个 @NotEmpty 注解类,代码如下:
@Documented
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NotEmptyValidator.class)
public @interface NotEmpty {
String message() default "not_empty";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
以上注解类必须包含 message、groups、payload 三个属性,因为这是规范所要求的,此外,需要通过 @Constraint 注解指定一个验证器类,这里对应的是 NotEmptyValidator,其代码如下:
public class NotEmptyValidator implements ConstraintValidator<NotEmpty, String> {
@Override
public void initialize(NotEmpty constraintAnnotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return StringUtil.isNotEmpty(value);
}
}
以上验证器类实现了 ConstraintValidator 接口,并在该接口的 isValid( ) 方法中完成了具体的参数验证逻辑。需要注意的是,实现接口时需要指定泛型,第一个参数表示验证注解类型(NotEmpty),第二个参数表示需要验证的参数类型(String)。
然后,我们需要在 Spring 配置文件中开启该特性,需添加如下配置:
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
最后,需要在全局异常处理类中添加参数验证处理方法,代码如下:
@ControllerAdvice
@ResponseBody
public class ExceptionAdvice {
/**
* 400 - Bad Request
*/
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ValidationException.class)
public Response handleValidationException(ValidationException e) {
logger.error("参数验证失败", e);
return new Response().failure("validation_exception");
}
}
至此,REST 框架已集成了 Bean Validation 特性,我们可以使用各种注解来完成所需的参数验证行为了。
看似该框架可以在本地成功跑起来,整个架构包含两个应用,前端应用提供纯静态的 HTML 页面,后端应用发布 REST API,前端需要通过 AJAX 调用后端发布的 REST API,然而 AJAX 是不支持跨域访问的,也就是说,前后端两个应用必须在同一个域名下才能访问。这是非常严重的技术障碍,一定需要找到解决方案。
4.5、解决跨域问题
比如,前端应用为静态站点且部署在 web.xxx.com 域下,后端应用发布 REST API 并部署在api.xxx.com 域下,如何使前端应用通过 AJAX 跨域访问后端应用呢?这需要使用到CORS 技术来实现,这也是目前最好的解决方案了。
CORS 全称为 Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出 AJAX 跨域请求。CORS 技术非常简单,易于实现,目前绝大多数浏览器均已支持该技术(IE8 浏览器也支持了),服务端可通过任何编程语言来实现,只要能将 CORS 响应头写入 response 对象中即可。
下面我们继续扩展 REST 框架,通过 CORS 技术实现 AJAX 跨域访问。
首先,我们需要编写一个 Filter,用于过滤所有的 HTTP 请求,并将 CORS 响应头写入 response 对象中,代码如下:
public class CorsFilter implements Filter {
private String allowOrigin;
private String allowMethods;
private String allowCredentials;
private String allowHeaders;
private String exposeHeaders;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
allowOrigin = filterConfig.getInitParameter("allowOrigin");
allowMethods = filterConfig.getInitParameter("allowMethods");
allowCredentials = filterConfig.getInitParameter("allowCredentials");
allowHeaders = filterConfig.getInitParameter("allowHeaders");
exposeHeaders = filterConfig.getInitParameter("exposeHeaders");
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (StringUtil.isNotEmpty(allowOrigin)) {
List<String> allowOriginList = Arrays.asList(allowOrigin.split(","));
if (CollectionUtil.isNotEmpty(allowOriginList)) {
String currentOrigin = request.getHeader("Origin");
if (allowOriginList.contains(currentOrigin)) {
response.setHeader("Access-Control-Allow-Origin", currentOrigin);
}
}
}
if (StringUtil.isNotEmpty(allowMethods)) {
response.setHeader("Access-Control-Allow-Methods", allowMethods);
}
if (StringUtil.isNotEmpty(allowCredentials)) {
response.setHeader("Access-Control-Allow-Credentials", allowCredentials);
}
if (StringUtil.isNotEmpty(allowHeaders)) {
response.setHeader("Access-Control-Allow-Headers", allowHeaders);
}
if (StringUtil.isNotEmpty(exposeHeaders)) {
response.setHeader("Access-Control-Expose-Headers", exposeHeaders);
}
chain.doFilter(req, res);
}
@Override
public void destroy() {
}
}
以上 CorsFilter 将从 web.xml 中读取相关 Filter 初始化参数,并将在处理 HTTP 请求时将这些参数写入对应的 CORS 响应头中,下面大致描述一下这些 CORS 响应头的意义:
Access-Control-Allow-Origin:允许访问的客户端域名,例如: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 的方法:
<filter>
<filter-name>corsFilter</filter-name>
<filter-class>com.xxx.api.cors.CorsFilter</filter-class>
<init-param>
<param-name>allowOrigin</param-name>
<param-value>http://web.xxx.com</param-value>
</init-param>
<init-param>
<param-name>allowMethods</param-name>
<param-value>GET,POST,PUT,DELETE,OPTIONS</param-value>
</init-param>
<init-param>
<param-name>allowCredentials</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>allowHeaders</param-name>
<param-value>Content-Type</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>corsFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
完成以上过程即可实现 AJAX 跨域功能了,但似乎还存在另外一个问题,由于 REST 是无状态的,后端应用发布的 REST API 可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?我们需要为 REST 请求提供安全机制。
4.6、提供安全机制
解决 REST 安全调用问题,可以做得很复杂,也可以做得特简单,可按照以下过程提供 REST 安全机制:
- 当用户登录成功后,在服务端生成一个 token,并将其放入内存中(可放入 JVM 或 Redis 中),同时将该 token 返回到客户端。
- 在客户端中将返回的 token 写入 cookie 中,并且每次请求时都将 token 随请求头一起发送到服务端。
- 提供一个 AOP 切面,用于拦截所有的 Controller 方法,在切面中判断 token 的有效性。
- 当登出时,只需清理掉 cookie 中的 token 即可,服务端 token 可设置过期时间,使其自行移除。
首先,我们需要定义一个用于管理 token 的接口,包括创建 token 与检查 token 有效性的功能。代码如下:
public interface TokenManager {
String createToken(String username);
boolean checkToken(String token);
}
然后,我们可提供一个简单的 TokenManager 实现类,将 token 存储到 JVM 内存中。代码如下:
public class DefaultTokenManager implements TokenManager {
private static Map<String, String> tokenMap = new ConcurrentHashMap<>();
@Override
public String createToken(String username) {
String token = CodecUtil.createUUID();
tokenMap.put(token, username);
return token;
}
@Override
public boolean checkToken(String token) {
return !StringUtil.isEmpty(token) && tokenMap.containsKey(token);
}
}
需要注意的是,如果需要做到分布式集群,建议基于 Redis 提供一个实现类,将 token 存储到 Redis 中,并利用 Redis 与生俱来的特性,做到 token 的分布式一致性。
然后,我们可以基于 Spring AOP 写一个切面类,用于拦截 Controller 类的方法,并从请求头中获取 token,最后对 token 有效性进行判断。代码如下:
public class SecurityAspect {
private static final String DEFAULT_TOKEN_NAME = "X-Token";
private TokenManager tokenManager;
private String tokenName;
public void setTokenManager(TokenManager tokenManager) {
this.tokenManager = tokenManager;
}


**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!**
**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新**
**[如果你需要这些资料,可以戳这里获取](https://gitee.com/vip204888)**