SpringCloud micro-services transformation process of a project

SSO is a company has existed for a number of years of the project, the back-end using SpringMVC, MyBatis, database use MySQL, use the front display Freemark. This year, the project we had a revolutionary improvement, transformed into SpringCloud architecture, and the separation of the front and rear end, front-end Vue framework.

First, the use of architecture to transform SpringCloud

1.1 Why SpringCloud

The core is SpringCloud SpringBoot, compared to conventional Spring, SpringCloud has the following advantages:

  • Deploying a simple, SpringBoot built Tomcat container, it can be directly translated into a JAR program, run by the java-jar.
  • Coding simple, SpringBoot just need to add a starter-web dependent in pom file, it can help developers quickly launch a web container, very convenient.
  • Configuration is simple, SpringBoot possible to replace the original Spring xml very complex way through simple annotation mode. If I want a normal class management to the Spring, just add @Configuration and @Bean two notes can be.
  • Monitoring is simple, we can introduce spring-boot-start-actuator depend directly using REST way to get the runtime performance parameters of the process, so as to achieve the purpose of monitoring.

1.2 What needs to project a regular part of the transformation

1.2.1 Profile

SSO renovation project before a huge number of configuration files, mainly contains the following sections:

  • Static resource-related
  • data source
  • mybatis Configuration
  • redis Configuration
  • Affairs
  • Interceptor to intercept content
  • Monitors, filters
  • Scan path configuration component

This article focuses on the following sections:

1) static resource handling

SpringMVC, if the mvc:interceptorsURL rule configuration is as follows, it will not intercept static resources.

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

However, if the configuration is:

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

Scheme 1: in web.xml configuration &lt;servlet-name&gt;default&lt;/servlet-name&gt;, with defaultServletthe first processing request such as:

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

Scenario 2: Using &lt;mvc:resources /&gt;label declares a static resource path

<mvc:resources mapping="/resources/js/**" location="/js/" /> 
<mvc:resources mapping="/resources/images/**" location="/images/" /> 
<mvc:resources mapping="/resources/css/**" location="/css/" />

Option 3: Use mvc:default-servlet-handler/labels

SpringBoot Solution: inheritance WebMvcConfigurerAdapter achieve addResourceHandlers method.

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

sso static resource file path as shown:

swagger

2) interceptor

SpringMVC profile content:

Intercept any request and initialization parameters, some do not need to intercept the request, after some requests to access that does not require permission to check the direct release.

<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 added interceptor simply extend WebMvcConfigurerAdapter, and override addInterceptors method can be.

 /*** 拦截器 
 * @param registry
 */ 
 @Override
 public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(permissionInterceptor).
            addPathPatterns("/**");
    super.addInterceptors(registry);
 }

Custom interceptors need to initialize some parameters, and therefore need to be registered before registering interceptors, here we set lazy loading. Login interception-free path, and the path does not require permission of the judge after logging files are written in yml to get through the system environment variables Environment value.

@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) configuration database and mybatis

A, data source configuration

Data source injected three cases:

[I.]

  • Conditions: no primer START druid-spring-boot-starter depends only druid.jar, not specified spring.datasource.type.
  • Results: The data source is injected tomcat data source.
  • Analysis: dependent mybatis-spring-boot-starter project relied tomcat data source, spring-boot-autoconfigure-starter of DataSourceAutoConfiguration automatic injection class will without specifying a data source, determines the path if there is a default four types data source (Hikari, Tomcat, Dbcp, Dbcp2) is one, if there are injected.

[Case 2]

  • Conditions: without introducing druid-spring-boot-starter depends only druid.jar, designated as spring.datasource.type DruidDataSource.
  • Result: The injected DruidDataSource data source, but the profile of druid configuration will not take effect.
  • Analysis: Specifies the data source dependency, spring will automatically injected starter specified data source implant, yml druid specified data source. @ConfigurationProperties not annotated DataSourceProperties processing performance parameter attributes druid portion, only the attribute data processing portion of the source.

[Three] case

  • Conditions: Primer START druid-spring-boot-starter does not depend druid.jar, designated as spring.datasource.type DruidDataSource.
  • Result: The injected DruidDataSource data source, druid configuration profile will take effect.
  • Analytical: druid-spring-boot-starter to automatically configure the data source class created first before DataSourceAutoConfiguration, and @ConfigurationProperties injected DataSourceProperties druid profile contains attributes.

pom.xml dependence:

    <!-- 情况一、二 测试引入的依赖 -->
    <!--<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 configuration:

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 configuration

By introducing mybatis-spring-boot-starter dependent, you can start to use mybatis simple configuration.

The following simple analysis mybatis-starter source code and how to configure mybatis.

Look at mybatis-spring-boot-starter in mybatis-spring-boot-autoconfigure of spring.factories file

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

You can see the class is automatically injected MybatisAutoConfiguration, we start with the analysis can know, you must first created after the data source will be loaded sqlSessionFactory mybatis from this class.

@EnableConfigurationProperties ({MybatisProperties.class}) specified in the configuration file annotation
prefix = "mybatis" attribute is valid portion, which is injected into the part attribute value SqlSessionFactoryBean created, the last generates a SqlSessionFactory.

@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 contain attributes:

@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, using mybatis

  • Profiles:

application.yml

mybatis:
config-location: classpath:mybatis.xml        # mybatis配置文件所在路径
type-aliases-package: com.creditease.permission.model    # 所有Entity别名类所在包
mapper-locations: classpath:mybatis/**/*.xml

As can be seen from the above MybatisProperties, mybatis can specify configuration, such as custom interceptor 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>
  • Join @MapperScan notes on startup class
@MapperScan("com.creditease.permission.dao")//mapper类所在目录
public class SsoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SsoApplication.class, args);
    }
}
4) Transaction

Spring transaction handled in two ways:

  • Programmatic

TransactionTemplate PlatformTransactionManager or directly with the underlying business transaction code written in the code.

Advantages: the transaction may be processed in the code block, more flexible.

Disadvantages: invasive code.

  • Declarative

@Transactional annotation or using file-based configuration mode, before and after the intercept method.

Advantages: noninvasive does not contaminate the code.

Disadvantages: The transaction can only methods and classes in the control, a smaller particle size.

A, the @Transactional comment

Non-SpringBoot engineering, configuration needs to be added in the configuration file:

  <tx:annotation-driven/>

SpringBoot project can @EnableTransactionManagement notes instead of arranging the contents of the above.

B, in the profile mode

Before sso is arranged based, configuration code is as follows:

   <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 After transformation based on Java code:

  @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) global exception handler

Abnormal we will try-catch catch exceptions when general coding, sometimes in order to distinguish between different exceptions will once catch multiple exceptions, a large number of try-catch statement, so that the code is not enough elegance; an identical exception handling code is written multiple times also more redundancy, so the introduction of a global exception handling is very necessary.

Before the transformation exception handling configuration file:

<!--定义异常处理页面-->
<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>

Use SimpleMappingExceptionResolver exception handling, provided NoPermissionException custom exception type, and the requested path after occurrence of abnormality / permission / noSecurity.

SpringBoot used @RestControllerAdvice @ControllerAdvice or set global exception class. This is similar to the difference between the two @Controller and @RestController comment.

Global SSO defines three types of exception handling: Common Exception handling; custom parameter calibrating the NopermissionException anomalies and exceptions.

Global exception handler code is as follows:

@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 combined @Validation, Bean can be verified, by not checking BindException will throw an exception. Can be less if-else code via write annotations, the interface determines whether the requested parameter is empty, to improve the aesthetics of the code. E.g:

    //常规做法
    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;

When sysCode its argument is empty, it will be thrown BindException global exception handling class, capturing the process returns parameter json format:

{
    "resultCode":2,
    "resultMsg":"系统编号不能为空",
    "resultData":null
}

1.3 Considerations

Built-in version 1.3.1 tomcat problems caused by too high

SpringBoot1.5 default embedded tomcat8.5 version, and the original SpringMVC the SSO deployed on tomcat7. tomcat upgrade affect most obvious is the cookie for this transformation.
After tomcat8 cookie checking protocol used is Rfc6265CookieProcessor. The agreement calls for the domain name must follow these rules:

  • Must be 1-9, az, AZ ,., - these characters.
  • Must be numeric or start with a letter (based on .creditease.corp before being given tomcat cookie domain validation abnormal, and finally into a creditease.corp).
  • It must be a number or the end of the letter.

Second, separate front and rear ends

2.1 solve cross-domain problems

Because it is two different applications, there must be two different ports. Will have different port cross-domain problem, SSO embodiment uses a request from the front end through nginx distinguishable reverse proxy request corresponds to a different service to.

  • sso.creditease.com corresponds to the back-end application services.
  • sso.creditease.com/web corresponds to the front of the static resource applications.

2.2 FBI facilitate efficient introduction swagger

swagger is back-end interface to display plug-ins, by modifying the code interceptor, mock objects Login Log-free, direct access to the front and rear end of the debugging interface. On the swagger plugin you can see the path and the specific interface request parameters, parameters are necessary, return values, interface statistics and so on.

  • Interface Statistics
    swagger

  • Path request parameters and

Request Description

  • return value

Back to Results

Jump 2.3 interface modification

Before modeAndview by way SpringMvc jump now done two treatments:

  • Restful interface into the form, then the front end of the control jumps directly to obtain data.
  • Directly through response.sendRedirect jump page.

Note: Old code branches is used in the form of SpringMvc page before adding redirect the path of return, such as: return "redirect: index", this default will add jessionID after the return of the URL.

2.4 Static Resource change of address problems that may arise

Of particular note path where the relevant check code. For example, in the path of the transformation process changes will affect the following aspects.

  • When checking permissions menu, before the people, and the role of the path has been bound, to modify the menu access path will lead to no authority.
  • Scan code login interface judgment refer sources, will modify the path request failed.
  • Before the sso-dome project references static resources, modify the path 404 will be reported.

Author: Huang Lingfeng

Source: CreditEase Institute of Technology

Guess you like

Origin blog.51cto.com/14159827/2426929