背景
最近在做一个事:下线一个超级大单体服务。单一统计代码行数其实不够全面,反正项目 git clone 下来文件就有这么大:
这是一个已经存在了十年以上的服务,随着业务的发展,这个服务已经无法满足我们的需求。
我们统计了一下,有近 400 个页面菜单需要迁移到已经划分好领域的各个微服务之中。
其实最麻烦的还是前端页面,因为页面重写是比较麻烦的,涉及到大量的细节梳理,可能还需要前端、设计和产品同学的介入,想在 1 年甚至几个月全部重写完成在有限的资源情况下是不可能完成的任务。所以我提出了一个方案,低改动迁移 + 按需重写:
-
低改动迁移
- 设计一套机制,能够快速、便捷地将单体服务的页面迁移至微服务 Web 工程。
- 保持页面几乎无改动,无需前端介入,以确保迁移过程的高效性。
- 不可避免的,后端代码需要进行改造,以适应新的 MVC 框架。
-
按需重写
- 结合实际业务需求和质量优化的工作,采用重写的方式。例如,对于一些核心的业务页面,我们可能会选择重写,以提供更好的用户体验和更高的性能。
接下来最关键的问题就是如何设计一套迁移机制:
-
技术栈迁移
- 从Spring + FreeMarker + Struts2 + Resin 迁移到 Spring Boot + Spring Web MVC + Embedded Tomcat + FreeMarker
-
架构转变
- 从纯动态数据交互最终演变为一个结合了静态页面渲染和动态数据交互的多模式 Web 应用(名词是我自己发明的)。这个新的架构可以更好地满足我们的业务需求,同时也提供了更好的用户体验。
-
理论分析
- Spring Web MVC 本身是一个基于 Java 的 MVC 设计模式的 Web 框架,且支持多种视图技术,包括但不限于 JSP、Freemarker、Thymeleaf 等
本文会记录在迁移的过程中碰到的一些有意思的点。在后续的文章中,我会详细介绍迁移经验,以及对其他面临类似问题的开发者的建议。
全局变量优化
工程结构设计
前面也提到了,后续我们的 Web 应用将最终演变为一个结合了静态页面渲染和动态数据交互的多模式 Web 应用,它融合了两种交互模式:一种是传统的服务器端渲染方式,利用FreeMarker模板引擎进行页面渲染;另一种是现在比较流行的前后端分离方式,通过 RESTful API 提供数据服务。
问题: 在之前的 Web 服务中,负责动态 JSON 数据交互的 Controller 通常使用 @RestController
或 @ResponseBody
注解。而负责 FreeMarker 静态页面渲染的 Controller 则标注为@Controller
,并且其方法(大部分)需要返回 String 类型。此外,FreeMarker 还需要使用全局变量和特定的响应参数等。
建议: 将原来的 Controller 和负责 FreeMarker 静态页面渲染的 Controller 进行区分。例如,可以将后者命名为XXFreeMarkerController
,以便于识别。同时应该将所有与 FreeMarker 相关的代码放在一个单独的 package 中,以实现更好的管理。
全局变量优化
前面也提到了,FreeMarker 还需要使用全局变量,在 Spring MVC 中可以使用 @ControllerAdvice
,比如:
@ControllerAdvice
public class GlobalFreeMarkerControllerAdvice extends BaseController {
@Autowired
private BasicService basicService;
@ModelAttribute("basicService")
public BasicService basicService() {
return basicService;
}
/**
* 获取当前登陆用户
*/
@ModelAttribute("admin")
public User admin() {
return tryGetCurrentUser();
}
}
在这个例子中,我定义了一个GlobalFreeMarkerControllerAdvice
类,并使用@ControllerAdvice
注解标记它。我们在这个类中定义了两个方法:basicService()
和admin()
,并使用@ModelAttribute
注解标记它们。这样,每次处理请求时,Spring MVC都会先调用这两个方法,并将它们的返回值添加到模型中。
然而,这种做法有一个问题。@ControllerAdvice
注解会对所有的Controller进行全局预处理,包括那些只提供RESTful API的Controller。这些Controller并不需要全局数据绑定。例如,"获取当前登录用户"的操作可能需要查询缓存或者Dubbo服务,这将会降低一部分请求的性能。
因此,当使用@ControllerAdvice
注解时,我们需要注意其可能带来的性能影响,并根据实际情况做出合理的设计和优化。
为了解决这个问题,可以利用 @ControllerAdvice
注解的 basePackages
或 annotations
属性来限制其作用范围。这样,只有特定的 Controller 会受到全局预处理的影响。这种做法与上文提到的"将原来的 Controller 和负责 FreeMarker 静态页面渲染的 Controller 进行区分,放在不同的 package 中"的策略是一致的。这样就可以确保只有需要的 Controller 会受到全局预处理的影响,从而避免不必要的性能损失。
BaseController
当然也可以创建一个 BaseController
:
public abstract class BaseController {
@ModelAttribute("attributeName")
public String myModelAttribute() {
return "attributeValue";
}
// ...
}
@Controller
public class MyController extends BaseController {
// ...
}
在这个例子中,myModelAttribute()
方法只会对 MyController
的请求进行预处理,因为只有 MyController
继承了 BaseController
。如果有其他的 Controller 不继承 BaseController
,那么 myModelAttribute()
方法就不会对它们的请求进行预处理。
这种做法的优点是简单明了,易于理解和实现。但是,它也有一个缺点,那就是如果有很多 Controller 需要使用同一个 @ModelAttribute
方法,那么你就需要让它们都继承 BaseController
,这可能会导致继承层次过深,代码结构复杂
欢迎关注公众号: