一个项目的SpringCloud微服务改造过程

Posted 分布式实验室

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一个项目的SpringCloud微服务改造过程相关的知识,希望对你有一定的参考价值。

SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用mysql,前端展示使用Freemark。今年,我们对该项目进行了一次革命性的改进,将其改造成Spring Cloud架构,并且把前后端分离,前端采用Vue框架。

一、使用SpringCloud架构进行改造

一个项目的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规则如下,则不会拦截静态资源。

 
   
   
 
  1. <mvc:mapping path="/*.do" />


但是如果配置的是:

 
   
   
 
  1. <mvc:mapping path="/**" />


方案1:在web.xml中配置default,用defaultServlet先处理请求如:

 
   
   
 
  1. <servlet-mapping>

  2. <servlet-name>default</servlet-name>

  3. <url-pattern>*.jpg</url-pattern>

  4. </servlet-mapping>

  5. <servlet-mapping>

  6. <servlet-name>default</servlet-name>

  7. <url-pattern>*.png</url-pattern>

  8. </servlet-mapping>

  9. <servlet-mapping>

  10. <servlet-name>default</servlet-name>

  11. <url-pattern>*.gif</url-pattern>

  12. </servlet-mapping>

  13. <servlet-mapping>

  14. <servlet-name>default</servlet-name>

  15. <url-pattern>*.ico</url-pattern>

  16. </servlet-mapping>

  17. <servlet-mapping>

  18. <servlet-name>default</servlet-name>

  19. <url-pattern>*.gif</url-pattern>

  20. </servlet-mapping>

  21. <servlet-mapping>

  22. <servlet-name>default</servlet-name>

  23. <url-pattern>*.js</url-pattern>

  24. </servlet-mapping>

  25. <servlet-mapping>

  26. <servlet-name>default</servlet-name>

  27. <url-pattern>*.css</url-pattern>

  28. </servlet-mapping>


方案2:使用标签声明静态资源路径:

 
   
   
 
  1. <mvc:resources mapping="/resources/js/**" location="/js/" />

  2. <mvc:resources mapping="/resources/images/**" location="/images/" />

  3. <mvc:resources mapping="/resources/css/**" location="/css/" />


方案3:使用mvc:default-servlet-handler/标签:

 
   
   
 
  1. SpringBoot解决方案:继承WebMvcConfigurerAdapter实现addResourceHandlers方法。

  2. public void addResourceHandlers(ResourceHandlerRegistry registry) {

  3. registry.addResourceHandler("/**")

  4. .addResourceLocations("classpath:/resource/")//sso静态资源

  5. .addResourceLocations("classpath:/META-INF/resources/")//swagger静态资源

  6. .setCachePeriod(0);//0表示不缓存

  7. }


SSO静态资源文件路径如图:

一个项目的SpringCloud微服务改造过程

2)拦截器:

SpringMVC配置文件内容:

拦截任何请求并且初始化参数,有些请求是不需要拦截的,有的请求登录后不需要经过权限校验直接放行。

 
   
   
 
  1. <mvc:interceptors>

  2. <mvc:interceptor>

  3. <mvc:mapping path="/**" />

  4. <bean class="自定义拦截器PermissionInterceptor">

  5. <property name="excludeUrls">

  6. </property>

  7. <!-- 只要登录了就不需要拦截的资源 -->

  8. <property name="LogInExcludeUrls">

  9. </property>

  10. </bean>

  11. </mvc:interceptor>

  12. </mvc:interceptors>


SpringBoot中添加拦截器只需继承WebMvcConfigurerAdapter,并重写addInterceptors方法即可。

 
   
   
 
  1. /*** 拦截器

  2. * @param registry

  3. */

  4. @Override

  5. public void addInterceptors(InterceptorRegistry registry) {

  6. registry.addInterceptor(permissionInterceptor).

  7. addPathPatterns("/**");

  8. super.addInterceptors(registry);

  9. }


自定义的拦截器需要初始化一些参数,因此需要在注册拦截器之前注册,这里我们设置为懒加载。免登录拦截的路径,以及登录后不需要判断权限的路径都写在yml文件了,通过系统环境变量Environment获取值。

 
   
   
 
  1. @Autowired

  2. @Lazy

  3. private PermissionInterceptor permissionInterceptor;

  4. @Autowired

  5. private Environment environment;


  6. /**

  7. *

  8. */

  9. @Bean

  10. public PermissionInterceptor permissionInterceptor() {

  11. PermissionInterceptor permissionInterceptor = new PermissionInterceptor();

  12. List<String> excludeUrls = Arrays.asList(environment.getProperty("intercept.exclude.path").split(","));

  13. List<String> commonUrls = Arrays.asList(environment.getProperty("intercept.login.exclude.path").split(","));

  14. permissionInterceptor.setCommonUrls(commonUrls);

  15. permissionInterceptor.setExcludeUrls(excludeUrls);

  16. return permissionInterceptor;

  17. }


3)数据库和MyBatis配置:

A、数据源配置

数据源注入的三种情况:

【情况一】

  • 条件:不引⼊druid-spring-boot-starter只依赖druid.jar,不指定spring.datasource.type。

  • 结果:注入的数据源是tomcat的数据源。

  • 解析:依赖的mybatis-spring-boot-starter工程依赖了tomcat的数据源,spring-boot-autoconfigure-starter的DataSourceAutoConfiguration自动注入类会在不指定数据源的情况下,判断路径中是否存在默认的4种数据源(Hikari,Tomcat,Dbcp,Dbcp2)的其一,如果有就注入。


【情况二】

  • 条件:不引入druid-spring-boot-starter只依赖druid.jar,指定spring.datasource.type为DruidDataSource。

  • 结果:注入了DruidDataSource数据源,但配置文件中的druid配置不会生效。

  • 解析:指定了依赖的数据源后,spring自动注入的starter会将指定的数据源注入,yml指定了druid数据源。@ConfigurationProperties注解的DataSourceProperties没处理druid部分的性能参数属性,只处理了数据源部分的属性。


【情况三】

  • 条件:引⼊ druid-spring-boot-starter 不依赖druid.jar,指定spring.datasource.type为DruidDataSource。

  • 结果:注入了DruidDataSource数据源,配置文件中的druid配置也会生效。

  • 解析:druid-spring-boot-starter自动配置类会在DataSourceAutoConfiguration之前先创建数据源,并且@ConfigurationProperties注入的DataSourceProperties包含了配置文件中druid的属性。


pom.xml依赖:

 
   
   
 
  1. <!-- 情况一、二 测试引入的依赖 -->

  2. <!--<dependency>-->

  3. <!--<groupId>com.alibaba</groupId>-->

  4. <!--<artifactId>druid</artifactId>-->

  5. <!--<version>${druid.version}</version>-->

  6. <!--</dependency>-->

  7. <dependency>

  8. <groupId>com.alibaba</groupId>

  9. <artifactId>druid-spring-boot-starter</artifactId>

  10. <version>1.1.10</version>

  11. </dependency>

  12. <dependency>

  13. <groupId>org.mybatis.spring.boot</groupId>

  14. <artifactId>mybatis-spring-boot-starter</artifactId>

  15. <version>RELEASE</version>

  16. </dependency>


yml配置:

 
   
   
 
  1. spring:

  2. datasource:

  3. type: com.alibaba.druid.pool.DruidDataSource # 当前数据源操作类型

  4. driver-class-name: com.mysql.jdbc.Driver # mysql驱动包

  5. url: jdbc:mysql://yourURL # 数据库名称

  6. username: yourusername

  7. password: yourpassword

  8. druid:

  9. initial-size: 5 # 初始化大小

  10. min-idle: 5 # 最小

  11. max-active: 20 # 最大

  12. max-wait: 60000 # 连接超时时间

  13. time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒

  14. min-evictable-idle-time-millis: 300000 # 指定一个空闲连接最少空闲多久后可被清除,单位是毫秒

  15. validationQuery: select 'x'

  16. test-while-idle: true # 当连接空闲时,是否执行连接测试

  17. test-on-borrow: false # 当从连接池借用连接时,是否测试该连接

  18. test-on-return: false # 在连接归还到连接池时是否测试该连接

  19. filters: configwallstat


B、MyBatis配置

通过引入mybatis-spring-boot-starter依赖,可以简单配置MyBatis上手使用。

下面简单分析mybatis-starter的源码以及如何配置MyBatis。

先看mybatis-spring-boot-starter中mybatis-spring-boot-autoconfigure的spring.factories文件。

 
   
   
 
  1. # Auto Configure

  2. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration


可以看到自动注入类是MybatisAutoConfiguration,我们从这个类入手分析可以知道,必须先创建好了数据源后,才会加载MyBatis的sqlSessionFactory。

@EnableConfigurationProperties({MybatisProperties.class})注解指定了配置文件中 prefix = "mybatis" 那部分属性有效,这部分属性值将注入到已创建的SqlSessionFactoryBean中,最后生成SqlSessionFactory对象。

 
   
   
 
  1. @Configuration

  2. //当SqlSessionFactory,SqlSessionFactoryBean存在的情况下加载当前Bean

  3. @ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})

  4. //当指定数据源在容器中只有一个或者有多个但是只指定首选数据源

  5. @ConditionalOnSingleCandidate(DataSource.class)

  6. @EnableConfigurationProperties({MybatisProperties.class})

  7. //当数据源注入到Spring容器后才开始加载当前Bean

  8. @AutoConfigureAfter({DataSourceAutoConfiguration.class})

  9. public class MybatisAutoConfiguration implements InitializingBean {

  10. private final MybatisProperties properties;

  11. @Bean

  12. @ConditionalOnMissingBean

  13. public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {

  14. SqlSessionFactoryBean factory = new SqlSessionFactoryBean();

  15. factory.setDataSource(dataSource);

  16. factory.setVfs(SpringBootVFS.class);

  17. //设置mybatis配置文件所在路径

  18. if (StringUtils.hasText(this.properties.getConfigLocation())) {

  19. factory.setConfigLocation(this.resourceLoader.getResource

  20. (this.properties.getConfigLocation())); }

  21. }

  22. //设置其他MyBatisProperties对象中有的属性略....

  23. return factory.getObject();

  24. }

  25. }


MybatisProperties含有的属性:

 
   
   
 
  1. @ConfigurationProperties(prefix = "mybatis" )

  2. public class MybatisProperties {

  3. public static final String MYBATIS_PREFIX = "mybatis";

  4. private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

  5. private String configLocation;

  6. private String[] mapperLocations;

  7. private String typeAliasesPackage;

  8. private Class<?> typeAliasesSuperType;

  9. private String typeHandlersPackage;

  10. private boolean checkConfigLocation = false;

  11. private ExecutorType executorType;

  12. private Properties configurationProperties;

  13. @NestedConfigurationProperty

  14. private Configuration configuration;

  15. }


C、使用MyBatis

配置文件:

application.yml

 
   
   
 
  1. mybatis:

  2. config-location: classpath:mybatis.xml # mybatis配置文件所在路径

  3. type-aliases-package: com.creditease.permission.model # 所有Entity别名类所在包

  4. mapper-locations: classpath:mybatis/**/*.xml


从上面的MybatisProperties可以看出,MyBatis可以指定一些configuration,比如自定义拦截器pageHelper。

 
   
   
 
  1. mybatis.xml

  2. <?xml version="1.0" encoding="UTF-8" ?>

  3. <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"

  4. "http://mybatis.org/dtd/mybatis-3-config.dtd">

  5. <configuration>

  6. <plugins>

  7. <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>

  8. <plugin interceptor="com.creditease.permission.manager.MybatisInterceptor"></plugin>

  9. </plugins>

  10. </configuration>


在启动类上加入@MapperScan注解:

 
   
   
 
  1. @MapperScan("com.creditease.permission.dao")//mapper类所在目录

  2. public class SsoApplication {

  3. public static void main(String[] args) {

  4. SpringApplication.run(SsoApplication.class, args);

  5. }

  6. }


4)事务

Spring事务有两种处理方式:

  • 编程式

    用TransactionTemplate或者直接使用底层的PlatformTransactionManager将事务代码写在业务代码中。

    优点:可以在代码块中处理事务,比较灵活。

    缺点:对代码具有侵入性。


  • 声明式

    采用@Transactional注解或者基于配置文件方式,在方法前后进行拦截。

    优点:非侵入性不会污染代码。

    缺点:事务只能在方法和类上控制,粒度较小。


A、使用@Transactional注解。

非SpringBoot工程,需要在配置文件中加入配置:

 
   
   
 
  1. <tx:annotation-driven/>


Spring Boot工程可以用@EnableTransactionManagement注解代替上面的配置内容。

B、采用配置文件方式 。

之前的SSO是基于配置的方式,配置代码如下:

 
   
   
 
  1. <aop:config>

  2. <aop:pointcut expression="execution(public * com.creditease.permission.service.impl.*Impl.*(..))" id="pointcut"/>

  3. <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>

  4. </aop:config>

  5. <tx:advice id="txAdvice" transaction-manager="transactionManager">

  6. <tx:attributes>

  7. <tx:method name="query*" propagation="REQUIRED" read-only="true"/>

  8. <tx:method name="find*" propagation="REQUIRED" read-only="true"/>

  9. <tx:method name="save*" propagation="REQUIRED"/>

  10. <tx:method name="delete*" propagation="REQUIRED"/>

  11. <tx:method name="add*" propagation="REQUIRED"/>

  12. <tx:method name="modify*" propagation="REQUIRED"/>

  13. </tx:attributes>

  14. </tx:advice>


改造后的Spring Boot基于Java代码:

 
   
   
 
  1. @Aspect

  2. @Configuration

  3. public class TransactionAdviceConfig {


  4. /**

  5. * 指定切入点

  6. */

  7. private static final String AOP_POINTCUT_EXPRESSION = "execution(public * com.creditease.permission.service.impl.*Impl.*(..))";


  8. @Resource

  9. DruidDataSource dataSource;


  10. /**

  11. * 指定处理事务的PlatformTransactionManager

  12. * @return

  13. */

  14. @Bean

  15. public DataSourceTransactionManager transactionManager() {


  16. return new DataSourceTransactionManager(dataSource);


  17. }


  18. /**

  19. * 指定切入点处理逻辑,执行事务

  20. * @return

  21. */

  22. @Bean

  23. public TransactionInterceptor txAdvice() {


  24. DefaultTransactionAttribute txAttrRequired = new DefaultTransactionAttribute();

  25. txAttrRequired.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);


  26. DefaultTransactionAttribute txAttrRequiredReadonly = new DefaultTransactionAttribute();

  27. txAttrRequiredReadonly.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);

  28. txAttrRequiredReadonly.setReadOnly(true);


  29. NameMatchTransactionAttributeSource source = new NameMatchTransactionAttributeSource();

  30. source.addTransactionalMethod("query*", txAttrRequiredReadonly);

  31. source.addTransactionalMethod("find*", txAttrRequiredReadonly);

  32. source.addTransactionalMethod("save*", txAttrRequired);

  33. source.addTransactionalMethod("delete*", txAttrRequired);

  34. source.addTransactionalMethod("add*", txAttrRequired);

  35. source.addTransactionalMethod("modify*", txAttrRequired);

  36. return new TransactionInterceptor(transactionManager(), source);

  37. }


  38. /**

  39. * Advisor组装配置,将Advice的代码逻辑注入到Pointcut位置

  40. * @return

  41. */

  42. @Bean

  43. public Advisor txAdviceAdvisor() {


  44. AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();

  45. pointcut.setExpression(AOP_POINTCUT_EXPRESSION);

  46. return new DefaultPointcutAdvisor(pointcut, txAdvice());

  47. }


5)全局异常处理

一般编码时有异常我们都会try-catch捕获异常,有时为了区分不同的异常还会一次catch多个异常,大量的try-catch语句,这样使得代码也不够优雅;一个相同的异常处理写多次代码也比较冗余,所以引入全局的异常处理非常必要。

改造前的异常处理配置文件:

 
   
   
 
  1. <!--定义异常处理页面-->

  2. <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

  3. <property name="exceptionMappings">

  4. <props>

  5. <prop key="com.creditease.permissionapi.exception.NopermissionException">/permission/noSecurity</prop>

  6. </props>

  7. </property>

  8. </bean>


使用SimpleMappingExceptionResolver类处理异常,设置自定义异常类型NopermissionException,以及异常发生后的请求路径/permission/noSecurity。

SpringBoot中采用@RestControllerAdvice或者@ControllerAdvice设置全局异常类。这两者区别类似于@Controller和@RestController注解。

SSO中定义了三种全局的异常处理:普通的Exception处理;自定的NopermissionException异常和参数校验异常。

全局异常处理代码如下:

 
   
   
 
  1. @Configuration

  2. @Slf4j

  3. @RestControllerAdvice

  4. public class GlobalExceptionConfig {


  5. //无权限处理

  6. @ExceptionHandler(value = {NopermissionException.class})

  7. public void noPermissionExceptionHandler(HttpServletRequest request, Exception ex, HttpServletResponse response, @Value("${sso.server.prefix}") String domain) throws IOException {

  8. printLog(request,ex);

  9. }


  10. //参数校验处理

  11. @ExceptionHandler(value = {BindException.class})

  12. public ResultBody BindExceptionHandler(BindException bindException){

  13. List<ObjectError> errors = bindException.getBindingResult().getAllErrors();

  14. //这个ResultBody是一个返回结果对象,这里需要返回json,里面包含了状态码和提示信息

  15. return ResultBody.buildFailureResult(errors.get(0).getDefaultMessage());

  16. }


  17. //所有未捕获的异常处理逻辑

  18. @ExceptionHandler(value = {Exception.class})

  19. public ResultBody exceptionHandler(HttpServletRequest request,Exception ex){

  20. printLog(request,ex);

  21. return ResultBody.buildExceptionResult();

  22. }


  23. //将请求参数和异常打印出来,结合@slf4j注解

  24. public void printLog(HttpServletRequest request,Exception ex){

  25. String parameters = JsonHelper.toString(request.getParameterMap());

  26. log.error("url>>>:{},params>>>:{} ,printLog>>>:{}",request.getRequestURL(),parameters,ex);

  27. }


  28. }


@RestControllerAdvice结合@Validation,可以对Bean进行校验,校验不通过会抛出BindException异常。通过注解可以少写if-else代码,判断请求的接口参数是否为空,提高代码的美观性。例如:

 
   
   
 
  1. //常规做法

  2. if(StringUtils.isEmpty(ssoSystem.getSysCode())


  3. //SSO做法

  4. //在Controller请求方法上添加@Valid注解

  5. @RequestMapping(value = "/add", method = RequestMethod.POST)

  6. public ResultBody add(@Valid @RequestBody SsoSystem ssoSystem) {


  7. }


  8. //在需要处理的SsoSystem Bean的属性上加@NotNull注解

  9. @NotNull(message = "系统编号不能为空")

  10. private String sysCode;


当sysCode传入参数为空时,就会抛出BindException被全局的异常处理类,捕获处理返回json格式的参数:

 
   
   
 
  1. {

  2. "resultCode":2,

  3. "resultMsg":"系统编号不能为空",

  4. "resultData":null

  5. }


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)。

  • 必须是数字或字母结尾。


二、前后端分离

一个项目的SpringCloud微服务改造过程


2.1 解决跨域问题

由于是两个不同的应用,必然会有两个不同的端口。不同的端口就会有跨域问题,SSO采用的方式是通过nginx区分来自前后端的请求,反向代理请求对应到不同的服务去。

  • sso.creditease.com对应的是后端的应用服务。

  • sso.creditease.com/web对应的是前端的静态资源应用服务。


2.2 方便联调效率,引入swagger

swagger是后端接口展示的插件,通过修改拦截器代码,mock登录对象免登录,直接访问接口进行前后端的调试。在swagger插件上可以看到具体接口请求路径和参数、参数是否必须、返回值、接口统计信息等。

接口统计信息:

一个项目的SpringCloud微服务改造过程

请求参数和路径:

一个项目的SpringCloud微服务改造过程

返回值:


2.3 跳转接口修改

之前是通过SpringMvc的modeAndview方式跳转的,现在做了两种处理:

  • 改成restful接口的形式,前端控制跳转然后直接获取数据。

  • 直接通过response.sendRedirect跳转页面。


注意:老代码跳转采用的是通过SpringMvc在return的页面路径前加redirect的形式,如:return "redirect:index",这样默认会在return的URL后加jessionID。

2.4 静态资源地址变更可能引发的问题

特别需要注意代码中的相关校验路径的地方。比如在这次改造过程中路径修改会影响以下几个方面。

  • 菜单权限校验的时候,之前人、角色和路径已经绑定了,修改菜单访问路径会导致没权限。

  • 扫码登录的接口判断了refer来源,修改路径会导致请求失败。

  • 之前的sso-dome工程引用了静态资源,修改路径会报404。


本文转载自公众号:宜信技术学院, 。


Kubernetes企业级实战训练营


Kubernetes企业级实战训练营将于2019年8月30日在北京举办,3天时间带你系统掌握Kubernetes,学习效果不好可以继续学习。本次培训包括:Docker基础、容器技术、Docker镜像、数据共享与持久化、Docker三驾马车、Docker实践、Kubernetes基础、Pod基础与进阶、常用对象操作、服务发现、Helm、Kubernetes核心组件原理分析、Kubernetes服务质量保证、调度详解与应用场景、网络、基于Kubernetes的CI/CD、基于Kubernetes的配置管理等, 点击下方图片或者阅读原文链接查看详情。

以上是关于一个项目的SpringCloud微服务改造过程的主要内容,如果未能解决你的问题,请参考以下文章

架构演化的步骤

小团队真的适合引入SpringCloud微服务吗?

微服务架构和SpringCloud实践总结

Springcloud踩坑记---使用feignclient远程调用服务404

基于springcloud的微服务实战

基于springcloud的微服务实战