一个项目的SpringCloud微服务改造过程
SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用MySQL,前端展示使用Freemark。今年,我们对该项目进行了一次革命性的改进,将其改造成Spring Cloud架构,并且把前后端分离,前端采用Vue框架。
一、使用SpringCloud架构进行改造
1.1 为什么使用Spring Cloud
Spring Cloud的核心是Spring Boot,相比较于传统的Spring,Spring Cloud具有以下优点:
- 部署简单,Spring Boot内置了Tomcat容器,可以将程序直接编译成一个jar,通过java-jar来运行。
- 编码简单,Spring Boot只需要在pom文件中添加一个starter-web依赖,即可帮助开发者快速启动一个Web容器,非常方便。
- 配置简单,Spring Boot可以通过简单的注解方式来代替原先Spring非常复杂的xml方式。如果我想把一个普通的类交给Spring管理,只需要添加@Configuration和@Bean两个注解即可。
- 监控简单,我们可以引入spring-boot-start-actuator依赖,直接使用REST方式来获取进程的运行期性能参数,从而达到监控的目的。
1.2 一个常规项目都需要改造哪些部分
1.2.1 配置文件
SSO项目改造前充斥着大量的配置文件,主要包含以下这些部分:
- 静态资源相关
- 数据源
- MyBatis配置
- Redis配置
- 事务
- 拦截器拦截内容
- 监听器、过滤器
- 组件扫描路径配置
本文着重介绍以下几个部分:
1)静态资源处理:
SpringMVC中,如果mvc:interceptors配置的URL规则如下,则不会拦截静态资源。
<mvc:mapping path="/*.do" />
1
2
3
4
5
6
7
但是如果配置的是:
1. ```
<mvc:mapping path="/**" />
方案1:在web.xml中配置default,用defaultServlet先处理请求如:
1 | <servlet-mapping> |
方案2:使用标签声明静态资源路径:
- ```
<mvc:resources mapping=”/resources/js/**” location=”/js/“ />
<mvc:resources mapping=”/resources/images/**” location=”/images/“ />
<mvc:resources mapping=”/resources/css/**” location=”/css/“ />public void addResourceHandlers(ResourceHandlerRegistry registry) {1
2
3
4
5
6
7
方案3:使用mvc:default-servlet-handler/标签:
1. SpringBoot解决方案:继承WebMvcConfigurerAdapter实现addResourceHandlers方法。
registry.addResourceHandler(“/**”)
.addResourceLocations(“classpath:/resource/“)//sso静态资源
.addResourceLocations(“classpath:/META-INF/resources/“)//swagger静态资源
.setCachePeriod(0);//0表示不缓存
}mvc:interceptors1
2
3
4
5
6
7
8
9
10
11
12
13
SSO静态资源文件路径如图:

2)拦截器:
SpringMVC配置文件内容:
拦截任何请求并且初始化参数,有些请求是不需要拦截的,有的请求登录后不需要经过权限校验直接放行。
mvc:interceptor
<mvc:mapping path=”/**” />请求地址 请求地址 /*** 拦截器 \* @param registry1
2
3
SpringBoot中添加拦截器只需继承WebMvcConfigurerAdapter,并重写addInterceptors方法即可。
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(permissionInterceptor).
addPathPatterns(“/**”);
super.addInterceptors(registry);
}
1 |
|
@Autowired
@Lazy
private PermissionInterceptor permissionInterceptor;
@Autowired
private Environment environment;
/**
*
*/
@Bean
public PermissionInterceptor permissionInterceptor {
PermissionInterceptor permissionInterceptor = new PermissionInterceptor;
List
List
permissionInterceptor.setCommonUrls(commonUrls);
permissionInterceptor.setExcludeUrls(excludeUrls);
return permissionInterceptor;
}
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
1 |
|
/**
- 指定切入点
- /
private static final String AOP_POINTCUT_EXPRESSION = “execution(public * com.creditease.permission.service.impl.Impl.(..))”;
@Resource
DruidDataSource dataSource;
/**
- 指定处理事务的PlatformTransactionManager
- @return
- /
@Bean
public DataSourceTransactionManager transactionManager {
return new DataSourceTransactionManager(dataSource);
}
/**
- 指定切入点处理逻辑,执行事务
- @return
- /
@Bean
public TransactionInterceptor txAdvice {
DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute;
txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
DefaultTransactionAttribute txAttrRequiredReadonly = new DefaultTransactionAttribute;
txAttrRequiredReadonly.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
txAttrRequiredReadonly.setReadOnly(true);
NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource;
source.addTransactionalMethod(“query*”, txAttrRequiredReadonly);
source.addTransactionalMethod(“find*”, txAttrRequiredReadonly);
source.addTransactionalMethod(“save*”, txAttrRequired);
source.addTransactionalMethod(“delete*”, txAttrRequired);
source.addTransactionalMethod(“add*”, txAttrRequired);
source.addTransactionalMethod(“modify*”, txAttrRequired);
return new TransactionInterceptor(transactionManager, source);
}
/**
- Advisor组装配置,将Advice的代码逻辑注入到Pointcut位置
- @return
- /
@Bean
public Advisor txAdviceAdvisor {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut;
pointcut.setExpression(AOP_POINTCUT_EXPRESSION);
return new DefaultPointcutAdvisor(pointcut, txAdvice);
}1
2
3
4
5
6
7
5)全局异常处理
一般编码时有异常我们都会try-catch捕获异常,有时为了区分不同的异常还会一次catch多个异常,大量的try-catch语句,这样使得代码也不够优雅;一个相同的异常处理写多次代码也比较冗余,所以引入全局的异常处理非常必要。
改造前的异常处理配置文件:/permission/noSecurity @Configuration @Slf4j @RestControllerAdvice public class GlobalExceptionConfig {1
2
3
4
5
6
7
8
9
使用SimpleMappingExceptionResolver类处理异常,设置自定义异常类型NopermissionException,以及异常发生后的请求路径/permission/noSecurity。
SpringBoot中采用@RestControllerAdvice或者@ControllerAdvice设置全局异常类。这两者区别类似于@Controller和@RestController注解。
SSO中定义了三种全局的异常处理:普通的Exception处理;自定的NopermissionException异常和参数校验异常。
全局异常处理代码如下:
//无权限处理
@ExceptionHandler(value = {NopermissionException.class})
public void noPermissionExceptionHandler(HttpServletRequest request, Exception ex, HttpServletResponse response, @Value(“${sso.server.prefix}”) String domain) throws IOException {
printLog(request,ex);
response.sendRedirect(“跳转到无权限页面地址”);
}
//参数校验处理
@ExceptionHandler(value = {BindException.class})
public ResultBody BindExceptionHandler(BindException bindException){
List
//这个ResultBody是一个返回结果对象,这里需要返回json,里面包含了状态码和提示信息
return ResultBody.buildFailureResult(errors.get(0).getDefaultMessage);
}
//所有未捕获的异常处理逻辑
@ExceptionHandler(value = {Exception.class})
public ResultBody exceptionHandler(HttpServletRequest request,Exception ex){
printLog(request,ex);
return ResultBody.buildExceptionResult;
}
//将请求参数和异常打印出来,结合@slf4j注解
public void printLog(HttpServletRequest request,Exception ex){
String parameters = JsonHelper.toString(request.getParameterMap);
log.error(“url>>>:{},params>>>:{} ,printLog>>>:{}”,request.getRequestURL,parameters,ex);
}
}
1 |
|
//常规做法
if(StringUtils.isEmpty(ssoSystem.getSysCode)
//SSO做法
//在Controller请求方法上添加@Valid注解
@RequestMapping(value = “/add”, method = RequestMethod.POST)
public ResultBody add(@Valid @RequestBody SsoSystem ssoSystem) {
}
//在需要处理的SsoSystem Bean的属性上加@NotNull注解
@NotNull(message = “系统编号不能为空”)
private String sysCode;
1 |
|
{
“resultCode”:2,
“resultMsg”:”系统编号不能为空”,
“resultData”:null
}
```
1.3 注意事项
1.3.1 内置Tomcat版本太高引发的问题
Spring Boot1.5默认使用内嵌Tomcat 8.5版本,而原来SpringMVC的SSO部署在Tomcat 7上。Tomcat的升级对这次改造影响最明显的就是cookie。Tomcat 8后采用的cookie校验协议是Rfc6265CookieProcessor。该协议要求domain的命名必须遵循以下规则:
- 必须是1-9、a-z、A-Z、. 、- 这几个字符组成。
- 必须是数字或字母开头(之前是以.creditease.corp 会报错tomcat cookie domain validation异常,最后改成了 creditease.corp)。
- 必须是数字或字母结尾。
二、前后端分离
2.1 解决跨域问题
由于是两个不同的应用,必然会有两个不同的端口。不同的端口就会有跨域问题,SSO采用的方式是通过nginx区分来自前后端的请求,反向代理请求对应到不同的服务去。
- sso.creditease.com对应的是后端的应用服务。
- sso.creditease.com/web对应的是前端的静态资源应用服务。
2.2 方便联调效率,引入swagger
swagger是后端接口展示的插件,通过修改拦截器代码,mock登录对象免登录,直接访问接口进行前后端的调试。在swagger插件上可以看到具体接口请求路径和参数、参数是否必须、返回值、接口统计信息等。
接口统计信息:
请求参数和路径:
返回值:
2.3 跳转接口修改
之前是通过SpringMvc的modeAndview方式跳转的,现在做了两种处理:
- 改成restful接口的形式,前端控制跳转然后直接获取数据。
- 直接通过response.sendRedirect跳转页面。
注意:老代码跳转采用的是通过SpringMvc在return的页面路径前加redirect的形式,如:return “redirect:index”,这样默认会在return的URL后加jessionID。
2.4 静态资源地址变更可能引发的问题
特别需要注意代码中的相关校验路径的地方。比如在这次改造过程中路径修改会影响以下几个方面。
- 菜单权限校验的时候,之前人、角色和路径已经绑定了,修改菜单访问路径会导致没权限。
- 扫码登录的接口判断了refer来源,修改路径会导致请求失败。
- 之前的sso-dome工程引用了静态资源,修改路径会报404。