8、SpringBoot 数据访问
- 对于数据访问层,无论是 SQL(关系型数据库) 还是 NOSQL(非关系型数据库),Spring Boot 底层都是采用 Spring Data 的方式进行统一处理。
- Spring Boot 底层都是采用 Spring Data 的方式进行统一处理各种数据库,Spring Data 也是 Spring 中与 Spring Boot、Spring Cloud 等齐名的知名项目。
- Sping Data 官网:https://spring.io/projects/spring-data
- 数据库相关的启动器官方文档:Spring Boot application starters
- 旧版官方文档(含 Pom):https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#using-boot-starter
8.1、整合JDBC使用
8.1.1、测试数据源
-
新建一个项目测试,引入相应的模块:JDBC API、MySQL Driver 等
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
-
编写yaml配置文件连接数据库,注意:要删除原本的properties配置文件(大坑)!
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver
-
此时,SpringBoot 已经默认进行了自动配置,在测试类测试如下:
package com.cwlin; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest class SpringBoot04DataApplicationTests { //SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出 @Autowired DataSource dataSource; @Test void contextLoads() throws SQLException { //查看默认数据源 System.out.println(dataSource.getClass()); //获得数据库连接 Connection connection = dataSource.getConnection(); System.out.println(connection); //关闭数据库连接 connection.close(); } }
-
得到结果:SpringBoot 默认自动配置的数据源为
class com.zaxxer.hikari.HikariDataSource
。
8.1.2、查看源码
-
查看数据源配置类:DataSourceProperties
@ConfigurationProperties( prefix = "spring.datasource" ) public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean { private ClassLoader classLoader; private String name; private boolean generateUniqueName = true; private Class<? extends DataSource> type; private String driverClassName; private String url; private String username; private String password; private String jndiName; private DataSourceInitializationMode initializationMode; private String platform; private List<String> schema; private String schemaUsername; private String schemaPassword; private List<String> data; private String dataUsername; private String dataPassword; private boolean continueOnError; private String separator; private Charset sqlScriptEncoding; private EmbeddedDatabaseConnection embeddedDatabaseConnection; private DataSourceProperties.Xa xa; private String uniqueName; //...... }
-
查看数据源的自动配置类:DataSourceAutoConfiguration
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) @ConditionalOnMissingBean( type = {"io.r2dbc.spi.ConnectionFactory"} ) @EnableConfigurationProperties({DataSourceProperties.class}) @Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class}) public class DataSourceAutoConfiguration { public DataSourceAutoConfiguration() { } //...... static class PooledDataSourceCondition extends AnyNestedCondition { PooledDataSourceCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); } @Conditional({DataSourceAutoConfiguration.PooledDataSourceAvailableCondition.class}) static class PooledDataSourceAvailable { PooledDataSourceAvailable() { } } @ConditionalOnProperty( prefix = "spring.datasource", name = {"type"} ) static class ExplicitType { ExplicitType() { } } } @Configuration( proxyBeanMethods = false ) @Conditional({DataSourceAutoConfiguration.PooledDataSourceCondition.class}) @ConditionalOnMissingBean({DataSource.class, XADataSource.class}) @Import({Hikari.class, Tomcat.class, Dbcp2.class, Generic.class, DataSourceJmxConfiguration.class}) protected static class PooledDataSourceConfiguration { protected PooledDataSourceConfiguration() { } } //...... }
-
这里通过
Import
注解导入的类,都在DataSourceConfiguration
配置类下。Spring Boot 2.3.7 默认使用 HikariDataSource 政伟数据源,而以前版本如 Spring Boot 1.5 默认使用 org.apache.tomcat.jdbc.pool.DataSource 作为数据源。 -
HikariDataSource 号称 Java WEB 当前速度最快的数据源,相比于传统的 C3P0 、DBCP、Tomcat jdbc 等连接池更加优秀;可以使用 spring.datasource.type 指定自定义的数据源类型,值为要使用的连接池实现的完全限定名。
8.1.3、JdbcTemplate
-
有了数据源(com.zaxxer.hikari.HikariDataSource),然后可以拿到数据库连接(java.sql.Connection),连接后就可以使用原生的 JDBC 语句来操作数据库。
-
即使不使用第三方第数据库操作框架,如 MyBatis等,Spring 本身也对原生的JDBC 做了轻量级的封装,即JdbcTemplate。
-
数据库操作的所有 CRUD 方法都在 JdbcTemplate 中。
-
Spring Boot 不仅提供了默认的数据源,同时默认已经配置好了 JdbcTemplate 放在了容器中,程序员只需自己注入即可使用。
-
JdbcTemplate 的自动配置是依赖 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 类。
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.boot.autoconfigure.jdbc; import javax.sql.DataSource; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.jdbc.JdbcProperties.Template; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; @Configuration( proxyBeanMethods = false ) @ConditionalOnMissingBean({JdbcOperations.class}) class JdbcTemplateConfiguration { JdbcTemplateConfiguration() { } @Bean @Primary JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); Template template = properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds()); } return jdbcTemplate; } }
-
JdbcTemplate主要提供以下几类方法:
- execute方法:可以用于执行任何SQL语句,一般用于执行DDL语句;
- update方法及batchUpdate方法:update方法用于执行新增、修改、删除等语句;batchUpdate方法用于执行批处理相关语句;
- query方法及queryForXXX方法:用于执行查询相关语句;
- call方法:用于执行存储过程、函数相关语句。
8.1.4、测试
-
Spring Boot 默认提供了数据源和 org.springframework.jdbc.core.JdbcTemplate,JdbcTemplate 会自己注入数据源,用于简化 JDBC操作;能够自动提交事务;还能避免一些常见的错误,使用起来也不用再自己来关闭数据库连接
-
编写一个Controller,注入 jdbcTemplate,编写测试方法进行访问测试
package com.cwlin.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.List; import java.util.Map; @RestController public class JdbcController { @Autowired JdbcTemplate jdbcTemplate; //查询数据库的所有信息 没有实体类,数据库中的东西,通过map获取 @GetMapping("/userList") public List<Map<String,Object>> userList(){ String sql = "select * from user"; List<Map<String, Object>> mapList = jdbcTemplate.queryForList(sql); return mapList; } @GetMapping("/insertUser") public String insertUser(){ String sql = "insert into mybatis.user(id,name,pwd) values(6,'cwlin','111111')"; jdbcTemplate.update(sql); return "insert: OK!"; } @GetMapping("/updateUser/{id}") public String updateUser(@PathVariable("id") int id){ String sql = "update mybatis.user set name=?,pwd=? where id="+id; //封装 Object[] objects = new Object[2]; objects[0] = "coder_lcw"; objects[1] = "123456"; jdbcTemplate.update(sql, objects); return "update: OK!"; } @GetMapping("/deleteUser/{id}") public String deleteUser(@PathVariable("id") int id){ String sql = "delete from mybatis.user where id=?"; jdbcTemplate.update(sql, id); return "delete: OK!"; } }
-
逐个测试CURD操作的请求!
8.2、整合Druid数据源(重点)
8.2.1、Druid简介
- Java程序很大一部分要操作数据库,为了提高性能操作数据库的时候,又不得不使用数据库连接池。Druid 是阿里巴巴开源平台上一个数据库连接池实现,结合了 C3P0、DBCP 等 DB 池的优点,同时加入了日志监控。
- Druid 可以很好的监控 DB 池连接和 SQL 的执行情况,天生就是针对监控而生的 DB 连接池。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
- Spring Boot 2.0 以上默认使用 Hikari 数据源,可以说 Hikari 与 Driud 都是当前 Java Web 上最优秀的数据源,我们来重点介绍 Spring Boot 如何集成 Druid 数据源,如何实现数据库监控。
- Github地址:https://github.com/alibaba/druid/
8.2.2、基本配置参数
- DRUID的DataSource类为:com.alibaba.druid.pool.DruidDataSource,基本配置参数如下:
配置 | 缺省值 | 说明 |
---|---|---|
name | 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this) | |
jdbcUrl | 连接数据库的url,不同数据库不一样。例如: mysql:jdbc:mysql://10.20.153.104:3306/druid2 oracle:jdbc:oracle:thin:@10.20.149.85:1521:ocnauto | |
username | 连接数据库的用户名 | |
password | 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter,详细看这里。 | |
driverClassName | 根据url自动识别 | 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) |
initialSize | 0 | 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 |
maxActive | 8 | 最大连接池数量 |
maxIdle | 8 | 已经不再使用,配置了也没效果 |
minIdle | 最小连接池数量 | |
maxWait | 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 | |
poolPreparedStatements | false | 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 |
maxOpenPreparedStatements | -1 | 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 |
validationQuery | 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 | |
validationQueryTimeout | 单位:秒,检测连接是否有效的超时时间。底层调用jdbc Statement对象的void setQueryTimeout(int seconds)方法 | |
testOnBorrow | true | 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 |
testOnReturn | false | 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 |
testWhileIdle | false | 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 |
timeBetweenEvictionRunsMillis | 1分钟(1.0.14) | 有两个含义:1) Destroy线程会检测连接的间隔时间,2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 |
numTestsPerEvictionRun | 不再使用,一个DruidDataSource只支持一个EvictionRun | |
minEvictableIdleTimeMillis | 30分钟(1.0.14) | 连接保持空闲而不被驱逐的最长时间 |
connectionInitSqls | 物理连接初始化的时候执行的sql | |
exceptionSorter | 根据dbType自动识别 | 当数据库抛出一些不可恢复的异常时,抛弃连接 |
filters | 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall | |
proxyFilters | 类型是List<com.alibaba.druid.filter.Filter>,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 |
8.2.3、配置数据源
-
在pom.xml中,导入 Druid 数据源依赖
<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency>
-
切换数据源:Spring Boot 2.0 以上默认使用 com.zaxxer.hikari.HikariDataSource 数据源,但可以通过 spring.datasource.type 指定数据源
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Druid数据源
-
切换数据源后,在测试类中注入 DataSource,测试数据库连接
class com.alibaba.druid.pool.DruidDataSource
-
设置数据源连接初始化大小、最大连接数、等待时间、最小连接数等设置项
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
导入Log4j 的依赖
<!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>
-
现在需要我们自己为 DruidDataSource 绑定全局配置文件中的参数,再添加到容器中,而不再使用 Spring Boot 的自动生成了,即添加 DruidDataSource 组件到容器中,并绑定属性
package com.cwlin.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Configuration public class DruidConfig { //将自定义的Druid配置进IOC容器 @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druidDataSource(){ return new DruidDataSource(); } }
-
在测试类中进行测试!
package com.cwlin; import com.alibaba.druid.pool.DruidDataSource; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest class SpringBoot04DataApplicationTests { //SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出 @Autowired DataSource dataSource; @Test void contextLoads() throws SQLException { //查看默认数据源 System.out.println(dataSource.getClass()); //获得数据库连接 Connection connection = dataSource.getConnection(); System.out.println(connection); //测试Druid数据源 DruidDataSource druidDataSource = (DruidDataSource) dataSource; System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive()); System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize()); //关闭数据库连接 connection.close(); } }
-
从输出结果中可以看到配置参数已经生效!
class com.alibaba.druid.pool.DruidDataSource com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@699d96bc druidDataSource 数据源最大连接数:20 druidDataSource 数据源初始化连接数:6
8.2.4、配置Druid数据源监控
-
Druid 数据源具有监控的功能,并提供了一个 web 界面方便用户查看,类似安装 路由器 时,人家也提供了一个默认的 web 页面。
-
设置 Druid 的后台管理页面,比如登录账号、密码等,并配置后台管理
package com.cwlin.config; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @Configuration public class DruidConfig { //将自定义的Druid配置进IOC容器 @ConfigurationProperties(prefix = "spring.datasource") @Bean public DataSource druidDataSource(){ return new DruidDataSource(); } //配置 Druid 监控管理后台的Servlet //内置 Servlet 容器时没有web.xml文件,所以使用 Spring Boot 的注册 Servlet 方式 @Bean public ServletRegistrationBean statViewServlet(){ ServletRegistrationBean<StatViewServlet> bean = new ServletRegistrationBean<>( new StatViewServlet(), "/druid/*"); //后台需要有人登录,账号密码配置 HashMap<String, String> initParameters = new HashMap<>(); //增加配置 //这些参数可以在com.alibaba.druid.support.http.StatViewServlet的父类ResourceServlet中找到 initParameters.put("loginUsername","admin"); //后台管理界面的登录账号 initParameters.put("loginPassword","123456"); //后台管理界面的登录密码 //Druid 后台允许谁可以访问 allow //initParameters.put("allow", "localhost"):表示只有本机可以访问 //initParameters.put("allow", ""):为空或者为null时,表示允许所有访问 initParameters.put("allow", ""); //Druid 后台禁止谁可以访问deny initParameters.put("deny", "192.168.1.66"); //表示禁止此ip访问 //设置初始化参数 bean.setInitParameters(initParameters); return bean; } }
-
配置完毕,访问:http://localhost:8080/druid/login.html,并登录账号密码;进行一次SQL查询,再返回查看Druid后台如下
-
配置 Druid web 监控 filter 过滤器
//配置 Druid 监控 之 web 监控的 filter //WebStatFilter:用于配置Web和Druid数据源之间的管理关联监控统计 @Bean public FilterRegistrationBean webStatFilter() { FilterRegistrationBean bean = new FilterRegistrationBean(); bean.setFilter(new WebStatFilter()); //exclusions:设置哪些请求进行过滤排除掉,从而不进行统计 Map<String, String> initParameters = new HashMap<>(); initParameters.put("exclusions", "*.js,*.css,/druid/*,/jdbc/*"); bean.setInitParameters(initParameters); //"/*" 表示过滤所有请求 bean.setUrlPatterns(Arrays.asList("/*")); return bean; }
-
在工作中,按需求进行配置即可,主要用作监控!
8.3、整合MyBatis框架
-
官方文档:http://mybatis.org/spring-boot-starter/mybatis-spring-boot-autoconfigure/
-
Maven仓库地址:org.mybatis.spring.boot » mybatis-spring-boot-starter
8.3.1、整合测试
-
导入 MyBatis 所需要的依赖
<!-- mybatis-spring-boot-starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>
-
配置数据库连接信息(不变)
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Druid数据源 #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
测试数据库是否连接成功(不变)
package com.cwlin; import com.alibaba.druid.pool.DruidDataSource; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @SpringBootTest class SpringBoot04DataApplicationTests { //SpringBoot只要配置了数据源,就自动将数据源封装进IOC容器,用户无需配置数据源组件,直接取出 @Autowired DataSource dataSource; @Test void contextLoads() throws SQLException { //查看默认数据源 System.out.println(dataSource.getClass()); //获得数据库连接 Connection connection = dataSource.getConnection(); System.out.println(connection); //测试Druid数据源 DruidDataSource druidDataSource = (DruidDataSource) dataSource; System.out.println("druidDataSource 数据源最大连接数:" + druidDataSource.getMaxActive()); System.out.println("druidDataSource 数据源初始化连接数:" + druidDataSource.getInitialSize()); //关闭数据库连接 connection.close(); } }
-
在 pojo 包下创建实体类 User,并导入 Lombok
package com.cwlin.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; private String pwd; }
-
在 mappe 包下创建对应的 Mapper 接口,或者在main方法上添加注解 @MapperScan(“com.cwlin.mapper”)
package com.cwlin.mapper; import com.cwlin.pojo.User; import org.apache.ibatis.annotations.Mapper; import org.springframework.stereotype.Repository; import java.util.List; //这个注解表示这是MyBatis的Mapper类 @Mapper @Repository public interface UserMapper { //public static final int age =18; //公共静态常量 List<User> selectUserList(); User selectUserById(int id); int insertUser(User user); int updateUser(User user); int deleteUser(int id); }
-
整合 mybatis
#整合mybatis mybatis: type-aliases-package: com.cwlin.pojo #解决绑定异常:mapper.xml最好和接口的包名路径一致 mapper-locations: classpath:mybatis/mapper/*.xml
-
在 resources/mybatis 路径下创建对应的Mapper映射文件,或者使用 SQL 注解标记 Mapper 接口中的方法
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.hxh.mapper.UserMapper"> <select id="selectUserList" resultType="User"> select * from user </select> <select id="selectUserById" resultType="User"> select * from user where id = #{id} </select> <insert id="insertUSer" parameterType="User"> insert into User (id,name,pwd) values (#{id},#{name},#{pwd}) </insert> <update id="updateUser" parameterType="User"> update user set name=#{name},pwd=#{pwd} where id=#{id} </update> <delete id="deleteUser" parameterType="User"> delete from user where id = #{id} </delete> </mapper>
package com.cwlin.mapper; import com.cwlin.pojo.User; import org.apache.ibatis.annotations.*; import org.springframework.stereotype.Repository; import java.util.List; //这个注解表示这是MyBatis的Mapper类 @Mapper @Repository public interface UserMapper { //public static final int age =18; //公共静态常量 @Select("select * from user") List<User> selectUserList(); @Select("select * from user where id = #{id}") User selectUserById(int id); @Insert("insert into User (id,name,pwd) values (#{id},#{name},#{pwd})") int insertUser(User user); @Update("update user set name=#{name},pwd=#{pwd} where id=#{id}") int updateUser(User user); @Delete("delete from user where id = #{id}") int deleteUser(int id); }
-
maven配置资源过滤问题(可以省略)
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>
-
编写 User 类的 UserController,并进行测试:
- http://localhost:8080/mybatis/selectUserList
- http://localhost:8080/mybatis/selectUserById
- http://localhost:8080/mybatis/insertUser
- http://localhost:8080/mybatis/updateUser
- http://localhost:8080/mybatis/deleteUser
- http://localhost:8080/druid
package com.cwlin.controller; import com.cwlin.mapper.UserMapper; import com.cwlin.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/mybatis") public class UserController { @Autowired private UserMapper userMapper; @GetMapping("/selectUserList") public List<User> selectUserList(){ List<User> userList = userMapper.selectUserList(); for (User user : userList) { System.out.println(user); } return userList; } @GetMapping("/selectUserById") public User selectUserById(){ User user = userMapper.selectUserById(1); System.out.println(user); return user; } @GetMapping("/insertUser") public String addUser(){ int i = userMapper.insertUser(new User(6, "coder_lcw", "123456")); System.out.println("返回在数据库中影响的行数为:"+i); return "insertUser: OK!"; } @GetMapping("/updateUser") public String updateUser(){ int i = userMapper.updateUser(new User(6, "lcw", "123456")); System.out.println("返回在数据库中影响的行数为:"+i); return "updateUser: OK!"; } @GetMapping("/deleteUser") public String deleteUser(){ int i = userMapper.deleteUser(6); System.out.println("返回在数据库中影响的行数为:"+i); return "deleteUser: OK!"; } }
8.3.2、建议使用注解
1、基础注解
-
MyBatis 主要提供了以下CRUD注解:
- @Select
- @Insert
- @Update
- @Delete
-
配置驼峰映射
mybatis: configuration: #配置项:开启下划线到驼峰的自动转换. 作用:将数据库字段根据驼峰规则自动注入到对象属性。 map-underscore-to-camel-case: true
2、映射注解
-
为了解决对象属性和字段驼峰不一致的问题,我们可以使用映射注解@Results来指定映射关系。
-
Mybatis主要提供这些映射注解:
- @Results 用于填写结果集的多个字段的映射关系
- @Result 用于填写结果集的单个字段的映射关系
- @ResultMap 根据ID关联XML里面
<resultMap>
-
例如上面的list方法,我们可以在查询SQL的基础上,指定返回的结果集的映射关系,其中property表示实体对象的属性名,column表示对应的数据库字段名。
@Results({ @Result(property = "userId", column = "USER_ID"), @Result(property = "username", column = "USERNAME"), @Result(property = "password", column = "PASSWORD"), @Result(property = "mobileNum", column = "PHONE_NUM") }) @Select("select * from t_user") List<User> list();
-
为了方便演示和免除手工编写映射关系的烦恼,这里提供了一个快速生成映射结果集的方法,具体内容如下:
//用于获取结果集的映射关系 public static String getResultsStr(Class origin) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("@Results({\n"); for (Field field : origin.getDeclaredFields()) { String property = field.getName(); //映射关系:对象属性(驼峰)->数据库字段(下划线) String column = new PropertyNamingStrategy.SnakeCaseStrategy().translate(field.getName()).toUpperCase(); stringBuilder.append(String.format("@Result(property = \"%s\", column = \"%s\"),\n", property, column)); } stringBuilder.append("})"); return stringBuilder.toString(); }
3、高级注解
-
MyBatis-3 主要提供了以下CRUD的高级注解:
- @SelectProvider
- @InsertProvider
- @UpdateProvider
- @DeleteProvider
-
这些高级注解主要用于动态SQL,这里以@SelectProvider 为例,主要包含两个注解属性,其中type表示工具类,method 表示工具类的某个方法,用于返回具体的SQL。
-
添加UserMapper接口用于数据查询:
package com.cwlin.mapper; @Mapper public interface UserMapper { //方式1:使用注解编写SQL。 @Select("select * from t_user") List<User> list(); //方式2:使用注解指定某个工具类的方法来动态编写SQL. @SelectProvider(type = UserSqlProvider.class, method = "listByUsername") List<User> listByUsername(String username); //延伸:上述两种方式都可以附加@Results注解来指定结果集的映射关系. //注意:如果符合下划线转驼峰的匹配项可以直接省略不写。 @Results({ @Result(property = "userId", column = "USER_ID"), @Result(property = "username", column = "USERNAME"), @Result(property = "password", column = "PASSWORD"), @Result(property = "mobileNum", column = "PHONE_NUM") }) @Select("select * from t_user") List<User> listSample(); //延伸:无论什么方式,如果涉及多个参数,则必须加上@Param注解;否则无法使用EL表达式获取参数。 @Select("select * from t_user where username like #{username} and password like #{password}") User get(@Param("username") String username, @Param("password") String password); @SelectProvider(type = UserSqlProvider.class, method = "getBadUser") User getBadUser(@Param("username") String username, @Param("password") String password); }
-
添加UserSqlProvider,用于生成SQL的工具类:
package com.cwlin.mapper; //作用:根据复杂的业务需求来动态生成SQL //目标:使用Java工具类来替代传统的XML文件(例如:UserSqlProvider.java <-- UserMapper.xml) public class UserSqlProvider { //方式1:在工具类的方法里,可以自己手工编写SQL public String listByUsername(String username) { return "select * from t_user where username =#{username}"; } //方式2:也可以根据官方提供的API来编写动态SQL public String getBadUser(@Param("username") String username, @Param("password") String password) { return new SQL() {{ SELECT("*"); FROM("t_user"); if (username != null && password != null) { WHERE("username like #{username} and password like #{password}"); } else { WHERE("1=2"); } }}.toString(); } }
8.3.3、员工管理系统的 Mybatis 框架
8.4、整合Redis
- 详细内容见第12章以及后续博客,这里不展开介绍!
9、SpringBoot 安全框架
-
在web开发中,安全第一位。即使,这是非功能性需求
-
认证(登录)、授权(VIP1,VIP2等)、功能权限、访问权限、菜单权限
-
我们使用过滤器、拦截器需要写大量的原生代码,这样很不方便
-
因此,在网址设计之初,就应该考虑到权限验证的安全问题,其中 SpringSecurity、Shiro 使用很多
9.1、Spring Security
9.1.1、简介
-
Spring Security 是针对 spring 项目的安全框架,也是 Springboot 底层安全模块默认的技术选型。它可以实现强大的Web安全控制,只需要引入
spring-boot-spring-security
模块的依赖,进行少量的配置,就可以实现强大的安全管理。 -
SpringBoot 中的 SpringSecurity 依赖:
<!--security--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
记住几个类:
WebSecurityConfigurerAdapter
:自定义Security策略AuthenticationManagerBuilder
:自定义认证策略@EnableWebSecurity
:开启WebSecurity模式
-
两个单词:(en是认证,or是权限)
- 认证方式:
Authentication
- 权限:
Authorization
- 认证方式:
9.1.2、授权和认证
-
导入静态资源(略)
-
在
controller
包下,编写RouterController
类,实现页面跳转package com.cwlin.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class RouterController { @RequestMapping({"/","/index"}) public String index(){ return "index"; } @RequestMapping("/toLogin") public String toLogin(){ return "views/login"; } @RequestMapping("/{level}/{id}") public String level1(@PathVariable("level") String level, @PathVariable("id") String id){ return "views/"+level+"/"+id; } }
-
写一个
SecurityConfig
类继承WebSecurityConfigurerAdapter
,使用@EnableWebSecurity
注解开启web安全服务- 地址授权:使用
HttpSecurity security
- 账户认证和给予权限:使用
AuthenticationManagerBuilder builder
package com.cwlin.config; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { //授权 @Override protected void configure(HttpSecurity http) throws Exception { //请求授权的规则:首页所有人都可访问,功能页需要相应权限 http.authorizeRequests() .antMatchers("/").permitAll() .antMatchers("/level1/**").hasRole("vip1") .antMatchers("/level2/**").hasRole("vip2") .antMatchers("/level3/**").hasRole("vip3"); //没有权限,默认跳转到登录页面 http.formLogin(); } //认证 //Spring Boot 2.1.x 可以直接使用 //Spring Security 5.0 以后默认需要密码加密方式,设置密码编码 passwordEncoder @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //这些数据一般从数据库中读取,这里是在内存中测试数据 auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()) .withUser("cwlin").password(new BCryptPasswordEncoder().encode("123456")) .roles("vip2","vip3") .and() .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")) .roles("vip1","vip2","vip3") .and() .withUser("guest").password(new BCryptPasswordEncoder().encode("123456")) .roles("vip1"); } }
- 地址授权:使用
9.1.3、登录注销和记住我
-
登录
- 开启登录功能:
.formLogin()
,SpringBoot默认的登录页面:/login
- 定制登录页:
.loginPage("/toLogin")
- 修改表单提交的用户名和密码参数:
.usernameParameter("username").passwordParameter("password")
- 指定表单提交的 url:
.loginProcessingUrl("/user/login")
//没有权限,跳转到默认登录页面:http://localhost:8080/login http.formLogin().loginPage("/toLogin") //自定义登录页面 .usernameParameter("username").passwordParameter("password") //自定义用户名和密码的参数 .loginProcessingUrl("/user/login"); //指定表单提交url
- 开启登录功能:
-
注销
- 开启注销功能:
.logout()
,SpringBoot默认的注销页面:/logout
- 注销成功后跳转到
"/"
控制器 - 关闭 csrf 功能:
.csrf().disable()
//开启注销功能,跳转到默认注销首页:http://localhost:8080/logout http.logout().logoutSuccessUrl("/"); //注销成功后跳转到 "/" 的Controller http.csrf().disable(); //版本不同问题,可能会出现注销失败,关闭csrf
- 开启注销功能:
-
记住我
- 开启记住我功能:
.rememberMe()
,本质就是存一个cookies,默认保存2周 - 自定义前端显示记住我的 name 属性:
.rememberMeParameter("remember")
- 在前端
login.html
中添加 checkbox 复选框,与记住我的 name 属性绑定
//开启记住我功能,本质就是记住一个cookies,默认保存2周 http.rememberMe().rememberMeParameter("remember");
<div class="field"> <input type="checkbox" name="remember"> 记住我 </div>
- 开启记住我功能:
9.1.4、前端权限验证
-
要实现前端根据用户权限选择性展示元素,使用 SpringSecurity 和 thymeleaf 的整合包,即导入依赖:
thymeleaf-extras-springsecurity5
,并在 html 中进行引用<dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> <version>3.0.4.RELEASE</version> </dependency>
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
-
在 RouterController 类中,添加 toUserCenter 方法,用于跳转到用户中心
@RequestMapping("/user/toUserCenter") public String toUserCenter(){ return "views/userCenter"; }
-
修改前端页面
index.html
- 登录授权检查:
sec:authorize="!isAuthenticated()"
- 如果登录,则显示用户名、角色和注销;如果未登录,则显示登录
<!--登录注销--> <div class="right menu"> <!--如果未登录:登录--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/toLogin}"> <i class="address card icon"></i>登录 </a> </div> <!--如果已登录:用户名,注销--> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/user/toUserCenter}"> <i class="address card icon"></i> 用户名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i>注销 </a> </div> </div>
- 登录授权检查:
-
根据用户角色的权限,动态展现功能菜单
<div> <br> <!--根据用户角色的权限,动态展现功能菜单--> <div class="ui three column stackable grid"> <div class="column" sec:authorize="hasRole('vip1')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 1</h5> <hr> <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div> <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div> <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip2')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 2</h5> <hr> <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div> <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div> <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div> </div> </div> </div> </div> <div class="column" sec:authorize="hasRole('vip3')"> <div class="ui raised segment"> <div class="ui"> <div class="content"> <h5 class="content">Level 3</h5> <hr> <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div> <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div> <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div> </div> </div> </div> </div> </div> </div>
9.2、Shiro
9.2.1、简介
-
什么是 Shiro:
- 官网:Apache Shiro | Simple. Java. Security.
- Github:https://github.com/apache/shiro
- Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications.
- Apache Shiro™ 是一个功能强大且易于使用的 Java 安全框架,可执行身份验证、授权、加密和会话管理。借助 Shiro 易于理解的 API,您可以快速轻松地保护任何应用程序——从最小的移动应用程序到最大的 Web 和企业应用程序(JavaSE 和 JavaEE环境)。
-
Shiro 的功能:
- Authentication:身份认证/登录,验证用户是不是拥有相应的身份
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能进行什么操作,如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限
- Session Management:会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境,也可以是Web 环境的
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储
- Web Support:Web 支持,可以非常容易的集成到Web 环境
- Caching:缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率
- Concurrency:Shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去
- Testing:提供测试支持
- “Run As”:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
-
Shiro架构(外部)
- 核心三大对象:用户Subject,管理用户SecurityManager,连接数据Realms:
Subject
:应用代码直接交互的对象是Subject,也就是说Shiro的对外API 核心就是Subject。Subject 代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等;与Subject 的所有交互都会委托给SecurityManager;Subject 其实是一个门面,SecurityManager才是实际的执行者。SecurityManager
:安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且其管理着所有Subject;可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC中DispatcherServlet的角色。Realm
:Shiro从Realm 获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm 看成DataSource。
- 另一种解释:
Subject
:即“当前操作用户”。但是,在Shiro中,Subject这一概念并不仅仅指人,也可以是第三方进程、后台帐户(Daemon Account)或其他类似事物。它仅仅意味着“当前跟软件交互的东西”。SecurityManager
:它是Shiro框架的核心,典型的Facade模式,Shiro通过SecurityManager来管理内部组件实例,并通过它来提供安全管理的各种服务。Realm
: Realm充当了Shiro与应用安全数据间的“桥梁”或者“连接器”。也就是说,当对用户执行认证(登录)和授权(访问控制)验证时,Shiro会从应用配置的Realm中查找用户及其权限信息。从这个意义上讲,Realm实质上是一个安全相关的DAO:它封装了数据源的连接细节,并在需要时将相关数据提供给Shiro。当配置Shiro时,你必须至少指定一个Realm,用于认证和(或)授权。配置多个Realm是可以的,但是至少需要一个。
- 核心三大对象:用户Subject,管理用户SecurityManager,连接数据Realms:
-
Shiro架构(内部)
- Subject:任何可以与应用交互的“用户”。
- SecurityManager:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证、授权、会话及缓存的管理。
- Authenticator:负责Subject 认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了。
- Authorizer:授权器、即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能。
- Realm:可以有1 个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC 实现,也可以是内存实现等等;由用户提供;所以一般在应用中都需要实现自己的Realm。
- SessionManager:管理Session 生命周期的组件;而Shiro并不仅仅可以用在Web 环境,也可以用在如普通的JavaSE环境。
- CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能。
- Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密。
9.2.2、快速开始
-
创建模块
SpringBoot-06-Shiro/Shiro-01-QuickStart
,导入pom依赖<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>SpringBoot-06-Shiro</artifactId> <groupId>com.cwlin</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>Shiro-01-QuickStart</artifactId> <dependencies> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.7.1</version> </dependency> <!-- configure logging --> <!-- https://mvnrepository.com/artifact/org.slf4j/jcl-over-slf4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>jcl-over-slf4j</artifactId> <version>2.0.0-alpha3</version> </dependency> <!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-log4j12 --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>2.0.0-alpha3</version> </dependency> <!--可以省略--> <!-- https://mvnrepository.com/artifact/log4j/log4j --> <!--<dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>--> </dependencies> <properties> <maven.compiler.source>15</maven.compiler.source> <maven.compiler.target>15</maven.compiler.target> </properties> </project>
-
在 resources 路径下,创建 shiro 配置文件 log4j.properties 和 shiro.ini(需要下载ini插件)
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # General Apache libraries log4j.logger.org.apache=WARN # Spring log4j.logger.org.springframework=WARN # Default Shiro logging log4j.logger.org.apache.shiro=INFO # Disable verbose logging log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
# ============================================================================= # Quickstart INI Realm configuration # # For those that might not understand the references in this file, the # definitions are all based on the classic Mel Brooks' film "Spaceballs". ;) # ============================================================================= # ----------------------------------------------------------------------------- # Users and their assigned roles # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc # ----------------------------------------------------------------------------- [users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ----------------------------------------------------------------------------- # Roles with assigned permissions # # Each line conforms to the format defined in the # org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ----------------------------------------------------------------------------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
-
在 Java 文件夹下,创建 Quickstart 类,测试 Shiro 使用
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple Quickstart application showing how to use Shiro's API. * * @since 0.9 RC2 */ public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } }
-
输出日志结果
2021-08-13 17:35:12,781 INFO [org.apache.shiro.session.mgt.AbstractValidatingSessionManager] - Enabling session validation scheduler... 2021-08-13 17:35:13,240 INFO [Quickstart] - Retrieved the correct value! [aValue] 2021-08-13 17:35:13,248 INFO [Quickstart] - User [lonestarr] logged in successfully. 2021-08-13 17:35:13,249 INFO [Quickstart] - May the Schwartz be with you! 2021-08-13 17:35:13,249 INFO [Quickstart] - You may use a lightsaber ring. Use it wisely. 2021-08-13 17:35:13,250 INFO [Quickstart] - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
9.2.3、Subject 分析
-
源码分析
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // 0、根据shiro.ini配置文件,创建factory对象,设置SecurityUtils工具类 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); // 1、获取当前的用户对象Subject Subject currentUser = SecurityUtils.getSubject(); // 2、通过当前用户拿到Session(不需要 Web 或 EJB 容器!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // 3、判断当前的用户是否被认证 if (!currentUser.isAuthenticated()) { // 3.1、通过用户名和密码生成一个token令牌 UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); // 3.2、设置记住我功能 token.setRememberMe(true); // 3.3、执行登录操作 try { currentUser.login(token); } catch (UnknownAccountException uae) { // 未知账户异常 log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { // 密码错误异常 log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { //账户锁定异常 log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { // 认证异常 //unexpected condition? error? } } // 4、获得当前用户的认证 //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); // 5、判断当前用户是否拥有什么角色,eg: schwartz //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } // 6、判断当前用户是否具有权限(粗粒度) //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } // 6、判断当前用户是否具有权限(细粒度) //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } // 7、注销当前用户 currentUser.logout(); System.exit(0); } }
-
方法总结(在 Spring Security 中都有,只是方法名称不一样)
-
获取当前的用户对象Subject:Subject currentUser = SecurityUtils.getSubject();
-
通过当前用户拿到Session:Session session = currentUser.getSession();
-
判断当前的用户是否被认证:currentUser.isAuthenticated()
执行登录操作:currentUser.login(token);
通过用户名和密码生成一个token令牌:new UsernamePasswordToken(“lonestarr”, “vespa”);
设置记住我功能:token.setRememberMe(true);
-
获得当前用户的认证:currentUser.getPrincipal()
-
判断当前用户是否拥有什么角色,eg: schwartz:currentUser.hasRole(“schwartz”)
-
判断当前用户是否具有权限(粗粒度):currentUser.isPermitted(“lightsaber:wield”)
判断当前用户是否具有权限(细粒度):currentUser.isPermitted(“winnebago:drive:eagle5”)
-
注销当前用户:currentUser.logout();
-
9.2.4、SpringBoot 集成 Shiro
0、Maven依赖
- 在这里,先给出需要用到的所有 pom 依赖。接下来,会逐个导入使用。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.cwlin</groupId>
<artifactId>Shiro-02-SpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Shiro-02-SpringBoot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
</properties>
<dependencies>
<!-- thymeleaf-extras-shiro -->
<dependency>
<groupId>com.github.theborakompanioni</groupId>
<artifactId>thymeleaf-extras-shiro</artifactId>
<version>2.1.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<!-- log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<!-- mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Shiro整合Spring -->
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.7.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.cwlin.Shiro02SpringBootApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
1、Shiro 环境搭建
-
新建一个模块 Shiro-02-SpringBoot,勾选依赖:Spring Web、Thymeleaf、Lombox等
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cwlin</groupId> <artifactId>Shiro-02-SpringBoot</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Shiro-02-SpringBoot</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.cwlin.Shiro02SpringBootApplication</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
-
编写三个前端页面:index.html、add.html、update.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <div> <h1>首页</h1> <p th:text="${msg}"></p> <hr> <a th:href="@{/user/insert}">insert</a> | <a th:href="@{/user/update}">update</a> </div> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>insert</h1> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>update</h1> </body> </html>
-
在 controller 包下,新建一个 MyController 类用于测试
package com.cwlin.Controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class MyController { @RequestMapping({"/","/index"}) public String toIndex(Model model) { model.addAttribute("msg","Hello, Shiro!"); return "index"; } @RequestMapping("/user/insert") public String add() { return "user/insert"; } @RequestMapping("/user/update") public String update() { return "user/update"; } }
-
在pom中,导入Shiro整合Spring的包
<!-- Shiro整合Spring --> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.7.1</version> </dependency>
-
在config包下,新建 ShiroConfig 配置类
-
使用自定义UserRealm类,创建Realm对象
-
创建DefaultWebSecurityManager对象,并关联Realm对象
-
创建ShiroFilterFactoryBean对象,关联SecurityManager对象
package com.cwlin.config; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; //自定义UserRealm public class UserRealm extends AuthorizingRealm { //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权操作:doGetAuthorizationInfo"); return null; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证操作:doGetAuthenticationInfo"); return null; } }
package com.cwlin.config; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ShiroConfig { //1.使用自定义UserRealm类,创建Realm对象 @Bean(name="Realm") public UserRealm getUserRealm(){ return new UserRealm(); } //2.创建DefaultWebSecurityManager对象,并关联Realm对象 @Bean(name="SecurityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager( @Qualifier("Realm") UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联Realm对象 securityManager.setRealm(userRealm); return securityManager; } //3.创建ShiroFilterFactoryBean对象,关联SecurityManager对象 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean( @Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //关联SecurityManager对象 bean.setSecurityManager(defaultWebSecurityManager); return bean; } }
-
2、Shiro 登录拦截
-
在
ShiroConfig
类的getShiroFilterFactoryBean
方法中,添加Shiro的内置过滤器,具体配置如下:- anon: 无需认证就可以访问
- authc: 必须认证了才能访问
- user: 必须拥有
记住我
功能才能用 - perms: 拥有对某个资源的权限才能访问
- role: 拥有某个角色权限才能访问
//创建ShiroFilterFactoryBean对象,关联SecurityManager对象 @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean( @Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager){ ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); //关联SecurityManager对象 bean.setSecurityManager(defaultWebSecurityManager); //添加Shiro的内置过滤器 Map<String, String> filterMap = new LinkedHashMap<>(); filterMap.put("/user/insert","authc"); filterMap.put("/user/update","authc"); //这里支持使用通配符 filterMap.put("/user/*","authc"); bean.setFilterChainDefinitionMap(filterMap); return bean; }
-
此时,点击首页的 insert 或者 update 之后,返回 404 错误。
-
编写拦截后跳转的登录页面
login.html
(这里直接使用9.1节中的login.html
),注意登录跳转请求:th:action="@{/login}"
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <title>登录</title> <!--semantic-ui--> <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet"> </head> <body> <!--主容器--> <div class="ui container"> <div class="ui segment"> <div style="text-align: center"> <h1 class="header">登录</h1> </div> <div class="ui placeholder segment"> <div class="ui column very relaxed stackable grid"> <div class="column"> <div class="ui form"> <form th:action="@{/login}" method="post"> <div class="field"> <label>Username</label> <div class="ui left icon input"> <input type="text" placeholder="Username" name="username"> <i class="user icon"></i> </div> </div> <div class="field"> <label>Password</label> <div class="ui left icon input"> <input type="password" name="password"> <i class="lock icon"></i> </div> </div> <div class="field"> <input type="checkbox" name="remember"> 记住我 </div> <input type="submit" class="ui blue submit button"/> </form> </div> </div> </div> </div> <div style="text-align: center"> <div class="ui label"> </i>注册 </div> <br><br> <small>https://blog.youkuaiyun.com/coder_lcw</small> </div> <div class="ui segment" style="text-align: center"> <h3>Spring Security Study by cwlin</h3> </div> </div> </div> <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script> <script th:src="@{/qinjiang/js/semantic.min.js}"></script> </body> </html>
-
在
MyController
类中添加toLogin
方法,实现登录请求跳转;在ShiroConfig
类的getShiroFilterFactoryBean
方法中,设置登录请求页面@RequestMapping("/toLogin") public String toLogin() { return "login"; }
//设置登录请求页面 bean.setLoginUrl("/toLogin");
3、Shiro 用户认证
-
在
MyController
类中添加login
方法,处理用户在登录页面提交的表单,注意:@RequestMapping("/login")
要对应login.html
中的登录跳转请求th:action="@{/login}"
@RequestMapping("/login") public String login(String username, String password, Model model) { //获取当前用户 Subject subject = SecurityUtils.getSubject(); //封装用户的登录数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); //执行登录操作 try { subject.login(token); return "index"; } catch (UnknownAccountException e) { //用户名不存在 model.addAttribute("msg","用户名不存在"); return "login"; } catch (IncorrectCredentialsException e) { //密码错误 model.addAttribute("msg","密码错误"); return "login"; } catch (LockedAccountException e) { //账户锁定 model.addAttribute("msg","账户锁定"); return "login"; } catch (AuthenticationException e) { //认证异常 model.addAttribute("msg","认证出现异常"); return "login"; } }
-
在
login.html
中,添加 “msg” 的显示<div><p th:text="${msg}" style="color: red;"></p></div>
-
此时,用户输入登录信息,会提示用户名错误。
-
在自定义的
UserRealm
类中,重写doGetAuthenticationInfo
方法,实现用户认证!//认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证操作:doGetAuthenticationInfo"); String username = "admin"; String password = "123456"; //获取当前用户的数据令牌 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //判断用户名 if(!token.getUsername().equals((username))){ return null; //抛出 UnknownAccountException 异常 } //判断密码(Shiro实现) return new SimpleAuthenticationInfo("",password,""); }
-
关于
new SimpleAuthenticationInfo("", currentUser.getPwd(), "")
的解释:- 第一个参数用于查询得到用户作为subject,这个subject等待
AuthorizationInfo
使用; - 第二个参数是用户密码;
- 第三个参数
realmName
。
- 第一个参数用于查询得到用户作为subject,这个subject等待
4、Shiro 整合 Mybatis
-
在pom中导入依赖:mysql、druid、log4j、mybatis-spring-boot-starter
<!-- mysql --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.23</version> </dependency> <!-- druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency> <!-- log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- mybatis-spring-boot-starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.0</version> </dependency>
-
编写配置文件 application.yml
spring: datasource: username: root password: 123456 url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&userUnicode=true&chctacterEncoding=utf-8 driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Druid数据源 #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500 #整合mybatis mybatis: type-aliases-package: com.cwlin.pojo
-
编写 User 类和 UserMapper 类
package com.cwlin.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class User { private Integer id; private String name; private String pwd; }
package com.cwlin.mapper; import com.cwlin.pojo.User; import org.apache.ibatis.annotations.*; import org.springframework.stereotype.Repository; import java.util.List; @Mapper @Repository public interface UserMapper { @Select("select * from user where name = #{name}") User selectUserByName(String name); }
-
编写 UserService 接口和 UserServiceImpl 实现类
package com.cwlin.service; import com.cwlin.pojo.User; import org.apache.ibatis.annotations.*; import java.util.List; public interface UserService { @Select("select * from user where name = #{name}") User selectUserByName(String name); }
package com.cwlin.service; import com.cwlin.mapper.UserMapper; import com.cwlin.pojo.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User selectUserByName(String name) { return userMapper.selectUserByName(name); } }
-
在测试类中进行测试
package com.cwlin; import com.cwlin.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class Shiro02SpringBootApplicationTests { @Autowired UserService userService; @Test void contextLoads() { System.out.println(userService.selectUserByName("cwlin")); } }
-
在自定义的
UserRealm
类中,重写doGetAuthenticationInfo
方法,通过连接真实数据库实现用户认证!//认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证操作:doGetAuthenticationInfo"); //获取当前用户的数据令牌 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //连接真实数据库 User user = userService.selectUserByName(token.getUsername()); //判断用户名 if(user == null){ return null; //抛出 UnknownAccountException 异常 } //判断密码(Shiro实现) return new SimpleAuthenticationInfo("",user.getPwd(),""); }
-
测试密码加密类型:
-
SimpleCredentialsMatcher
加密(默认) -
MD5加密
-
MD5盐值加密
-
所有加密
-
-
修改 MyController 类中的登录请求 login,提交方式改为 RequestMethod.POST
@RequestMapping(value = "/login", method = RequestMethod.POST) public String login(String username, String password, Model model){ //...... }
5、Shiro 用户授权
-
在
ShiroConfig
类的getShiroFilterFactoryBean
方法中,修改授权配置//权限授权,访问url需要权限,没有授权会跳转到为授权页面 filterMap.put("/user/insert", "perms[user:insert]"); filterMap.put("/user/update", "perms[user:update]");
-
访问页面并登录,点击 insert 按钮,页面报错如下:
There was an unexpected error (type=Unauthorized, status=401).
-
在
MyController
类中添加login
方法,处理未授权的跳转请求;同时,在ShiroConfig
类的getShiroFilterFactoryBean
方法中设置未授权请求页面@RequestMapping("/unauthorized") @ResponseBody public String unauthorized() { return "未经授权,无法访问此页面!"; }
//设置未授权请求页面 bean.setUnauthorizedUrl("/unauthorized");
-
再次测试,页面显示:
"未经授权,无法访问此页面!"
。因此,需要在自定义的UserRealm
类中进行用户授权 -
在 mybatis 数据库中,为 user 表添加 perms 字段,同时修改 User 实体类属性
id name pwd perms 1 cwlin 123456 user:update 2 张三 abcdef None 3 李四 987654 None 4 王五 654321 None 5 李五 666666 None 6 coder_lcw 123456 user:update 7 admin 123456 user:insert,update 8 guest 123456 user:select -
在自定义的
UserRealm
类中,重写doGetAuthorizationInfo
方法,修改doGetAuthenticationInfo
方法的返回值,实现用户授权!package com.cwlin.config; import com.cwlin.pojo.User; import com.cwlin.service.UserService; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.springframework.beans.factory.annotation.Autowired; //自定义UserRealm public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("执行授权操作:doGetAuthorizationInfo"); //初始化授权消息对象 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //获取当前登录用户 Subject subject = SecurityUtils.getSubject(); //获取User对象 User currentUser = (User) subject.getPrincipal(); //设置当前用户的权限 info.addStringPermission(currentUser.getPerms()); return info; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { System.out.println("执行认证操作:doGetAuthenticationInfo"); //获取当前用户的数据令牌 UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; //连接真实数据库 User user = userService.selectUserByName(token.getUsername()); //判断用户名 if(user == null){ return null; //抛出 UnknownAccountException 异常 } //判断密码(Shiro实现) return new SimpleAuthenticationInfo(user, user.getPwd(),""); } }
-
测试各个用户的权限!
6、Shiro 整合 Thymeleaf
-
导入 thymeleaf-extras-shiro 整合包
<!-- thymeleaf-extras-shiro --> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.1.0</version> </dependency>
-
在
ShiroConfig
类的getShiroFilterFactoryBean
方法中,配置 ShiroDialect//配置ShiroDialect,用于整合Shiro-Thymeleaf @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
-
修改 index.html 页面,根据用户权限动态显示前端页面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"> <head> <meta charset="UTF-8"> <title>首页</title> </head> <body> <div> <h1>首页</h1> <p th:text="${msg}"></p> <!--用session实现,配合 UserRealm 中的session实现--> <!--<div th:if="${session.loginUser==null}"> <a th:href="@{/toLogin}">登录</a> </div>--> <div shiro:notAuthenticated> <a th:href="@{/toLogin}">登录</a> </div> <hr> <div shiro:hasPermission="user:insert"> <a th:href="@{/user/insert}">insert</a> </div> <div shiro:hasPermission="user:update"> <a th:href="@{/user/update}">update</a> </div> </div> </body> </html>
9.2.5、总结
- 一个完美的解释:让 Apache Shiro 保护你的应用
- Apache Shiro 是一个功能齐全、健壮、通用的 Java 安全框架,你可以用其为你的应用护航。通过简化应用安全的四个领域,即认证、授权、会话管理和加密,在真实应用中,应用安全能更容易被理解和实现。Shiro 的简单架构和兼容 JavaBean 使其几乎能够在任何环境下配置和使用。附加的 Web 支持和辅助功能,比如多线程和测试支持,让这个框架为应用安全提供了“一站式”服务。Apache Shiro 开发团队将继续前进,精炼代码库和支持社区。随着持续被开源和商业应用采纳,可以预期 Shiro 会继续发展壮大。
10、Swagger 文档注释
10.1、简介
学习目标
- 了解 Swagger 的概念及作用
- 掌握在项目中集成 Swagger 自动生成 API 文档
前后端分离
- 前端 -> 前端控制层、视图层
- 后端 -> 后端控制层、服务层、数据访问层
- 前后端通过API进行交互
- 前后端相对独立且松耦合
- 前后端甚至可以部署在不同的服务器上
产生的问题
- 前后端集成,前端或者后端无法做到 “及时协商,尽早解决”,最终导致问题集中爆发。
解决方案
- 首先定义schema [ 计划的提纲 ],并实时跟踪最新的API,降低集成风险
- 前后端分离
- 前端测试后端接口:postman
- 后端提供接口,需要实时更新最新的消息及改动
Swagger
- 号称世界上最流行的API框架
- Restful Api 文档在线自动生成器 => API 文档 与API 定义同步更新
- 直接运行,在线测试 API
- 支持多种语言,如:Java,PHP等
- 官网:https://swagger.io/
10.2、SpringBoot 集成 Swagger
10.2.1、配置 Swagger 环境
-
SpringBoot集成Swagger:springfox,需要两个jar包:
- Springfox-swagger2
- swagger-springmvc
-
新建一个 SpringBoot 项目:SpringBoot-07-Swagger,勾选依赖:Spring Web、Thymeleaf、Lombox等
-
添加 Maven 依赖:
springfox-swagger2
、springfox-swagger-ui
<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>3.0.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>3.0.0</version> </dependency>
-
在 controller 包下,编写 HelloController 类,用于测试项目正常运行,测试请求:http://localhost:8080/hello
package com.cwlin.swagger.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @RequestMapping(value = "/hello") public String hello(){ return "Hello, swagger!"; } }
-
在 config 包下,编写 SwaggerConfig 类(空类),用于配置 Swagger,测试请求:http://localhost:8080/swagger-ui.html
package com.cwlin.swagger.config; import org.springframework.context.annotation.Configuration; import springfox.documentation.swagger2.annotations.EnableSwagger2; @Configuration @EnableSwagger2 //开启Swagger2 public class SwaggerConfig { }
-
此时,页面出现 404 错误。这是因为,在 Swagger3.0 中,swagger-ui.html的位置发生了变化
-
解决方案:导入
springfox-boot-starter 3.0.0
依赖,代替springfox-swagger2
、springfox-swagger-ui
,并在主程序上添加 @EnableOpenApi
注解<!-- 使用3.0版本需要添加springfox启动器 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <!--<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>3.0.0</version> </dependency>--> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <!--<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>3.0.0</version> </dependency>-->
package com.cwlin.swagger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import springfox.documentation.oas.annotations.EnableOpenApi; @SpringBootApplication @EnableOpenApi public class SpringBoot07SwaggerApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot07SwaggerApplication.class, args); } }
-
测试请求:http://localhost:8080/swagger-ui/index.html,可以访问 swagger 界面!其内容包括:Swagger信息、接口信息、实体类信息、分组信息
10.2.2、配置 Swagger 信息
-
Swagger 实例 Bean 是 Docket。因此,编写 docket() 方法,后面将通过配置 Docket 实例来配置 Swaggger 的上述各类信息!
//配置Swagger的docket实例 @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2); }
-
编写 apiInfo() 方法,通过 ApiInfo 对象来配置 Swagger 文档信息
//配置Swagger信息,即 apiInfo() private ApiInfo apiInfo(){ Contact authorContact = new Contact("cwlin", "https://blog.youkuaiyun.com/coder_lcw", "xxxxxx@qq.com"); return new ApiInfo( "cwlin's Swagger Api Documentation", //标题 "愿世界依旧热闹,愿我永远是自己。", //描述 "v1.0", //版本 "https://blog.youkuaiyun.com/coder_lcw", //组织链接 authorContact, //联系人信息 "Apache 2.0", //许可 "http://www.apache.org/licenses/LICENSE-2.0", //许可链接 new ArrayList<>()); //扩展 }
-
将 Docket 实例关联上 apiInfo() 方法
package com.cwlin.swagger.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; import java.util.ArrayList; @Configuration @EnableSwagger2 //开启Swagger2 public class SwaggerConfig { //配置Swagger的docket实例 @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); } //配置Swagger信息,即 apiInfo() private ApiInfo apiInfo(){ Contact authorContact = new Contact("cwlin", "https://blog.youkuaiyun.com/coder_lcw", "xxxxxx@qq.com"); return new ApiInfo( "cwlin's Swagger Api Documentation", //标题 "愿世界依旧热闹,愿我永远是自己。", //描述 "v1.0", //版本 "https://blog.youkuaiyun.com/coder_lcw", //组织链接 authorContact, //联系人信息 "Apache 2.0", //许可 "http://www.apache.org/licenses/LICENSE-2.0", //许可链接 new ArrayList<>()); //扩展 } }
10.2.3、配置接口信息
-
在
SwaggerConfig
类的docket()
方法中,配置 Docket 实例时,通过select()
方法配置如何扫描接口apis()
:扫描类paths()
:扫描路径
//配置Swagger的docket实例 @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller") .and(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class))) .paths(PathSelectors.any()) .build(); }
-
重启项目进行测试,由于根据上述配置扫描接口,因此只能看到一个 HelloController 类
-
除了通过扫描指定包的方式外,还可以通过 RequestHandlerSelectors 类配置其他方式扫描类:
- basePackage():扫描指定包
- withClassAnnotation():扫描类上的注解
- withMethodAnnotation():扫描方法上的注解
- any():扫描全部包
- none():任何包都不扫描
-
此外,还可以通过 PathSelectors 类配置其他方式扫描路径:
- ant(final String antPattern):通过 ant() 方法控制扫描 URL 路径
- regex(final String pathRegex):通过正则表达式控制扫描路径
- any():任何路径都扫描
- none():任何路径都不扫描
10.2.4、配置 Swagger 开关
-
在
SwaggerConfig
类的docket()
方法中,通过enable(true)
方法配置 Swagger 文档是否启动//配置Swagger的docket实例 @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .enable(true) .select() .apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller") .and(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class))) .paths(PathSelectors.ant("/**")) .build(); }
-
在 application.properties 配置文件中,设置项目环境:dev、pro、test,新建 application-dev.properties 和 application-dev.properties 两种环境下的配置文件
# 应用名称 spring.application.name=SpringBoot-07-Swagger # 应用环境 spring.profiles.active=dev
-
根据开发环境或生产环境,配置 Swagger 文档是否启动
//配置Swagger的docket实例 @Bean public Docket docket(Environment environment){ //设置要显示的Swagger环境 Profiles profiles = Profiles.of("dev","test"); //获取项目环境 //String[] activeProfiles = environment.getActiveProfiles(); //通过环境监听判断是否处在设置显示的环境中 boolean flag = environment.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .enable(flag) .select() .apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller") .and(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class))) .paths(PathSelectors.ant("/**")) .build(); }
10.2.5、配置 Swagger 分组信息
-
在
SwaggerConfig
类的docket()
方法中,通过groupName()
方法配置 Swagger 分组信息。如果没有配置分组,默认是default//配置Swagger的docket实例 @Bean public Docket docket(Environment environment){ //设置要显示的Swagger环境 Profiles profiles = Profiles.of("dev","test"); //获取项目环境 //String[] activeProfiles = environment.getActiveProfiles(); //通过环境监听判断是否处在设置显示的环境中 boolean flag = environment.acceptsProfiles(profiles); return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .enable(flag) .groupName("cwlin") .select() .apis(RequestHandlerSelectors.basePackage("com.cwlin.swagger.controller") .and(RequestHandlerSelectors.withClassAnnotation(RestController.class)) .and(RequestHandlerSelectors.withMethodAnnotation(RequestMapping.class))) .paths(PathSelectors.ant("/**")) .build(); }
-
在
SwaggerConfig
类,配置多个 Docket 实例,设置多个 API 分组@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group1"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group2"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group3"); }
10.2.6、配置实体类信息
-
新建一个 User 实体类,并给类及其属性加上注解,在 Swagger 中显示为文档注释:@ApiModel 为类添加注释,@ApiModelProperty 为类的属性添加注释
-
package com.cwlin.swagger.pojo; import io.swagger.annotations.Api; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor //@Api(tags = "实体类") @ApiModel("用户实体类") public class User { @ApiModelProperty("ID") private Integer id; @ApiModelProperty("用户名") private String name; @ApiModelProperty("密码") private String pwd; @ApiModelProperty("权限") private String perms; }
-
**只要这个实体类在请求接口的返回值上(即使是泛型),都能映射到实体项中。**注意:这里并不是因为 @ApiModel 这个注解让实体类显示在这里,而是只要出现在接口方法的返回值上的实体类都会显示;而 @ApiModel 和 @ApiModelProperty 这两个注解只是为实体类添加注释的。
@PostMapping("/getUser") public User getUser(){ return new User(); }
-
还可以给 Controller 类、类的方法、方法的参数设置文档注释,注意:Controller 类使用的注解为
@Api(tags = "")
package com.cwlin.swagger.controller; import com.cwlin.swagger.pojo.User; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @Api(tags = "Hello控制类") public class HelloController { @RequestMapping(value = "/hello") public String hello(){ return "Hello, swagger!"; } @PostMapping("/getUser") public User getUser(){ int num = 6/0; //500错误 return new User(); } @PostMapping("/helloUser") @ApiOperation("helloUser方法") public String helloUser(@ApiParam("用户名") @RequestParam("username") String username){ return "Hello, " + username + "!"; } }
-
注意:Swagger 可以为一些比较难理解的属性或者接口,增加一些配置信息。但是,出于安全考虑、节省内存的运行,在正式发布时应关闭Swagger,否则会暴露项目接口信息!
-
Swagger 的所有注解定义在 io.swagger.annotations包下。下面列举一些常用注解:
Swagger注解 简单说明 @Api(tags = “xxx模块说明”) 作用在模块类上 @ApiOperation(“xxx接口说明”) 作用在接口方法上 @ApiModel(“xxxPOJO说明”) 作用在模型类上:如VO、BO @ApiModelProperty(value = “xxx属性说明”,hidden = true) 作用在类方法和属性上,hidden设置为true可以隐藏该属性 @ApiParam(“xxx参数说明”) 作用在参数、方法和字段上,类似@ApiModelProperty
10.3、总结
- 相较于传统的Postman或Curl方式测试接口,使用swagger简直就是傻瓜式操作,不需要额外说明文档(写得好本身就是文档)而且更不容易出错,只需要录入数据然后点击Execute,如果再配合自动化框架,可以说基本就不需要人为操作了。
- Swagger是个优秀的工具,现在国内已经有很多的中小型互联网公司都在使用它,相较于传统的要先出Word接口文档再测试的方式,显然这样也更符合现在的快速迭代开发行情。当然了,提醒下大家在正式环境要记得关闭Swagger,一来出于安全考虑二来也可以节省运行时内存。
10.4、拓展:其他皮肤
【注意】可以通过导入不同的包实现不同的皮肤定义
-
默认皮肤:http://localhost:8080/swagger-ui/index.html
<dependency> <groupId>io.springfox</groupId> <artifactId>springfox-boot-starter</artifactId> <version>3.0.0</version> </dependency>
-
bootstrap-ui:http://localhost:8080/doc.html(实用)
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>swagger-bootstrap-ui</artifactId> <version>1.9.6</version> </dependency>
-
Layui-ui:http://localhost:8080/docs.html(好看)
<dependency> <groupId>com.github.caspar-chen</groupId> <artifactId>swagger-ui-layer</artifactId> <version>1.1.3</version> </dependency>
-
mg-ui:http://localhost:8080/document.html
<dependency> <groupId>com.zyplayer</groupId> <artifactId>swagger-mg-ui</artifactId> <version>1.0.6</version> </dependency>
11、异步、邮件、定时任务
11.1、异步任务
-
新建一个 SpringBoot 项目:SpringBoot-08-Task,勾选依赖:Spring Web、Thymeleaf、Lombox等
-
在 service 包下创建一个 AsyncService 类,编写 hello 方法,假装正在处理数据,使用线程设置一些延时,模拟同步等待的情况
package com.cwlin.service; import org.springframework.stereotype.Service; @Service public class AsyncService { public void hello(){ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("正在处理数据......"); } }
-
在 controller 包下创建一个 AsyncController 类
package com.cwlin.controller; import com.cwlin.service.AsyncService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AsyncController { @Autowired AsyncService asyncService; @RequestMapping("/hello") public String hello(){ asyncService.hello(); //停止5秒 return "Success!"; } }
-
测试访问:http://localhost:8080/hello,3秒后页面出现 Success,这是同步等待的结果!
-
如果想让用户直接得到消息,那么在后台使用多线程的方式进行处理即可,但是每次都需要手动去编写多线程的实现,太麻烦了。可以通过给 hello 方法添加 @Async 注解,告诉Spring这是一个异步方法。此时,SpringBoot 会自动开启一个线程池,进行调用!
package com.cwlin.service; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @Service public class AsyncService { @Async //告诉Spring这是一个异步方法 public void hello(){ try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("正在处理数据......"); } }
-
但是,要让这个注解 @Async 生效,需要在主程序上添加一个注解 @EnableAsync,开启异步注解功能
package com.cwlin; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; @SpringBootApplication @EnableAsync //开启异步注解功能 public class SpringBoot08TaskApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot08TaskApplication.class, args); } }
-
测试访问:http://localhost:8080/hello,页面瞬间响应出现 Success,后台代码依旧执行,实现了异步处理!
-
异步处理还是十分常用的,比如在网站上发送邮件,后台会去发送邮件,此时前台会造成响应不动,直到邮件发送完毕,响应才会成功。因此,一般会采用多线程的方式去处理这些任务。
11.2、邮件任务
-
导入 Maven 依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
点击查看 spring-boot-starter-mail(源码),发现它引用了
jakarta.mail
依赖<?xml version="1.0" encoding="UTF-8"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <!-- This module was also published with a richer model, Gradle metadata, --> <!-- which should be used instead. Do not delete the following line which --> <!-- is to indicate to Gradle or any Gradle module metadata file consumer --> <!-- that they should prefer consuming it instead. --> <!-- do_not_remove: published-with-gradle-metadata --> <modelVersion>4.0.0</modelVersion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> <version>2.3.7.RELEASE</version> <name>spring-boot-starter-mail</name> <description>Starter for using Java Mail and Spring Framework's email sending support</description> <url>https://spring.io/projects/spring-boot</url> <organization> <name>Pivotal Software, Inc.</name> <url>https://spring.io</url> </organization> <licenses> <license> <name>Apache License, Version 2.0</name> <url>https://www.apache.org/licenses/LICENSE-2.0</url> </license> </licenses> <developers> <developer> <name>Pivotal</name> <email>info@pivotal.io</email> <organization>Pivotal Software, Inc.</organization> <organizationUrl>https://www.spring.io</organizationUrl> </developer> </developers> <scm> <connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection> <developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection> <url>https://github.com/spring-projects/spring-boot</url> </scm> <issueManagement> <system>GitHub</system> <url>https://github.com/spring-projects/spring-boot/issues</url> </issueManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.3.7.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>5.2.12.RELEASE</version> <scope>compile</scope> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <version>1.6.5</version> <scope>compile</scope> </dependency> </dependencies> </project>
-
双击 shift 键,查看自动配置类:MailSenderAutoConfiguration(源码)
package org.springframework.boot.autoconfigure.mail; import javax.activation.MimeType; import javax.mail.internet.MimeMessage; import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; import org.springframework.mail.MailSender; @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({MimeMessage.class, MimeType.class, MailSender.class}) @ConditionalOnMissingBean({MailSender.class}) @Conditional({MailSenderAutoConfiguration.MailSenderCondition.class}) @EnableConfigurationProperties({MailProperties.class}) @Import({MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class}) public class MailSenderAutoConfiguration { public MailSenderAutoConfiguration() { } static class MailSenderCondition extends AnyNestedCondition { MailSenderCondition() { super(ConfigurationPhase.PARSE_CONFIGURATION); } @ConditionalOnProperty( prefix = "spring.mail", name = {"jndi-name"} ) static class JndiNameProperty { JndiNameProperty() { } } @ConditionalOnProperty( prefix = "spring.mail", name = {"host"} ) static class HostProperty { HostProperty() { } } } }
-
点击 MailProperties 配置类(源码),查看邮件配置属性
package org.springframework.boot.autoconfigure.mail; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.Map; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties( prefix = "spring.mail" ) public class MailProperties { private static final Charset DEFAULT_CHARSET; private String host; private Integer port; private String username; private String password; private String protocol = "smtp"; private Charset defaultEncoding; private Map<String, String> properties; private String jndiName; public MailProperties() { this.defaultEncoding = DEFAULT_CHARSET; this.properties = new HashMap(); } //getter and setter static { DEFAULT_CHARSET = StandardCharsets.UTF_8; } }
-
在
application.properties
中,添加 mail 配置# 邮件配置 # 用户名 spring.mail.username=******@qq.com # 授权码,获取:QQ邮箱设置->账户->开启pop3和smtp服务 spring.mail.password=****** # 服务器 spring.mail.host=smtp.qq.com # QQ邮箱需要开启加密验证 spring.mail.properties.mail.smtl.ssl.enable=true
-
在测试类中,测试邮件发送
package com.cwlin; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.mail.SimpleMailMessage; import org.springframework.mail.javamail.JavaMailSenderImpl; import org.springframework.mail.javamail.MimeMessageHelper; import javax.mail.MessagingException; import javax.mail.internet.MimeMessage; import java.io.File; @SpringBootTest class SpringBoot08TaskApplicationTests { @Autowired JavaMailSenderImpl mailSender; @Test void contextLoads() { //邮件设置1:一个简单的邮件 SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setSubject("通知--秋季开学"); mailMessage.setText("于2021.08.01开学,请按时到校报到!"); mailMessage.setTo("******@qq.com"); mailMessage.setFrom("******@qq.com"); mailSender.send(mailMessage); } @Test void contextLoads2() throws MessagingException { //邮件设置2:一个复杂的邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, true,"utf-8"); //支持多文件 mimeMessageHelper.setSubject("通知--秋季开学"); mimeMessageHelper.setText("<b style='color:red'>于2021.08.01开学,请按时到校报到!</b>",true); //支持html mimeMessageHelper.addAttachment("1.jpg", new File("C:/Users/***/Desktop/1.jpg")); mimeMessageHelper.addAttachment("2.jpg", new File("C:/Users/***/Desktop/2.jpg")); mimeMessageHelper.setTo("******@qq.com"); mimeMessageHelper.setFrom("******@qq.com"); mailSender.send(mimeMessage); } }
-
在测试类中,将上述的复杂邮件发送封装为 sendMailMessage 方法
@Test /** * @author cwlin * @creed Talk is cheap,show me the code * @description TODO * @date 2021/8/16 13:22 * @param multipart: 是否支持多文件 * @param encoding: 邮件编码 * @param subject: 标题 * @param text: 正文 * @param html: 是否支持html * @return void */ void sendMailMessage(boolean multipart, String encoding, String subject, String text, boolean html) throws MessagingException { //邮件设置2:一个复杂的邮件 MimeMessage mimeMessage = mailSender.createMimeMessage(); MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, multipart, encoding); mimeMessageHelper.setSubject(subject); mimeMessageHelper.setText(text, html); mimeMessageHelper.addAttachment("1.jpg", new File("C:/Users/醉染/Desktop/1.jpg")); mimeMessageHelper.addAttachment("2.jpg", new File("C:/Users/醉染/Desktop/2.jpg")); mimeMessageHelper.setTo("1208793450@qq.com"); mimeMessageHelper.setFrom("1208793450@qq.com"); mailSender.send(mimeMessage); }
11.3、定时任务
-
项目开发中经常需要执行一些定时任务,比如需要在每天凌晨的时候,分析一次前一天的日志信息。Spring为我们提供了异步执行任务调度的方式,提供了两个接口:TaskExecutor 接口、TaskScheduler 接口
package org.springframework.core.task; import java.util.concurrent.Executor; @FunctionalInterface public interface TaskExecutor extends Executor { void execute(Runnable var1); }
package org.springframework.scheduling; import java.time.Clock; import java.time.Duration; import java.time.Instant; import java.util.Date; import java.util.concurrent.ScheduledFuture; import org.springframework.lang.Nullable; public interface TaskScheduler { default Clock getClock() { return Clock.systemDefaultZone(); } @Nullable ScheduledFuture<?> schedule(Runnable var1, Trigger var2); default ScheduledFuture<?> schedule(Runnable task, Instant startTime) { return this.schedule(task, Date.from(startTime)); } ScheduledFuture<?> schedule(Runnable var1, Date var2); default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Instant startTime, Duration period) { return this.scheduleAtFixedRate(task, Date.from(startTime), period.toMillis()); } ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, Date var2, long var3); default ScheduledFuture<?> scheduleAtFixedRate(Runnable task, Duration period) { return this.scheduleAtFixedRate(task, period.toMillis()); } ScheduledFuture<?> scheduleAtFixedRate(Runnable var1, long var2); default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay) { return this.scheduleWithFixedDelay(task, Date.from(startTime), delay.toMillis()); } ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, Date var2, long var3); default ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, Duration delay) { return this.scheduleWithFixedDelay(task, delay.toMillis()); } ScheduledFuture<?> scheduleWithFixedDelay(Runnable var1, long var2); }
-
在主程序上添加
@EnableScheduling
注解,开启定时任务功能,后续才能使用@Scheduled
注解package com.cwlin; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableAsync //开启异步注解功能 @EnableScheduling //开启定时任务功能 public class SpringBoot08TaskApplication { public static void main(String[] args) { SpringApplication.run(SpringBoot08TaskApplication.class, args); } }
-
在 service 包下,创建一个 ScheduledService 类,编写一个执行定时任务的方法 hello,
@Scheduled
表示在约定时间执行该方法package com.cwlin.service; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @Service public class ScheduledService { //在约定时间执行计划好的方法,需要用到Cron表达式 //Cron表达式:秒 分 时 日 月 星期 {年}(0-7均表示星期日) @Scheduled(cron = "0 27 14 * * ?") //每天14点27分执行一次 @Scheduled(cron = "0 * * * * 0-7") //每分钟执行一次 //@Scheduled(cron = "30 0/10 10,18 * * ?") //每天10点和18点,每隔10分钟执行一次 //@Scheduled(cron = "15 30 10 ? * 1-6") //每个月的周一到周六10点30分15秒执行一次 public void hello(){ System.out.println("Hello, Scheduler!"); } }
-
接下来介绍一下 Cron 表达式:
-
格式
- {秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}(0-7均表示星期日)
-
允许值
字段 允许值 允许的特殊字符 秒数 0-59 , - * / 分钟 0-59 , - * / 小时 0-23 , - * / 日期 1-31 , - * ? / L W C 月份 1-12 或者 JAN-DEC , - * / 星期 1-7 或者 SUN-SAT , - * ? / L C # 年份(可为空) 留空, 1970-2099 , - * / -
特殊字符(Cron表达式对特殊字符的大小写不敏感,对代表星期的缩写英文大小写也不敏感)
- 星号(*):可用在所有字段中,表示对应时间域的每一个时刻,例如,*在分钟字段时,表示“每分钟”;
- 问号(?):该字符只在日期和星期字段中使用,它通常指定为“无意义的值”,相当于点位符;
- 减号(-):表达一个范围,如在小时字段中使用“10-12”,则表示从10到12点,即10,11,12;
- 逗号(,):表达一个列表值,如在星期字段中使用“MON,WED,FRI”,则表示星期一,星期三和星期五;
- 斜杠(/):x/y表达一个等步长序列,x为起始值,y为增量步长值。如在分钟字段中使用0/15,则表示为0,15,30和45秒,而5/15在分钟字段中表示5,20,35,50,你也可以使用*/y,它等同于0/y;
- L:该字符只在日期和星期字段中使用,代表“Last”的意思,但它在两个字段中意思不同。L在日期字段中,表示这个月份的最后一天,如一月的31号,非闰年二月的28号;如果L用在星期中,则表示星期六,等同于7。但是,如果L出现在星期字段里,而且在前面有一个数值 X,则表示“这个月的最后X天”,例如,6L表示该月的最后星期五;
- W:该字符只能出现在日期字段里,是对前导日期的修饰,表示离该日期最近的工作日。例如15W表示离该月15号最近的工作日,如果该月15号是星期六,则匹配14号星期五;如果15日是星期日,则匹配16号星期一;如果15号是星期二,那结果就是15号星期二。但必须注意关联的匹配日期不能够跨月,如你指定1W,如果1号是星期六,结果匹配的是3号星期一,而非上个月最后的那天。W字符串只能指定单一日期,而不能指定日期范围;
- LW:在日期字段可以组合使用LW,它的意思是当月的最后一个工作日;
- #:该字符只能在星期字段中使用,表示当月某个工作日。如6#3表示当月的第三个星期五(6表示星期五,#3表示当前的第三个),而4#5表示当月的第五个星期三,假设当月没有第五个星期三,忽略不触发;
- C:该字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是计划所关联的日期,如果日期没有被关联,则相当于日历中所有日期。例如5C在日期字段中就相当于日历5日以后的第一天。1C在星期字段中相当于星期日后的第一天。
-
Cron 表达式生成器
-
参考博客
-
举例
"0/2 * * * * ?" 表示每2秒 执行任务 "0 0/2 * * * ?" 表示每2分钟 执行任务 "0 0 2 1 * ?" 表示在每月的1日的凌晨2点调整任务 "0 15 10 ? * MON-FRI" 表示周一到周五每天上午10:15执行作业 "0 15 10 ? 6L 2002-2006" 表示2002-2006年的每个月的最后一个星期五上午10:15执行作 "0 0 10,14,16 * * ?" 每天上午10点,下午2点,4点 "0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时 "0 0 12 ? * WED" 表示每个星期三中午12点 "0 0 12 * * ?" 每天中午12点触发 "0 15 10 ? * *" 每天上午10:15触发 "0 15 10 * * ?" 每天上午10:15触发 "0 15 10 * * ? *" 每天上午10:15触发 "0 15 10 * * ? 2005" 2005年的每天上午10:15触发 "0 * 14 * * ?" 在每天下午2点到下午2:59期间的每1分钟触发 "0 0/5 14 * * ?" 在每天下午2点到下午2:55期间的每5分钟触发 "0 0/5 14,18 * * ?" 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发 "0 0-5 14 * * ?" 在每天下午2点到下午2:05期间的每1分钟触发 "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44触发 "0 15 10 ? * MON-FRI" 周一至周五的上午10:15触发 "0 15 10 15 * ?" 每月15日上午10:15触发 "0 15 10 L * ?" 每月最后一日的上午10:15触发 "0 15 10 ? * 6L" 每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最后一个星期五上午10:15触发 "0 15 10 ? * 6#3" 每月的第三个星期五上午10:15触发
-
12、整合 Redis 使用
12.1、简介
12.1.1、概述
- 什么是 Redis?
- Redis(Remote Dictionary Server ),即远程字典服务。是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。
- 与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
- 作用
- 内存存储、持久化,内存是断电即失的,所以需要持久化(RDB、AOF)
- 高效率、用于高速缓冲
- 发布订阅系统
- 地图信息分析
- 计时器、计数器(eg:浏览量)
- …
- 特性
- 多样的数据类型
- 持久化
- 集群
- 事务
- …
12.1.2、lettuce
-
SpringBoot 操作数据都是使用 SpringData:jpa、jdbc、mongodb、redis等
-
SpringData 也是和 SpringBoot 齐名的项目
-
说明:在 SpringBoot2.x 之后,原来使用的jedis 被替换为了
lettuce
?<dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> <version>5.3.5.RELEASE</version> <scope>compile</scope> </dependency>
-
Jedis 和 lettuce 区别:
- jedis:采用的是直连的 Service 服务,如果有多个线程操作的话是不安全的,如果想要避免不安全的操作,使用 Jedis Pool 连接池去解决,更像 BIO 模式
- lettuce:底层采用 netty,实例可以在多个线程中进行共享,不存在线程不安全的情况!这样可以减少线程数据,性能更高,更像 NIO 模式
12.2、自动配置类源码
-
在学习SpringBoot自动配置的原理时,整合一个组件并进行配置一定会有一个自动配置类xxxAutoConfiguration,并且在spring.factories中也一定能找到这个类的完全限定名,Redis也不例外
spring-boot-autoconfigure-2.3.7.RELEASE.jar META-INF spring.factories org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
-
点击查看 RedisAutoConfiguration 自动配置类,发现绑定了一个 RedisProperties 配置文件
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package org.springframework.boot.autoconfigure.data.redis; import java.net.UnknownHostException; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }
-
点击 RedisProperties 配置文件,查看 redis 可以配置的属性,以及连接池相关的配置。注意:使用时一定要使用 Lettuce 连接池
@ConfigurationProperties( prefix = "spring.redis" ) public class RedisProperties { private int database = 0; private String url; private String host = "localhost"; private String password; private int port = 6379; // 默认端口 private boolean ssl; private Duration timeout; private String clientName; private RedisProperties.Sentinel sentinel; private RedisProperties.Cluster cluster; private final RedisProperties.Jedis jedis = new RedisProperties.Jedis(); private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce(); //...... public static class Pool { private int maxIdle = 8; private int minIdle = 0; private int maxActive = 8; private Duration maxWait = Duration.ofMillis(-1); private Duration timeBetweenEvictionRuns; //...... } }
-
回到 RedisAutoConfiguration 自动配置类,发现两个 Template 模板方法,可以使用这些 Template 来间接操作组件
@Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) //可以自定义一个redisTemplate来替换默认的Redis模板! public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { //默认的 RedisTemplate 没有过多的设置,redis对象都是需要序列化! //两个泛型都是 Object, Object 的类型,我们后使用需要强制转换 <String, Object> RedisTemplate<Object, Object> template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean //由于String是redis中最常使用的类型,因此单独提出来一个bean! public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; }
-
再注意到 RedisAutoConfiguration 自动配置类导入的两个类
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
-
先看 Jedis 的实现类 JedisConnectionConfiguration,其中 @ConditionalOnClass 注解中有两个类 GenericObjectPool.class 和 Jedis.class 默认是不存在的,所以 Jedis 是无法生效的
@ConditionalOnClass({GenericObjectPool.class, JedisConnection.class, Jedis.class})
-
再看 Lettuce 的实现类 LettuceConnectionConfiguration,其中 @ConditionalOnClass 注解的 RedisClient.class 类是可用的
@ConditionalOnClass({RedisClient.class})
-
显然,SpringBoot 更推荐使用 Lettuce 来实现 Redis。
12.3、整合测试
-
新建一个 SpringBoot-09-Redis 项目,导入依赖。其中,
spring-boot-starter-data-redis
依赖是 SpringBoot 整合 Redis 的包<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cwlin</groupId> <artifactId>SpringBoot-09-Redis</artifactId> <version>0.0.1-SNAPSHOT</version> <name>SpringBoot-09-Redis</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.7.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.cwlin.SpringBoot09RedisApplication</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
-
编写 application.yml 配置文件
# Redis配置 spring: redis: host: 127.0.0.1 port: 6379
-
在测试类中进行测试
package com.cwlin; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; @SpringBootTest class SpringBoot09RedisApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void contextLoads() { /* redisTemplate 操作不同的数据类型,API 和 Redis 中的是一样的 * opsForValue 类似于 Redis 中的 String * opsForList 类似于 Redis 中的 List * opsForSet 类似于 Redis 中的 Set * opsForHash 类似于 Redis 中的 Hash * opsForZSet 类似于 Redis 中的 ZSet * opsForGeo 类似于 Redis 中的 Geospatial * opsForHyperLogLog 类似于 Redis 中的 HyperLogLog */ //除了基本的操作,常用的命令都可以直接通过redisTemplate操作,比如事务和CURD。和数据库相关的操作都需要通过连接操作 /* 获取连接对象 * RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); * connection.flushDb(); * connection.flushAll(); */ redisTemplate.opsForValue().set("myKey", "cwlin"); System.out.println(redisTemplate.opsForValue().get("myKey")); } }
-
由于没有安装 Redis,因此测试失败。在后续的学习中,将会系统地学习 Redis 的使用!
13、整合 editor.md 富文本编辑器
- 狂神说SpringBoot16:富文本编辑器 (qq.com)
- 狂神说SpringBoot16:富文本编辑器_狂神说-优快云博客
- 狂神说springboot整合富文本编辑器editor.md_兴趣使然的草帽路飞-优快云博客
14、分布式 Dubbo+Zookeeper
14.1、分布式理论
14.1.1、什么是分布式系统?
- 在《分布式系统原理与范型》一书中有如下定义:“分布式系统是若干独立计算机的集合,这些计算机对于用户来说就像单个相关系统”;
- 分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统。分布式系统的出现是为了用廉价的、普通的机器完成单个计算机无法完成的计算、存储任务。其目的是利用更多的机器,处理更多的数据。
- 分布式系统(distributed system)是建立在网络之上的软件系统。
- 首先需要明确的是,只有当单个节点的处理能力无法满足日益增长的计算、存储任务的时候,且硬件的提升(加内存、加磁盘、使用更好的CPU)高昂到得不偿失的时候,应用程序也不能进一步优化的时候,我们才需要考虑分布式系统。因为,分布式系统要解决的问题本身就是和单机系统一样的,而由于分布式系统多节点、通过网络通信的拓扑结构,会引入很多单机系统没有的问题,为了解决这些问题又会引入更多的机制、协议,带来更多的问题。
14.1.2、Dubbo文档
- 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,急需一个治理系统确保架构有条不紊的演进。
- Apache Dubbo 是一款微服务开发框架,它提供了 RPC通信 与 微服务治理 两大关键能力。**这意味着,使用 Dubbo 开发的微服务,将具备相互之间的远程发现与通信能力, 同时利用 Dubbo 提供的丰富服务治理能力,可以实现诸如服务发现、负载均衡、流量调度等服务治理诉求。**同时 Dubbo 是高度可扩展的,用户几乎可以在任意功能点去定制自己的实现,以改变框架的默认行为来满足自己的业务需求。
- Dubbo3 基于 Dubbo2 演进而来,在保持原有核心功能特性的同时, Dubbo3 在易用性、超大规模微服务实践、云原生基础设施适配等几大方向上进行了全面升级。
14.1.3、单一应用架构
- 当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

- 适用于小型网站,小型管理系统,将所有功能都部署到一个功能里,简单易用。
- 缺点:1、性能扩展比较难,2、协同开发问题,3、不利于升级维护。
14.1.4、垂直应用架构
- 当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

- 通过切分业务来实现各个模块独立部署,降低了维护和部署的难度,团队各司其职更易管理,性能扩展也更方便,更有针对性。
- 缺点:公用模块无法重复利用,开发性的浪费。
14.1.5、分布式服务架构
- 当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的**分布式服务框架(RPC)**是关键。

14.1.6、流动计算架构
- 当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)[ Service Oriented Architecture]是关键。
14.2、RPC 远程过程调用
14.2.1、什么是RPC?
-
RPC【Remote Procedure Call】是指远程过程调用,是一种进程间通信方式,他是一种技术的思想,而不是规范。它允许程序调用另一个地址空间(通常是共享网络的另一台机器上)的过程或函数,而不用程序员显式编码这个远程调用的细节。即程序员无论是调用本地的还是远程的函数,本质上编写的调用代码基本相同。
-
也就是说两台服务器A,B,一个应用部署在A服务器上,想要调用B服务器上应用提供的函数/方法,由于不在一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。为什么要用RPC呢?就是无法在一个进程内,甚至一个计算机内通过本地调用的方式完成的需求,比如不同的系统间的通讯,甚至不同的组织间的通讯,由于计算能力需要横向扩展,需要在多台机器组成的集群上部署应用。RPC就是要像调用本地的函数一样去调远程函数;
-
RPC基本原理
-
步骤解析:
-
HTTP 和 RPC 的比较:
- Http:是无状态的,基于网络通信的协议。
- RPC:远程过程调用,是允许程序调用另一个地址空间的接口,两个核心模块:通讯和序列化。
14.2.2、RPC 协议的选择
-
协议是 RPC 的核心,它规范了数据在网络中的传输内容和格式。除必须的请求、响应数据外,通常还会包含额外控制数据,如单次请求的序列化方式、超时时间、压缩方式和鉴权信息等。
-
协议的内容包含三部分
- 数据交换格式: 定义 RPC 的请求和响应对象在网络传输中的字节流内容,也叫作序列化方式
- 协议结构: 定义包含字段列表和各字段语义以及不同字段的排列方式
- 协议通过定义规则、格式和语义来约定数据如何在网络间传输。一次成功的 RPC 需要通信的两端都能够按照协议约定进行网络字节流的读写和对象转换。如果两端对使用的协议不能达成一致,就会出现鸡同鸭讲,无法满足远程通信的需求。
-
RPC 协议的设计需要考虑以下内容:
- 通用性: 统一的二进制格式,跨语言、跨平台、多传输层协议支持
- 扩展性: 协议增加字段、升级、支持用户扩展和附加业务元数据
- 性能:As fast as it can be
- 穿透性:能够被各种终端设备识别和转发:网关、代理服务器等 通用性和高性能通常无法同时达到,需要协议设计者进行一定的取舍。
14.3、Dubbo
14.3.1、什么是Dubbo?
- Apache Dubbo 是一款高性能、轻量级、基于 Java 的开源 RPC 框架。
- Apache Dubbo 提供了六大核心能力:面向接口代理的高性能RPC调用,智能容错和负载均衡,服务自动注册和发现,高度可扩展能力,运行期流量调度,可视化的服务治理与运维。
- 官方文档:
14.3.2、Dubbo 架构

- 节点角色说明
- 服务提供者(Provider):暴露服务的服务提供方,服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者(Consumer):调用远程服务的服务消费方,服务消费者在启动时,向注册中心订阅自己所需的服务,服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 注册中心(Registry):服务注册与发现的注册中心,注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者
- 监控中心(Monitor):统计服务的调用次数和调用时间的监控中心,服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
- 服务运行容器(Container):服务运行容器
- 调用关系说明
- 服务容器负责启动,加载,运行服务提供者。
- 服务提供者在启动时,向注册中心注册自己提供的服务。
- 服务消费者在启动时,向注册中心订阅自己所需的服务。
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
- Dubbo 架构具有以下几个特点,分别是连通性、健壮性、伸缩性、以及向未来架构的升级性。
14.3.3、注册中心、配置中心和元数据中心
- 作为一个微服务框架,Dubbo sdk 跟随着微服务组件被部署在分布式集群各个位置,为了在分布式环境下实现各个微服务组件间的协作, Dubbo 定义了一些中心化组件,这包括:
- 注册中心:
- 协调 Consumer 与 Provider 之间的地址注册与发现
- 配置中心:
- 存储 Dubbo 启动阶段的全局配置,保证配置的跨环境共享与全局一致性
- 负责服务治理规则(路由规则、动态配置等)的存储与推送
- 元数据中心:
- 接收 Provider 上报的服务接口元数据,为 Admin 等控制台提供运维能力(如服务测试、接口文档等)
- 作为服务发现机制的补充,提供额外的接口/方法级别配置信息的同步能力,相当于注册中心的额外扩展
- 注册中心:
- 上图完整的描述了 Dubbo 微服务组件与各个中心的交互过程。
- 以上三个中心并不是运行 Dubbo 的必要条件,用户完全可以根据自身业务情况决定只启用其中一个或多个,以达到简化部署的目的。通常情况下,所有用户都会以独立的注册中心 开始 Dubbo 服务开发,而配置中心、元数据中心则会在微服务演进的过程中逐步的按需被引入进来。
- 当然在 Dubbo + Mesh 的场景下,随着 Dubbo 服务注册能力的弱化,注册中心也不再是必选项,其职责开始被控制面取代。
14.3.4、ZooKeeper 注册中心
- 查看 Dubbo 官方文档,点击注册中心参考手册,官方推荐使用 Zookeeper 注册中心!
- ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,是Hadoop和Hbase的重要组件。它是一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、域名服务、分布式同步、组服务等。
- ZooKeeper的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。
- ZooKeeper包含一个简单的原语集,提供Java和C的接口。
- ZooKeeper代码版本中,提供了分布式独享锁、选举、队列的接口,代码在$zookeeper_home\src\recipes。其中分布锁和队列有Java和C两个版本,选举只有Java版本。
- ZooKeeper 原理:ZooKeeper是以Fast Paxos算法为基础的,Paxos 算法存在活锁的问题,即当有多个proposer交错提交时,有可能互相排斥导致没有一个proposer能提交成功,而Fast Paxos做了一些优化,通过选举产生一个leader (领导者),只有leader才能提交proposer,具体算法可见Fast Paxos。因此,要想弄懂ZooKeeper首先得对Fast Paxos有所了解。
- ZooKeeper的基本运转流程如下:
- 选举Leader;
- 同步数据;
- 选举Leader过程中算法有很多,但要达到的选举标准是一致的;
- Leader要具有最高的执行ID,类似root权限;
- 集群中大多数的机器得到响应并接受选出的Leader。
14.4、环境安装测试
14.4.1、Window下安装zookeeper
-
下载 zookeeper:https://mirror.bit.edu.cn/apache/zookeeper/,下载最新版
apache-zookeeper-3.7.0-bin.tar.gz
-
解压 zookeeper 到 Java 环境文件夹下,运行
/bin/zkServer.cmd
,初次运行会报错,没有zoo.cfg
配置文件-
出现问题:闪退 !
-
解决方案:编辑
zkServer.cmd
文件,在末尾添加 pause。这样运行出错就不会退出,会提示错误信息,方便找到原因。 -
运行结果:
-
-
修改
zoo.cfg
配置文件:将 conf 文件夹下面的 zoo_sample.cfg 复制一份改名为 zoo.cfg 即可,再次启动zookeeper。注意几个重要属性:- dataDir=/tmp/zookeeper:临时数据存储的目录(可写相对路径)
- clientPort=2181:zookeeper的默认端口号
-
使用 zkCli.cmd 测试:
-
ls /
:列出zookeeper根下保存的所有节点[zk: localhost:2181(CONNECTED) 0] ls / [zookeeper]
-
create –e /cwlin 123
:创建一个 cwlin 节点,值为123[zk: localhost:2181(CONNECTED) 1] create -e /cwlin 123 Created /cwlin
-
get /cwlin
:获取 cwlin 节点的值[zk: localhost:2181(CONNECTED) 2] get /cwlin 123
-
ls /
:再次查看所有节点[zk: localhost:2181(CONNECTED) 3] ls / [cwlin, zookeeper]
-
14.4.2、window下安装dubbo-admin
- dubbo 本身并不是一个服务软件。它其实就是一个 jar 包,能够帮你的 Java 程序连接到 zookeeper,并利用 zookeeper 消费、提供服务。
- 但是为了让用户更好的管理监控众多的 dubbo 服务,官方提供了一个可视化的监控程序 dubbo-admin,不过这个监控即使不装也不影响使用。
-
下载 dubbo-admin:https://github.com/apache/dubbo-admin/tree/master
-
解压得到
dubbo-admin-master-0.2.0
,打开\dubbo-admin\src\main\resources\application.properties
,查看 zookeeper 注册中心地址(这是旧版的内容!新版的dubbo-admin-master
运行出现错误!)server.port=7001 spring.velocity.cache=false spring.velocity.charset=UTF-8 spring.velocity.layout-url=/templates/default.vm spring.messages.fallback-to-system-locale=false spring.messages.basename=i18n/message spring.root.password=root spring.guest.password=guest # 注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
-
在项目目录下打包 dubbo-admin。第一次打包的过程有点慢,需要耐心等待!
mvn clean package -Dmaven.test.skip=true [INFO] ------------------------------------------------------------------------ [INFO] Reactor Summary for dubbo-admin 0.3.0: [INFO] [INFO] dubbo-admin ........................................ SUCCESS [04:06 min] [INFO] dubbo-admin-ui ..................................... SUCCESS [07:19 min] [INFO] dubbo-admin-server ................................. SUCCESS [04:10 min] [INFO] dubbo-admin-distribution ........................... SUCCESS [ 8.592 s] [INFO] dubbo-admin-test ................................... SUCCESS [01:59 min] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 18:08 min [INFO] Finished at: 2021-08-17T17:55:43+08:00 [INFO] ------------------------------------------------------------------------
-
执行
\dubbo-admin-master\dubbo-admin-server\target
下的dubbo-admin-server-0.3.0.jar
,注意:zookeeper的服务一定要打开!java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
-
执行完毕,访问:http://localhost:7001/;此时,需要输入登录账户和密码,默认是 root-root;登录成功后,即可查看界面。
14.5、SpringBoot + Dubbo + zookeeper
- 官网 “快速启动” 中提供了 Spring 整合 Dubbo 的操作,很简单;但是没有提供 SpringBoot 整合 Dubbo 的操作
14.5.1、框架搭建
-
在 IDEA 中创建一个空项目,创建一个 provider-server 模块,实现服务提供者,选择相应的 web 依赖,修改端口号为 8081
-
编写一个服务,比如卖票的服务,其 service 接口和实现类如下:
package com.cwlin.service; public interface TicketService { String getTicket(); }
package com.cwlin.service; public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《觉醒年代》"; } }
-
创建一个 consumer-server 模块,实现服务消费者,选择相应的 web 依赖,修改端口号为 8082
-
编写一个服务,比如用户的服务,编写 service 类如下:
package com.cwlin; public class UserService { //需要去拿注册中心的服务 }
-
需求:现在用户想使用买票的服务,要怎么处理?
14.5.2、服务提供者
-
将服务提供者注册到注册中心,需要整合 Dubbo 和 zookeeper,导入相关依赖
- dubbo-spring-boot-starter 和 zkclient
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.8</version> </dependency> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency>
- 【新版的坑】zookeeper及其依赖包,解决日志冲突,还需要剔除日志依赖
<!-- 引入zookeeper,解决日志冲突 --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.6.1</version> <!--排除slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
-
在 springboot 配置文件中,配置 dubbo 的相关参数
# 应用名称 spring.application.name=provider-service # 应用服务 WEB 访问端口 server.port=8081 #当前应用名字 dubbo.application.name=provider-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181 #扫描指定包下服务 dubbo.scan.base-packages=com.cwlin.service
-
在 service 的实现类 TicketServiceImpl 中配置 Dubbo 服务注解 @DubboService
-
注意导包问题!不过,在新版Dubbo中,注解 @Service 改为 @DubboService!
-
逻辑理解:应用启动起来,dubbo就会扫描指定的包下带有@component注解的服务,将它发布在指定的注册中心中!
package com.cwlin.service; import org.apache.dubbo.config.annotation.DubboService; import org.springframework.stereotype.Component; @DubboService //可以被扫描到,在项目一启动就自动注册到注册中心 @Component //使用Dubbo后尽量不要用@Service注解,Dubbo包里也有@Service注解,容易出现导包问题! public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《觉醒年代》"; } }
-
-
先启动 zookeeper,运行 dubbo-admin-0.0.1-SNAPSHOT.jar,再运行 provider-server 模块
-
请求访问:http://localhost:7001/,并登录 Dubbo Admin 进行测试
14.5.3、服务消费者
-
导入和 provider-service 模块中相同的依赖
<dependency> <groupId>org.apache.dubbo</groupId> <artifactId>dubbo-spring-boot-starter</artifactId> <version>2.7.8</version> </dependency> <dependency> <groupId>com.github.sgroschupf</groupId> <artifactId>zkclient</artifactId> <version>0.1</version> </dependency> <!-- 引入zookeeper,解决日志冲突 --> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>5.1.0</version> </dependency> <dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.6.1</version> <!--排除slf4j-log4j12--> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency>
-
在 springboot 配置文件中,配置 dubbo 的相关参数
# 应用名称 spring.application.name=consumer-server # 应用服务 WEB 访问端口 server.port=8082 #当前应用名字 dubbo.application.name=consumer-server #注册中心地址 dubbo.registry.address=zookeeper://127.0.0.1:2181
-
本来正常步骤是需要将服务提供者的接口打包,然后用pom文件导入,我们这里使用简单的方式,直接将服务的接口拿过来,路径必须保证正确,即和服务提供者相同;
-
在 service 包下,完善服务消费者的 service 类
package com.cwlin.service; import org.apache.dubbo.config.annotation.DubboReference; import org.springframework.stereotype.Service; @Service //注入到容器中 public class UserService { //想要拿到provider-server提供的票,需要去拿注册中心的服务 @DubboReference //引用,Pom坐标,可以定义路径相同的接口名 TicketService ticketService; public void bugTicket(){ String ticket = ticketService.getTicket(); System.out.println("在注册中心买到"+ticket); } }
-
编写测试类进行测试,运行 consumer-server 模块
package com.cwlin; import com.cwlin.service.UserService; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class ConsumerServerApplicationTests { @Autowired UserService userService; @Test void contextLoads() { userService.bugTicket(); } }
-
刷新 Dubbo Admin 监控中心,查看消费者:
14.5.4、启动测试
- 启动 zookeeper:运行 zkServer.cmd
- 打开 dubbo-admin 实现监控:运行 dubbo-admin-0.0.1-SNAPSHOT.jar【可以省略】
- 运行服务提供者和服务消费者两个模块,进行消费者的消费测试
- 请求访问:http://localhost:7001/,并登录 Dubbo Admin 监控中心查看提供者和消费者信息
- 这就是SpingBoot + dubbo + zookeeper实现分布式开发的应用,其实就是一个服务拆分的思想!
15、总结
15.1、开发框架
-
spring
- IOC AOP
- IOC:控制反转,依赖注入
- 以前自己是 new 对象,现在交给spring ioc容器统一管理,我们需要什么直接去拿。ioc容器就像是一个对象工厂,把对象交给工厂,工程管理对象的创建以及依赖关系,我们需要的时候直接去工厂中获取
- 控制反转:是一种思想,原本自己控制的事情,交给ico控制,将对象统一管理以降低耦合
- 依赖注入:是控制反转的实现方式,对象无需自行创建或者管理依赖关系,依赖关系将被自动注入到需要他们的对象中
- AOP:面向切面编程(本质动态代理)
- 解决非业务代码抽取问题,底层是动态代理,在spring内实现依赖是BeanPostProcessor,在方法前后增加非业务代码
- 不影响业务本来的情况下实现动态增加功能,大量应用在日志、事务等方面
- IOC:控制反转,依赖注入
- Spring是一个轻量级的Java开源框架,容器
- 目的:解决企业开发的复杂性问题
- IOC AOP
-
SpringBoot
- SpringBoot是Spring的升级版
- 新一代JavaEE的开发标准,开箱即用,拿过来就可以用
- SpringBoot 内置大量的自动配置类,自动配置了许多属性
- 特性:约定大于配置
15.2、微服务架构
- 学习资源:狂神说 SSM 及 SpringBoot 系列文章
- 随着公司体系越来越大,用户越来越多!产生了一个新架构 —> 微服务架构:
- 模块化,功能化:用户、支付、签到、娱乐等
- 人过于多,一台服务器解决不了,增加服务器,横向解决问题
- 假设A服务器占用98%,B服务器只占用了10% ------ 负载均衡
- 将原来的整体项目模块化,则用户是一个单独的项目,签到也是一个单独的项目,项目和项目之间需要通信
- 用户非常多而签到十分少,给用户项目多一点服务器,给签到项目少一点服务器
- 微服务架构问题
- 分布式架构会遇到的四个核心问题
- 这么多服务,客户端该如何去访问
- 这么多服务,服务之间如何进行通信
- 这么多服务,如何进行治理
- 服务器崩溃了,服务挂了,怎么处理
- Spring Cloud 的产生
- SpringCloud 是一套生态,就是用来解决分布式架构的以上四个问题
- 使用 SpringCloud 必须掌握 SpringBoot,SpringCloud 是基于 SpringBoot 的
- 分布式架构会遇到的四个核心问题
- 解决方案
-
Spring Cloud NetFlix,提出了一套一站式解决方案,可以直接使用
- API 网关,zuul 组件
- Feigh,HTTPClien,基于HTTP的通信方式,同步并阻塞
- 服务注册与发现:Eureka
- 熔断机制:Hystrix
- 2018年年底,NetFlix 宣布无限期停止维护,生态不再维护,就会脱节
-
Apache Dubbo zookeeper:第二套解决系统
- API:没有。要么找第三方组件,要么自己实现
- Dubbo 是一个高性能的基于 Java 实现的 RPC 通信框架
- 服务注册与发现:zookeeper,动物园管理者(Hadoop,Hive)
- 熔断机制:没有。借助了Hystrix
-
SpringCloud Alibaba:一站式解决方案
-
目前又提出一种方法:
-
服务网格:下一代微服务标准,Service Mesh;代表解决方案:Istio
-
- 上述四个解决方案可以概括如下:
- API网关,服务路由
- HTTP、RPC框架,异步调用
- 服务注册与发现,高可用
- 熔断机制,服务降级
- 为什么要解决这个问题?本质是网络不可靠!