⼀个项⽬的SpringCloud微服务改造过程
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规则如下,则不会拦截静态资源。
<mvc:mapping path="/*.do" />
但是如果配置的是:
<mvc:mapping path="/**" />
⽅案1: 在l中配置<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/")//sso静态资源
.addResourceLocations("classpath:/META-INF/resources/")//swagger静态资源
.setCachePeriod(0);//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.Property("lude.path").split(","));
List<String> commonUrls = Arrays.Property("lude.path").split(","));
permissionInterceptor.setCommonUrls(commonUrls);
permissionInterceptor.setExcludeUrls(excludeUrls);
return permissionInterceptor;
}
3)数据库和mybatis配置
A、数据源配置
数据源注⼊的三种情况:
【情况⼀】
条件:不引⼊druid-spring-boot-starter只依赖druid.jar,不指定pe。
结果:注⼊的数据源是tomcat的数据源。
解析:依赖的mybatis-spring-boot-starter⼯程依赖了tomcat的数据源,spring-boot-autoconfigure-starter的DataSourceAutoConfiguration⾃动注⼊类会在不指定数据源的情况下,判断路径中是否存在默认的4种数据源(Hikari,Tomcat,Dbcp,Dbcp2)的其⼀,如果有就注⼊。
【情况⼆】
条件:不引⼊druid-spring-boot-starter只依赖druid.jar ,指定pe为DruidDataSource。
结果:注⼊了DruidDataSource数据源,但配置⽂件中的druid配置不会⽣效。
解析: 指定了依赖的数据源后,spring⾃动注⼊的starter会将指定的数据源注⼊,yml指定了druid数据源。@ConfigurationProperties注解的DataSourceProperties没处理druid部分的性能参数属性,只处理了数据源部分的属性。
【情况三】
条件:引⼊ druid-spring-boot-starter 不依赖druid.jar,指定pe为DruidDataSource。
结果:注⼊了DruidDataSource数据源, 配置⽂件中的druid配置也会⽣效。
解析:druid-spring-boot-starter⾃动配置类会在DataSourceAutoConfiguration之前先创建数据源,并且@ConfigurationProperties注⼊的DataSourceProperties包含了配置⽂件中druid的属性。
<!-- 情况⼀、⼆测试引⼊的依赖 -->
<!--<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&batis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>RELEASE</version>
</dependency>
yml配置:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource            # 当前数据源操作类型
driver-class-name: sql.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⽂件
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\batis.spring.boot.autoconfigure.MybatisAutoConfiguration
可以看到⾃动注⼊类是MybatisAutoConfiguration,我们从这个类⼊⼿分析可以知道,必须先创建好了数据源后,才会加载mybatis的sqlSessionFactory。
@EnableConfigurationProperties({MybatisProperties.class})注解指定了配置⽂件中 prefix = "mybatis" 那部分属性有效,这部分属性值将注⼊到已创建的SqlSessionFactoryBean中,最后⽣成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(ConfigLocation())) {
factory.Resource
(ConfigLocation())); }
}
//设置其他MyBatisProperties对象中有的属性略....
Object();
}
}
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;
}
C、使⽤mybatis
配置⽂件:
mybatis:
config-location: l        # mybatis配置⽂件所在路径
type-aliases-package: del    # 所有Entity别名类所在包
mapper-locations: classpath:mybatis/**/*.xml
从上⾯的MybatisProperties可以看出,mybatis可以指定⼀些configuration,⽐如⾃定义pageHelper。l
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-////DTD Config 3.0//EN"
"/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
<plugin interceptor="ditease.permission.manager.MybatisInterceptor"></plugin>
</plugins>
</configuration>
在启动类上加⼊@MapperScan注解
@MapperScan("ditease.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⼯程,需要在配置⽂件中加⼊配置:
<tx:annotation-driven/>
SpringBoot⼯程可以⽤@EnableTransactionManagement注解代替上⾯的配置内容。
B、采⽤配置⽂件⽅式
之前的sso是基于配置的⽅式,配置代码如下:
<aop:config>
<aop:pointcut expression="execution(public * ditease.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 * ditease.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();
springboot和过滤器
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="ption.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, HttpServletRes
ponse 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 = BindingResult().getAllErrors();
//这个ResultBody是⼀个返回结果对象,这⾥需要返回json,⾥⾯包含了状态码和提⽰信息
return  ResultBody.(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 = ParameterMap());
<("url>>>:{},params>>>:{} ,printLog>>>:{}",RequestURL(),parameters,ex);
}
}
@RestControllerAdvice结合@Validation,可以对Bean进⾏校验,校验不通过会抛出BindException异常。通过注解可以少写if-else代码,判断请求的接⼝参数是否为空,提⾼代码的美观性。例如:
//常规做法
if(StringUtils.SysCode())
//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