从 MVC 到前后端分离 黄勇

前言:

    文章还是不错的,要不然也不会转载,基础知识很重要,文章整个的流程很清晰,条条大路通罗马,解决方式或者某些处理方式不止一种,算作参考;

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发展挺快的,具体的看这篇博客吧

https://blog.csdn.net/qq_37192800/article/details/79821789 

https://blog.csdn.net/gaowenhui2008/article/details/49447403#commentBox

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、安全机制

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

 

总结

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

                                (责编/ 钱曙光,关注架构和算法领域,[email protected]


作者简介:黄勇,从事近十年的 JavaEE 应用开发工作,曾任阿里巴巴公司系统架构师,现任特赞(tezign.com)公司 CTO。对分布式服务架构与大数据技术有深入研究,具有丰富的 B/S 架构开发经验与项目实战经验,擅长敏捷开发模式。国内开源软件推动者之一,活跃于“开源中国”社区网站,Smart Framework 开源框架创始人。热爱技术交流,乐于分享自己的工作经验。

原文昨天还看到了,现在找不到了,好在别人也转过,^_^

https://blog.csdn.net/gaowenhui2008/article/details/49447403#commentBox

猜你喜欢

转载自blog.csdn.net/ma15732625261/article/details/82285864
今日推荐