1、理解MVC
经典的设计模式,model-view-controller 模型-视图-控制器
- 模型:封装数据的载体,在java中同简单的POJO(plain ordinary java object)本质是java bean
- 视图:偏重于展现,决定了界面的模样,java中可通过jsp从当视图,或纯HTML的方式
- 控制器:粘合模型和视图:http请求 进入控制器 器获取数据 封装为模型 模型传递给视图展现(敲!黑!板 !)
2、模式的优缺
开发高效,耦合度低,程序各部职责清晰
- 每次请求都要走 :控制器-模型-视图 这个流程,复杂
- 视图依赖于模型
- 渲染视图在服务端完成,呈现给浏览器的是带有模型的视图页,性能无法很好优化
改进:
浏览器ajax请求,服务器接受并返回json数据,浏览器界面渲染
REST:Representational State Transfer(表述性状态转移)
前后端分离
分工明确
职责清晰
REST:无状态、轻量级SOA,面向资源,使用URL访问资源
URL:请求方式、路径
方式: GET 查 / POST增 / PUT改 / DELETE删 / HEAD / OPTIONS;
资源:领域对象,通过领域对象进行数据建模;
无状态的架构模式:当前请求不受上次的影响,服务端讲内部资源发布rest服务,客户端通URL访问这些资源;
实现rest框架
1、统一响应结构
统一返回的json结构:元数据和返回值
- 元数据:操作成功与否、返回值消息;返回值:服务端方法所返回的数据
2、对象序列化
序列化:简单讲 将json转普通Java对象,反之 反序列化 均称序列化
注解
@RequestBody注解定义需反序列化参数即可
@ResponseBody对返回值序列化
@Controller
public class AdvertiserController {
//@ResponseBody
@RequestMapping(value = "/advertiser", method = RequestMethod.POST)
public @ResponseBody Response createAdvertiser(@RequestBody AdvertiserParam advertiserParam) {
...
}
}
Jackson
<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>
3、异常处理
SpringMVC 可以使用AOP技术,编写全局的异常处理切面累,统一处理all异常,Spring3.2之中开始提供:
定义类并用@ControllerAdvice注解将其标注,使用@ResponseBody注解
@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());
}
}
通过@ResponseStatus注解定义了响应状态码,此外还通过@ExceptionHandler注解指定了具体需要拦截的异常类。
在运行时从上往下依次调用每个异常处理方法,匹配当前异常类型是否与@ExceptionHandler注解所定义的异常相匹配,若匹配,则执行该方法,同时忽略后续所有的异常处理方法,最终会返回经JSON序列化后的Response对象。
4、支持参数验证
这个地方需要这么复杂吗?看来spring发展挺快的,具体的看这篇博客吧
5、跨域问题
CORS全称为Cross Origin Resource Sharing(跨域资源共享),服务端只需添加相关响应头信息,即可实现客户端发出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() {
}
}
- 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的方法:
<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>
由于REST是无状态的,后端应用发布的REST API可在用户未登录的情况下被任意调用,这显然是不安全的,如何解决这个问题呢?
6、安全机制
- 当用户登录成功后,在服务端生成一个token,并将其放入内存中(可放入JVM或Redis中),同时将该token返回到客户端。
- 在客户端中将返回的token写入cookie中,并且每次请求时都将token随请求头一起发送到服务端。
- 提供一个AOP切面,用于拦截所有的Controller方法,在切面中判断token的有效性。
- 当登出时,只需清理掉cookie中的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;
}
public void setTokenName(String tokenName) {
if (StringUtil.isEmpty(tokenName)) {
tokenName = DEFAULT_TOKEN_NAME;
}
this.tokenName = tokenName;
}
public Object execute(ProceedingJoinPoint pjp) throws Throwable {
// 从切点上获取目标方法
MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
Method method = methodSignature.getMethod();
// 若目标方法忽略了安全性检查,则直接调用目标方法
if (method.isAnnotationPresent(IgnoreSecurity.class)) {
return pjp.proceed();
}
// 从 request header 中获取当前 token
String token = WebContext.getRequest().getHeader(tokenName);
// 检查 token 有效性
if (!tokenManager.checkToken(token)) {
String message = String.format("token [%s] is invalid", token);
throw new TokenException(message);
}
// 调用目标方法
return pjp.proceed();
}
}
若要使SecurityAspect生效,则需要添加如下Spring 配置:
<bean id="securityAspect" class="com.xxx.api.security.SecurityAspect">
<property name="tokenManager" ref="tokenManager"/>
<property name="tokenName" value="X-Token"/>
</bean>
<aop:config>
<aop:aspect ref="securityAspect">
<aop:around method="execute" pointcut="@annotation(org.springframework.web.bind.annotation.RequestMapping)"/>
</aop:aspect>
</aop:config>
别忘了在web.xml中添加允许的X-Token响应头,配置如下
<init-param>
<param-name>allowHeaders</param-name>
<param-value>Content-Type,X-Token</param-value>
</init-param