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

  SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用MySQL,前端展现使用Freemark。今年,咱们对该项目进行了一次革命性的改进,改形成SpringCloud架构,而且把先后端分离,前端采用Vue框架。
  
  1、使用SpringCloud架构进行改造
  
  1.1 为何使用SpringCloud
  
  SpringCloud的核心是SpringBoot,相比较于传统的Spring,SpringCloud具备如下优势:
  
  部署简单,SpringBoot内置了Tomcat容器,能够将程序直接编译成一个jar,经过java-jar来运行。
  
  编码简单,SpringBoot只须要在pom文件中添加一个starter-web依赖,便可帮助开发者快速启动一个web容器,很是方便。
  
  配置简单,SpringBoot能够经过简单的注解方式来代替原先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中配置<servlet-name>default</servlet-name>,用defaultServlet先处理请求如:
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.jpg</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.png</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.gif</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.ico</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.gif</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.js</url-pattern>
  
  </servlet-mapping>
  
  <servlet-mapping>
  
  <servlet-name>default</servlet-name>
  
  <url-pattern>*.css</url-pattern>
  
  </servlet-mapping>
  
  方案2:使用<mvc:resources />标签声明静态资源路径
  
  <mvc:resources mapping="/resources/js/**" location="/js/" />
  
  <mvc:resources mapping="/resources/images/**" location="/images/" />
  
  <mvc:resources mapping="/resources/css/**" location="/css/" />
  
  方案3:使用mvc:default-servlet-handler/标签
  
  SpringBoot解决方案:继承WebMvcConfigurerAdapter实现addResourceHandlers方法。
  
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
  
  registry.addResourceHandler("/**")
  
  .addResourceLocations("classpath:/resource/"chenghylpt.com)//sso静态资源
  
  .addResourceLocations(kunlunyule.com"classpath:/META-INF/resources/")//swagger静态资源
  
  .setCachePeriod(www.hnawesm.com);www.rhyL158.com//0表示不缓存
  
  }
  
  sso静态资源文件路径如图:
  
  2)拦截器
  
  SpringMVC配置文件内容:
  
  拦截任何请求而且初始化参数,有些请求是不须要拦截的,有的请求登陆后不须要通过权限校验直接放行。
  
  <mvc:interceptors>
  
  <mvc:interceptor>
  
  <mvc:mapping path="/**" />
  
  <bean class="自定义拦截器PermissionInterceptor">
  
  <!-- 未登陆便可访问的地址 -->
  
  <property name="excludeUrls">
  
  <list><value>请求地址<value></list>
  
  </property>
  
  <!-- 只要登陆了就不须要拦截的资源 -->
  
  <property name="LogInExcludeUrls">
  
  <list><value>请求地址<value></list>
  
  </property>
  
  </bean>
  
  </mvc:interceptor>
  
  </mvc:interceptors>
  
  SpringBoot中添加拦截器只需继承WebMvcConfigurerAdapter,并重写addInterceptors方法便可。
  
  /*** 拦截器
  
  * @param registry
  
  */
  
  @Override
  
  public void addInterceptors(InterceptorRegistry registry) {
  
  registry.addInterceptor(permissionInterceptor).
  
  addPathPatterns("/**");
  
  super.addInterceptors(registry);
  
  }
  
  自定义的拦截器须要初始化一些参数,所以须要在注册拦截器以前注册,这里咱们设置为懒加载。免登陆拦截的路径,以及登陆后不须要判断权限的路径都写在yml文件了,经过系统环境变量Environment获取值
  
  @Autowired
  
  @Lazy
  
  private PermissionInterceptor permissionInterceptor;
  
  @Autowired
  
  private Environment environment;
  
  /**
  
  *
  
  */
  
  @Bean
  
  public PermissionInterceptor permissionInterceptor() {
  
  PermissionInterceptor permissionInterceptor = new PermissionInterceptor();
  
  List<String> excludeUrls = Arrays.asList(environment.getProperty("intercept.exclude.path").split(","));
  
  List<String> commonUrls = Arrays.asList(environment.getProperty("intercept.login.exclude.path").split(","));
  
  permissionInterceptor.setCommonUrls(commonUrls);
  
  permissionInterceptor.setExcludeUrls(excludeUrls);
  
  return permissionInterceptor;
  
  }
  
  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的属性。
  
  <!-- 状况1、二 测试引入的依赖 -->
  
  <!--<dependency>-->
  
  <!--<groupId>com.alibaba</groupId>-->
  
  <!--<artifactId>druid</artifactId>-->
  
  <!--<version>${druid.version}</version>-->
  
  <!--</dependency>-->
  
  <dependency>
  
  <groupId>com.alibaba</groupId>
  
  <artifactId>druid-spring-boot-starter</artifactId>
  
  <version>1.1.10</version>
  
  </dependency>
  
  <dependency>
  
  <groupId>org.mybatis.spring.boot</groupId>
  
  <artifactId>mybatis-spring-boot-starter</artifactId>
  
  <version>RELEASE<www.xcdeyiju.com /version>
  
  </dependency>
  
  spring:
  
  datasource:
  
  type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操做类型
  
  driver-class-name: com.mysql.jdbc.Driver            # mysql驱动包
  
  url: jdbc:mysql://yourURL        # 数据库名称
  
  username: yourusername
  
  password: yourpassword
  
  druid:
  
  initial-size: 5  # 初始化大小
  
  min-idle: 5  # 最小
  
  max-active: 20  # 最大
  
  max-wait: 60000  # 链接超时时间
  
  time-between-eviction-runs-millis: 60000  # 配置间隔多久才进行一次检测,检测须要关闭的空闲链接,单位是毫秒
  
  min-evictable-idle-time-millis: 300000  # 指定一个空闲链接最少空闲多久后可被清除,单位是毫秒
  
  validationQuery: select 'x'
  
  test-while-idle: true  # 当链接空闲时,是否执行链接测试
  
  test-on-borrow: false  # 当从链接池借用链接时,是否测试该链接
  
  test-on-return: false  # 在链接归还到链接池时是否测试该链接
  
  filters: config,wall,stat
  
  B、MyBatis配置
  
  经过引入mybatis-spring-boot-starter依赖,能够简单配置mybatis上手使用。
  
  下面简单分析mybatis-starter的源码以及如何配置mybatis。
  
  先看mybatis-spring-boot-starter中mybatis-spring-boot-autoconfigure的spring.factories文件
  
  1
  
  2
  
  # Auto Configure
  
  org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
  
  能够看到自动注入类是MybatisAutoConfiguration,咱们从这个类入手分析能够知道,必须先建立好了数据源后,才会加载mybatis的sqlSessionFactory。
  
  @EnableConfigurationProperties({MybatisProperties.class})注解指定了配置文件中 prefix = "mybatis" 那部分属性有效,这部分属性值将注入到已建立的SqlSessionFactoryBean中,最后生成SqlSessionFactory对象。
  
  @Configuration
  
  //当SqlSessionFactory,SqlSessionFactoryBean存在的状况下加载当前Bean
  
  @ConditionalOnClass(www.yacuangyl.com {SqlSessionFactory.class, SqlSessionFactoryBean.class})
  
  //当指定数据源在容器中只有一个或者有多个可是只指定首选数据源
  
  @ConditionalOnSingleCandidate(DataSource.class)
  
  @EnableConfigurationProperties({MybatisProperties.class})
  
  //当数据源注入到Spring容器后才开始加载当前Bean
  
  @AutoConfigureAfter({DataSourceAutoConfiguration.class})
  
  public class MybatisAutoConfiguration implements InitializingBean {
  
  private final MybatisProperties properties;
  
  @Bean
  
  @ConditionalOnMissingBean
  
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
  
  SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
  
  factory.setDataSource(dataSource);
  
  factory.setVfs(SpringBootVFS.class);
  
  //设置mybatis配置文件所在路径
  
  if (StringUtils.hasText(this.properties.getConfigLocation())) {
  
  factory.setConfigLocation(this.resourceLoader.getResource
  
  (this.properties.getConfigLocation())); }
  
  }
  
  //设置其余MyBatisProperties对象中有的属性略....
  
  @ConfigurationProperties(prefix = "mybatis" )
  
  public class MybatisProperties {
  
  public static final String MYBATIS_PREFIX = "mybatis";
  
  private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
  
  private String configLocation;
  
  private String[] mapperLocations;
  
  private String typeAliasesPackage;
  
  private Class<?> typeAliasesSuperType;
  
  private String typeHandlersPackage;
  
  private boolean checkConfigLocation = false;
  
  private ExecutorType executorType;
  
  private Properties configurationProperties;
  
  @NestedConfigurationProperty
  
  private Configuration configuration;
  
  mybatis:
  
  config-location: classpath:mybatis.xml        # mybatis配置文件所在路径
  
  type-aliases-package: com.creditease.permission.model    # 全部Entity别名类所在包
  
  mapper-locations: classpath:mybatis/**/*.xml
  
  从上面的MybatisProperties能够看出,mybatis能够指定一些configuration,好比自定义拦截器pageHelper。
  
  mybatis.xml
  
  <?xml version="1.0" encoding="UTF-8" ?>
  
  <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
  
  <configuration>
  
  <plugins>
  
  <plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
  
  <plugin interceptor="com.creditease.permission.manager.MybatisInterceptor"></plugin>
  
  </plugins>
  
  </configuration>
  
  在启动类上加入@MapperScan注解
  
  @MapperScan("com.creditease.permission.dao")//mapper类所在目录
  
  public class SsoApplication {
  
  public static void main(String[] args) {
  
  SpringApplication.run(SsoApplication.class, args);
  
  }
  
  }
  
  4)事务
  
  Spring事务有两种处理方式:
  
  编程式
  
  用TransactionTemplate或者直接使用底层的PlatformTransactionManager将事务代码写在业务代码中。
  
  优势:能够在代码块中处理事务,比较灵活。
  
  缺点:对代码具备侵入性。
  
  声明式
  
  采用@Transactional注解或者基于配置文件方式,在方法先后进行拦截。
  
  优势:非侵入性不会污染代码。
  
  缺点:事务只能在方法和类上控制,粒度较小。
  
  A、使用@Transactional注解
  
  非SpringBoot工程,须要在配置文件中加入配置:
  
  <aop:config>
  
  <aop:pointcut expression="execution(public * com.creditease.permission.service.impl.*Impl.*(..))" id="pointcut"/>
  
  <aop:advisor advice-ref="txAdvice" pointcut-ref="pointcut"/>
  
  </aop:config>
  
  <tx:advice id="txAdvice" transaction-manager="transactionManager">
  
  <tx:attributes>
  
  <tx:method name="query*" propagation="REQUIRED" read-only="true"/>
  
  <tx:method name="find*" propagation="REQUIRED" read-only="true"/>
  
  <tx:method name="save*" propagation="REQUIRED"/>
  
  <tx:method name="delete*" propagation="REQUIRED"/>
  
  <tx:method name="add*" propagation="REQUIRED"/>
  
  <tx:method name="modify*" propagation="REQUIRED"/>
  
  </tx:attributes>
  
  </tx:advice>
  
  改造后的SpringBoot基于Java代码:
  
  @Aspect
  
  @Configuration
  
  public class TransactionAdviceConfig {
  
  /**
  
  * 指定切入点
  
  */
  
  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());
  
  }
  
  5)全局异常处理
  
  通常编码时有异常咱们都会try-catch捕获异常,有时为了区分不一样的异常还会一次catch多个异常,大量的try-catch语句,这样使得代码也不够优雅;一个相同的异常处理写屡次代码也比较冗余,因此引入全局的异常处理很是必要。
  
  改造前的异常处理配置文件:
  
  <!--常处理页面-->
  
  <bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
  
  <property name="exceptionMappings">
  
  <props>
  
  <prop key="com.creditease.permissionapi.exception.NopermissionException">/permission/noSecurity</prop>
  
  </props>
  
  </property>
  
  </bean>
  
  使用SimpleMappingExceptionResolver类处理异常,设置自定义异常类型NopermissionException,以及异常发生后的请求路径/permission/noSecurity。
  
  SpringBoot中采用@RestControllerAdvice或者@ControllerAdvice设置全局异常类。这二者区别相似于@Controller和@RestController注解。
  
  SSO中定义了三种全局的异常处理:普通的Exception处理;自定的NopermissionException异常和参数校验异常。
  
  @Configuration
  
  @Slf4j
  
  @RestControllerAdvice
  
  public class GlobalExceptionConfig {
  
  //无权限处理
  
  @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<ObjectError> errors = bindException.getBindingResult().getAllErrors();
  
  //这个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);
  
  }
  
  }
  
  @RestControllerAdvice结合@Validation,能够对Bean进行校验,校验不经过会抛出BindException异常。经过注解能够少写if-else代码,判断请求的接口参数是否为空,提升代码的美观性。例如:
  
  //常规作法
  
  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;
  
  当sysCode传入参数为空时,就会抛出BindException被全局的异常处理类,捕获处理返回json格式的参数:
  
  "resultCode":2,
  
  "resultMsg":"系统编号不能为空",
  
  "resultData":null
  
  }
  
  1.3 注意事项
  
  1.3.1 内置tomcat版本过高引起的问题
  
  SpringBoot1.5默认使用内嵌tomcat8.5版本,而原来SpringMVC的SSO部署在tomcat7上。tomcat的升级对此次改造影响最明显的就是cookie。 tomcat8后采用的cookie校验协议是Rfc6265CookieProcessor。该协议要求domain的命名必须遵循如下规则:
  
  必须是1-九、a-z、A-Z、. 、- 这几个字符组成。
  
  必须是数字或字母开头 (以前是以.creditease.corp 会报错tomcat cookie domain validation异常,最后改为了 creditease.corp)。
  
  必须是数字或字母结尾。
  
  2、先后端分离
  
  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。css

相关文章
相关标签/搜索