微服务改造须知

 

 

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

一、使用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先处理请求如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<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 />标签声明静态资源路径

1
2
3
<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方法。

1
2
3
4
5
6
public  void  addResourceHandlers(ResourceHandlerRegistry registry) {
     registry.addResourceHandler( "/**" )
     .addResourceLocations( "classpath:/resource/" ) //sso静态资源
     .addResourceLocations( "classpath:/META-INF/resources/" ) //swagger静态资源
     .setCachePeriod(0); //0表示不缓存
}

  

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

2)拦截器

SpringMVC配置文件内容:

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<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方法即可。

1
2
3
4
5
6
7
8
9
/*** 拦截器
  * @param registry
  */
  @Override
  public  void  addInterceptors(InterceptorRegistry registry) {
     registry.addInterceptor(permissionInterceptor).
             addPathPatterns( "/**" );
     super.addInterceptors(registry);
  }

  

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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的属性。

pom.xml依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 情况一、二 测试引入的依赖 -->
     <!--<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</version>
     </dependency>

  

yml配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Configuration
//当SqlSessionFactory,SqlSessionFactoryBean存在的情况下加载当前Bean
@ConditionalOnClass({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对象中有的属性略....
        return  factory.getObject();
    }
}

  

MybatisProperties含有的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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;
}

  

C、使用mybatis

  • 配置文件:

application.yml

1
2
3
4
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 

1
2
3
4
5
6
7
8
9
<?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注解
1
2
3
4
5
6
@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工程,需要在配置文件中加入配置:

1
<tx:annotation-driven/>

  

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

B、采用配置文件方式 

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@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语句,这样使得代码也不够优雅;一个相同的异常处理写多次代码也比较冗余,所以引入全局的异常处理非常必要。

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

1
2
3
4
5
6
7
8
<!--定义异常处理页面-->
<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异常和参数校验异常。

全局异常处理代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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代码,判断请求的接口参数是否为空,提高代码的美观性。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//常规做法
     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格式的参数:

1
2
3
4
5
{
     "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-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。

 

原文地址:https://www.cnblogs.com/yixinjishu/p/11307121.html

转载于:https://www.cnblogs.com/lyc88/articles/11307841.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值