一個項目的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

相關文章
相關標籤/搜索