SSO是公司一个已经存在了若干年的项目,后端采用SpringMVC、MyBatis,数据库使用MySQL,前端展现使用Freemark。今年,咱们对该项目进行了一次革命性的改进,改形成SpringBoot架构,而且把先后端分离,前端采用Vue框架。css
相比较于传统的Spring,SpringBoot具备如下优势:前端
SSO项目改造前充斥着大量的配置文件,主要包含如下这些部分:java
本文着重介绍如下几个部分:mysql
SpringMVC中,若是mvc:interceptors
配置的URL规则以下,则不会拦截静态资源。nginx
<mvc:mapping path="/*.do" />
可是若是配置的是:git
<mvc:mapping path="/**" />
方案1: 在web.xml中配置<servlet-name>default</servlet-name>
,用defaultServlet
先处理请求如:github
<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 />
标签声明静态资源路径web
<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/
标签redis
SpringBoot解决方案:继承WebMvcConfigurerAdapter实现addResourceHandlers方法。spring
public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/resource/")//sso静态资源 .addResourceLocations("classpath:/META-INF/resources/")//swagger静态资源 .setCachePeriod(0);//0表示不缓存 }
sso静态资源文件路径如图:
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; }
A、数据源配置
数据源注入的三种状况:
【状况一】
【状况二】
【状况三】
pom.xml依赖:
<!-- 状况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</version> </dependency>
yml配置:
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文件
# 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({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含有的属性:
@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
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("com.creditease.permission.dao")//mapper类所在目录 public class SsoApplication { public static void main(String[] args) { SpringApplication.run(SsoApplication.class, args); } }
Spring事务有两种处理方式:
用TransactionTemplate或者直接使用底层的PlatformTransactionManager将事务代码写在业务代码中。
优势:能够在代码块中处理事务,比较灵活。
缺点:对代码具备侵入性。
采用@Transactional注解或者基于配置文件方式,在方法先后进行拦截。
优势:非侵入性不会污染代码。
缺点:事务只能在方法和类上控制,粒度较小。
A、使用@Transactional注解
非SpringBoot工程,须要在配置文件中加入配置:
<tx:annotation-driven/>
SpringBoot工程能够用@EnableTransactionManagement注解代替上面的配置内容。
B、采用配置文件方式
以前的sso是基于配置的方式,配置代码以下:
<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()); }
通常编码时有异常咱们都会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 }
SpringBoot1.5默认使用内嵌tomcat8.5版本,而原来SpringMVC的SSO部署在tomcat7上。tomcat的升级对此次改造影响最明显的就是cookie。 tomcat8后采用的cookie校验协议是Rfc6265CookieProcessor。该协议要求domain的命名必须遵循如下规则:
因为是两个不一样的应用,必然会有两个不一样的端口。不一样的端口就会有跨域问题,SSO采用的方式是经过nginx区分来自先后端的请求,反向代理请求对应到不一样的服务去。
swagger是后端接口展现的插件,经过修改拦截器代码,mock登陆对象免登陆,直接访问接口进行先后端的调试。在swagger插件上能够看到具体接口请求路径和参数、参数是否必须、返回值、接口统计信息等。
接口统计信息
请求参数和路径
以前是经过SpringMvc的modeAndview方式跳转的,如今作了两种处理:
注意:老代码跳转采用的是经过SpringMvc在return的页面路径前加redirect的形式,如:return "redirect:index",这样默认会在return的URL后加jessionID。
特别须要注意代码中的相关校验路径的地方。好比在此次改造过程当中路径修改会影响如下几个方面。
做者: 黄玲峰
来源:宜信技术学院