admin管理员组文章数量:1122850
文章目录
- 关键技术
- 第一篇 后端实现篇
- 1. 搭建开发环境
- 2. 集成Swagger文档
- 3. 集成MyBatis框架
- 4. 集成Druid数据源
- 5. 跨域解决方案
- 6. 业务功能实现
- 6.1 工程结构规划
- 6.1.1 mango-admin
- 6.1.2 mango-common
- 6.1.3 mango-core
- 6.1.4 mango-pom
- 6.1.5 打包测试
- 6.2 业务代码封装
- 6.2.1 通用CURD接口
- 6.2.2 分页请求封装
- 6.2.3 分页结果封装
- 6.2.4 分页助手封装
- 6.2.5 HTTP结果封装
- 6.3 MyBatis分页查询
- 6.3.1 添加依赖
- 6.3.2 添加配置
- 6.3.3 分页代码
- 6.3.4 接口测试
- 6.4 业务功能开发
- 6.4.1 编写DAO接口
- 6.4.2 编写映射文件
- 6.4.3 编写服务接口
- 6.4.4 编写服务实现
- 6.4.5 编写控制器
- 6.5 业务接口汇总
- 6.6 导出Excel报表
- 6.6.1 添加依赖
- 6.6.2 编写服务接口
- 6.6.3 编写服务实现
- 6.6.4 编写控制器
- 6.6.5 工具类代码
- 6.6.6 接口测试
- 7. 登录流程实现
- 7.1 登录验证码
- 7.2 Spring Security
- 7.2.1 添加依赖
- 7.2.2 添加配置
- 7.2.3 登录认证过滤器
- 7.2.4 身份验证组件
- 7.2.5 认证信息查询
- 7.2.6 添加权限注解
- 7.2.7 Swagger添加令牌函数
- 7.3 登录接口实现
- 7.4 Spring Security执行流程剖析
- 8. 数据备份还原
- 8.1 新建工程
- 8.2 添加依赖
- 8.3 添加配置
- 8.4 自定义Banner
- 8.5 启动类
- 8.6 跨域配置
- 8.7 Swagger配置
- 8.8 数据源属性
- 8.9 备份还原接口
- 8.10 备份还原实现
- 8.11 备份还原逻辑
- 8.12 备份还原控制器
- 8.12.1 数据备份接口
- 8.12.2 数据还原接口
- 8.12.3 查找备份接口
- 8.12.4 删除备份接口
- 9. 系统服务监控
- 9.1 新建工程
- 9.2 添加依赖
- 9.3 添加配置
- 9.4 自定义Banner
- 9.5 启动类
- 9.6 监控客户端
- 9.7 启动服务端
- 10. 注册中心(Consul)
- 10.1 什么是Consul
- 10.2 Consul安装
- 10.3 monitor改造
- 10.3.1 添加依赖
- 10.3.2 配置文件
- 10.3.3 启动类
- 10.3.4 测试效果
- 10.4 backup改造
- 10.4.1 添加依赖
- 10.4.2 配置文件
- 10.4.3 启动类
- 10.4.4 测试效果
- 10.5 admin改造
- 10.5.1 添加依赖
- 10.5.2 配置文件
- 10.5.3 启动类
- 10.5.4 测试效果
- 11. 服务消费(Ribbon、Feign)
- 11.1 技术背景
- 11.2 服务提供者
- 11.2.1 新建项目
- 11.2.2 配置文件
- 11.2.3 启动类
- 11.2.4 自定义Banner
- 11.2.5 添加控制器
- 11.3 服务消费者
- 11.3.1 新建项目
- 11.3.2 添加配置
- 11.3.3 启动类、
- 11.3.4 自定义Banner
- 11.3.5 服务消费
- 11.3.6 负载均衡器(Ribbon)
- 11.3.7 修改启动类
- 11.3.8 添加服务
- 11.3.9 页面测试
- 11.3.10 负载策略
- 11.4 服务消费(Feign)
- 11.4.1 添加依赖
- 11.4.2 启动类
- 11.4.3 添加Feign接口
- 11.4.4 添加控制器
- 11.4.5 页面测试
- 12. 服务熔断(Hystrix、Turbine)
- 12.1 雪崩效应
- 12.2 熔断器(CircuitBreaker)
- 12.3 Hystrix特性
- 12.3.1 断路器机制%
- 12.3.2 fallback
- 12.3.3 资源隔离
- 12.4 Feign Hystrix
- 12.4.1 修改配置
- 12.4.2 创建回调类
- 12.4.3 页面测试
- 12.5 Hystrix Dashboard
- 12.5.1 添加依赖
- 12.5.2 启动类
- 12.5.3 自定义Banner
- 12.5.4 配置文件
- 12.5.5 配置监控路径
- 12.5.6 页面测试
- 12.6 Spring Cloud Turbine
- 12.6.1 添加依赖
- 12.6.2 启动类
- 12.6.3 配置文件
- 12.6.4 测试效果
- 13. 服务网关(Zuul)
- 13.1 技术背景
- 13.2 Spring Cloud Zuul
- 13.3 Zuul工作机制
- 13.3.1 过滤器机制
- 13.3.2 过滤器的生命周期
- 13.3.3 禁用指定的Filter
- 13.4 实现案例
- 13.4.1 新建工程
- 13.4.2 添加依赖
- 13.4.3 启动类
- 13.4.4 配置文件
- 13.4.5 页面测试
- 13.4.6 配置接口前缀
- 13.4.7 默认路由规则
- 13.4.8 路由熔断
- 13.4.9 自定义Filter
- 14. 链路追踪(Sleuth、Zipkin)
- 14.1 技术背景
- 14.2 ZipKin
- 14.3 Spring Cloud Sleuth
- 14.4 实现案例
- 14.4.1 下载镜像
- 14.4.2 编写启动文件
- 14参考
- 15. 配置中心(Config、Bus)
- 15.1 技术背景
- 15.2 Spring Cloud Config
- 15.3 实现案例
- 15.3.1 准备配置文件
- 15.3.2 服务端实现
- 15.3.3 客户端实现
- 15.3.4 Refresh机制
- 15.3.5 Spring Cloud Bus
前后端源码以及markdown笔记及转化的pdf文件均已上传网盘:
链接:https://pan.baidu/s/1BVXTBAJfAYGS01n8fP2GvA 提取码:ziyi
关键技术
-
Spring Boot
官方教程:https://spring.io/projects/spring-boot/
-
Spring Cloud
官方教程:https://spring.io/projects/spring-cloud
博客教程:https://wwwblogs/xifengxiaoma/
-
Spring Security
官方教程:https://spring.io/projects/spring-security
博客教程:https://wwwblogs/xifengxiaoma/
-
MaBatis
官方教程:http://www.mybatis/mybatis-3/zh/
博客教程:https://www.w3cschool/mybatis/
-
Vue.js
官方教程:https://cn.vuejs/v2/guide/
博客教程:https://www.runoob/vue2/vue-tutorial.html
-
Element
官网教程:http://element.eleme.io/#/zh-CN
第一篇 后端实现篇
1. 搭建开发环境
-
登录spring initializer生成spring boot项目模板,保存到本地,网站地址为https://start.spring.io/。
-
将maven项目导入到eclipse,修改application配置文件为yml后缀,清理掉不需要的mvnw、mvnw.cmd和test目录下的测试文件。
-
编译打包运行,右击pom.xml,选择run as→maven install。
-
启动应用,右击DemoApplication,选择run as→Java application。
-
修改启动端口。(默认为8080),在application.yml可修改启动端口:(下例修改为8001,注意port前不能使用tab,否则会报错)
server: port: 8001
-
自定义Banner
spring boot启动后会在控制台输出banner信息,默认是显示spring字样,如下:
如果要定制自己的Banner只需要在resources下放置一个banner.txt文件,输入自己的banner字符即可。
Banner字符可通过类似以下网站生成:
- http://patorjk/software/taag
- http://wwwwork-science.de/ascii/
复制字符到banner.txt并附上应用和版本信息,重启应用,如下:
-
接口测试
新建一个controller包,并在其下创建一个hellocontroller类,添加一个hello接口:
在浏览器中访问,如下:
2. 集成Swagger文档
使用Swagger集成文档有以下几个优势:
- 功能丰富:支持多种注解,自动生成接口文档界面,支持在界面中测试API接口功能。
- 及时更新:开发过程中花一点写注释的时间,就可以及时更新API文档、
- 整合简单:通过添加pom依赖和简单配置,内嵌于应用中就可以同时发布API接口文档界面,不需要部署独立服务。
官网:https://swagger.io/
官方文档:https://swagger.io/docs/
-
添加依赖
<!--swagger--> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>
-
配置类
添加config包,并在其下添加Swagger配置类SwaggerConfig.java:
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket createRestApi(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select() .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build(); } private ApiInfo apiInfo(){ return new ApiInfoBuilder().build(); } }
-
页面测试
启动应用,在浏览器中访问http://localhost:8001/swagger-ui.html#/,就可以看到Swagger的接口文档页面了:
还可以选择接口进行测试,单击右侧的try it out→execute,发现接口成功返回“hello mango!”:
3. 集成MyBatis框架
MyBatis是一款优秀的持久层框架,支持定制化SQL、存储过程以及高级映射。MyBatis可以使用简单的XML或注解来配置和映射原生信息,并将接口和Java的POJOs映射成数据库中的记录。
中文官网:http://www.mybatis/
参考教程:https://www.w3cschool/mybatis/
-
添加依赖
<!-- mybatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
-
添加配置
-
添加MyBatis配置
添加MyBatis配置类,配置相关扫描路径,包括DAO、Model、XML映射文件的扫描,在config包下新建一个MyBatis配置类MybatisConfig.java:
@Configuration @MapperScan("com.louis.mango.**.dao") // 扫描DAO public class MybatisConfig { @Autowired private DataSource dataSource; @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean(); sessionFactory.setDataSource(dataSource); sessionFactory.setTypeAliasesPackage("com.louis.mango.**.model"); // 扫描Model PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); sessionFactory.setMapperLocations(resolver.getResources("classpath*:**/sqlmap/*.xml")); // 扫描映射文件 return sessionFactory.getObject(); } }
-
添加数据源配置
打开应用配置文件,添加MySQL数据源连接信息。
application.yml:
spring: datasource: driverClassName: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root password: admin123
-
修改启动类
给启动类的@SpringBootApplication注解配置包扫描,表示在应用启动时自动扫描com.louis.mango包下的内容,当然Spring Boot默认会扫描启动类包及子包的组件,所以如果启动类就是放在com.louis.mango下,那么默认配置其实就是com.louis.mango了。
@SpringBootApplication(scanBasePackages={"com.louis.mango"}) public class MangoApplication { public static void main(String[] args) { SpringApplication.run(MangoApplication.class, args); } }
-
-
生成MyBatis模块
手动编写MyBatis的Model、DAO、XML映射文件比较繁琐,通常都会通过一些生成工具来生成。MyBatis官方也提供了生成工具(MyBatis Generator),另外还有一些基于官方基础改进的第三方工具,比如MyBatis Plus就是国内提供的一款非常优秀的开源工具。
- MyBatis Generator官网:http://www.mybatis/generator/index.html
- MyBatis Generator教程:https://blog.csdn/testcs_dn/article/details/77881776
- MyBatis Plus官网:http://mp.baomidou/#/
- MyBatis Plus教程:http://mp.baomidou/#/quick-start
生成好代码之后,分别将Domain、DAO、XML映射文件复制到相应的包里。
打开生成的Mapper,我们可以看到默认生成的一些增删改查方法。
-
编写服务接口
在Mapper中新增一个findAll方法,用于查询所有的用户信息:
SysUserMapper.java:
public interface SysUserMapper { int deleteByPrimaryKey(Long id); int insert(SysUser record); int insertSelective(SysUser record); SysUser selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(SysUser record); int updateByPrimaryKey(SysUser record); /** * 查询全部 * @return */ List<SysUser> findAll(); }
在映射文件中添加一个查询方法,编写findAll的查询语句:
SysUserMapper.xml:
<select id="findAll" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from sys_user </select>
然后编写用户管理接口,包含一个findAll方法:
SysUserService.java
public interface SysUserService { /** * 查找所有用户 * @return */ List<SysUser> findAll(); }
接着编写用户管理实现类,调用SysUserMapper方法完成查询操作:
SysUserServiceImpl.java:
@Service public class SysUserServiceImpl implements SysUserService { @Autowired private SysUserMapper sysUserMapper; @Override public List<SysUser> findAll() { return sysUserMapper.findAll(); } }
然后编写用户管理RESTful接口,返回JSON数据格式,提供外部调用。被@RestController注解的接口控制器默认使用JSON格式交互,返回JSON结果:
SysUserController.java:
@RestController @RequestMapping("user") public class SysUserController { @Autowired private SysUserService sysUserService; @GetMapping(value="/findAll") public Object findAll() { return sysUserService.findAll(); } }
-
配置打包资源
虽然代码编写已经完成,但是此时启动运行还是会有问题的,因为在编译打包的时候,我们的XML映射文件是不在默认打包范围内的,所以需要修改打包资源配置。
修改pom.xml,在build标签加入形式如下的resource标签的打包配置,这样打包时就会把MyBatis映射文件也复制过去了:
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> <!-- 打包时拷贝MyBatis的映射文件 --> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/sqlmap/*.xml</include> </includes> <filtering>false</filtering> </resource> <resource> <directory>src/main/resources</directory> <includes> <include>**/*.*</include> </includes> <filtering>true</filtering> </resource> </resources> </build>
-
编译运行测试
编译并启动应用,访问http://localhost:8001/user/findAll,可以看到查询接口成功返回了所有的用户信息,如下:
也可以用Swagger进行测试:
4. 集成Druid数据源
数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新再建立一个,释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。通过数据库连接池能明显提高对数据库操作的性能。
-
Druid介绍
Druid是阿里开源的一个JDBC应用组件,其主要包括三个部分:
- DruidDriver:代理Driver,能够提供基于Filter-Chain模式的插件体系。
- DruidDataSource:高效可管理的数据库连接池。
- SQLParser:实用的SQL语法分析。
通过Druid连接池中间件,我们可以实现:
- 监控数据库访问性能。Druid内置了一个功能强大的StatFilter插件,能够详细统计SQL的执行性能,对于线上分析数据库访问性能有所帮助。
- 替换传统的DBCP和C3P0连接池中间件。Druid提供了一个高效、功能强大、可扩展性好的数据库连接池。
- 数据库密码加密。直接把数据库密码写在配置文件中,容易导致安全问题。DruidDriver和DruidDataSource都支持PasswordCallback。
- SQL执行日志。Druid提供了不同的LogFilter,能够支持Common-Logging、Log4j和JdkLog,可以按需选取相应的LogFilter,监控你应用的数据库访问情况。
- 扩展JDBC。如果对JDBC层有编程的需求,可以通过Druid提供的Filter-Chain机制很方便地编写JDBC层的扩展插件。
更多详细信息可参考官方文档,https://github/alibaba/druid/wiki。
-
添加依赖
在pom文件中添加Druid相关的maven依赖:
<!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency>
druid-spring-boot-starter是阿里官方提供的Spring Boot插件,用于帮助在Spring Boot项目中轻松集成Druid数据库连接池和监控。
更多资料可以参考:
- Druid:https://github/alibaba/druid。
- Druid Sping Starter: https://github/alibaba/druid/tree/master/druid-spring-boot-starter。
-
添加配置
修改配置文件,把原有的数据源配置替换成Druid数据源并配置数据源相关参数。
application.yml:
spring: datasource: name: druidDataSource type: com.alibaba.druid.pool.DruidDataSource druid: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8 username: root password: admin123 filters: stat,wall,log4j,config max-active: 100 initial-size: 1 max-wait: 60000 min-idle: 1 time-between-eviction-runs-millis: 60000 min-evictable-idle-time-millis: 300000 validation-query: select 'x' test-while-idle: true test-on-borrow: false test-on-return: false pool-prepared-statements: true max-open-prepared-statements: 50 max-pool-prepared-statement-per-connection-size: 20
参数说明:
- max-active:最大连接数。
- initial-size:初始化大小。
- min-idle:最小连接数。
- max-wait:获取连接等待超时时间。
- time-between-eviction-runs-millis:间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒。
- min-evictable-idle-time-millis:一个连接在池中最小生存的时间,单位是毫秒。
- filters: stat,wall,log4j,config:配置监控统计拦截的filters,去掉后监控界面SQL无法进行统计,wall用于防火墙。
Druid Spring Starter简化了很多配置,如果默认配置不能满足需求,可以自定义配置,更多配置参考如下:
Druid Spring Starter:https://github/alibaba/druid/tree/master/druid-spring-boot-starter。
-
配置Servlet和Filter
在config包下新建一个DruidConfig配置类,主要是注入属性和配置连接池相关的配置,如黑白名单、监控管理后台登录账户密码等,内容如下:
DruidConfig.java:
@Configuration @EnableConfigurationProperties({DruidDataSourceProperties.class}) public class DruidConfig { @Autowired private DruidDataSourceProperties properties; @Bean @ConditionalOnMissingBean public DataSource druidDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(properties.getDriverClassName()); druidDataSource.setUrl(properties.getUrl()); druidDataSource.setUsername(properties.getUsername()); druidDataSource.setPassword(properties.getPassword()); druidDataSource.setInitialSize(properties.getInitialSize()); druidDataSource.setMinIdle(properties.getMinIdle()); druidDataSource.setMaxActive(properties.getMaxActive()); druidDataSource.setMaxWait(properties.getMaxWait()); druidDataSource.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis()); druidDataSource.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis()); druidDataSource.setValidationQuery(properties.getValidationQuery()); druidDataSource.setTestWhileIdle(properties.isTestWhileIdle()); druidDataSource.setTestOnBorrow(properties.isTestOnBorrow()); druidDataSource.setTestOnReturn(properties.isTestOnReturn()); druidDataSource.setPoolPreparedStatements(properties.isPoolPreparedStatements()); druidDataSource.setMaxPoolPreparedStatementPerConnectionSize(properties.getMaxPoolPreparedStatementPerConnectionSize()); try { druidDataSource.setFilters(properties.getFilters()); druidDataSource.init(); } catch (SQLException e) { e.printStackTrace(); } return druidDataSource; } /** * 注册Servlet信息, 配置监控视图 * * @return */ @Bean @ConditionalOnMissingBean public ServletRegistrationBean<Servlet> druidServlet() { ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<Servlet>(new StatViewServlet(), "/druid/*"); //白名单: // servletRegistrationBean.addInitParameter("allow","127.0.0.1,139.196.87.48"); //IP黑名单 (存在共同时,deny优先于allow) : 如果满足deny的话提示:Sorry, you are not permitted to view this page. servletRegistrationBean.addInitParameter("deny","192.168.1.119"); //登录查看信息的账号密码, 用于登录Druid监控后台 servletRegistrationBean.addInitParameter("loginUsername", "admin"); servletRegistrationBean.addInitParameter("loginPassword", "admin"); //是否能够重置数据. servletRegistrationBean.addInitParameter("resetEnable", "true"); return servletRegistrationBean; } /** * 注册Filter信息, 监控拦截器 * * @return */ @Bean @ConditionalOnMissingBean public FilterRegistrationBean<Filter> filterRegistrationBean() { FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }
代码说明:
- @EnableConfigurationProperties注解用于导入上一步Druid的配置信息。
- public ServletRegistrationBean druidServlet()相当于Web Servlet配置。
- public FilterRegistrationBean filterRegistrationBean()相当于Web Filter配置。
-
编译运行
添加log4j依赖:
<!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
添加log4j配置:在resources目录下,新建一个log4j参数配置文件:
log4j.properties:
### set log levels ### log4j.rootLogger = INFO,DEBUG, console, infoFile, errorFile ,debugfile,mail LocationInfo=true log4j.appender.console = org.apache.log4j.ConsoleAppender log4j.appender.console.Target = System.out log4j.appender.console.layout = org.apache.log4j.PatternLayout log4j.appender.console.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n log4j.appender.infoFile = org.apache.log4j.DailyRollingFileAppender log4j.appender.infoFile.Threshold = INFO log4j.appender.infoFile.File = C:/logs/log log4j.appender.infoFile.DatePattern = '.'yyyy-MM-dd'.log' log4j.appender.infoFile.Append=true log4j.appender.infoFile.layout = org.apache.log4j.PatternLayout log4j.appender.infoFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n log4j.appender.errorFile = org.apache.log4j.DailyRollingFileAppender log4j.appender.errorFile.Threshold = ERROR log4j.appender.errorFile.File = C:/logs/error log4j.appender.errorFile.DatePattern = '.'yyyy-MM-dd'.log' log4j.appender.errorFile.Append=true log4j.appender.errorFile.layout = org.apache.log4j.PatternLayout log4j.appender.errorFile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n log4j.appender.debugfile = org.apache.log4j.DailyRollingFileAppender log4j.appender.debugfile.Threshold = DEBUG log4j.appender.debugfile.File = C:/logs/debug log4j.appender.debugfile.DatePattern = '.'yyyy-MM-dd'.log' log4j.appender.debugfile.Append=true log4j.appender.debugfile.layout = org.apache.log4j.PatternLayout log4j.appender.debugfile.layout.ConversionPattern =[%d{yyyy-MM-dd HH:mm:ss,SSS}]-[%p]:%m %x %n
配置完成后,编译启动。
-
查看监控
启动应用,访问http://localhost:8001/druid/login.html,进入Druid监控后台页面:
登录之后,Druid后台的管理首页如下所示:
数据源页显示连接数据源的相关信息:
访问http://localhost:8001/user/findAll。接口调用成功之后可以看到SQL监控的执行记录,可以查看和分析执行的SQL性能,方便进行数据库性能优化:
5. 跨域解决方案
如果一个请求地址里面的协议、域名和端口号都相同,就属于同源。
依据浏览器同源策略,非同源脚本不可操作其他源下面的对象,想要操作其他源下的对象就需要跨域。在同源策略的限制下,非同源的网站之间不能发生AJAX请求。如果需要,可通过降域或其他技术实现。
-
CORS技术
CORS可以在不破坏既有规则的基础下,通过后端服务器实现CORS接口,从而实现跨域通信。CORS将请求分为两类:简单请求和非简单请求,分别对跨域通信提供了支持。
-
简单请求
在CORS出现前,发生HTTP请求时在头信息中不能包含任何自定义字段,且HTTP信息不超过以下几个字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type。
一个简单请求的例子:
GET /test HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate, sdch, br Origin: http://www.test Host: www.test
对于简单请求,CORS的策略是请求时在请求头中增加一个Origin字段,服务器收到请求后根据该字段判断是否允许该请求访问。如果允许,就在HTTP头信息中增加Access-Control-Allow-Origin字段,并返回正确的结果。如果不允许,就不在HTTP头信息中添加Access-Control-Allow-Origin字段。
除了上面提到的Access-Control-Allow-Origin,还有几个字段用于描述CORS返回结果。Access-Control-Allow-Credentials:可选,用户是否可以发送、处理cookie。Access-Control-Expose-Headers:可选,可以让用户拿到的字段。有几个字段无论设置与否都可以拿到的,包括Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。
-
非简单请求
对于非简单请求的跨源请求,浏览器会在真实请求发出前增加一次OPTION请求,称为预检请求(preflight request)。预检请求将真实请求的信息,包括请求方法、自定义头字段、源信息添加到HTTP头信息字段中,询问服务器是否允许这样的操作。
例如一个GET请求:
OPTIONS /test HTTP/1.1 Origin: http://www.test Access-Control-Request-Method: GET Access-Control-Request-Headers: X-Custom-Header Host: www.test
与CORS相关的字段有:请求使用的HTTP方法Access-Control-Request-Method;请求中包含的自定义头字段Access-Control-Request-Headers。
服务器收到请求时,需要分别对Origin、Access-Control-Request-Method、Access-Control-Request-Headers进行验证,验证通过后会在返回HTTP头信息中添加:
Access-Control-Allow-Origin: http://www.test Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: X-Custom-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 1728000
它们的含义分别是:Access-Control-Allow-Methods(真实请求允许的方法)、Access-Control-Allow-Headers(服务器允许使用的字段)、Access-Control-Allow-Credentials(是否允许用户发送、处理cookie)、Access-Control-Max-Age(预检请求的有效期,单位为秒。有效期内,不会重复发送预检请求)。
当预检请求通过后,浏览器才会发送真实请求到服务器。这样就实现了跨域资源的请求访问。
-
-
CORS实现
CORS的代码实现比较简单,主要是要理解CORS实现跨域的原理和方式。在config包下新建一个CORS配置类,实现WebMvcConfigurer接口。
CorsConfig.java:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // 允许跨域访问的路径 .allowedOrigins("*") // 允许跨域访问的源 .allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") // 允许请求方法 .maxAge(168000) // 预检间隔时间 .allowedHeaders("*") // 允许头部设置 .allowCredentials(true); // 是否发送cookie } }
这样每当客户端发送请求的时候,都会在头部附上跨域信息,就可以支持跨域访问了。
6. 业务功能实现
这一章节开始实现各个业务功能接口,包括用户管理、机构管理、角色管理、菜单管理、字典管理、系统配置、操作日志、登录日志等业务功能的实现。
6.1 工程结构规划
我们要采用的是微服务架构,虽然我们现在只有一个工程,但随着项目越来越大,代码的可重用性和可维护性就会变得越来越难,所以尽早对工程结构进行合理的规划,对项目于的前期开发、后期的扩展和维护都是非常重要的。
经过重新规划,我们的工程结构如下:
- mango-common:公共代码模块,主要放置一些工具类。
- mango-core:核心业务代码模块,主要封装公共业务模块。
- mango-admin:后台管理模块,包含用户、角色、菜单管理等。
- mango-pom:聚合模块,仅为简化打包,一键执行打包所有模块。
6.1.1 mango-admin
将原先mango工程更名为mango-admin,遵循以下步骤进行工程重构:
- 关闭应用,在Eclipse上选择删除mango工程,注意不要勾选删除磁盘。
- 找到工程所在位置,修改mango工程名为mango-admin。
- 编辑pom.xml,将需要替换的mango字符替换为mango-admin。
- 右击Eclipse导航栏,选择import→exist maven project重新导入maven工程。
- 重构包结构,将基础包重构为com.louis.mango.admin,以区分不同工程的包。
- 由于重构了包路径,但是XML映射文件的内容无法同步更新,要将所有的MyBatis的XML映射文件出现的Mapper和Model的包路径修改为正确的路径。可以通过将"com.louis.mango"全部替换为"com.louis.mango.admin"进行统一修改。
- 将MangoApplication改名为MangoAdminApplication,编译启动应用,服务访问正常就启动成功了。
6.1.2 mango-common
新建一个空的maven工程,除了一个pom.xml没有其他内容,后续放置一些工具方法和常量。
6.1.3 mango-core
新建一个空的maven工程,后续放置一些公共核心业务代码封装,如HTTP交互格式封装、业务基类封装和分页工具封装等。
6.1.4 mango-pom
为了方便统一打包,新建一个mango-pom工程。这个工程依赖所有模块,负责统一进行打包(不然编译的时候需要逐个编译,工程一多很是麻烦),但因我们采用的是微服务架构,每个工程模块使用的依赖版本可能都是不一样的,所以这里的mango-pom与所有模块不存在实质性的父子模块关系,也不由mango-pom进行统一版本和依赖管理,只是为了便利打包。
6.1.5 打包测试
下面进行统一打包测试,我们最终所要的效果是:只要在mango-pom下的pom.xml运行打包就可以编译打包所有模块。现在各个子模块都没有编译过,模块间的依赖也还没有加,所以第一次还需要遵循以下步骤进行操作:
-
右击mango-common下的pom.xml,执行Maven Install编译打包。
-
在mango-core下的pom.xml内添加mango-common为dependency依赖,然后执行编译打包命令。
<dependency> <groupId>com.louis</groupId> <artifactId>mango-common</artifactId> <version>1.0.0</version> </dependency>
-
在mango-admin下的pom.xml内添加,mango-core为dependency依赖,然后执行编译打包命令。
<dependency> <groupId>com.louis</groupId> <artifactId>mango-core</artifactId> <version>1.0.0</version> </dependency>
-
在mango-pom下的pom.xml内添加以上所有模块的modules依赖,然后执行编译打包命令。
<modules> <module>../mango-admin</module> <module>../mango-common</module> <module>../mango-core</module> </modules>
注:如果工程出现红叉,可以尝试右击工程Maven→Update Project进行解决。
以后只要对mango-pom下的pom.xml执行命令,就可以统一打包所有模块了。如果控制台输出信息如下所示的打包信息,就表示打包成功了。
6.2 业务代码封装
为了统一业务代码接口、保持代码整洁、提升代码性能,这里对一些通用的代码进行了统一封装,封装内容如下所示:
6.2.1 通用CURD接口
CurdService是对通用增删改查接口的封装,统一定义了包含保存、删除、批量删除、根据ID查询和分页查询方法,一般的业务Service接口会继承此接口,提供基础增删改查服务,这几个接口能满足大部分基础CRUD业务的需求,封装详情参见代码注释。
CurdService.java:
public interface CurdService<T> {
/**
* 保存操作
* @param record
* @return
*/
int save(T record);
/**
* 删除操作
* @param record
* @return
*/
int delete(T record);
/**
* 批量删除操作
* @param entities
*/
int delete(List<T> records);
/**
* 根据ID查询
* @param id
* @return
*/
T findById(Long id);
/**
* 分页查询
* 这里统一封装了分页请求和结果,避免直接引入具体框架的分页对象, 如MyBatis或JPA的分页对象
* 从而避免因为替换ORM框架而导致服务层、控制层的分页接口也需要变动的情况,替换ORM框架也不会
* 影响服务层以上的分页接口,起到了解耦的作用
* @param pageRequest 自定义,统一分页查询请求
* @return PageResult 自定义,统一分页查询结果
*/
PageResult findPage(PageRequest pageRequest);
}
6.2.2 分页请求封装
对分页请求的参数做了统一封装,传入分页查询的页码和数量即可。
PageRequest.java:
public class PageRequest {
/**
* 当前页码
*/
private int pageNum = 1;
/**
* 每页数量
*/
private int pageSize = 10;
/**
* 查询参数
*/
private Map<String, Object> params = new HashMap<>();
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public Map<String, Object> getParams() {
return params;
}
public void setParams(Map<String, Object> params) {
this.params = params;
}
public Object getParam(String key) {
return getParams().get(key);
}
}
6.2.3 分页结果封装
对分页查询的结果进行了统一封装,结果返回业务数据和分页数据。
PageResult.java:
public class PageResult {
/**
* 当前页码
*/
private int pageNum;
/**
* 每页数量
*/
private int pageSize;
/**
* 记录总数
*/
private long totalSize;
/**
* 页码总数
*/
private int totalPages;
/**
* 分页数据
*/
private List<?> content;
public int getPageNum() {
return pageNum;
}
public void setPageNum(int pageNum) {
this.pageNum = pageNum;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public long getTotalSize() {
return totalSize;
}
public void setTotalSize(long totalSize) {
this.totalSize = totalSize;
}
public int getTotalPages() {
return totalPages;
}
public void setTotalPages(int totalPages) {
this.totalPages = totalPages;
}
public List<?> getContent() {
return content;
}
public void setContent(List<?> content) {
this.content = content;
}
}
6.2.4 分页助手封装
对MyBatis的分页查询业务代码进行统一的封装,通过分页助手可以极大简化Service查询业务的编写。
MybatisPageHelper.java:
public class MybatisPageHelper {
public static final String findPage = "findPage";
/**
* 分页查询, 约定查询方法名为 “findPage”
* @param pageRequest 分页请求
* @param mapper Dao对象,MyBatis的 Mapper
* @param args 方法参数
* @return
*/
public static PageResult findPage(PageRequest pageRequest, Object mapper) {
return findPage(pageRequest, mapper, findPage);
}
/**
* 调用分页插件进行分页查询
* @param pageRequest 分页请求
* @param mapper Dao对象,MyBatis的 Mapper
* @param queryMethodName 要分页的查询方法名
* @param args 方法参数
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static PageResult findPage(PageRequest pageRequest, Object mapper, String queryMethodName, Object... args) {
// 设置分页参数
int pageNum = pageRequest.getPageNum();
int pageSize = pageRequest.getPageSize();
PageHelper.startPage(pageNum, pageSize);
// 利用反射调用查询方法
Object result = ReflectionUtils.invoke(mapper, queryMethodName, args);
return getPageResult(pageRequest, new PageInfo((List) result));
}
/**
* 将分页信息封装到统一的接口
* @param pageRequest
* @param page
* @return
*/
private static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
PageResult pageResult = new PageResult();
pageResult.setPageNum(pageInfo.getPageNum());
pageResult.setPageSize(pageInfo.getPageSize());
pageResult.setTotalSize(pageInfo.getTotal());
pageResult.setTotalPages(pageInfo.getPages());
pageResult.setContent(pageInfo.getList());
return pageResult;
}
}
6.2.5 HTTP结果封装
对接口调用返回结果进行了统一的封装,方便前端或者移动端对返回结果进行统一的处理。
HttpResult.java:
public class HttpResult {
private int code = 200;
private String msg;
private Object data;
public static HttpResult error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static HttpResult error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static HttpResult error(int code, String msg) {
HttpResult r = new HttpResult();
r.setCode(code);
r.setMsg(msg);
return r;
}
public static HttpResult ok(String msg) {
HttpResult r = new HttpResult();
r.setMsg(msg);
return r;
}
public static HttpResult ok(Object data) {
HttpResult r = new HttpResult();
r.setData(data);
return r;
}
public static HttpResult ok() {
return new HttpResult();
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
}
6.3 MyBatis分页查询
使用MyBatis时,最头疼的就是分页,需要先写一个查询count的select语句,再写一个真正分页查询的语句,当查询条件多了之后,就会发现真不想花双倍的时间写count和select。幸好我们有pagehelper分页插件。pagehelper是一个强大实用的MyBatis分页插件,可以帮助我们快速实现分页功能。
6.3.1 添加依赖
在mango-core下的pom.xml文件内添加分页插件依赖包,因为mango-admin依赖mango-core模块,所以mango-admin模块也能获取分页插件依赖。
<!-- pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
6.3.2 添加配置
在mango-admin配置文件内添加分页插件配置
application.yml:
pagehelper:
helper-dialect: mysql
reasonable: true
support-methods-arguments: true
params: count=countSql
6.3.3 分页代码
首先我们在DAO层添加一个分页查询方法:
SysUserMapper.java:
List<SysUser> findPage();
给SysUserMapper.xml添加查询方法,这是一个普通的查找全部记录的查询语句,并不需要写分页SQL,分页插件会拦截查询请求,并读取前台传来的分页查询参数重新生成分页查询语句。
SysUserMapper.xml:
<select id="findPage" resultMap="BaseResultMap">
select u.*, (select d.name from sys_dept d where d.id = u.dept_id) deptName from sys_user u
</select>
服务层调用DAO层完成分页查询,让SysUserService继承CurdService接口。
SysUserService.java:
/**
* 查找所有用户
* @return
*/
List<SysUser>findAll();
在实现类中编写分页查询业务类实现,我们可以看到,经过对分页查询业务的封装,普通分页查询非常简单,只需调用MyBatisPageHelper.findPage(pageRequest,sysUserMapper)一行代码即可完成分页查询功能.
SysUserServiceImpl.java:
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserMapper sysUserMapper;
@Override
public PageResult findPage(PageRequest pageRequest) {
return MyBatisPageHelper.findPage(pageRequest,sysUserMapper)
}
}
编写分页查询接口,简单调用Service的查询接口。
SysUserController.java:
@RestController
@RequestMapping("user")
public class SysUserController {
@Autowired
private SysUserService sysUserService;
@PostMapping(value="/findPage")
public HttpResult findPage(@RequestBody PageRequest pageRequest) {
return HttpResult.ok(sysUserService.findPage(pageRequest));
}
}
6.3.4 接口测试
通过swagger接口测试,结果如下:
6.4 业务功能开发
业务功能的开发基本都是各种增删改查业务的编写,大多数是重复性工作,业务编写也没有多少技术要点,这里就拿机构管理的开发作为基础CURD的开发范例。
首先需要事先规划一下,根据需求设计好需要的接口,比如字典管理除了通用的保存、删除、分页查询接口外,还需要一个根据标签名称查询记录的查询方法。
6.4.1 编写DAO接口
打开DAO接口,添加findPage、findPageByLabel和findByLable三个接口:
SysDictMapper.java:
public interface SysDictMapper {
List<SysDict> findPage();
List<SysDict> findPageByLabel(@Param(value="label") String label);
List<SysDict> findByLable(@Param(value="label") String label);
}
6.4.2 编写映射文件
打开映射文件,编写三个查询方法:findPage、findPageByLabel和findByLable。
SysDictMapper.xml:
<select id="findPage" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_dict
</select>
<select id="findPageByLabel" parameterType="java.lang.String" resultMap="BaseResultMap">
<bind name="pattern" value="'%' + _parameter.label + '%'" />
select
<include refid="Base_Column_List" />
from sys_dict
where label like #{pattern}
</select>
<select id="findByLable" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from sys_dict
where label = #{label,jdbcType=VARCHAR}
</select>
6.4.3 编写服务接口
新建一个字典接口并继承通用业务接口CurdService,额外添加一个findByLable接口。
SysDictService.java:
public interface SysDictService extends CurdService<SysDict> {
/**
* 根据名称查询
* @param lable
* @return
*/
List<SysDict> findByLable(String lable);
}
6.4.4 编写服务实现
新建一个实现类并实现SysDictService,调用DAO实现相应的业务功能。
SysDictServiceImpl.java:
@Service
public class SysDictServiceImpl implements SysDictService {
@Autowired
private SysDictMapper sysDictMapper;
@Override
public int save(SysDict record) {
if(record.getId() == null || record.getId() == 0) {
return sysDictMapper.insertSelective(record);
}
return sysDictMapper.updateByPrimaryKeySelective(record);
}
@Override
public int delete(SysDict record) {
return sysDictMapper.deleteByPrimaryKey(record.getId());
}
@Override
public int delete(List<SysDict> records) {
for(SysDict record:records) {
delete(record);
}
return 1;
}
@Override
public SysDict findById(Long id) {
return sysDictMapper.selectByPrimaryKey(id);
}
@Override
public PageResult findPage(PageRequest pageRequest) {
Object label = pageRequest.getParam("label");
if(label != null) {
return MybatisPageHelper.findPage(pageRequest, sysDictMapper, "findPageByLabel", label);
}
return MybatisPageHelper.findPage(pageRequest, sysDictMapper);
}
@Override
public List<SysDict> findByLable(String lable) {
return sysDictMapper.findByLable(lable);
}
}
6.4.5 编写控制器
新建一个字典管理控制器,注入Service并调用Service方法实现接口:
SysDictController.java:
@RestController
@RequestMapping("dict")
public class SysDictController {
@Autowired
private SysDictService sysDictService;
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysDict record) {
return HttpResult.ok(sysDictService.save(record));
}
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysDict> records) {
return HttpResult.ok(sysDictService.delete(records));
}
@PostMapping(value="/findPage")
public HttpResult findPage(@RequestBody PageRequest pageRequest) {
return HttpResult.ok(sysDictService.findPage(pageRequest));
}
@GetMapping(value="/findByLable")
public HttpResult findByLable(@RequestParam String lable) {
return HttpResult.ok(sysDictService.findByLable(lable));
}
}
其他业务功能还有诸如用户管理、角色管理、机构管理、菜单管理、系统日志等业务同理。
6.5 业务接口汇总
略。看源码吧,太多了。
6.6 导出Excel报表
在实际项目中,报表导出是非常普遍的需求,特别是Excel报表,对数据的汇总和传递都是非常便利,Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读写的功能。这里我们将使用POI实现用户信息的Excel报表作为范例进行讲解。
官网地址:http://poi.apache/
相关教程:https://www.yiibai/apache_poi/
6.6.1 添加依赖
在mango0common下的pom文件中添加POI的相关依赖包:
<!-- poi -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.0.1</version>
</dependency>
6.6.2 编写服务接口
在用户管理接口中添加一个导出用户信息Excel报表的方法,采用分页查询的方式,可以传入要导出数据的范围,如需导出全部,把页数调至很大即可,同时因为调用的是分页查询方法查询数据,所以同样支持传入过滤字段进行数据过滤。
SysUserService.java:
/**
* 生成用户信息Excel文件
* @param pageRequest 要导出的分页查询参数
* @return
*/
File createUserExcelFile(PageRequest pageRequest);
6.6.3 编写服务实现
在用户管理服务实现类中编写实现代码,生成Excel文件。
SysUserServiceImpl.java:
@Override
public File createUserExcelFile(PageRequest pageRequest) {
PageResult pageResult = findPage(pageRequest);
return createUserExcelFile(pageResult.getContent());
}
public static File createUserExcelFile(List<?> records) {
if (records == null) {
records = new ArrayList<>();
}
Workbook workbook = new XSSFWorkbook();
Sheet sheet = workbook.createSheet();
Row row0 = sheet.createRow(0);
int columnIndex = 0;
row0.createCell(columnIndex).setCellValue("No");
row0.createCell(++columnIndex).setCellValue("ID");
row0.createCell(++columnIndex).setCellValue("用户名");
row0.createCell(++columnIndex).setCellValue("昵称");
row0.createCell(++columnIndex).setCellValue("机构");
row0.createCell(++columnIndex).setCellValue("角色");
row0.createCell(++columnIndex).setCellValue("邮箱");
row0.createCell(++columnIndex).setCellValue("手机号");
row0.createCell(++columnIndex).setCellValue("状态");
row0.createCell(++columnIndex).setCellValue("头像");
row0.createCell(++columnIndex).setCellValue("创建人");
row0.createCell(++columnIndex).setCellValue("创建时间");
row0.createCell(++columnIndex).setCellValue("最后更新人");
row0.createCell(++columnIndex).setCellValue("最后更新时间");
for (int i = 0; i < records.size(); i++) {
SysUser user = (SysUser) records.get(i);
Row row = sheet.createRow(i + 1);
for (int j = 0; j < columnIndex + 1; j++) {
row.createCell(j);
}
columnIndex = 0;
row.getCell(columnIndex).setCellValue(i + 1);
row.getCell(++columnIndex).setCellValue(user.getId());
row.getCell(++columnIndex).setCellValue(user.getName());
row.getCell(++columnIndex).setCellValue(user.getNickName());
row.getCell(++columnIndex).setCellValue(user.getDeptName());
row.getCell(++columnIndex).setCellValue(user.getRoleNames());
row.getCell(++columnIndex).setCellValue(user.getEmail());
row.getCell(++columnIndex).setCellValue(user.getStatus());
row.getCell(++columnIndex).setCellValue(user.getAvatar());
row.getCell(++columnIndex).setCellValue(user.getCreateBy());
row.getCell(++columnIndex).setCellValue(DateTimeUtils.getDateTime(user.getCreateTime()));
row.getCell(++columnIndex).setCellValue(user.getLastUpdateBy());
row.getCell(++columnIndex).setCellValue(DateTimeUtils.getDateTime(user.getLastUpdateTime()));
}
return PoiUtils.createExcelFile(workbook, "download_user");
}
6.6.4 编写控制器
在用户管理控制器类中添加一个接口,并调用Service获取File,最终通过文件操作工具类将File下载到本地。
SysUserController.java:
@PostMapping(value="/exportExcelUser")
public void exportExcelUser(@RequestBody PageRequest pageRequest, HttpServletResponse res) {
File file = sysUserService.createUserExcelFile(pageRequest);
FileUtils.downloadFile(res, file, file.getName());
}
6.6.5 工具类代码
为了简化代码,前面代码的实现封装了一些工具类。
-
PoiUtils
在编写服务实现的时候我们通过PoiUtils中的createUserExcelFile方法生成Excel文件:
public class PoiUtils { /** * 生成Excel文件 * @param workbook * @param fileName * @return */ public static File createExcelFile(Workbook workbook, String fileName) { OutputStream stream = null; File file = null; try { file = File.createTempFile(fileName, ".xlsx"); stream = new FileOutputStream(file.getAbsoluteFile()); workbook.write(stream); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } finally { IOUtils.closeQuietly(workbook); IOUtils.closeQuietly(stream); } return file; } }
-
FileUtils
在编写导出接口时我们通过FileUtils中的downloadFile将Excel文件下载到本地:
public class FileUtils { /** * 下载文件 * @param response * @param file * @param newFileName */ public static void downloadFile(HttpServletResponse response, File file, String newFileName) { try { response.setHeader("Content-Disposition", "attachment; filename=" + new String(newFileName.getBytes("ISO-8859-1"), "UTF-8")); BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream()); InputStream is = new FileInputStream(file.getAbsolutePath()); BufferedInputStream bis = new BufferedInputStream(is); int length = 0; byte[] temp = new byte[1 * 1024 * 10]; while ((length = bis.read(temp)) != -1) { bos.write(temp, 0, length); } bos.flush(); bis.close(); bos.close(); is.close(); } catch (Exception e) { e.printStackTrace(); } } }
6.6.6 接口测试
编译启动应用,访问http://localhost:8001/swagger-ui.html#/,进入Swagger接口测试页。
输入分页查询信息,指定要导出的用户数据范围,单击Execute按钮发送请求。
7. 登录流程实现
用户登录流程是后台管理系统必备的功能,接下来我们将实现用户登录流程。在这个过程中我们将利用kaptcha实现登录验证码,利用Spring Security进行安全控制。
7.1 登录验证码
-
添加依赖
在mango-admin下的pom文件添加kaptcha依赖包:
<!-- kaptcha --> <dependency> <groupId>com.github.axet</groupId> <artifactId>kaptcha</artifactId> <version>0.0.9</version> </dependency>
-
添加配置
在config包下创建一个kaptcha配置类,配置验证码的一些生成属性:
KaptchaConfig.java:
/** * 验证码配置 */ @Configuration public class KaptchaConfig { @Bean public DefaultKaptcha producer() { Properties properties = new Properties(); properties.put("kaptcha.border", "no"); properties.put("kaptcha.textproducer.font.color", "black"); properties.put("kaptcha.textproducer.char.space", "5"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } }
-
生成代码
新建一个控制器,提供系统登录相关的API,在其中添加验证码生成接口。
SysLoginController.java:
@RestController public class SysLoginController { @Autowired private Producer producer; @Autowired private SysUserService sysUserService; @Autowired private AuthenticationManager authenticationManager; @GetMapping("captcha.jpg") public void captcha(HttpServletResponse response, HttpServletRequest request) throws ServletException, IOException { response.setHeader("Cache-Control", "no-store, no-cache"); response.setContentType("image/jpeg"); // 生成文字验证码 String text = producer.createText(); // 生成图片验证码 BufferedImage image = producer.createImage(text); // 保存到验证码到 session request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, text); ServletOutputStream out = response.getOutputStream(); ImageIO.write(image, "jpg", out); IOUtils.closeQuietly(out); } /** * 登录接口 */ @PostMapping(value = "/login") public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException { String username = loginBean.getAccount(); String password = loginBean.getPassword(); String captcha = loginBean.getCaptcha(); // 从session中获取之前保存的验证码跟前台传来的验证码进行匹配 Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY); if(kaptcha == null){ return HttpResult.error("验证码已失效"); } if(!captcha.equals(kaptcha)){ return HttpResult.error("验证码不正确"); } // 用户信息 SysUser user = sysUserService.findByName(username); // 账号不存在、密码错误 if (user == null) { return HttpResult.error("账号不存在"); } if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) { return HttpResult.error("密码不正确"); } // 账号锁定 if (user.getStatus() == 0) { return HttpResult.error("账号已被锁定,请联系管理员"); } // 系统登录认证 JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager); return HttpResult.ok(token); } }
-
接口测试
编译启动应用在Swagger测试页进行测试:
7.2 Spring Security
在Web应用开发中,安全一直是非常重要的一个方面。Spring Security基于Spring框架,提供了一套Web应用安全性的完整解决方案。JWT(JSON Web Token)是当前比较主流的Token令牌生成方案,非常适合作为登录和授权认证的凭证。这里我们使用Spring Security并结合JWT实现用户认证(Authentication)和用户授权(Authorization)两个主要部分的安全内容。
- JWT官网:https://jwt.io/introduction/
- Spring Security官网:https://spring.io/projects/spring-security
- Spring Security教程:https://www.w3cschool/springsecurity/
7.2.1 添加依赖
在mango-admin下的pom文件中添加Spring Security和JWT依赖包。
<!-- spring security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
7.2.2 添加配置
在config包下新建一个Spring Security的配置类WebSecurityConfig,主要是进行一些安全相关的配置,比如权限URL匹配策略、认证过滤器配置、定制身份验证组件、开启权限认证注解等。
WebSecurityConfig.java:
@Configuration
@EnableWebSecurity // 开启Spring Security
@EnableGlobalMethodSecurity(prePostEnabled = true) // 开启权限注解,如:@PreAuthorize注解
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
// 使用自定义身份验证组件
auth.authenticationProvider(new JwtAuthenticationProvider(userDetailsService));
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 禁用 csrf, 由于使用的是JWT,我们这里不需要csrf
http.cors().and().csrf().disable()
.authorizeRequests()
// 跨域预检请求
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
// web jars
.antMatchers("/webjars/**").permitAll()
// 查看SQL监控(druid)
.antMatchers("/druid/**").permitAll()
// 首页和登录页面
.antMatchers("/").permitAll()
.antMatchers("/login").permitAll()
// swagger
.antMatchers("/swagger-ui.html").permitAll()
.antMatchers("/swagger-resources/**").permitAll()
.antMatchers("/v2/api-docs").permitAll()
.antMatchers("/webjars/springfox-swagger-ui/**").permitAll()
// 验证码
.antMatchers("/captcha.jpg**").permitAll()
// 服务监控
.antMatchers("/actuator/**").permitAll()
// 其他所有请求需要身份认证
.anyRequest().authenticated();
// 退出登录处理器
http.logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler());
// token验证过滤器
http.addFilterBefore(new JwtAuthenticationFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
}
7.2.3 登录认证过滤器
登录认证过滤器负责登录认证时检查并生产令牌保存到上下文,接口权限认证过程时,系统从上下文获取令牌校验接口访问权限,新建一个security包,在其下创建JwtAuthenticationFilter并继承BasicAuthenticationFilter,覆写其中的doFilterInternal方法进行Token校验。
JwtAuthenticationFilter.java:
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取token, 并检查登录状态
SecurityUtils.checkAuthentication(request);
chain.doFilter(request, response);
}
}
这里我们把验证逻辑抽取到了SecurityUtils的checkAuthentication方法中,checkAuthentication通过JwtTokenUtils的方法获取认证信息并保存到Spring Security上下文。
SecurityUtils.java:
/**
* 获取令牌进行认证
* @param request
*/
public static void checkAuthentication(HttpServletRequest request) {
// 获取令牌并根据令牌获取登录认证信息
Authentication authentication = JwtTokenUtils.getAuthenticationeFromToken(request);
// 设置登录认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
}
JwtTokenUtils的getAuthenticationeFromToken方法获取并校验请求携带的令牌。
JwtTokenUtils.java:
/**
* 根据请求令牌获取登录认证信息
* @param token 令牌
* @return 用户名
*/
public static Authentication getAuthenticationeFromToken(HttpServletRequest request) {
Authentication authentication = null;
// 获取请求携带的令牌
String token = JwtTokenUtils.getToken(request);
if(token != null) {
// 请求令牌不能为空
if(SecurityUtils.getAuthentication() == null) {
// 上下文中Authentication为空
Claims claims = getClaimsFromToken(token);
if(claims == null) {
return null;
}
String username = claims.getSubject();
if(username == null) {
return null;
}
if(isTokenExpired(token)) {
return null;
}
Object authors = claims.get(AUTHORITIES);
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
if (authors != null && authors instanceof List) {
for (Object object : (List) authors) {
authorities.add(new GrantedAuthorityImpl((String) ((Map) object).get("authority")));
}
}
authentication = new JwtAuthenticatioToken(username, null, authorities, token);
} else {
if(validateToken(token, SecurityUtils.getUsername())) {
// 如果上下文中Authentication非空,且请求令牌合法,直接返回当前登录认证信息
authentication = SecurityUtils.getAuthentication();
}
}
}
return authentication;
}
JwtTokenUtils的getToken尝试从请求头中获取请求携带的令牌,默认从请求头中的"Authorization"参数以"Bearer"开头的信息为令牌信息,若为空则尝试从"Token"参数获取。
JwtTokenUtils.java:
/**
* 获取请求token
* @param request
* @return
*/
public static String getToken(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String tokenHead = "Bearer ";
if(token == null) {
token = request.getHeader("token");
} else if(token.contains(tokenHead)){
token = token.substring(tokenHead.length());
}
if("".equals(token)) {
token = null;
}
return token;
}
7.2.4 身份验证组件
Spring Security的登录验证是由ProviderManager负责的,ProviderManager在实际验证的适合又会通过调用AuthenticationProvider的authenticate方法进行认证。数据库类型的默认实现方案是DaoAuthenticationProvider。我们这里通过继承DaoAuthenticationProvider定制默认的登录认证逻辑,在Security包下新建验证器JwtAuthenticationProvider并继承DaoAuthenticationProvider,覆盖实现additionalAuthenticationChecks方法进行密码匹配,我们这里没有使用默认的密码认证器(我们使用盐salt来对密码进行加密,默认密码验证其没有加盐),所以在这里定制了自己的密码校验逻辑。也可以直接覆写authenticate方法来完成更大范围的登录认证需求定制。
JwtAuthenticationProvider.java:
public class JwtAuthenticationProvider extends DaoAuthenticationProvider {
public JwtAuthenticationProvider(UserDetailsService userDetailsService) {
setUserDetailsService(userDetailsService);
}
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
String salt = ((JwtUserDetails) userDetails).getSalt();
// 覆写密码验证逻辑
if (!new PasswordEncoder(salt).matches(userDetails.getPassword(), presentedPassword)) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
7.2.5 认证信息查询
我们上面提到的登录认证默认是通过DaoAuthenticationProvider来完成登录认证的,而我们知道登录验证器在进行时肯定要从数据库获取用户信息进行匹配的,而这个获取用户信息的任务是通过Spring Security的UserDetailsService组件完成的。
在security包下新建一个UserDetailsServiceImpl并实现UserDetailsService接口,覆写其中的方法loadUserByUsername,查询用户的密码信息和权限信息并封装在UserDetails的实现类对象,作为结果JwtUserDetails返回给DaoAuthenticationProvider做进一步处理。
UserDetailsServiceImpl.java:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = sysUserService.findByName(username);
if (user == null) {
throw new UsernameNotFoundException("该用户不存在");
}
// 用户权限列表,根据用户拥有的权限标识与如 @PreAuthorize("hasAuthority('sys:menu:view')") 标注的接口对比,决定是否可以调用接口
Set<String> permissions = sysUserService.findPermissions(user.getName());
List<GrantedAuthority> grantedAuthorities = permissions.stream().map(GrantedAuthorityImpl::new).collect(Collectors.toList());
return new JwtUserDetails(user.getName(), user.getPassword(), user.getSalt(), grantedAuthorities);
}
}
JwtUserDetails是对认证信息的封装,实现Spring Security,提供UserDetails接口,主要包含用户名、密码、加密盐和权限等信息。
JwtUserDetails.java:
public class JwtUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private String username;
private String password;
private String salt;
private Collection<? extends GrantedAuthority> authorities;
JwtUserDetails(String username, String password, String salt, Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.salt = salt;
this.authorities = authorities;
}
@Override
public String getUsername() {
return username;
}
@JsonIgnore
@Override
public String getPassword() {
return password;
}
public String getSalt() {
return salt;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return true;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return true;
}
}
GrantedAuthorityImpl实现Spring Security的GrantedAuthority接口,是对权限的封装,内部包含一个字符串类型的权限标识authority,对应菜单表的perms字段的权限字符串,比如用户管理的增删改查权限标志sys:user:view、sys:user:add、sys:user:edit、sys:user:delete。
GrantedAuthorityImpl.java:
public class GrantedAuthorityImpl implements GrantedAuthority {
private static final long serialVersionUID = 1L;
private String authority;
public GrantedAuthorityImpl(String authority) {
this.authority = authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String getAuthority() {
return this.authority;
}
}
7.2.6 添加权限注解
在用户拥有某个后台访问权限才能访问,这叫做接口保护。我们这里通过Spring Security提供的权限注解来保护后台接口免受非法访问,这里以字典管理模块为例。
在SysDictController的接口上添加类似@PreAuthorize(“hasAuthority(‘sys:dict:view’)”)的注解,表示只有当前登录用户拥有sys:dict:view权限标识才能访问此接口,这里的权限标识需对应菜单表中的perms权限信息,所以可通过配置菜单表的权限来灵活控制接口的访问权限。
SysDictController.java:
@RestController
@RequestMapping("dict")
public class SysDictController {
@Autowired
private SysDictService sysDictService;
@PreAuthorize("hasAuthority('sys:dict:add') AND hasAuthority('sys:dict:edit')")
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysDict record) {
return HttpResult.ok(sysDictService.save(record));
}
@PreAuthorize("hasAuthority('sys:dict:delete')")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysDict> records) {
return HttpResult.ok(sysDictService.delete(records));
}
@PreAuthorize("hasAuthority('sys:dict:view')")
@PostMapping(value="/findPage")
public HttpResult findPage(@RequestBody PageRequest pageRequest) {
return HttpResult.ok(sysDictService.findPage(pageRequest));
}
@PreAuthorize("hasAuthority('sys:dict:view')")
@GetMapping(value="/findByLable")
public HttpResult findByLable(@RequestParam String lable) {
return HttpResult.ok(sysDictService.findByLable(lable));
}
}
7.2.7 Swagger添加令牌函数
由于我们引入Spring Security安全框架,接口受到保护,需要携带合法的token令牌(一般是登录成功之后由后台返回)才能正常访问,但是Swagger本身的接口测试页面默认没有提供传送token参数的地方,因此需要简单定制一下,修改SwaggerConfig配置类即可。
SwaggerConfig.java:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi(){
// 添加请求参数,我们这里把token作为请求头部参数传入后端
ParameterBuilder parameterBuilder = new ParameterBuilder();
List<Parameter> parameters = new ArrayList<Parameter>();
parameterBuilder.name("token").description("令牌")
.modelRef(new ModelRef("string")).parameterType("header").required(false).build();
parameters.add(parameterBuilder.build());
return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
.build().globalOperationParameters(parameters);
// return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
// .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder().build();
}
}
配置之后重新启动就可以进行传参了,测试接口时先将登录接口返回的token复制到此处即可.
7.3 登录接口实现
在登录控制器中添加一个登录接口login,在其中验证验证码、用户名、密码信息。匹配成功之后,执行Spring Security的登录认证机制。登录成功之后返回Token令牌凭证。
SysLoginController.java:
/**
* 登录接口
*/
@PostMapping(value = "/login")
public HttpResult login(@RequestBody LoginBean loginBean, HttpServletRequest request) throws IOException {
String username = loginBean.getAccount();
String password = loginBean.getPassword();
String captcha = loginBean.getCaptcha();
// 从session中获取之前保存的验证码跟前台传来的验证码进行匹配
Object kaptcha = request.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);
if(kaptcha == null){
return HttpResult.error("验证码已失效");
}
if(!captcha.equals(kaptcha)){
return HttpResult.error("验证码不正确");
}
// 用户信息
SysUser user = sysUserService.findByName(username);
// 账号不存在、密码错误
if (user == null) {
return HttpResult.error("账号不存在");
}
if (!PasswordUtils.matches(user.getSalt(), password, user.getPassword())) {
return HttpResult.error("密码不正确");
}
// 账号锁定
if (user.getStatus() == 0) {
return HttpResult.error("账号已被锁定,请联系管理员");
}
// 系统登录认证
JwtAuthenticatioToken token = SecurityUtils.login(request, username, password, authenticationManager);
return HttpResult.ok(token);
}
这里将Spring Security的登录认证逻辑封装到了工具类SecurityUtils的login方法中,认证流程大致分为以下四步:
- 将用户名密码的认证信息封装到JwtAuthenticatioToken对象。
- 通过调用authenticationManager.authenticate(token)执行认证流程。
- 通过SecurityContextHolder将认证信息保存到上下文。
- 通过JwtTokenUtils.generateToken(authentication)生成token并返回。
SecurityUtils.java:
/**
* 系统登录认证
* @param request
* @param username
* @param password
* @param authenticationManager
* @return
*/
public static JwtAuthenticatioToken login(HttpServletRequest request, String username, String password, AuthenticationManager authenticationManager) {
JwtAuthenticatioToken token = new JwtAuthenticatioToken(username, password);
token.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 执行登录认证过程
Authentication authentication = authenticationManager.authenticate(token);
// 认证成功存储认证信息到上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 生成令牌并返回给客户端
token.setToken(JwtTokenUtils.generateToken(authentication));
return token;
}
关于JwtTokenUtils如何生成Token的逻辑参见以下两个方法:
/**
* 生成令牌
*
* @param userDetails 用户
* @return 令牌
*/
public static String generateToken(Authentication authentication) {
Map<String, Object> claims = new HashMap<>(3);
claims.put(USERNAME, SecurityUtils.getUsername(authentication));
claims.put(CREATED, new Date());
claims.put(AUTHORITIES, authentication.getAuthorities());
return generateToken(claims);
}
/**
* 从数据声明生成令牌
*
* @param claims 数据声明
* @return 令牌
*/
private static String generateToken(Map<String, Object> claims) {
Date expirationDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);
return Jwts.builder().setClaims(claims).setExpiration(expirationDate).signWith(SignatureAlgorithm.HS512, SECRET).compact();
}
LoginBean是对登录认证信息的简单封装,包含账号密码和验证码信息:
LoginBean.java:
public class LoginBean {
private String account;
private String password;
private String captcha;
public String getAccount() {
return account;
}
public void setAccount(String account) {
this.account = account;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getCaptcha() {
return captcha;
}
public void setCaptcha(String captcha) {
this.captcha = captcha;
}
}
JwtAuthenticatioToken继承UsernamePasswordAuthenticationToken,是对令牌信息的简单封装,用来作为认证和授权的信任凭证,其中的token信息由JWT负责生成:
JwtAuthenticatioToken.java:
public class JwtAuthenticatioToken extends UsernamePasswordAuthenticationToken {
private static final long serialVersionUID = 1L;
private String token;
public JwtAuthenticatioToken(Object principal, Object credentials){
super(principal, credentials);
}
public JwtAuthenticatioToken(Object principal, Object credentials, String token){
super(principal, credentials);
this.token = token;
}
public JwtAuthenticatioToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities, String token) {
super(principal, credentials, authorities);
this.token = token;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public static long getSerialversionuid() {
return serialVersionUID;
}
}
接口测试见上节图片。
7.4 Spring Security执行流程剖析
Spring Security功能强大,使用也稍显复杂,因为涉及的内容比较多,所以入门门槛比较高,很多从业人员深受其烦,不少人就算跟着网上的教程会使用项目案例了,但是遇到问题还是摸不着头脑,这都是对其执行流程不熟悉造成的。(焯!你再骂!)
作者大大是个好人,为我这种菜鸡附上了他博客的一篇剖析文章,通过追踪与解读源码的方式为读者详细剖析Spring Security的执行流程。在熟悉整个流程之后,许多问题就迎刃而解了。https://wwwblogs/xifengxiaoma/p/10020960.html。(哼!在我收藏夹吃灰去吧)
8. 数据备份还原
很多时候,我们需要对系统数据进行备份和还原。当然,实际生产环境的数据备份和还原通常是由专业数据库维护人员在数据库端通过命令执行的。这里提供的是通过代码进行数据备份,主要是方便一些日常的数据恢复,比如说想把数据恢复到某一时间节点的数据。这一章节讲解如何通过代码调用MySQL的备份还原命令实现系统备份还原的功能。
8.1 新建工程
新建mango-backup工程,这是一个独立运行于admin的服务模块,可以分开独立部署。
8.2 添加依赖
在pom.xml中添加以下相关依赖,主要包含Web、Swagger和common依赖包:
<dependencies>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>com.louis</groupId>
<artifactId>mango-common</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
8.3 添加配置
在配置文件中添加以下配置,定义启动端口为8002,应用名称是mango-backup,定义系统数据备份还原的数据库连接信息:
# tomcat
server:
port: 8002
spring:
application:
name: mango-backup
# backup datasource
mango:
backup:
datasource:
host: localhost
userName: root
password: admin123
database: mango
8.4 自定义Banner
8.5 启动类
修改启动类MangoBackupApplication,指定包扫描路径为com.louis.mango。
MangoBackupApplication.java:
@SpringBootApplication(scanBasePackages={"com.louis.mango"})
public class MangoBackupApplication {
public static void main(String[] args) {
SpringApplication.run(MangoBackupApplication.class, args);
}
}
8.6 跨域配置
在config包下添加跨域配置类。
CorsConfig.java:
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 允许跨域访问的路径
.allowedOrigins("*") // 允许跨域访问的源
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE") // 允许请求方法
.maxAge(168000) // 预检间隔时间
.allowedHeaders("*") // 允许头部设置
.allowCredentials(true); // 是否发送cookie
}
}
8.7 Swagger配置
在config包下添加Swagger配置类。
SwaggerConfig.java:
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2).select()
.apis(RequestHandlerSelectors.any()).paths(PathSelectors.any()).build();
}
}
8.8 数据源属性
添加一个数据源属性配置类,配置@ConfigurationProperties(prefix = “mango.backup.datasource”) 注解,这样就可以通过注入BackupDataSourceProperties读取数据源属性了。
BackupDataSourceProperties.java:
@Component
@ConfigurationProperties(prefix = "mango.backup.datasource")
public class BackupDataSourceProperties {
private String host;
private String userName;
private String password;
private String database;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDatabase() {
return database;
}
public void setDatabase(String database) {
this.database = database;
}
}
遇到springboot configuration annotation precessor not configured情况时在pom.xml中添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
8.9 备份还原接口
在service包下添加一个MysqlBackupService接口,包含backup和restore两个接口,分别对应备份和还原两个接口。
MysqlBackupService.java:
public interface MysqlBackupService {
/**
* 备份数据库
* @param host host地址,可以是本机也可以是远程
* @param userName 数据库的用户名
* @param password 数据库的密码
* @param savePath 备份的路径
* @param fileName 备份的文件名
* @param databaseName 需要备份的数据库的名称
* @return
* @throws IOException
*/
boolean backup(String host, String userName, String password, String backupFolderPath, String fileName, String database) throws Exception;
/**
* 恢复数据库
* @param restoreFilePath 数据库备份的脚本路径
* @param host IP地址
* @param database 数据库名称
* @param userName 用户名
* @param password 密码
* @return
*/
boolean restore(String restoreFilePath, String host, String userName, String password, String database) throws Exception;
}
8.10 备份还原实现
在service.impl下添加MysqlBackupServiceImpl,实现backup和restore两个接口。
MysqlBackupServiceImpl.java:
@Service
public class MysqlBackupServiceImpl implements MysqlBackupService {
@Override
public boolean backup(String host, String userName, String password, String backupFolderPath, String fileName,
String database) throws Exception {
return MySqlBackupRestoreUtils.backup(host, userName, password, backupFolderPath, fileName, database);
}
@Override
public boolean restore(String restoreFilePath, String host, String userName, String password, String database)
throws Exception {
return MySqlBackupRestoreUtils.restore(restoreFilePath, host, userName, password, database);
}
}
8.11 备份还原逻辑
为了方便复用,我们将系统数据备份和还原逻辑封装到了MySqlBackupRestoreUtils工具类,主要是通过代码调用MySQL的数据库备份和还原命令实现。
MySqlBackupRestoreUtils.java:
package com.louis.mango.backup.util;
import java.io.File;
import java.io.IOException;
/**
* MySQL备份还原工具类
* @author Louis
* @date Jan 15, 2019
*/
public class MySqlBackupRestoreUtils {
/**
* 备份数据库
* @param host host地址,可以是本机也可以是远程
* @param userName 数据库的用户名
* @param password 数据库的密码
* @param savePath 备份的路径
* @param fileName 备份的文件名
* @param databaseName 需要备份的数据库的名称
* @return
* @throws IOException
*/
public static boolean backup(String host, String userName, String password, String backupFolderPath, String fileName,
String database) throws Exception {
File backupFolderFile = new File(backupFolderPath);
if (!backupFolderFile.exists()) {
// 如果目录不存在则创建
backupFolderFile.mkdirs();
}
if (!backupFolderPath.endsWith(File.separator) && !backupFolderPath.endsWith("/")) {
backupFolderPath = backupFolderPath + File.separator;
}
// 拼接命令行的命令
String backupFilePath = backupFolderPath + fileName;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mysqldump --opt ").append(" --add-drop-database ").append(" --add-drop-table ");
stringBuilder.append(" -h").append(host).append(" -u").append(userName).append(" -p").append(password);
stringBuilder.append(" --result-file=").append(backupFilePath).append(" --default-character-set=utf8 ").append(database);
// 调用外部执行 exe 文件的 Java API
Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
if (process.waitFor() == 0) {
// 0 表示线程正常终止
System.out.println("数据已经备份到 " + backupFilePath + " 文件中");
return true;
}
return false;
}
/**
* 还原数据库
* @param restoreFilePath 数据库备份的脚本路径
* @param host IP地址
* @param database 数据库名称
* @param userName 用户名
* @param password 密码
* @return
*/
public static boolean restore(String restoreFilePath, String host, String userName, String password, String database)
throws Exception {
File restoreFile = new File(restoreFilePath);
if (restoreFile.isDirectory()) {
for (File file : restoreFile.listFiles()) {
if (file.exists() && file.getPath().endsWith(".sql")) {
restoreFilePath = file.getAbsolutePath();
break;
}
}
}
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mysql -h").append(host).append(" -u").append(userName).append(" -p").append(password);
stringBuilder.append(" ").append(database).append(" < ").append(restoreFilePath);
try {
Process process = Runtime.getRuntime().exec(getCommand(stringBuilder.toString()));
if (process.waitFor() == 0) {
System.out.println("数据已从 " + restoreFilePath + " 导入到数据库中");
}
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
private static String[] getCommand(String command) {
String os = System.getProperty("os.name");
String shell = "/bin/bash";
String c = "-c";
if(os.toLowerCase().startsWith("win")){
shell = "cmd";
c = "/c";
}
String[] cmd = { shell, c, command };
return cmd;
}
public static void main(String[] args) throws Exception {
String host = "localhost";
String userName = "root";
String password = "admin123";
String database = "mango";
System.out.println("开始备份");
String backupFolderPath = "c:/dev/";
String fileName = "mango.sql";
backup(host, userName, password, backupFolderPath, fileName, database);
System.out.println("备份成功");
System.out.println("开始还原");
String restoreFilePath = "c:/dev/mango.sql";
restore(restoreFilePath, host, userName, password, database);
System.out.println("还原成功");
}
}
8.12 备份还原控制器
在controller包下新建一个控制器,备份还原控制器对外提供数据备份还原的服务。包含以下几个接口。
8.12.1 数据备份接口
backup是数据备份接口,读取数据源信息及备份信息生成数据备份。
@GetMapping("/backup")
public HttpResult backup() {
String backupFodlerName = BackupConstants.DEFAULT_BACKUP_NAME + "_" + (new SimpleDateFormat(BackupConstants.DATE_FORMAT)).format(new Date());
return backup(backupFodlerName);
}
private HttpResult backup(String backupFodlerName) {
String host = properties.getHost();
String userName = properties.getUserName();
String password = properties.getPassword();
String database = properties.getDatabase();
String backupFolderPath = BackupConstants.BACKUP_FOLDER + backupFodlerName + File.separator;
String fileName = BackupConstants.BACKUP_FILE_NAME;
try {
boolean success = mysqlBackupService.backup(host, userName, password, backupFolderPath, fileName, database);
if(!success) {
HttpResult.error("数据备份失败");
}
} catch (Exception e) {
return HttpResult.error(500, e.getMessage());
}
return HttpResult.ok();
}
8.12.2 数据还原接口
restore是数据还原接口,读取数据源信息和还原版本进行数据还原。
@GetMapping("/restore")
public HttpResult restore(@RequestParam String name) throws IOException {
String host = properties.getHost();
String userName = properties.getUserName();
String password = properties.getPassword();
String database = properties.getDatabase();
String restoreFilePath = BackupConstants.RESTORE_FOLDER + name;
try {
mysqlBackupService.restore(restoreFilePath, host, userName, password, database);
} catch (Exception e) {
return HttpResult.error(500, e.getMessage());
}
return HttpResult.ok();
}
8.12.3 查找备份接口
findRecords是备份查找接口,用于查找和向用户展示数据备份版本。
@GetMapping("/findRecords")
public HttpResult findBackupRecords() {
if(!new File(BackupConstants.DEFAULT_RESTORE_FILE).exists()) {
// 初始默认备份文件
backup(BackupConstants.DEFAULT_BACKUP_NAME);
}
List<Map<String, String>> backupRecords = new ArrayList<>();
File restoreFolderFile = new File(BackupConstants.RESTORE_FOLDER);
if(restoreFolderFile.exists()) {
for(File file:restoreFolderFile.listFiles()) {
Map<String, String> backup = new HashMap<>();
backup.put("name", file.getName());
backup.put("title", file.getName());
if(BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(file.getName())) {
backup.put("title", "系统默认备份");
}
backupRecords.add(backup);
}
}
// 排序,默认备份最前,然后按时间戳排序,新备份在前面
backupRecords.sort((o1, o2) -> BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(o1.get("name")) ? -1
: BackupConstants.DEFAULT_BACKUP_NAME.equalsIgnoreCase(o2.get("name")) ? 1 : o2.get("name").compareTo(o1.get("name")));
return HttpResult.ok(backupRecords);
}
8.12.4 删除备份接口
delete是备份删除接口,通过备份还原管理界面删除数据备份版本。
@GetMapping("/delete")
public HttpResult deleteBackupRecord(@RequestParam String name) {
if(BackupConstants.DEFAULT_BACKUP_NAME.equals(name)) {
return HttpResult.error("系统默认备份无法删除!");
}
String restoreFilePath = BackupConstants.BACKUP_FOLDER + name;
try {
FileUtils.deleteFile(new File(restoreFilePath));
} catch (Exception e) {
return HttpResult.error(500, e.getMessage());
}
return HttpResult.ok();
}
9. 系统服务监控
Spring Boot Admin是一个管理和监控Spring Boot应用程序的开源监控软件,针对spring-boot的actuator接口进行UI美化并封装,可以在管理界面中浏览所有被监控的spring-boot项目的基本信息,详细的Health信息、内存信息、JVM信息、垃圾回收信息、各种配置信息(比如数据源、缓存列表和命中率)等,还可以直接修改logger的level,Spring Boot Admin提供的丰富详细的监控信息给Spring Boot应用的监控、维护和优化都带来了极大的便利。
9.1 新建工程
新建一个mango-monitor项目,作为服务监控服务端。
9.2 添加依赖
在pom.xml中添加依赖包,主要是Spring Boot和Spring Boot Admin依赖。
<dependencies>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--spring-boot-admin-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-server-ui</artifactId>
<version>2.1.2</version>
</dependency>
</dependencies>
9.3 添加配置
在配置文件中添加配置,指定启动端口为8000、应用名称为mango-monitor。
server:
port: 8000
spring:
application:
name: mango-monitor
9.4 自定义Banner
9.5 启动类
修改启动类MangoMonitorApplication并在头部添加EnableAdminServer注解,开启监控服务。
@EnableAdminServer
@SpringBootApplication
public class MangoMonitorApplication {
public static void main(String[] args) {
SpringApplication.run(MangoMonitorApplication.class, args);
}
}
9.6 监控客户端
把后台服务mango-admin和数据备份还原服务mango-backup注册到监控服务。
分别在mango-admin和mango-backup的pom文件中添加监控客户端依赖:
<!--spring-boot-admin-client-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.1.2</version>
</dependency>
分别在mango-admin和mango-backup的配置文件中配置服务监控信息。主要是指定监控的服务器地址,另外endpoints是开放监控接口,因为处于安全的考虑,Spring Boot默认是没有开放健康检查接口的,可以通过endpoints设置开放特定的接口," * "表示全部开放。
server:
port: 8001
spring:
application:
name: mango-admin
boot:
admin:
client:
url: "http://localhost:8000"
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: admin123
filters: stat,wall,log4j,config
max-active: 100
initial-size: 1
max-wait: 60000
min-idle: 1
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-open-prepared-statements: 50
max-pool-prepared-statement-per-connection-size: 20
management:
endpoints:
web:
exposure:
include: "*"
9.7 启动服务端
编译启动MangoMonitorApplication,访问http://localhost:8000/#/applications,进入应用监控界面,如下:
10. 注册中心(Consul)
10.1 什么是Consul
Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置。与其他分布式服务注册与发现的方案相比,Consul的方案更"一站式",内置了服务注册与发现框架、分布一致性协议实现、健康检查,Key/Value存储、多数据中心方案,不再需要依赖其他工具(比如ZooKeeper等)。使用起来也较为简单。Consul使用Go语言编写,因此具有天然可移植性(支持Linux、Windows和Max OS X);安装包仅包含一个可执行文件,方便部署,与Docker等轻量级容器可无缝配合。
10.2 Consul安装
访问Consul官网,根据操作系统类型,选择下载Consul最新版本。下载下是一个zip压缩包,解压之后是一个exe可执行文件。打开CMD终端进入consul.exe所在目录,执行如下命令启动Consul服务:
consul agent -dev
启动过程信息如下:
启动成功后,访问http://localhost:8500,可以看到如下所示的Consul服务管理界面。
10.3 monitor改造
改造mango-monitor工程,作为服务注册到注册中心。
10.3.1 添加依赖
在pom.xml中添加Spring Cloud和Consul注册中心依赖。
注意:Spring Boot2.1后的版本会出现Consul服务注册上的问题,可能是因为配置变更或者支持方式改变,由于版本太新,网上也没有找到相关解决方案,所以这里把Spring Boot版本调整为2.0.4,Spring Cloud版本使用最新的稳定发布版。(解决方案参考https://blog.csdn/weixin_46041797/article/details/119006411)
<!--consul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--srping cloud-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
10.3.2 配置文件
修改配置文件,添加服务注册配置。
application.yml:
server:
port: 8000
spring:
application:
name: mango-monitor
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
10.3.3 启动类
修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持.
10.3.4 测试效果
启动服务监控服务器,访问http://localhost:8500,发现服务已经成功注册到注册中心,如下:
访问http://localhost:8000,查看服务监控管理界面,看到如下所示界面就没问题了:
10.4 backup改造
改造mango-backup工程,作为服务注册到注册中心。
10.4.1 添加依赖
同上。
10.4.2 配置文件
修改配置文件,添加服务注册配置。
application.yml:
# tomcat
server:
port: 8002
spring:
application:
name: mango-backup
boot:
admin:
client:
url: "http://localhost:8000"
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
# 开放健康检查接口
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
# backup datasource
mango:
backup:
datasource:
host: localhost
userName: root
password: admin123
database: mango
10.4.3 启动类
修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持。
10.4.4 测试效果
测试效果同上差不多。
10.5 admin改造
改造mango-admin工程,作为服务注册到服务中心。
10.5.1 添加依赖
同上。
10.5.2 配置文件
修改配置文件,添加服务注册配置。
application.yml:
server:
port: 8001
spring:
application:
name: mango-admin
boot:
admin:
client:
url: "http://localhost:8000"
datasource:
name: druidDataSource
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/mango?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: admin123
filters: stat,wall,log4j,config
max-active: 100
initial-size: 1
max-wait: 60000
min-idle: 1
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 'x'
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-open-prepared-statements: 50
max-pool-prepared-statement-per-connection-size: 20
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
# 开放健康检查接口
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
10.5.3 启动类
同上。
10.5.4 测试效果
同上差不多。
11. 服务消费(Ribbon、Feign)
11.1 技术背景
在上一章中,我们利用Consul注册中心实现了服务的注册和发现功能。在单体应用中,代码可以直接依赖,在代码中直接调用即可;但是微服务架构(分布式架构)中服务都运行在各自的进程之中,甚至部署在不同的主机和不同的地区,就需要相关的远程调用技术了。
Spring Cloud体系里应用比较广泛的服务调用方式有两种:
- 使用RestTemplate进行服务调用,可以通过Ribbon注解RestTemplate模板,使其拥有负载均衡的功能。
- 使用Feign进行声明式服务调用,声明之后就像调用本地方法一样,Feign默认使用Ribbon实现负载均衡。
两种方式都可以实现服务之间的调用,可根据情况选择使用,下面我们分别用实现案例进行详解。
11.2 服务提供者
11.2.1 新建项目
新建一个mango-producer,添加以下依赖:
- Swagger:API文档
- Consul:注册中心
- Spring Boot Admin:服务监控
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- swagger -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--spring-boot-admin-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring.boot.admin.version}</version>
</dependency>
<!--consul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
11.2.2 配置文件
在配置文件添加内容如下,将服务注册到注册中心并添加服务监控相关配置。
application.yml:
server:
port: 8003
spring:
application:
name: mango-producer
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
boot:
admin:
client:
url: "http://localhost:8000"
# 开放健康检查接口
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
11.2.3 启动类
修改启动器类,添加@EnableDiscoveryClient注解,开启服务发现支持。
11.2.4 自定义Banner
同之前。
11.2.5 添加控制器
新建一个控制器,提供一个hello接口,返回字符串信息。
HelloController.java:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello Mango !";
}
}
为了模拟均衡负载,复制一份上面的项目,重命名为mango-producer2,修改对应的端口为8004,修改hello方法的返回值为"hello Mango 2!"
依次启动注册中心、服务监控和两个服务提供者,启动成功之后刷新Consul管理界面,发现我们注册的mango-producer服务以及有两个节点实例。
访问http://localhost:8500,查看两个服务提供者已经注册到注册中心,如下:
访问http://localhost:8000,查看两个服务提供者已经成功显示在监控列表中,如下:
访问http://localhost:8003/hello,输出"hello Mango !"。
访问http://localhost:8004/hello,输出"hello Mango 2!"。
11.3 服务消费者
11.3.1 新建项目
新建一个项目mango-consumer,添加以下依赖:
- Swagger
- Consul
- Spring Boot Admin
代码同上
11.3.2 添加配置
修改配置如下:
server:
port: 8005
spring:
application:
name: mango-consumer
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
boot:
admin:
client:
url: "http://localhost:8000"
# 开放健康检查接口
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: ALWAYS
11.3.3 启动类、
修改启动类,添加@EnableDiscoveryClient注解,开启服务发现支持
11.3.4 自定义Banner
同上。
11.3.5 服务消费
添加消费服务测试类,添加两个接口,一个查询我们注册的服务,另一个从我们注册的服务中选取一个服务,采用轮询的方式。
ServiceController.java:
@RestController
public class ServiceController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@Autowired
private DiscoveryClient discoveryClient;
/**
* 获取所有服务
*/
@RequestMapping("/services")
public Object services() {
return discoveryClient.getInstances("mango-producer");
}
/**
* 从所有服务中选择一个服务(轮询)
*/
@RequestMapping("/discover")
public Object discover() {
return loadBalancerClient.choose("mango-producer").getUri().toString();
}
}
添加完成之后启动项目,访问http://localhost:8500/,服务消费者已经成功注册到注册中心:
访问http://localhost:8000/,服务消费者已经成功显示到监控列表中,如下:
反复访问http://localhost:8005/discover,结果交替返回服务8003和8004,因为默认负载均衡器采用轮询方式,如下所示。8003和8004两个服务交替出现,从而实现了获取服务端地址的均衡负载:
大多数情况下我们希望使用均衡负载的形式去获取服务端提供的服务,因此使用第二种方法来模拟调用服务端提供的hello方法,创建一个控制器CallHelloController。
CallHelloController.java:
@RestController
public class CallHelloController {
@Autowired
private LoadBalancerClient loadBalancer;
@RequestMapping("/call")
public String call() {
ServiceInstance serviceInstance = loadBalancer.choose("mango-producer");
System.out.println("服务地址:" + serviceInstance.getUri());
System.out.println("服务名称:" + serviceInstance.getServiceId());
String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
System.out.println(callServiceResult);
return callServiceResult;
}
}
使用RestTemplate进行远程调用。添加完之后重启项目。在浏览器访问http://localhost:8005/call。依次往复返回的结果如下所示:
11.3.6 负载均衡器(Ribbon)
上面教程中,我们是这样调用服务的,先通过LoadBalancerClient选取出对应的服务,然后使用RestTemplate进行远程调用。
LoadBalancerClient就是负载均衡器,RibbonLoadBalancerClient是Ribbon默认使用的负载均衡器,采用的负载均衡策略是轮询。
-
查找服务,通过LoadBalancer查询服务
ServiceInstance serviceInstance = loadBalancer.choose("mango-producer");
-
调用服务,通过RestTemplate远程调用服务
String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
这样就完成了一个简单的服务调用和负载均衡。接下来说说Ribbon。
Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者地址后,Ribbon就可基于某种负载均衡算法自动帮助服务消费者去请求。Ribbon默认为我们提供了很多负载均衡算法,例如轮询、随机等。当然我们也可为Ribbon实现自定义的负载均衡算法。
Ribbon内置负载均衡策略可参考https://blog.csdn/weixin_30408309/article/details/94870464。
11.3.7 修改启动类
我们修改一个启动器类,注入RestTemplate,并添加@LoadBalanced注解(用于拦截请求),以使用ribbon来进行负载均衡。
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class MangoConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(MangoConsumerApplication.class, args);
}
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
11.3.8 添加服务
新建一个控制器类注入RestTemplate并调用服务提供者的hello服务。
@RestController
public class RibbonHelloController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/ribbon/call")
public String call() {
// 调用服务, service-producer为注册的服务名称,LoadBalancerInterceptor会拦截调用并根据服务名找到对应的服务
String callServiceResult = restTemplate.getForObject("http://mango-producer/hello", String.class);
return callServiceResult;
}
}
11.3.9 页面测试
启动消费者服务,访问http://localhost:8005/ribbon/call,依次返回的结果同11.3.6.
说明ribbon的负载均衡已经成功启动了。
11.3.10 负载策略
修改负载均衡策略很简单,只需要在配置文件指定对应的负载均衡器即可。
11.4 服务消费(Feign)
Spring Cloud Feign是一套基于Netflix Feign实现的声明式服务调用客户端,使编写Web服务客户端变得更加简单。我们只需要通过创建接口并用注解来配置它即可完成对Web服务接口的绑定。它具有可插拔的注解支持,包括Feign注解、JAX-RS注解。它也支持可插拔的编码器和解码器。Spring Cloud Feign还扩展了对Spring MVC注解的支持,同时还整合了Ribbon来提供负载均衡的HTTP客户端实现。
11.4.1 添加依赖
修改mango-consumer的pom文件,添加feign依赖。
<!--feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
11.4.2 启动类
修改启动器类,添加@EnableFeignClients 注解,开启扫描Spring Cloud Feign客户端的功能。
11.4.3 添加Feign接口
添加MangoProducerService接口,在类头添加注解@FeignClient(name = “mango-producer”),mango-producer是调用的服务名。
@FeignClient(name = "mango-producer")
public interface MangoProducerService {
@RequestMapping("/hello")
public String hello();
}
11.4.4 添加控制器
添加FeignHelloController控制器,注入MangoProducerService,就可以像使用本地方法一样进行调用了。
@RestController
public class FeignHelloController {
@Autowired
private MangoProducerService mangoProducerService;
@RequestMapping("/feign/call")
public String call() {
// 像调用本地服务一样
return mangoProducerService.hello();
}
}
11.4.5 页面测试
启动成功之后访问http://localhost:8005/feign/call,发现调用成功,且依次往复返回的结果同上。
Feign是声明式调用,会产生一些相关的Feign定义接口,所以建议将Feign定义的接口都统一放置管理,以区别内部服务。
12. 服务熔断(Hystrix、Turbine)
12.1 雪崩效应
在微服务架构中,服务众多,通常会涉及多个服务层级的调用,一旦基础服务发生故障,很可能会导致级联故障,进而造成整个系统不可用,这种现象被称为服务雪崩效应。服务雪崩效应是一种因"服务提供者"的不可用导致"服务消费者"的不可用并将这种不可用逐渐放大的过程。
比如在一个系统中,A是服务提供者,B是A的服务消费者,C和D又是B的服务消费者。如果此时A发生故障,则会引起B的不可用,而B的不可用又将导致C和D的不可用,当这种不可用像滚雪球一样逐渐放大的时候,雪崩效应就形成了。(感觉和数据库的多米诺效应一个道理)
12.2 熔断器(CircuitBreaker)
熔断器的原理非常简单,如同电力过载保护器。它可以实现快速失败,如果它在一段时间内侦测到许多类似的错误,就会强迫其以后的多个调用快速失败,不再访问远程服务器,从而防止应用程序不断地尝试执行可能会失败的操作,使得应用程序继续执行而不用等待修正错误,或者浪费CPU时间去等到长时间的超时产生。熔断器也可以使应用程序能够诊断错误是否已经修正,如果已经修正,应用程序会再次尝试调用操作。熔断器模式就像是那些容易导致错误操作的一种代理。这种代理能够记录最近调用发生错误的次数,然后决定使用允许操作继续,或者立即返回错误。熔断器是保护服务高可用的最后一道防线。
12.3 Hystrix特性
12.3.1 断路器机制%
断路器很好理解,当Hystrix Command请求后端服务失败数量超过一定比例(默认为50%)。断路器会切换到开路状态(open)。这时所有请求会直接失败而不会发送到后端服务。断路器保持在开路状态一段时间后(默认为5s),自动切换到半开路状态(HALF-OPEN).这时会判断下一次请求的返回情况,如果请求成功,断路器切回闭路状态(CLOSED),否则重新切换为开路状态(OPEN)。Hystrix的断路器就像我们家庭电路中的保险丝,一旦后端服务不可用,断路器就会直接切断请求链,避免发送大量无效请求,从而影响系统吞吐量,并且断路器有自我检测并恢复的能力。
12.3.2 fallback
fallback相当于降级操作。对于查询操作,我们可以实现一个fallback方法,当请求后端服务出现异常的时候,可以使用fallback方法返回的值。fallback返回的值一般是设置的默认值或来自缓存。
12.3.3 资源隔离
在Hystrix中,主要通过线程池来实现资源隔离。通常在使用的时候我们会根据调用的远程服务划分出多个线程池。例如,调用产品服务的Command放入A线程池,调用账户服务的Command放入B线程池。这样做的优点是运行环境被隔离开了。这样就算调用服务的代码存在bug或者由于其他原因导致自己所在线程池被耗尽,也不会对系统的其他服务造成影响,但是带来的代价是维护多个线程池会对系统带来额外的性能开销。如果是对性能有严格要求且确信自己调用服务的客户端代码不会出问题,就可以使用Hystrix的信号模式(Semaphores)来隔离资源。
12.4 Feign Hystrix
因为Feign中已经依赖了Hystrix,所以在maven配置上不用做任何改动就可以使用了,我们可以在mango-consumer项目中直接改造。
12.4.1 修改配置
在配置文件中添加配置,开启Hystrix熔断器。
application.yml:
#开启熔断器
feign:
hystrix:
enabled: true
12.4.2 创建回调类
创建一个回调类MangoProducerHystrix,实现MangoProducerService接口,并实现对应的方法,返回调用失败后的信息。
MangoProducerHystrix.java:
@Component
public class MangoProducerHystrix implements MangoProducerService {
@RequestMapping("/hello")
public String hello() {
return "sorry, hello service call failed.";
}
}
添加fallback属性。修改MangoProducerService,在@FeignClient注解中加入fallback属性,绑定我们创建的失败回调处理类。
MangoProducerService.java:
@FeignClient(name = "mango-producer", fallback = MangoProducerHystrix.class)
public interface MangoProducerService {
@RequestMapping("/hello")
public String hello();
}
到此,所有改动代码就完成了。
12.4.3 页面测试
启动成功之后,多次访问http://localhost:8005/feign/call,结果如同之前一样交替返回"hello mango!“和"hello mango 2!”,说明熔断器的启动不会影响正常服务访问。
把mango-producer服务停掉,再次访问,返回我们提供的熔断回调信息,熔断成功,mango-producer2服务正常.
重启mango-producer服务,再次访问发现服务又可以访问了,说明熔断器有自我诊断修复的功能。
注:在重启成功之后可能需要一些时间,等待熔断器进行自我诊断和修复完成之后,方可正常提供服务。
12.5 Hystrix Dashboard
Hystrix-Dashboard是一款针对Hystrix进行实时监控的工具,通过Hystrix Dashboard我们可以直观地看到各Hystrix Command的请求响应时间、请求成功率等数据。
12.5.1 添加依赖
新建一个mango-hystrix工程,修改pom文件,添加相关依赖。
pom.xml:
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--consul-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!--actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--spring-boot-admin-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>${spring.boot.admin.version}</version>
</dependency>
<!--hystrix-dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
Spring Cloud依赖:
<!--srping cloud-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
12.5.2 启动类
在启动类中添加注解@EnableHystrixDashboard,开启熔断监控支持。
12.5.3 自定义Banner
同上。
12.5.4 配置文件
修改配置文件,把服务注册到注册中心。
application.yml:
server:
port: 8501
spring:
application:
name: mango-hystrix
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
turbine:
instanceUrlSuffix: hystrix.stream # 指定收集路径
appConfig: kitty-consumer # 指定了需要收集监控信息的服务名,多个以“,”进行区分
clusterNameExpression: "'default'" # 指定集群名称,若为default则为默认集群,多个集群则通过此配置区分
combine-host-port: true # 此配置默认为false,则服务是以host进行区分,若设置为true则以host+port进行区分
12.5.5 配置监控路径
注意,如果使用的是2.x等版本,就需要在Hystrix的消费端配置监控路径。打开消费端mango-consumer工程添加依赖。
pom.xml:
<!--actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hystrix-dashboard-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
修改启动类,添加服务监控路径配置。
MangoConsumerApplication.java:
// 此配置是为了服务监控而配置,与服务容错本身无关,
// ServletRegistrationBean因为springboot的默认路径不是"/hystrix.stream",
// 只要在自己的项目里配置上下面的servlet就可以了
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
12.5.6 页面测试
先后启动monitor、producer、consumer、hystrix等服务,访问http://localhost:8501/hystrix会看到如下界面:
此时没有任何具体的监控信息,需要输入要监控的消费者地址及监控信息的轮询时间和标题。
Hystrix Dashboard共支持以下三种不同的监控方式:
- 单体Hystrix消费者:通过URL http://hystrix-app:port/hystrix.stream开启,实现对具体某个服务实例的监控。
- 默认集群监控:通过URL http://turbine-hostname:port/turbine.stream开启,实现对默认集群的监控。
- 自定集群监控:通过URL http://turbine-hostname:port/turbine.stream?cluster=[clusterName]开启,实现对clusterName集群的监控。
这里先介绍对单体Hystrix消费者的监控,后面整个Turbine集群的时候再说明后两种监控方式。
首先访问http://localhost:8005/feign/call,查看要监控的服务是否可以正常访问。
确认服务可以正常访问之后,在监控地址内输入http://localhost:8005/hystrix.stream,然后单击Monitor Stream开始监控,如下所示:
刚进去页面先显示loading…信息,在多次间断访问http://localhost:8005/hystrix.stream之后统计图表信息如下:
12.6 Spring Cloud Turbine
上面我们集成了Hystrix Dashboard,使用Hystrix Dashboard可以看到单个应用内的服务信息。显然这是不够的,我们还需要一个工具能让我们汇总系统内多个服务的数据并显示到Hystrix Dashboard上,这个工具就是Turbine。
12.6.1 添加依赖
修改mango-hystrix的pom文件,添加turbine依赖包(因为我们使用的注册中心是consul,所以需要排除默认的euraka包,不然会出现冲突,导致启动过程出错)
pom.xml:
<!--turbine-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</exclusion>
</exclusions>
</dependency>
12.6.2 启动类
为启动类添加@EnableTurbine注解,开启Turbine支持。
12.6.3 配置文件
修改配置文件,添加Turbine的配置信息。
application.yml:
server:
port: 8501
spring:
application:
name: mango-hystrix
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
turbine:
instanceUrlSuffix: hystrix.stream # 指定收集路径
appConfig: mango-consumer # 指定了需要收集监控信息的服务名,多个以“,”进行区分
clusterNameExpression: "'default'" # 指定集群名称,若为default则为默认集群,多个集群则通过此配置区分
combine-host-port: true # 此配置默认为false,则服务是以host进行区分,若设置为true则以host+port进行区分
12.6.4 测试效果
依次启动monitor、producer、consumer、hystrix等服务,确认服务启动无误后访问http://localhost:8501/hystrix/,输入http://localhost:8501/turbine.stream,查看熔断监控图表信息,下图就是利用Turbine集合多个Hystrix消费者的熔断监控信息结果:
13. 服务网关(Zuul)
13.1 技术背景
前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢?
因为是REST API接口,所以外部客户端直接调用各个微服务是没有问题的,但是出于种种原因,这并不是一个好的选择。
让客户端直接与各个微服务通信,会有以下几个问题:
- 客户端会多次请求不同的微服务,增加客户端的复杂性。
- 存在跨域请求,在一定场景下处理会变得相对比较复杂。
- 实现认证复杂,每个微服务都需要独立认证。
- 难以重构,项目迭代可能导致微服务重新划分。如果客户端直接与微服务通信,那么重构将会很难实施。
- 如果某些微服务使用的防火墙/浏览器不友好的协议,直接访问会有一定困难。
- 面对类似上面的问题,解决方案就是服务网关。
使用服务网关有以下几个优点:
- 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可在服务网关上进行认证,然后转发请求到微服务,无须在每个微服务中进行认证。
- 客户端只和服务网关打交道,减少了客户端与各个微服务之间的交互次数。
- 多渠道支持。可根据不同客户端(Web端、移动端、桌面端)提供不同的API服务网关。
13.2 Spring Cloud Zuul
服务网关是微服务架构中一个不可或缺的部分。在通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。
Spring Cloud Netflix中的Zuul就担任了这一角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主题能够具备更高的可复用性和可测试性。
在Spring Cloud体系中,Spring Cloud Zuul封装了Zuul组件,作为一个API网关,负责提供负载均衡、反向代理和权限认证。
13.3 Zuul工作机制
13.3.1 过滤器机制
Zuul的核心是一系列的filters,其作用类似Servlet框架中的Filter,Zuul把客户端请求路由到业务处理逻辑的过程中,这些filter在路由的特定时期参与了一些过滤处理,比如实现鉴权、流量转发、请求统计等功能。Zuul的整个运行机制如下:
13.3.2 过滤器的生命周期
Filter的生命周期有四个,分别是"PRE" “ROUTING” “POST” “ERROR”,整个声明周期可以用下图形容:
基于Zuul的这些过滤器可以实现各种丰富的功能,而这些过滤器类型则对应于请求的典型生命周期。
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netflix Ribbon请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发送错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
Zuul默认实现了很多的Filter,如下表:
类型 | 顺序 | 过滤器 | 功能 |
---|---|---|---|
pre | -3 | ServletDetectionFilter | 标记处理Servlet的类型 |
pre | -2 | Servlet30WrapperFilter | 包装HttpServletRequest请求 |
pre | -1 | FormBodyWrapperFilter | 包装请求体 |
route | 1 | DebugFilter | 标记调试标志 |
route | 5 | PreDecorationFilter | 处理请求上下文供后续使用 |
route | 10 | RibbonRoutingFilter | serviceId请求转发 |
route | 100 | SimpleHostRoutingFilter | url请求转发 |
route | 500 | SendForwardFilter | forward请求转发 |
post | 0 | SendErrorFilter | 处理有错误的请求响应 |
post | 1000 | SendResponseFilter | 处理正常的请求响应 |
13.3.3 禁用指定的Filter
可以在application.yml中配置需要禁用的filter,格式为 zuul.< SimpleClassName >.< filterType >.disable=true。比如要禁用org.springframework.cloudflix.zuul.filters.post.SendResponseFilter,进行如下设置即可:
zuul:
SendResponseFilter:
post:
disable: true
自定义filter。实现自定义过滤器需要继承ZuulFilter,并实现ZuulFilter中的抽象方法:
public class MyFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre"; // 定义filter的类型,有pre、route、post、error四种
}
@Override
public int filterOrder() {
return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
}
@Override
public boolean shouldFilter() {
return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
}
@Override
public Object run() throws ZuulException {
return null; //filter需要执行的具体操作
}
}
13.4 实现案例
13.4.1 新建工程
新建一个项目mango-zuul作为服务网关
13.4.2 添加依赖
添加consul、zuul相关依赖。
pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
13.4.3 启动类
为启动类添加@EnableZuulProxy注解,开启服务网关支持。
13.4.4 配置文件
配置启动端口为8010,注册服务到注册中心,配置Zuul转发规则。这里配置在返回http://localhost:8010/feign/call和http://localhost:8010/ribbon/call时调用消费者相关接口。
application.yml:
server:
port: 8010
spring:
application:
name: mango-zuul
cloud:
consul:
host: localhost
port: 8500
discovery:
serviceName: ${spring.application.name} # 注册到consul的服务名称
zuul:
routes:
ribbon:
path: /ribbon/**
serviceId: mango-consumer # 转发到消费者 /ribbon/
feign:
path: /feign/**
serviceId: mango-consumer # 转发到消费者 /feign/
13.4.5 页面测试
依次启动注册中心、监控、服务提供者、服务消费者、服务网关等项目。
访问http://localhost:8010/feign/call和http://localhost:8010/ribbon/call,效果如下:
说明Zuul已经成功转发请求,并成功调用后端服务。
13.4.6 配置接口前缀
如果想给每个服务的API接口加上一个前缀,可使用zuul.prefix进行配置。例如http://localhost:8010/v1/feign/call,即在所有的API接口上加一个v1作为版本号。
zuul:
prefix: /v1
routes:
ribbon:
path: /ribbon/**
serviceId: mango-consumer # 转发到消费者 /ribbon/
feign:
path: /feign/**
serviceId: mango-consumer # 转发到消费者 /feign/
13.4.7 默认路由规则
上面我们通过添加路由配置进行请求转发。
但是如果后端服务非常多,每一个都这样配置挺麻烦的。Spring Cloud Zuul已经帮我们做了默认配置。默认情况下,Zuul会代理所有注册到注册中心的微服务,并且Zuul的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务注册中心的serviceId/**会被转发到serviceId对应的微服务。如果遵循默认路由规则,基本上就没什么配置了。
比如我们直接通过serviceId/feign/call的方式访问也是可以正常访问的。访问http://localhost:8010/mango-consumer/feign/call,结果也是一样的。
13.4.8 路由熔断
Zuul作为Netflix的组件,可以与Ribbon、Eureka和Hystrix等组件相结合,实现负载均衡、熔断器的功能。默认情况下Zuul和Ribbon相结合,实现了负载均衡。实现熔断器功能需求实现FallbackProvider接口。实现该接口有两个方法,一个是getRoute(),用于指定熔断器功能应用于哪些路由的服务;另一个方法是fallbackResponse(),为进入熔断器功能时执行的逻辑。
创建MyFallbackProvider类,getRoute()方法返回"mango-consumer",只针对consumer服务进行熔断。如果需要所有的路由服务都加熔断功能,需要在getRoute()上返回"*“匹配符。getBody()方法返回发送熔断时的反馈信息,这里在发送熔断时返回信息"Sorry, the service is unavailable now.”。
@Component
public class MyFallbackProvider implements FallbackProvider {
@Override
public String getRoute() {
return "mango-consumer";
}
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
System.out.println("route:"+route);
System.out.println("exception:"+cause.getMessage());
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "ok";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("Sorry, the service is unavailable now.".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
重新启动,访问http://localhost:8010/mango-consumer/feign/call,可以正常访问。停掉mango-consumer服务,f昂问http://localhost:8010/mango-consumer/feign/call,返回效果如下:
结果返回了我们自定义的信息,说明我们自定义的熔断器起作用了。
13.4.9 自定义Filter
创建一个MyFilter,继承ZuulFilter类,覆写run()逻辑,在转发请求之前进行token认证,如果请求没有携带token,返回"there is no request token"提示。
MyFilter.java:
@Component
public class MyFilter extends ZuulFilter {
private static Logger log=LoggerFactory.getLogger(MyFilter.class);
@Override
public String filterType() {
return "pre"; // 定义filter的类型,有pre、route、post、error四种
}
@Override
public int filterOrder() {
return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
}
@Override
public boolean shouldFilter() {
return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
}
@Override
public Object run() throws ZuulException {
// filter需要执行的具体操作
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
String token = request.getParameter("token");
System.out.println(token);
if(token==null){
log.warn("there is no request token");
ctx.setSendZuulResponse(false);
ctx.setResponseStatusCode(401);
try {
ctx.getResponse().getWriter().write("there is no request token");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
log.info("ok");
return null;
}
}
这样,Zuul就会自动加载Filter执行过滤了。重新启动Zuul项目,访问http://localhost:8010/mango-consumer/feign/call,结果如下所示:
请求时带上token,访问http://localhost:8010/mango-consumer/feign/call?token=115,接口返回正确结果,如下所示:
Zuul作为API服务网关,不同的客户端使用不同的负载将请求统一分发到后端的Zuul,再由Zuul转发到后端服务。为了保证Zuul的高可用性,前端可以同时开启多个Zuul实例进行负载均衡。另外,在Zuul的前端还可以使用Nginx或者F5再进进行负载转发,从而保证Zuul的高可用性。
14. 链路追踪(Sleuth、Zipkin)
14.1 技术背景
在微服务架构中,随着业务发展,系统拆分导致系统调用链路愈发复杂,一个看似简单的前端请求可能最终需要调用很多次后端服务才能完成,那么当整个请求出现问题时,我们很难得知到底是哪个服务出了问题导致的,这时就需要解决一个问题,即如何快速定位服务故障点,分布式系统调用链追踪技术就此诞生了。
14.2 ZipKin
ZipKin是一个由Twitter公司提供并开放源代码分布式的跟踪系统。,它可以帮助收集服务的时间数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
每个服务向ZipKin报告定时数据,ZipKin会根据调用关系通过ZipKin UI生成依赖关系图,展示多少追踪请求经过了哪些服务,该系统让开发者可通过一个Web前端轻轻松松收集和分析数据,例如用户每次请求服务的处理时间等,可非常方便地监测系统中存在的瓶颈。
ZipKin提供了可插拔数据存储方式:In-Memory、MySQL、Cassandra、以及Elasticsearch。我们可以根据需求选择不同的存储方式,生成环境一般都需要持久化。我们这里采用Elasticsearch作为ZipKin的数据存储器。
14.3 Spring Cloud Sleuth
一般而言,一个分布式服务追踪系统,主要由三部分组成:数据收集、数据存储和数据展示。
Spring Cloud Sleuth为服务之间的调用提供链路追踪,通过Sleuth可以很清楚地了解到一个服务请求经过了哪些服务,每个服务处理花费了多少时间,从而让我们可以很方便地理清各微服务之间的调用关系,此外,Sleuth还可以帮助我们:
- 耗时分析:通过Sleuth可以很方便地了解到每个采样请求的耗时,从而分析出哪些服务调用比较耗时。
- 可视化错误:对于程序未捕捉的异常,可以通过集成ZipKin服务在界面上看到。
- 链路优化:对于调用比较频繁的服务,可以针对这些服务实施一些优化措施。
Spring Cloud Sleuth可以结合ZipKin,将信息发送到ZipKin,利用ZipKin的存储来存储信息,利用ZipKin UI来展示数据。
14.4 实现案例
在早前的Spring Cloud版本里是需要自建ZipKin服务端的,但是从Spring Cloud 2.0以后,官方已经不支持自建Server了,改成提供编译好的jar包供用户使用。这里我们使用Docker方式部署ZipKin服务,并采用Elasticsearch作为ZipKin的数据存储器。
14.4.1 下载镜像
此前先安装好Dockers环境(安装环境用了小半天,一直出问题一直出问题)。
- 首先在阿里云镜像下载Docker Desktop Installer:http://mirrors.aliyun/docker-toolbox/windows/docker-for-windows/stable/
- 下载后参考该篇https://blog.csdn/lanxingxing666666/article/details/111354089,在Win10中添加Hyper-v(本来就有的话跳过)
- 这样完了下载好Docker Desktop后打开应该是会报错的,此时再参考该篇https://blog.csdn/xiewensui8810/article/details/115679237。
(我不李姐,为什么,现在看好像就这几步就行的事,我怎么搞了那么久,甚至创建了虚拟机想在Linux系统上试试,结果试试就逝世,白浪费了时间。都是题外话了)
使用以下命令分别拉取ZipKin和Elasticsearch镜像。
docker pull openzipkin/zipkin
docker pull docker.elastic.co/elasticsearch/elasticsearch:6.3.0
拉取镜像的过程及其十分非常缓慢,甚至还可能报错,再下载一次应该可能大概就行吧。
通过docker images查看下载镜像,如下所示:
14.4.2 编写启动文件
到这里就失败了,多次尝试未果,遂放弃。
14参考
对于链路追踪参考https://blog.csdn/qq_40587263/article/details/117338097。
经测试发现可行。
15. 配置中心(Config、Bus)
15.1 技术背景
如今微服务架构盛行,在分布式系统中,项目日益庞大,子项目日益增多,每个项目都散落着各种配置文件,且随着服务的增加而不断增多。此时,往往某一个基础服务信息变更都会导致一系列服务的更新与重启,运维也是苦不堪言,而且很容易出错。配置中心便由此应运而生。
目前市面上开源的配置中心很多,像Spring家族的Spring Cloud Config、Apache的Apache Commons Configuration、淘宝的diamond、百度的disconf、360的QConf等,都是为了解决这类问题。
15.2 Spring Cloud Config
Spring Cloud Config 是一套为分布式系统中的基础设施和微服务应用提供集中化配置的管理方案,分为服务端和客户端两个部分。服务端也称为分布式配置中心,是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息。客户端是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理服务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。
Spring Cloud Config对服务端和客户端中的环境变量和属性配置实现了抽象映射,所以除了适用于Spring应用,也是可以在任何其他语言应用中使用的。Spring Cloud Config实现的配置中心默认采用Git来存储配置信息,所以使用Spring Cloud Config构建的配置服务器天然就支持对微服务应用配置信息的版本管理,并且可以通过Git客户端工具非常方便地管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如SVN仓库、本地化文件系统等。
15.3 实现案例
15.3.1 准备配置文件
首先在Git下新建一个config-repo目录,用来存放配置文件,如下所示,这里分别模拟了三个环境的配置文件,分别编辑三个文件,配置hello属性的值为"consumer.hello=hello, xx configurations"。
15.3.2 服务端实现
-
新建工程
新建mango-config工程,作为配置中心的服务端,负责把git仓库的配置文件发布为RESTFul接口。
-
添加依赖
除了Spring Cloud依赖外,另需添加配置中心依赖包。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency>
-
启动类
启动类添加注解@EnableConfigServer。
-
配置文件
修改配置文件,添加如下内容。如果是私有仓库,需要填写用户名、密码,如果是公有仓库可以不配置密码。
server: port: 8020 spring: application: name: mango-config cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注册到consul的服务名称 config: label: master # git仓库分支 server: git: uri: https://gitee/lhtyw/config-repo.git # 配置git仓库的地址 search-paths: src/config-repo # git仓库地址下的相对地址,可以配置多个,用,分割。 management: endpoints: web: exposure: include: "*"
Spring Cloud Config也提供本地存储配置方式,只需设置属性spring.profiles.active=native,Config Server会默认从应用的src/main/resource目录下检索配置文件。另外也可以通过spring.cloud.config.server.native.searchLocations=file:D:/properties/属性来指定配置文件的位置。虽然Spring Cloud Config提供了这样的功能,但是为了更好的支持内容管理和版本控制,还是推荐使用GIT的方式。
-
页面测试
启动注册中心,配置中心服务,访问http://localhost:8020/consumer/dev,返回dev配置文件的信息:
访问http://localhost:8020/consumer/pro,返回pro配置文件的信息:
上述的返回信息包含了配置文件的位置、版本、配置文件的名称一以及配置文件的具体内容,说明server端已经成功获取了GIT仓库的配置信息。
访问http://localhost:8020/consumer-dev.properties返回结果如下所示:
将dev配置文件中的内容修改为"hello=hello, dev configurations2."重新访问http://localhost:8020/consumer-dev.properties,发现读取的是修改后提交的东西,说明服务端是会自动读取最新提交的数据。
仓库中的配置文件会被转换成相应的Web接口,访问可参照以下规则:
- /{application}/{profile}[/{label}]
- /{application}-{profile}.yml
- /{label}/{application}-{profile}.yml
- /{application}-{profile}.properties
- /{label}/{application}-{profile}.properties
以consumer-dev.properties为例,它的application是consumer、profile是dev。客户端会根据填写的参数来选择读取对应的配置。
15.3.3 客户端实现
-
添加依赖
打开mango-consumer工程,添加相关依赖。
<!-- spring-cloud-config --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
-
配置文件
添加一个叫bookstrap.yml的配置文件,添加配置中心,并把注册中心的配置移到这里,因为在通过配置中心查找配置时需要通过注册中心的发现服务。
bookstrap.yml:
spring: cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注册到consul的服务名称 config: discovery: enabled: true # 开启服务发现 serviceId: mango-config # 配置中心服务名称 name: consumer # 对应{application}部分 profile: dev # 对应{profile}部分 label: master # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用
配置说明:
- spring.cloud.config.uri:配置中心的具体地址。
- spring.cloud.config.name:对应{application}部分。
- spring.cloud.config.profile:对应{profile}部分。
- spring.cloud.config.label:对应git的分支。如果配置中心使用的是本地存储,则该参数无用。
- spring.cloud.config.discovery.serviceId:指定配置中心的service-id,便于扩展为高可用的配置集群。
(上面这些与spring cloud有关的属性必须配置在bookstrap.yml中,这样config部分内容才能被正确加载,因为config的相关配置会先于application.yml,而bookstrap.yml的加载也是先于application.yml的)
application.yml:
server: port: 8005 spring: application: name: mango-consumer boot: admin: client: instance: instance.service-base-url: ${spring.application.name} zipkin: base-url: http://localhost:9411/ sleuth: sampler: probability: 1 #样本采集量,默认为0.1,为了测试这里修改为1,正式环境一般使用默认值 # 开放健康检查接口 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS #开启熔断器 feign: hystrix: enabled: true
-
控制器
添加一个SpringConfigController控制器,添加注解@Value("${hello}"),声明hello属性从配置文件中读取。
@RestController class SpringConfigController { @Value("${hello}") private String hello; @RequestMapping("/hello") public String from() { return this.hello; } }
-
页面测试
启动注册中心、配置中心和服务消费者,访问http://localhost:8005/hello,返回结果如下图:
说明客户端已经成功从服务端获取了配置信息。
手动修改一下仓库配置文件的内容,修改完成并提交。再次访问http://localhost:8005/hello发现返回结果并没有读取最新提交的内容,这是因为springboot项目只有在启动的时候才会获取配置文件的内容,虽然GIT配置信息被修改了,但是客户端并未重新去获取,所以导致读取的信息仍然是旧配置。
15.3.4 Refresh机制
Refresh机制是Spring Cloud Config提供的一种刷新机制,它允许客户端通过POST方法触发各自的/refresh,只要依赖spring-boot-starter-actuator包就拥有了/refresh的功能。下面我们为客户端添加刷新功能,以支持更新配置的获取。
-
添加依赖
添加actuator依赖。actuator是健康检查依赖包,依赖包里携带了/refresh功能。
<!--actuator--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
在使用配置属性的类型中加上@RefreshScopezh蟹,这样在客户端执行/refresh的时候就会刷新此类下面的配置属性了。
-
修改配置
健康检查接口开放需要在配置文件添加如下内容,开放Refresh的相关接口,因为之前已经配置过,所以无需添加。
# 开放健康检查接口 management: endpoints: web: exposure: include: "*" endpoint: health: show-details: ALWAYS
通过上面的接口开放配置,以后以post请求的方式访问http://localhost:8005/actuator/refresh时就会更新修改后的配置文件了。
这里存在版本大坑,1.x和2.x的配置不太一样,我们用的是2.0+版本,务必注意: 安全配置变更:新版本为management.endpoints.web.exposure.include="*" 访问地址变更:新版本为http://localhost:8005/actuator/refresh,老版本为http://localhost:8005/refresh
这里解释一下上面这个配置起到什么作用。actuator是一个健康检查包,提供了一些健康检查数据接口。Refresh功能是其中的一个接口,为了安全起见,默认只开放了health和info接口.而上面的配置就是设置要开放哪些接口,我们设置成"*"是开放所有接口。也可以指定开放几个,比如health、info、refresh,而这里因为我们需要用的是Refresh功能,所以需要把Refresh接口开放出来。
-
页面测试
重新启动服务,访问http://localhost:8005/hello,返回结果同上。
修改仓库配置内容再次访问结果也没有更新。因为我们没有调用refresh方法,通过工具或自写代码发生post请求http://localhost:8005/actuator/refresh,刷新配置。
刷新后再次访问http://localhost:8005/hello返回结果更新:
查看返回结果,刷新之后已经可以获取最新提交的配置内容,但是每次都需要手动刷新客户端还是很麻烦,如果客户端数量一多就简直难以忍受,一个好的解决方法就是Spring Cloud Bus。
15.3.5 Spring Cloud Bus
Spring Cloud Bus称为消息总线,通过轻量级的消息代理来连接各个分布的节点,可以利用像消息队列的广播机制在分布式系统中进行消息传播。通过消息总线可以实现很多业务功能,其中对于配置中心客户端刷新就是一个非常典型的使用场景。
消息总线作用流程如下:(图源:https://wwwblogs/xifengxiaoma/p/9857110.html).
Spring Cloud Bus进行配置更新的步骤如下:
- 提交代码触发post请求给/actuator/bus-refresh。
- Server端接收到请求并发送给Spring Cloud Bus。
- Spring Cloud Bus接到消息并通知给其他客户端。
- 其他客户端接收到通知,请求Server端获取最新配置。
- 全部客户端均获取到最新的配置。
-
安装RabbitMQ
参考https://developer.aliyun/article/769883 and https://blog.csdn/weixin_40822435/article/details/105738375。
用系统提供的默认账号登录后管理界面如下:(用户名密码皆为guest)
-
客户端实现
添加依赖,打开客户端mango-consumer添加消息总线相关依赖:
<!-- bus-amqp --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改配置,添加RabbitMQ的相关配置,这样客户端代码就改造完成了。
bookstrap.yml:
spring: cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注册到consul的服务名称 config: discovery: enabled: true # 开启服务发现 serviceId: mango-config # 配置中心服务名称 name: consumer # 对应{application}部分 profile: dev # 对应{profile}部分 label: master # 对应git的分支,如果配置中心使用的是本地存储,则该参数无用 rabbitmq: host: localhost port: 5672 username: guest password: guest
-
服务端实现
添加依赖,修改mango-config,添加相关依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改配置,添加RabbitMQ和接口开放相关的配置,这样服务端代码就改造完成了。
application.yml:
server: port: 8020 spring: application: name: mango-config cloud: consul: host: localhost port: 8500 discovery: serviceName: ${spring.application.name} # 注册到consul的服务名称 config: label: master # git仓库分支 server: git: uri: https://gitee/lhtyw/config-repo.git # 配置git仓库的地址 search-paths: src/config-repo # git仓库地址下的相对地址,可以配置多个,用,分割。 rabbitmq: host: localhost port: 5672 username: guest password: guest management: endpoints: web: exposure: include: "*"
-
页面测试
启动服务端,成功集成消息总线后,启动信息可以看到如下所示信息:
启动客户端。会报错,启动追踪代码发现下图中对应位置找不到相应的Bean.
在刷新的时候缺少一个拦截器,可以自己设置一个。加一个配置类,并在resources下新建一个META-INF目录和一个spring.factories文件,如下:
RetryConfiguration.java:
public class RetryConfiguration {
@Bean
@ConditionalOnMissingBean(name = "configServerRetryInterceptor")
public RetryOperationsInterceptor configServerRetryInterceptor() {
return RetryInterceptorBuilder.stateless().backOffOptions(1000, 1.2, 5000).maxAttempts(10).build();
}
}
spring.factories:
org.springframework.cloud.bootstrap.BootstrapConfiguration=com.louis.mango.consumer.RetryConfiguration
指定新建的拦截器,这样系统初始化时就会加载这个Bean。然后重新启动就没有报错了。
http://localhost:8005/hello,界面打印"hello, dev configurations."。
修改仓库配置文件,修改完成提交。
再次访问发现还是旧信息。
用工具发送post请求:http://localhost:8020/actuator/bus-refresh.
这次是向注册中心服务端发送请求,发送成功之后服务端会通过消息总线通知所有的客户端进行刷新。另外,开启消息总线后的请求地址是/actuator/bus-refresh,不再是refresh了。
给服务端发送刷新请求后,再次访问http://localhost:8005/hello,结果如下:
最终,我们愉快地发现客户端已经能够通过消息总线获取最新配置了。
版权声明:本文标题:权限管理系统项目文档——SpringBoot后端 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/biancheng/1724541650a896494.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论