SpringBoot-14-WinterChenS
https://github.com/ChenCurry/springboot-learning-experience.git
forked from-->https://github.com/WinterChenS/springboot-learning-experience.git
(该篇博文算是学习 WinterChenS 博文系列教程的一个笔记和分享)
工程总览
spring-boot-actuator spring-boot-actuator-admin spring-boot-actuator-client spring-boot-admin spring-boot-cache-redis spring-boot-config spring-boot-data-jpa spring-boot-dubbo-client spring-boot-dubbo-service spring-boot-exception spring-boot-file-upload spring-boot-jdbctemplate spring-boot-lettuce-redis spring-boot-mybatis spring-boot-mybatis-hikaricp spring-boot-mybatis-mutil-database spring-boot-mybatis-plugin spring-boot-rabbit-amqp spring-boot-rabbitmq spring-boot-rabbitmq-delay spring-boot-rest-template spring-boot-start spring-boot-swagger spring-boot-task spring-boot-thymeleaf spring-boot-validation1
第一篇:构建第一个SpringBoot工程:spring-boot-start
@RestController//主启动类标注成一个Controller类 @SpringBootApplication public class SpringBootStartApplication { public static void main(String[] args) { SpringApplication.run(SpringBootStartApplication.class, args); } @GetMapping("/demo1") public String demo1() { return "Hello Luis"; } @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) { // 目的是 return args -> { System.out.println("来看看 SpringBoot 默认为我们提供的 Bean:"); String[] beanNames = ctx.getBeanDefinitionNames(); Arrays.sort(beanNames); Arrays.stream(beanNames).forEach(System.out::println);//看到默认装载了120几个bean对象 }; } }
访问
http://localhost:8080/demo1
见识一下这个 命令行Runner + lambda expression + .forEach
( 关于 lambda 表达式,这里实操一下 https://www.cnblogs.com/coprince/p/8692972.html)
第二篇:SpringBoot配置体验:spring-boot-config
这个配置大概是用来让Idea编辑配置文件时有提示
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
先说多文档配置,访问的时候需要加一层dev:http://localhost:8080/dev/properties/1;说明激活不同的配置,读取的属性值是不一样的。
属性配置,用操作对象的形式来获取配置文件的内容
形式一
形式二
装载对象
外部命令引导(就是在不重新打包的情况下通过命令行修改项目加载的配置?)
java -jar spring-boot-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=test --my1.age=32
第三篇:SpringBoot日志配置
默认Commons Logging、Logback 支持Java Util Logging、Log4J2等
日志级别
ERROR(FATAL) WARN INFO DEBUG(默认不输出) TRACE(默认不输出)
配置日志输出
命令模式:java -jar app.jar --debug=true 资源文件:application.properties配置debug=true 自己的项目想要输出DEBUG:logging.level.<logger-name>=<level>
例如
logging.level.root = WARN logging.level.org.springframework.web = DEBUG logging.level.org.hibernate = ERROR #比如 mybatis sql日志 logging.level.org.mybatis = INFO logging.level.mapper所在的包 = DEBUG
日志输出格式
**logging.pattern.console:**定义输出到控制台的格式 **logging.pattern.file:**定义输出到文件的格式
日志输出到文件(application.properties配置)
logging.file:将日志写入到指定的文件中,默认为相对路径,可以设置成绝对路径 logging.path:将名为spring.log写入到指定的文件夹中,如(/var/log)
**logging.file.max-size:**限制日志文件大小 **logging.file.max-history:**限制日志保留天数
Logback扩展配置
springProfile标签实现:开发环境日志级别为DEBUG/并且开发环境不写日志文件;测试环境日志级别为INFO/并且记录日志文件 springProperty读application.properties配置
第四篇:整合Thymeleaf模板:spring-boot-thymeleaf
模板依赖
<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>
主要的点
xxx.html 写在 src/main/resources/templates 目录下;
js 写在 resources 下的 static/js 目录下;
在标签中添加额外属性动态绑定数据;
View
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <!-- 可以看到 thymeleaf 是通过在标签里添加额外属性来绑定动态数据的 --> <title th:text="${title}">Title</title> <!-- 在/resources/static/js目录下创建一个hello.js 用如下语法依赖即可--> <script type="text/javascript" th:src="@{/js/hello.js}"></script> </head> <body> <h1 th:text="${desc}">Hello World</h1> <h2>=====作者信息=====</h2> <p th:text="${author?.name}"></p> <p th:text="${author?.age}"></p> <p th:text="${author?.email}"></p> </body> </html>
Controller
@Controller @RequestMapping public class ThymeleafController { @GetMapping("/index") public ModelAndView index() { ModelAndView view = new ModelAndView(); // 设置跳转的视图 默认映射到 src/main/resources/templates/{viewName}.html view.setViewName("index"); // 设置属性 view.addObject("title", "我的第一个WEB页面"); view.addObject("desc", "欢迎进入luis-web 系统"); Author author = new Author(); author.setAge(22); author.setEmail("1085143002@qq.com"); author.setName("Luis"); view.addObject("author", author); return view; } @GetMapping("/index1") public String index1(HttpServletRequest request) { // TODO 与上面的写法不同,但是结果一致。 // 设置属性 request.setAttribute("title", "我的第一个WEB页面"); request.setAttribute("desc", "欢迎进入luis-web 系统"); Author author = new Author(); author.setAge(22); author.setEmail("1085143002@qq.com"); author.setName("Luis"); request.setAttribute("author", author); // 返回的 index 默认映射到 src/main/resources/templates/xxxx.html return "index"; } class Author { private int age; private String name; private String email; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } } }
小操作
spring.thymeleaf.cache 设置成 false # 开发过程中,修改静态页面不重启,Ctrl+Shift+F9 重新加载 src/main/static/放favicon.ico # 修改图标
第五篇:使用JdbcTemplate访问数据库:spring-boot-jdbctemplate
对比动能强大的ORM框架,JdbcTemplate有速度优势; 对JDBC进行简单封装,更像是一个DBUtils; Spring自家出品,配置简单;
依赖
<!-- Spring JDBC 的依赖包,使用 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 将会自动获得HikariCP依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MYSQL包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
application.properties
server.port=1111 spring.datasource.url=jdbc:mysql://localhost:3306/chapter4?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root #spring.datasource.type #更多细微的配置可以通过下列前缀进行调整 #spring.datasource.hikari #spring.datasource.tomcat #spring.datasource.dbcp2
连接池
默认连接池:HikariCP/tomcat-jdbc/Commons DBCP2; spring.datasource.type属性指定连接池;
响应前端数据请求
@RestController @RequestMapping("/users") public class SpringJdbcController { private final JdbcTemplate jdbcTemplate; @Autowired public SpringJdbcController(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } @GetMapping public List<User> queryUsers() { // 查询所有用户 String sql = "select * from t_user"; return jdbcTemplate.query(sql, new Object[]{}, new BeanPropertyRowMapper<>(User.class)); } @GetMapping("/{id}") public User getUser(@PathVariable Long id) { // 根据主键ID查询 String sql = "select * from t_user where id = ?"; return jdbcTemplate.queryForObject(sql, new Object[]{id}, new BeanPropertyRowMapper<>(User.class)); } @DeleteMapping("/{id}") public int delUser(@PathVariable Long id) { // 根据主键ID删除用户信息 String sql = "DELETE FROM t_user WHERE id = ?"; return jdbcTemplate.update(sql, id); } @PostMapping public int addUser(@RequestBody User user) { // 添加用户 String sql = "insert into t_user(username, password) values(?, ?)"; return jdbcTemplate.update(sql, user.getUsername(), user.getPassword()); } @PutMapping("/{id}") public int editUser(@PathVariable Long id, @RequestBody User user) { // 根据主键ID修改用户信息 String sql = "UPDATE t_user SET username = ? ,password = ? WHERE id = ?"; return jdbcTemplate.update(sql, user.getUsername(), user.getPassword(), id); } }
新增
修改
删除
第六篇:整合SpringDataJpa:spring-boot-data-jpa
JPA即Java Persistence API,一说Java持久层API Sun为了简化开发,整合了ORM框架技术
JPA包括以下3方面的技术
ORM映射元数据:支持XML和注解两种元数据的形式,元数据描述对象和表之间的映射关系,框架据此将实体对象持久化到数据库表中; API:操作实体对象来执行CRUD操作,框架在后台替代我们完成所有的事情,开发者从繁琐的JDBC和SQL代码中解脱出来。 查询语言:通过面向对象而非面向数据库的查询语言查询数据,避免程序的SQL语句紧密耦合。
JPA与Hibernate与Spring Data JPA
JPA只是一种规范,它需要第三方自行实现其功能,在众多框架中Hibernate是最为强大的一个; 从功能上来说,JPA就是Hibernate功能的一个子集; 常见的ORM框架中Hibernate的JPA最为完整,因此Spring Data JPA是采用基于JPA规范的Hibernate框架基础上提供了Repository层的实现; Spring Data Repository极大地简化了实现各种持久层的数据库访问而写的样板代码量,同时CrudRepository提供了丰富的CRUD功能去管理实体类。
缺点
学习成本较大,需要学习HQL; 配置复杂,虽然SpringBoot简化的大量的配置,关系映射多表查询配置依旧不容易; 性能较差,对比JdbcTemplate、Mybatis等ORM框架,它的性能无异于是最差的;
依赖
<!-- Spring JDBC 的依赖包,使用 spring-boot-starter-jdbc 或 spring-boot-starter-data-jpa 将会自动获得HikariCP依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- MYSQL包 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 默认就内嵌了Tomcat 容器,如需要更换容器也极其简单--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 测试包,当我们使用 mvn package 的时候该包并不会被打入,因为它的生命周期只在 test 之内--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
配置
spring.datasource.url=jdbc:mysql://localhost:3306/chapter5?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root #spring.datasource.type # JPA配置 spring.jpa.hibernate.ddl-auto=update # 输出日志 spring.jpa.show-sql=true # 数据库类型 spring.jpa.database=mysql
ddl-auto 几种属性
create:每次运行程序时,都会重新创建表,故而数据会丢失; create-drop:每次运行程序时会先创建表结构,然后待程序结束时清空表; upadte:每次运行程序,没有表时会创建表,如果对象发生改变会更新表结构,原有数据不会清空,只会更新(推荐使用); validate:运行程序会校验数据与数据库的字段类型是否相同,字段不同会报错;
加到实体类上的JPA规范注解(javax.persistence包下)
@Id主键 @GeneratedValue(strategy = GenerationType.IDENTITY)自增策略 @Transient不需要映射的字段可以通过该注解排除掉
常见的几种自增策略
TABLE:使用一个特定的数据库表格来保存主键 SEQUENCE:根据底层数据库的序列来生成主键,条件是数据库支持序列。 这个值要与generator一起使用,generator 指定生成主键使用的生成器(可能是orcale中自己编写的序列)。 IDENTITY:主键由数据库自动生成(主要是支持自动增长的数据库,如mysql) AUTO:主键由程序控制,也是GenerationType的默认值。
实体类
@Entity(name = "t_user") public class User implements Serializable { private static final long serialVersionUID = 8655851615465363473L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; /** * TODO 忽略该字段的映射 */ @Transient private String email; public User() { } public User(String username, String password){ this.username = username; this.password = password; } public User(Long id, String username, String password){ this.id = id; this.username = username; this.password = password; } // get set 略 }
XxxRepository接口
创建UserRepository数据访问层接口,需要继承JpaRepository<T,K> 第一个泛型参数是实体对象的名称,第二个是主键类型。 只需要这样简单的配置,该UserRepository就拥常用的CRUD功能。 JpaRepository本身就包含了常用功能,剩下的查询我们按照规范写接口即可。 JPA支持@Query注解写HQL,也支持findAllByUsername这种根据字段名命名的方式。
UserRepository
@Repository public interface UserRepository extends JpaRepository<User, Long> { /** * 根据用户名查询用户信息 * @param username 用户名 * @return 查询结果 */ List<User> findAllByUsername(String username); }
测试
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootDataJpaApplicationTests { @Test public void contextLoads() { } private static final Logger log = LoggerFactory.getLogger(SpringBootDataJpaApplicationTests.class); @Autowired private UserRepository userRepository; @Test public void test1() throws Exception { final User user = userRepository.save(new User("u1", "p1")); log.info("[添加成功] - [{}]", user); final List<User> u1 = userRepository.findAllByUsername("u1"); log.info("[条件查询] - [{}]", u1); Pageable pageable = PageRequest.of(0, 10, Sort.by(Sort.Order.desc("username"))); final Page<User> users = userRepository.findAll(pageable); log.info("[分页+排序+查询所有] - [{}]", users.getContent()); userRepository.findById(users.getContent().get(0).getId()).ifPresent(user1 -> log.info("[主键查询] - [{}]", user1)); final User edit = userRepository.save(new User(user.getId(), "修改后的ui", "修改后的p1")); log.info("[修改成功] - [{}]", edit); userRepository.deleteById(user.getId()); log.info("[删除主键为 {} 成功] - [{}]", user.getId()); } }
解析
几个操作中,只有findAllByUsername是自己编写的,其它的都是继承自JpaRepository接口中的方法; 更关键的是分页及排序是如此的简单实例化一个Pageable即可;
第七篇:整合Mybatis:spring-boot-mybatis
对比
依赖
<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
配置
spring.datasource.url=jdbc:mysql://localhost:3306/chapter6?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root # 注意注意 mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.winterchen.entity # 驼峰命名规范 如:数据库字段是 order_id 那么 实体字段就要写成 orderId mybatis.configuration.map-underscore-to-camel-case=true
建表
CREATE TABLE `t_user` ( `id` int(8) NOT NULL AUTO_INCREMENT COMMENT '主键自增', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(50) NOT NULL COMMENT '密码', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
实体
public class User implements Serializable { private static final long serialVersionUID = 8655851615465363473L; private Long id; private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } //get set 略 }
接口
/** * t_user 操作:演示两种方式 * <p>第一种是基于mybatis3.x版本后提供的注解方式<p/> * <p>第二种是早期写法,将SQL写在 XML 中<p/> * * Created by Donghua.Chen on 2018/6/7. */ @Mapper public interface UserMapper { /** * 根据用户名查询用户结果集 * * @param username 用户名 * @return 查询结果 */ @Select("SELECT * FROM t_user WHERE username = #{username}") List<User> findByUsername(@Param("username") String username); /** * 保存用户信息 * * @param user 用户信息 * @return 成功 1 失败 0 */ int insert(User user); }
映射文件
<?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.winterchen.mapper.UserMapper"> <insert id="insert" parameterType="com.winterchen.entity.User"> INSERT INTO `t_user`(`username`,`password`) VALUES (#{username},#{password}) </insert> </mapper>
测试类
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootMybatisApplicationTests { private static final Logger log = LoggerFactory.getLogger(SpringBootMybatisApplicationTests.class); @Autowired private UserMapper userMapper; @Test public void test1() throws Exception { final int row1 = userMapper.insert(new User("u1", "p1")); log.info("[添加结果] - [{}]", row1); final int row2 = userMapper.insert(new User("u2", "p2")); log.info("[添加结果] - [{}]", row2); final int row3 = userMapper.insert(new User("u1", "p3")); log.info("[添加结果] - [{}]", row3); final List<User> u1 = userMapper.findByUsername("u1"); log.info("[根据用户名查询] - [{}]", u1); } }
第八篇:通用Mapper与分页插件的集成:spring-boot-mybatis-plugin
依赖
<!-- 分页插件 文档地址:https://github.com/pagehelper/Mybatis-PageHelper/blob/master/wikis/zh/HowToUse.md --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency>
配置
spring.datasource.url=jdbc:mysql://localhost:3306/chapter7?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root # 如果想看到mybatis日志需要做如下配置 logging.level.com.battcn=DEBUG ########## Mybatis 自身配置 ########## mybatis.mapper-locations=classpath:mapper/*.xml mybatis.type-aliases-package=com.winterchen.entity # 驼峰命名规范 如:数据库字段是 order_id 那么 实体字段就要写成 orderId mybatis.configuration.map-underscore-to-camel-case=true ########## 通用Mapper ########## # 主键自增回写方法,默认值MYSQL,详细说明请看文档 mapper.identity=MYSQL mapper.mappers=tk.mybatis.mapper.common.BaseMapper # 设置 insert 和 update 中,是否判断字符串类型!='' mapper.not-empty=true # 枚举按简单类型处理 mapper.enum-as-simple-type=true ########## 分页插件 ########## pagehelper.helper-dialect=mysql pagehelper.params=count=countSql pagehelper.reasonable=false pagehelper.support-methods-arguments=true
建表
CREATE TABLE `t_user` ( `id` int(8) NOT NULL AUTO_INCREMENT COMMENT '主键自增', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(50) NOT NULL COMMENT '密码', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
实体
@Table(name = "t_user") public class User implements Serializable{ private static final long serialVersionUID = 8655851615465363473L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } //get set 略 }
接口(在这里搞鬼)
@Mapper public interface UserMapper extends BaseMapper<User> { /** * 根据用户名统计(TODO 假设它是一个很复杂的SQL) * * @param username 用户名 * @return 统计结果 */ int countByUsername(String username); }
映射文件
<?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.winterchen.mapper.UserMapper"> <select id="countByUsername" resultType="java.lang.Integer"> SELECT count(1) FROM t_user WHERE username = #{username} </select> </mapper>
测试
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootMybatisPluginApplicationTests { private static final Logger log = LoggerFactory.getLogger(SpringBootMybatisPluginApplicationTests.class); @Autowired private UserMapper userMapper; @Test public void test1() throws Exception { final User user1 = new User("u1", "p1"); final User user2 = new User("u1", "p2"); final User user3 = new User("u3", "p3"); userMapper.insertSelective(user1); log.info("[user1回写主键] - [{}]", user1.getId()); userMapper.insertSelective(user2); log.info("[user2回写主键] - [{}]", user2.getId()); userMapper.insertSelective(user3); log.info("[user3回写主键] - [{}]", user3.getId()); final int count = userMapper.countByUsername("u1"); log.info("[调用自己写的SQL] - [{}]", count); // TODO 模拟分页 userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); userMapper.insertSelective(new User("u1", "p1")); // TODO 分页 + 排序 this.userMapper.selectAll() 这一句就是我们需要写的查询,有了这两款插件无缝切换各种数据库 final PageInfo<Object> pageInfo = PageHelper.startPage(1, 10).setOrderBy("id desc").doSelectPageInfo(() -> this.userMapper.selectAll()); log.info("[lambda写法] - [分页信息] - [{}]", pageInfo.toString()); PageHelper.startPage(1, 10).setOrderBy("id desc"); final PageInfo<User> userPageInfo = new PageInfo<>(this.userMapper.selectAll()); log.info("[普通写法] - [{}]", userPageInfo); } }
第九篇:整合Lettuce Redis:spring-boot-lettuce-redis
Jedis在实现上是直连redis server,多线程环境下非线程安全,除非使用连接池,为每个Jedis实例增加物理连接。 Lettuce基于Netty的连接实例(StatefulRedisConnection),可以在多个线程间并发访问,且线程安全,满足多线程环境下的并发访问
,同时它是可伸缩的设计,一个连接实例不够的情况也可以按需增加连接实例。
安装并启动单体redis
docker pull redis docker run -itd --name redis-test -p 6379:6379 redis
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
配置
spring.redis.host=106.75.32.166 spring.redis.port=6379 #spring.redis.password=root #根据需要 # 连接超时时间(毫秒) spring.redis.timeout=10000 # Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0 spring.redis.database=0 # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0
实体类
public class User implements Serializable{ private static final long serialVersionUID = 8655851615465363473L; private Long id; private String username; private String password; public User() { } public User(String username, String password) { this.username = username; this.password = password; } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } //get set 略 }
自定义配置类
/** * 默认情况下的模板只能支持RedisTemplate<String, String>,也就是只能存入字符串,这在开发中是不友好的 * ,所以自定义模板是很有必要的,当自定义了模板又想使用String存储这时候就可以使用StringRedisTemplate的方式,它们并不冲突 */ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisCacheAutoConfiguration { @Bean public RedisTemplate<String, Serializable> redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) { RedisTemplate<String, Serializable> template = new RedisTemplate<>(); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setConnectionFactory(redisConnectionFactory); return template; } }
测试
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootLettuceRedisApplicationTests { private static final Logger log = LoggerFactory.getLogger(SpringBootLettuceRedisApplicationTests.class); @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private RedisTemplate<String, Serializable> redisCacheTemplate; @Test public void get() { // TODO 测试线程安全 ExecutorService executorService = Executors.newFixedThreadPool(1000); IntStream.range(0, 1000).forEach(i -> executorService.execute(() -> stringRedisTemplate.opsForValue().increment("kk", 1)) ); stringRedisTemplate.opsForValue().set("k1", "v1"); final String k1 = stringRedisTemplate.opsForValue().get("k1"); log.info("[字符缓存结果] - [{}]", k1); // TODO 以下只演示整合,具体Redis命令可以参考官方文档,Spring Data Redis 只是改了个名字而已,Redis支持的命令它都支持 String key = "battcn:user:1"; redisCacheTemplate.opsForValue().set(key, new User(1L, "u1", "pa")); // TODO 对应 String(字符串) final User user = (User) redisCacheTemplate.opsForValue().get(key); log.info("[对象缓存结果] - [{}]", user); } }
第十篇:使用Spring Cache集成Redis:spring-boot-cache-redis
基于annotation即可使得现有代码支持缓存 开箱即用Out-Of-The-Box,不用安装和部署额外第三方组件即可使用缓存 支持Spring Express Language,能使用对象的任何属性或者方法来定义缓存的key和condition 支持AspectJ,并通过其实现任何方法的缓存支持 支持自定义key和自定义缓存管理者,具有相当的灵活性和扩展性
未使用 Spring Cache 时
public String get(String key) { String value = userMapper.selectById(key); if (value != null) { cache.put(key,value); } return value; }
使用 Spring Cache 后
@Cacheable(value = "user", key = "#key") public String get(String key) { return userMapper.selectById(key); }
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
配置
spring.redis.host=106.75.32.166 spring.redis.port=6379 # 一般来说是不用配置的,Spring Cache 会根据依赖的包自行装配:JCache -> EhCache -> Redis -> Guava spring.cache.type=redis # 连接超时时间(毫秒) spring.redis.timeout=10000 # Redis默认情况下有16个分片,这里配置具体使用的分片 spring.redis.database=0 # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0
实体
public class User implements Serializable { private static final long serialVersionUID = 8655851615465363473L; private Long id; private String username; private String password; public User() { } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } // TODO get set }
接口
public interface UserService { /** * 保存 更新 * @param user 用户对象 * @return 操作结果 */ User saveOrUpdate(User user); /** * 添加 * @param id key值 * @return 返回结果 */ User get(Long id); /** * 删除 * @param id key值 */ void delete(Long id); }
实现(@Cacheable、@CachePut、@CacheEvict)
@Service public class UserServiceImpl implements UserService{ private static final Map<Long, User> DATABASES = new HashMap<>(); static { DATABASES.put(1L, new User(1L, "u1", "p1")); DATABASES.put(2L, new User(2L, "u2", "p2")); DATABASES.put(3L, new User(3L, "u3", "p3")); } private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class); @Cacheable(value = "user", key = "#id") @Override public User get(Long id) { // TODO 我们就假设它是从数据库读取出来的 log.info("进入 get 方法"); return DATABASES.get(id); } @CachePut(value = "user", key = "#user.id") @Override public User saveOrUpdate(User user) { DATABASES.put(user.getId(), user); log.info("进入 saveOrUpdate 方法"); return user; } @CacheEvict(value = "user", key = "#id") @Override public void delete(Long id) { DATABASES.remove(id); log.info("进入 delete 方法"); } }
主函数需要注解开启
@SpringBootApplication @EnableCaching public class SpringBootCacheRedisApplication { public static void main(String[] args) { SpringApplication.run(SpringBootCacheRedisApplication.class, args); } }
测试
@RunWith(SpringRunner.class) @SpringBootTest public class SpringBootCacheRedisApplicationTests { private static final Logger log = LoggerFactory.getLogger(SpringBootCacheRedisApplicationTests.class); @Autowired private UserService userService; @Test public void get() { final User user = userService.saveOrUpdate(new User(5L, "u5", "p5")); log.info("[saveOrUpdate] - [{}]", user); final User user1 = userService.get(5L); log.info("[get] - [{}]", user1); userService.delete(5L); } }
根据条件操作缓存
根据条件操作缓存内容并不影响数据库操作,条件表达式返回一个布尔值,true/false,当条件为true,则进行缓存操作。 长度:@CachePut(value = "user", key = "#user.id",condition = "#user.username.length() < 10")只缓存用户名长度少于10的数据 大小:@Cacheable(value = "user", key = "#id",condition = "#id < 10")只缓存ID小于10的数据 组合:@Cacheable(value="user",key="#user.username.concat(##user.password)") 提前操作:@CacheEvict(value="user",allEntries=true,beforeInvocation=true)加上beforeInvocation=true后,不管内部是否报错,缓存都将被清除,默认情况为false
第十一篇:集成Swagger在线调试:spring-boot-swagger
swagger优缺点
集成方便,功能强大 在线调试与文档生成 代码耦合,需要注解支持,但不影响程序性能
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.battcn</groupId> <artifactId>swagger-spring-boot-starter</artifactId> <version>1.4.5-RELEASE</version> </dependency>
配置
# 扫描的包路径,默认扫描所有 spring.swagger.base-package=com.winterchen # 默认为 true 生产环境设置为false spring.swagger.enabled=true
实体类(用@ApiModel、@ApiModelProperty注解)
@ApiModel public class User implements Serializable { private static final long serialVersionUID = 8655851615465363473L; private Long id; @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; public User() { } public User(Long id, String username, String password) { this.id = id; this.username = username; this.password = password; } // get set }
控制类(restful 风格接口)
@RestController @RequestMapping("/users") @Api(tags = "1.1", description = "用户管理", value = "用户管理") public class UserController { private static final Logger log = LoggerFactory.getLogger(UserController.class); @GetMapping @ApiOperation(value = "条件查询(DONE)") @ApiImplicitParams({ @ApiImplicitParam(name = "username", value = "用户名", dataType = ApiDataType.STRING, paramType = ApiParamType.QUERY), @ApiImplicitParam(name = "password", value = "密码", dataType = ApiDataType.STRING, paramType = ApiParamType.QUERY), }) public User query(String username, String password) { log.info("多个参数用 @ApiImplicitParams"); return new User(1L, username, password); } @GetMapping("/{id}") @ApiOperation(value = "主键查询(DONE)") @ApiImplicitParams({ @ApiImplicitParam(name = "id", value = "用户编号", dataType = ApiDataType.LONG, paramType = ApiParamType.PATH), }) public User get(@PathVariable Long id) { log.info("单个参数用 @ApiImplicitParam"); return new User(id, "u1", "p1"); } @DeleteMapping("/{id}") @ApiOperation(value = "删除用户(DONE)") @ApiImplicitParam(name = "id", value = "用户编号", dataType = ApiDataType.LONG, paramType = ApiParamType.PATH) public void delete(@PathVariable Long id) { log.info("单个参数用 ApiImplicitParam"); } @PostMapping @ApiOperation(value = "添加用户(DONE)") public User post(@RequestBody User user) { log.info("如果是 POST PUT 这种带 @RequestBody 的可以不用写 @ApiImplicitParam"); return user; } @PutMapping("/{id}") @ApiOperation(value = "修改用户(DONE)") public void put(@PathVariable Long id, @RequestBody User user) { log.info("如果你不想写 @ApiImplicitParam 那么 swagger 也会使用默认的参数名作为描述信息 "); } }
注解用法
@Api:描述Controller @ApiIgnore:忽略该Controller,指不对当前类做扫描 @ApiOperation:描述Controller类中的method接口 @ApiParam:单个参数描述,与@ApiImplicitParam不同的是,他是写在参数左侧的。如(@ApiParam(name = "username",value = "用户名") String username) @ApiModel:描述POJO对象 @ApiProperty:描述POJO对象中的属性值 @ApiImplicitParam:描述单个入参信息 @ApiImplicitParams:描述多个入参信息 @ApiResponse:描述单个出参信息 @ApiResponses:描述多个出参信息 @ApiError:接口错误所返回的信息
主函数上需要加注解
@SpringBootApplication @EnableSwagger2Doc public class SpringBootSwaggerApplication { public static void main(String[] args) { SpringApplication.run(SpringBootSwaggerApplication.class, args); } }
查看
http://localhost:8080/swagger-ui.html
第十二篇:初探RabbitMQ消息队列:spring-boot-rabbit-amqp
安装 RabbitMQ
docker pull rabbitmq:3.8.9-management
docker run -d --name 3.8.9-management -p 5672:5672 -p 15672:15672 -v `pwd`/data:/var/lib/rabbitmq --hostname myRabbit -e \ RABBITMQ_DEFAULT_VHOST=my_vhost -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.8.9-management
# http://106.75.32.166:15672
MQ
全称(Message Queue)又名消息队列,是一种异步通讯的中间件。可以将它理解成邮局,发送者将消息传递到邮局,然后由邮局帮我们发送给具体的消息接收者(消费者),具体发送过程与时间我们无需关心,它也不会干扰我进行其它事情。 常见的MQ有kafka、activemq、zeromq、rabbitmq等
RabbitMQ
RabbitMQ是一个遵循AMQP协议,由面向高并发的erlanng语言开发而成,用在实时的对可靠性要求比较高的消息传递上,支持多种语言客户端。 支持延迟队列(这是一个非常有用的功能)
基础概念
Broker:简单来说就是消息队列服务器实体 Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列 Queue:消息队列载体,每个消息都会被投入到一个或多个队列 Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来 Routing Key:路由关键字,exchange根据这个关键字进行消息投递 vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离 producer:消息生产者,就是投递消息的程序 consumer:消息消费者,就是接受消息的程序 channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务
常见应用场景
邮箱发送:用户注册后投递消息到rabbitmq中,由消息的消费方异步的发送邮件,提升系统响应速度 流量削峰:一般在秒杀活动中应用广泛,秒杀会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。用于控制活动人数,将超过此一定阀值的订单直接丢弃。缓解短时间的高流量压垮应用。 订单超时:利用rabbitmq的延迟队列,可以很简单的实现订单超时的功能,比如用户在下单后30分钟未支付取消订单
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
配置
spring.rabbitmq.host=106.75.32.166 spring.rabbitmq.port=5672 spring.rabbitmq.username=admin spring.rabbitmq.password=admin # 以实际后台页面配置为准 spring.rabbitmq.virtual-host=my_vhost # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none spring.rabbitmq.listener.simple.acknowledge-mode=manual
配置类(定义队列)
@Configuration public class RabbitConfig { public static final String DEFAULT_BOOK_QUEUE = "dev.book.register.default.queue"; public static final String MANUAL_BOOK_QUEUE = "dev.book.register.manual.queue"; @Bean public Queue defaultBookQueue() { // 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理 return new Queue(DEFAULT_BOOK_QUEUE, true); } @Bean public Queue manualBookQueue() { // 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理 return new Queue(MANUAL_BOOK_QUEUE, true); } }
实体
public class Book implements Serializable { private static final long serialVersionUID = -2164058270260403154L; private String id; private String name; public Book() { } public Book(String id, String name) { this.id = id; this.name = name; } // get set }
控制器生产消息
@RestController @RequestMapping(value = "/books") public class BookController { private final RabbitTemplate rabbitTemplate; @Autowired public BookController(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } /** * this.rabbitTemplate.convertAndSend(RabbitConfig.DEFAULT_BOOK_QUEUE, book); 对应 {@link BookHandler#listenerAutoAck} * this.rabbitTemplate.convertAndSend(RabbitConfig.MANUAL_BOOK_QUEUE, book); 对应 {@link BookHandler#listenerManualAck} */ @GetMapping public void defaultMessage() { Book book = new Book(); book.setId("1"); book.setName("一起来学Spring Boot"); this.rabbitTemplate.convertAndSend(RabbitConfig.DEFAULT_BOOK_QUEUE, book); this.rabbitTemplate.convertAndSend(RabbitConfig.MANUAL_BOOK_QUEUE, book); } }
消费消息
@Component public class BookHandler { private static final Logger log = LoggerFactory.getLogger(BookHandler.class); /** * <p>TODO 该方案是 spring-boot-data-amqp 默认的方式,不太推荐。具体推荐使用 listenerManualAck()</p> * 默认情况下,如果没有配置手动ACK(确认消息), 那么Spring Data AMQP 会在消息消费完毕后自动帮我们去ACK * 存在问题:如果报错了,消息不会丢失,但是会无限循环消费,一直报错,如果开启了错误日志很容易就吧磁盘空间耗完 * 解决方案:手动ACK,或者try-catch 然后在 catch 里面讲错误的消息转移到其它的系列中去 * spring.rabbitmq.listener.simple.acknowledge-mode=manual * <p> * * @param book 监听的内容 */ @RabbitListener(queues = {RabbitConfig.DEFAULT_BOOK_QUEUE}) public void listenerAutoAck(Book book, Message message, Channel channel) { // TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉 final long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { log.info("[listenerAutoAck 监听的消息] - [{}]", book.toString()); // TODO 通知 MQ 消息已被成功消费,可以ACK了 channel.basicAck(deliveryTag, false); } catch (IOException e) { try { // TODO 处理失败,重新压入MQ channel.basicRecover(); } catch (IOException e1) { e1.printStackTrace(); } } } @RabbitListener(queues = {RabbitConfig.MANUAL_BOOK_QUEUE}) public void listenerManualAck(Book book, Message message, Channel channel) { log.info("[listenerManualAck 监听的消息] - [{}]", book.toString()); try { // TODO 通知 MQ 消息已被成功消费,可以ACK了 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { // TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列 } } }
测试
http://localhost:8080/books
第十三篇:RabbitMQ延迟队列:spring-boot-rabbitmq-delay
所谓延时消息就是指当消息被发送以后,并不想让消费者立即拿到消息,而是等待指定时间后,消费者才拿到这个消息进行消费。
应用场景
订单业务:在电商/点餐中,都有下单后 30 分钟内没有付款,就自动取消订单。 短信通知:下单成功后 60s 之后给用户发送短信通知。 失败重试:业务操作失败后,间隔一定的时间进行失败重试。
DelayQueue
用Java中的 DelayQueue 位于 java.util.concurrent 包下,本质是由 PriorityQueue 和 BlockingQueue 实现的阻塞优先级队列。但是这玩意不支持分布式与持久化
RabbitMQ 实现机制
RabbitMQ队列本身是没有直接实现支持延迟队列的功能,但可以通过它的Time-To-Live Extensions与Dead Letter Exchange的特性模拟出延迟队列的功能。
Time-To-Live Extensions
RabbitMQ支持为队列或者消息设置TTL(time to live 存活时间)。 TTL表明了一条消息可在队列中存活的最大时间。当某条消息被设置了TTL或者当某条消息进入了设置了TTL的队列时,这条消息会在TTL时间后死亡成为Dead Letter。 如果既配置了消息的TTL,又配置了队列的TTL,那么较小的那个值会被取用。
Dead Letter Exchange
死信交换机,上文中提到设置了 TTL 的消息或队列最终会成为Dead Letter。 如果为队列设置了Dead Letter Exchange(DLX),那么这些Dead Letter就会被重新发送到Dead Letter Exchange中,然后通过Dead Letter Exchange路由到其他队列,即可实现延迟队列的功能。
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
配置
spring.rabbitmq.username=admin spring.rabbitmq.password=admin spring.rabbitmq.host=106.75.32.166 spring.rabbitmq.port=5672 spring.rabbitmq.virtual-host=my_vhost # 手动ACK 不开启自动ACK模式,目的是防止报错后未正确处理消息丢失 默认 为 none spring.rabbitmq.listener.simple.acknowledge-mode=manual
配置类(定义队列)
/** * RabbitMQ 配置 */ @Configuration public class RabbitConfig { private static final Logger log = LoggerFactory.getLogger(RabbitConfig.class); @Bean public RabbitTemplate rabbitTemplate(CachingConnectionFactory connectionFactory){ connectionFactory.setPublisherConfirms(true); connectionFactory.setPublisherReturns(true); RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> log.info("消息发送成功:correlationData({}),ack({}),cause({})",correlationData,ack,cause)); rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> log.info("消息丢失:
message({}),replyCode({}),replytext({}),exchange({}),routingKey({})",message,replyCode,replyText,exchange,routingKey)); return rabbitTemplate; } /** * 延迟队列 TTL 名称 */ private static final String REGISTER_DELAY_QUEUE = "dev.book.register.delay.queue"; /** * DLX,dead letter发送到的 exchange * TODO 此处的 exchange 很重要,具体消息就是发送到该交换机的 */ public static final String REGISTER_DELAY_EXCHANGE = "dev.book.register.delay.exchange"; /** * routing key 名称 * TODO 此处的 routingKey 很重要要,具体消息发送在该 routingKey 的 */ public static final String DELAY_ROUTING_KEY = ""; public static final String REGISTER_QUEUE_NAME = "dev.book.register.queue"; public static final String REGISTER_EXCHANGE_NAME = "dev.book.register.exchange"; public static final String ROUTING_KEY = "all"; /** * 延迟队列配置 * <p> * 1、params.put("x-message-ttl", 5 * 1000); * TODO 第一种方式是直接设置 Queue 延迟时间 但如果直接给队列设置过期时间,这种做法不是很灵活,(当然二者是兼容的,默认是时间小的优先) * 2、rabbitTemplate.convertAndSend(book, message -> { * message.getMessageProperties().setExpiration(2 * 1000 + ""); * return message; * }); * TODO 第二种就是每次发送消息动态设置延迟时间,这样我们可以灵活控制 **/ @Bean public Queue delayProcessQueue() { Map<String, Object> params = new HashMap<>(); // x-dead-letter-exchange 声明了队列里的死信转发到的DLX名称, params.put("x-dead-letter-exchange", REGISTER_EXCHANGE_NAME); // x-dead-letter-routing-key 声明了这些死信在转发时携带的 routing-key 名称。 params.put("x-dead-letter-routing-key", ROUTING_KEY); return new Queue(REGISTER_DELAY_QUEUE, true, false, false, params); } /** * 需要将一个队列绑定到交换机上,要求该消息与一个特定的路由键完全匹配。 * 这是一个完整的匹配。如果一个队列绑定到该交换机上要求路由键 “dog”,则只有被标记为“dog”的消息才被转发,不会转发dog.puppy,也不会转发dog.guard,只会转发dog。 * TODO 它不像 TopicExchange 那样可以使用通配符适配多个 * * @return DirectExchange */ @Bean public DirectExchange delayExchange() { return new DirectExchange(REGISTER_DELAY_EXCHANGE); } @Bean public Binding dlxBinding() { return BindingBuilder.bind(delayProcessQueue()).to(delayExchange()).with(DELAY_ROUTING_KEY); } @Bean public Queue registerBookQueue() { return new Queue(REGISTER_QUEUE_NAME, true); } /** * 将路由键和某模式进行匹配。此时队列需要绑定要一个模式上。 * 符号“#”匹配一个或多个词,符号“*”匹配不多不少一个词。因此“audit.#”能够匹配到“audit.irs.corporate”,但是“audit.*” 只会匹配到“audit.irs”。 **/ @Bean public TopicExchange registerBookTopicExchange() { return new TopicExchange(REGISTER_EXCHANGE_NAME); } @Bean public Binding registerBookBinding() { // TODO 如果要让延迟队列之间有关联,这里的 routingKey 和 绑定的交换机很关键 return BindingBuilder.bind(registerBookQueue()).to(registerBookTopicExchange()).with(ROUTING_KEY); } }
实体类
public class Book implements Serializable { private static final long serialVersionUID = -2164058270260403154L; private String id; private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
控制类(生产消息,并指定了延时时间)
@RestController @RequestMapping("/books") public class BookController { private static final Logger log = LoggerFactory.getLogger(BookController.class); private final RabbitTemplate rabbitTemplate; @Autowired public BookController(RabbitTemplate rabbitTemplate) { this.rabbitTemplate = rabbitTemplate; } /** * this.rabbitTemplate.convertAndSend(RabbitConfig.REGISTER_DELAY_EXCHANGE, RabbitConfig.DELAY_ROUTING_KEY, book); 对应 {@link BookHandler#listenerDelayQueue} */ @GetMapping public void defaultMessage() { Book book = new Book(); book.setId("1"); book.setName("一起来学Spring Boot"); // 添加延时队列 this.rabbitTemplate.convertAndSend(RabbitConfig.REGISTER_DELAY_EXCHANGE, RabbitConfig.DELAY_ROUTING_KEY, book, message -> { // TODO 第一句是可要可不要,根据自己需要自行处理 message.getMessageProperties().setHeader(AbstractJavaTypeMapper.DEFAULT_CONTENT_CLASSID_FIELD_NAME, Book.class.getName()); // TODO 如果配置了 params.put("x-message-ttl", 5 * 1000); 那么这一句也可以省略,具体根据业务需要是声明 Queue 的时候就指定好延迟时间还是在发送自己控制时间 message.getMessageProperties().setExpiration(5 * 1000 + ""); return message; }); log.info("[发送时间] - [{}]", LocalDateTime.now()); } }
消费者
/** * BOOK_QUEUE 消费者 * * 默认情况下spring-boot-data-amqp是自动ACK机制,就意味着 MQ 会在消息消费完毕后自动帮我们去ACK, * 这样依赖就存在这样一个问题:如果报错了,消息不会丢失,会无限循环消费,很容易就吧磁盘空间耗完, * 虽然可以配置消费的次数但这种做法也有失优雅。 * 目前比较推荐的就是我们手动ACK然后将消费错误的消息转移到其它的消息队列中,做补偿处理。 * 由于我们需要手动控制ACK,因此下面监听完消息后需要调用basicAck通知rabbitmq消息已被正确消费,可以将远程队列中的消息删除 */ @Component public class BookHandler { private static final Logger log = LoggerFactory.getLogger(BookHandler.class); @RabbitListener(queues = {RabbitConfig.REGISTER_QUEUE_NAME}) public void listenerDelayQueue(Book book, Message message, Channel channel) { log.info("[listenerDelayQueue 监听的消息] - [消费时间] - [{}] - [{}]", LocalDateTime.now(), book.toString()); try { // TODO 通知 MQ 消息已被成功消费,可以ACK了 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (IOException e) { // TODO 如果报错了,那么我们可以进行容错处理,比如转移当前消息进入其它队列 } } }
http://localhost:8080/books
2020-09-29 14:10:24.025 INFO 10564 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2020-09-29 14:10:24.025 INFO 10564 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2020-09-29 14:10:24.046 INFO 10564 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 21 ms 2020-09-29 14:10:24.724 INFO 10564 --- [.75.32.166:5672] com.winterchen.config.RabbitConfig : 消息发送成功:correlationData(null),ack(true),cause(null) 2020-09-29 14:10:24.732 INFO 10564 --- [nio-8080-exec-1] c.winterchen.controller.BookController : [发送时间] - [2020-09-29T14:10:24.732] 2020-09-29 14:10:29.702 INFO 10564 --- [cTaskExecutor-1] com.winterchen.handler.BookHandler : [listenerDelayQueue 监听的消息] - [消费时间] - [2020-09-29T14:10:29.702] - [com.winterchen.model.Book@5626bb5b]
第十四篇:强大的 actuator 服务监控与管理:spring-boot-actuator(未get真意)
actuator 是 spring boot 项目中非常强大的一个功能,有助于对应用程序进行监视和管理,通过 restful api 请求来监管、审计、收集应用的运行情况,针对微服务而言它是必不可少的一个环节…
Endpoints
actuator 的核心部分,它用来监视应用程序及交互,spring-boot-actuator 中已经内置了非常多的 Endpoints(health、info、beans、httptrace、shutdown等等),同时也允许我们自己扩展自己的端点
内置 Endpoints
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
<build> <plugins> <!--如果要访问info接口想获取maven中的属性内容需要添加如下内容--> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
配置
# 描述信息 info.blog-url=http://winterchen.com info.author=Luis info.version=@project.version@ # 加载所有的端点/默认只加载了 info / health management.endpoints.web.exposure.include=* management.endpoint.health.show-details=always # 可以关闭制定的端点 management.endpoint.shutdown.enabled=false # 路径映射,将 health 路径映射成 rest_health 那么在访问 health 路径将为404,因为原路径已经变成 rest_health 了,一般情况下不建议使用 # management.endpoints.web.path-mapping.health=rest_health
启动项目,访问 http://localhost:8080/actuator/info 看到json数据表示配置成功
自定义 - 重点
默认装配 HealthIndicators
健康端点(第一种方式)
实现HealthIndicator接口,根据自己的需要判断返回的状态是UP还是DOWN,功能简单。
/** * 自定义健康端点 */ @Component("my1") public class MyHealthIndicator implements HealthIndicator { private static final String VERSION = "v1.0.0"; @Override public Health health() { int code = check(); if (code != 0) { Health.down().withDetail("code", code).withDetail("version", VERSION).build(); } return Health.up().withDetail("code", code) .withDetail("version", VERSION).up().build(); } private int check() { return 0; } }
测试 http://localhost:8080/actuator/health
健康端点(第二种方式)
继承AbstractHealthIndicator抽象类,重写doHealthCheck方法,功能比第一种要强大一点点,默认的 DataSourceHealthIndicator 、 RedisHealthIndicator 都是这种写法,内容回调中还做了异常的处理。
/** * 自定义健康端点 * <p>功能更加强大一点,DataSourceHealthIndicator / RedisHealthIndicator 都是这种写法</p> */ @Component("my2") public class MyAbstractHealthIndicator extends AbstractHealthIndicator { private static final String VERSION = "v1.0.0"; @Override protected void doHealthCheck(Health.Builder builder) throws Exception { int code = check(); if (code != 0) { builder.down().withDetail("code", code).withDetail("version", VERSION).build(); } builder.withDetail("code", code) .withDetail("version", VERSION).up().build(); } private int check() { return 0; } }
定义自己的端点
上面介绍的 info、health 都是spring-boot-actuator内置的,真正要实现自己的端点还得通过 @Endpoint、 @ReadOperation、@WriteOperation、@DeleteOperation。
/** * * <p>@Endpoint 是构建 rest 的唯一路径 </p> * 不同请求的操作,调用时缺少必需参数,或者使用无法转换为所需类型的参数,则不会调用操作方法,响应状态将为400(错误请求) * <P>@ReadOperation = GET 响应状态为 200 如果没有返回值响应 404(资源未找到) </P> * <P>@WriteOperation = POST 响应状态为 200 如果没有返回值响应 204(无响应内容) </P> * <P>@DeleteOperation = DELETE 响应状态为 200 如果没有返回值响应 204(无响应内容) </P> */ @Endpoint(id = "luis") public class MyEndPoint { @ReadOperation public Map<String, String> hello() { Map<String, String> result = new HashMap<>(); result.put("author", "Luis"); result.put("age", "25"); result.put("email", "1085143002@qq.com"); return result; } }
主函数
@SpringBootApplication public class SpringBootActuatorApplication { public static void main(String[] args) { SpringApplication.run(SpringBootActuatorApplication.class, args); } @Configuration static class MyEndpointConfiguration { @Bean @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint public MyEndPoint myEndPoint() { return new MyEndPoint(); } } }
测试 http://localhost:8080/actuator/luis
第十五篇:actuator与spring-boot-admin:spring-boot-actuator-admin
什么是SBA
SBA 全称 Spring Boot Admin是一个管理和监控Spring Boot应用程序的开源项目。
分为admin-server与admin-client两个组件
,admin-server通过采集actuator端点数据,显示在spring-boot-admin-ui上,已知的端点几乎都有进行采集,通过spring-boot-admin可以动态切换日志级别、导出日志、导出heapdump、监控各项指标 等等…. Spring Boot Admin在对单一应用服务监控的同时也提供了集群监控方案,支持通过eureka、consul、zookeeper等注册中心的方式实现多服务监控与管理…
依赖(单机版,自己监控自己)
<!-- 服务端:带UI界面 --> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-server</artifactId> <version>2.0.0</version> </dependency> <!-- 客户端包 --> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>2.0.0</version> </dependency> <!-- 安全认证 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- 端点 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 在管理界面中与 JMX-beans 进行交互所需要被依赖的 JAR --> <dependency> <groupId>org.jolokia</groupId> <artifactId>jolokia-core</artifactId> </dependency> <!-- 如果要访问info接口想获取maven中的属性内容 --> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>build-info</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
application.properties
# 描述信息 info.blog-url=https://winterchen.com info.author=Luis info.version=@project.version@ info.name=@project.artifactId@ # 选择激活对应环境的配置,如果是dev则代表不用认证就能访问监控页,prod代表需要认证 spring.profiles.active=prod # 加载所有的端点/默认只加载了 info / health management.endpoints.web.exposure.include=* # 比较重要,默认 /actuator spring-boot-admin 扫描不到 management.endpoints.web.base-path=/ management.endpoint.health.show-details=always # 可以关闭制定的端点 management.endpoint.shutdown.enabled=false # 日志文件 logging.file=./target/admin-server.log spring.boot.admin.client.url=http://localhost:8080 # 不配置老喜欢用主机名,看着不舒服.... spring.boot.admin.client.instance.prefer-ip=true
application-prod.properties
# 登陆所需的账号密码 spring.security.user.name=luis spring.security.user.password=luis # 便于客户端可以在受保护的服务器上注册api spring.boot.admin.client.username=luis spring.boot.admin.client.password=luis # 便服务器可以访问受保护的客户端端点 spring.boot.admin.client.instance.metadata.user.name=luis spring.boot.admin.client.instance.metadata.user.password=luis
主函数
@EnableAdminServer @SpringBootApplication public class SpringBootActuatorAdminApplication { public static void main(String[] args) { SpringApplication.run(SpringBootActuatorAdminApplication.class, args); } /** * dev 环境加载 */ @Profile("dev") @Configuration public static class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().permitAll() .and().csrf().disable(); } } /** * prod 环境加载 */ @Profile("prod") @Configuration public static class SecuritySecureConfig extends WebSecurityConfigurerAdapter { private final String adminContextPath; public SecuritySecureConfig(AdminServerProperties adminServerProperties) { this.adminContextPath = adminServerProperties.getContextPath(); } @Override protected void configure(HttpSecurity http) throws Exception { SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler(); successHandler.setTargetUrlParameter("redirectTo"); http.authorizeRequests() .antMatchers(adminContextPath + "/assets/**").permitAll() .antMatchers(adminContextPath + "/login").permitAll() .anyRequest().authenticated() .and() .formLogin().loginPage(adminContextPath + "/login").successHandler(successHandler).and() .logout().logoutUrl(adminContextPath + "/logout").and() .httpBasic().and() .csrf().disable(); } } }
测试 http://localhost:8080/login
第十六篇:定时任务详解:spring-boot-task
日常定时任务
数据定时增量同步 定时发送邮件 爬虫定时抓取 …
实现方式
Timer:JDK自带的java.util.Timer;通过调度java.util.TimerTask的方式让程序按照某一个频度执行,但不能在指定时间运行。一般用的较少。 ScheduledExecutorService:JDK1.5新增的,位于java.util.concurrent包中;是基于线程池设计的定时任务类,每个调度任务都会被分配到线程池中,并发执行,互不影响。 Spring Task:Spring3.0 以后新增了task,一个轻量级的Quartz,功能够用,用法简单。 Quartz:功能最为强大的调度器,可以让程序在指定时间执行,也可以按照某一个频度执行,它还可以动态开关,但是配置起来比较复杂。现如今开源社区中已经很多基于Quartz实现的分布式定时任务项目(xxl-job、elastic-job)。
Timer 方式
(基于Timer实现的定时调度,基本就是手撸代码,目前应用较少,不是很推荐)
public class TimerDemo { public static void main(String[] args) { TimerTask timerTask = new TimerTask() { @Override public void run() { System.out.println("执行任务:" + LocalDateTime.now()); } }; Timer timer = new Timer(); // timerTask:需要执行的任务 // delay:延迟时间(以毫秒为单位) // period:间隔时间(以毫秒为单位) timer.schedule(timerTask, 5000, 3000); } }
基于 ScheduledExecutorService
(与Timer很类似,但它的效果更好,多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中有一个因任务报错没有捕获抛出的异常,其它任务便会自动终止运行,但是使用ScheduledExecutorService则可以规避这个问题)
public class ScheduledExecutorServiceDemo { public static void main(String[] args) { ScheduledExecutorService service = Executors.newScheduledThreadPool(10); // 参数:1、具体执行的任务 2、首次执行的延时时间 // 3、任务执行间隔 4、间隔时间单位 service.scheduleAtFixedRate(() -> System.out.println("执行任务A:" + LocalDateTime.now()), 0, 3, TimeUnit.SECONDS); } }
Spring Task(本章关键)
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
@Scheduled定时任务的核心
cron:cron表达式,根据表达式循环执行,与fixedRate属性不同的是它是将时间进行了切割。(@Scheduled(cron = "0/5 * * * * *")任务将在5、10、15、20...这种情况下进行工作) fixedRate:每隔多久执行一次,无视工作时间(@Scheduled(fixedRate = 1000)假设第一次工作时间为2018-05-29 16:58:28,工作时长为3秒,那么下次任务的时候就是2018-05-29 16:58:31) fixedDelay:当前任务执行完毕后等待多久继续下次任务(@Scheduled(fixedDelay = 3000)假设第一次任务工作时间为2018-05-29 16:54:33,工作时长为5秒,那么下次任务的时间就是2018-05-29 16:54:41) initialDelay:第一次执行延迟时间,只是做延迟的设定,与fixedDelay关系密切,配合使用,相辅相成。
具体使用
@Component public class SpringTaskDemo { private static final Logger log = LoggerFactory.getLogger(SpringTaskDemo.class); @Async //代表该任务可以进行异步工作,由原本的串行改为并行 @Scheduled(cron = "0/1 * * * * *") public void scheduled1() throws InterruptedException { Thread.sleep(3000); log.info("scheduled1 每1秒执行一次:{}", LocalDateTime.now()); } @Scheduled(fixedRate = 1000) public void scheduled2() throws InterruptedException { Thread.sleep(3000); log.info("scheduled2 每1秒执行一次:{}", LocalDateTime.now()); } @Scheduled(fixedDelay = 3000) public void scheduled3() throws InterruptedException { Thread.sleep(5000); log.info("scheduled3 上次执行完毕后隔3秒继续执行:{}", LocalDateTime.now()); } }
主函数
@EnableScheduling注解表示开启对@Scheduled注解的解析;
同时new ThreadPoolTaskScheduler()也是相当的关键,通过阅读过源码可以发现默认情况下的private volatile int poolSize = 1;
这就导致了多个任务的情况下容易出现竞争情况(多个任务的情况下,如果第一个任务没执行完毕,后续的任务将会进入等待状态)。 @EnableAsync注解表示开启@Async注解的解析;
作用就是将串行化的任务给并行化了。(@Scheduled(cron = "0/1 * * * * *")假设第一次工作时间为2018-05-29 17:30:55,工作周期为3秒;
如果不加@Async那么下一次工作时间就是2018-05-29 17:30:59;如果加了@Async下一次工作时间就是2018-05-29 17:30:56)
@EnableAsync @EnableScheduling @SpringBootApplication public class SpringBootTaskApplication { public static void main(String[] args) { SpringApplication.run(SpringBootTaskApplication.class, args); } /** * 很关键:默认情况下 TaskScheduler 的 poolSize = 1 * * @return 线程池 */ @Bean public TaskScheduler taskScheduler() { ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.setPoolSize(10); return taskScheduler; } }
第十七篇:轻松搞定文件上传:spring-boot-file-upload
将文件通过IO流传输到服务器的某一个特定的文件夹下
依赖
spring-boot-starter-web spring-boot-starter-thymeleaf
配置
(默认情况下Spring Boot无需做任何配置也能实现文件上传的功能,但有可能因默认配置不符而导致文件上传失败问题,所以了解相关配置信息更有助于我们对问题的定位和修复)
# 禁用 thymeleaf 缓存 spring.thymeleaf.cache=false # 是否支持批量上传 (默认值 true) spring.servlet.multipart.enabled=true # 上传文件的临时目录 (一般情况下不用特意修改) spring.servlet.multipart.location= # 上传文件最大为 1M (默认值 1M 根据自身业务自行控制即可) spring.servlet.multipart.max-file-size=1048576 # 上传请求最大为 10M(默认值10M 根据自身业务自行控制即可) spring.servlet.multipart.max-request-size=10485760 # 文件大小阈值,当大于这个阈值时将写入到磁盘,否则存在内存中,(默认值0 一般情况下不用特意修改) spring.servlet.multipart.file-size-threshold=0 # 判断是否要延迟解析文件(相当于懒加载,一般情况下不用特意修改) spring.servlet.multipart.resolve-lazily=false
上传页面
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>文件上传</title> </head> <body> <h2>单一文件上传示例</h2> <div> <form method="POST" enctype="multipart/form-data" action="/uploads/upload1"> <p> 文件1:<input type="file" name="file"/> <input type="submit" value="上传"/> </p> </form> </div> <hr/> <h2>批量文件上传示例</h2> <div> <form method="POST" enctype="multipart/form-data" action="/uploads/upload2"> <p> 文件1:<input type="file" name="file"/> </p> <p> 文件2:<input type="file" name="file"/> </p> <p> <input type="submit" value="上传"/> </p> </form> </div> <hr/> <h2>Base64文件上传</h2> <div> <form method="POST" action="/uploads/upload3"> <p> BASE64编码:<textarea name="base64" rows="10" cols="80"></textarea> <input type="submit" value="上传"/> </p> </form> </div> </body> </html>
控制层
@Controller @RequestMapping("/uploads") public class FileUploadController { private static final Logger log = LoggerFactory.getLogger(FileUploadController.class); @GetMapping public String index() { return "index"; } @PostMapping("/upload1") @ResponseBody public Map<String, String> upload1(@RequestParam("file") MultipartFile file) throws IOException { log.info("[文件类型] - [{}]", file.getContentType()); log.info("[文件名称] - [{}]", file.getOriginalFilename()); log.info("[文件大小] - [{}]", file.getSize()); // TODO 将文件写入到指定目录(具体开发中有可能是将文件写入到云存储/或者指定目录通过 Nginx 进行 gzip 压缩和反向代理,此处只是为了演示故将地址写成本地电脑指定目录) file.transferTo(new File("/Users/Winterchen/Desktop/javatest" + file.getOriginalFilename())); Map<String, String> result = new HashMap<>(16); result.put("contentType", file.getContentType()); result.put("fileName", file.getOriginalFilename()); result.put("fileSize", file.getSize() + ""); return result; } @PostMapping("/upload2") @ResponseBody public List<Map<String, String>> upload2(@RequestParam("file") MultipartFile[] files) throws IOException { if (files == null || files.length == 0) { return null; } List<Map<String, String>> results = new ArrayList<>(); for (MultipartFile file : files) { // TODO Spring Mvc 提供的写入方式 file.transferTo(new File("/Users/Winterchen/Desktop/javatest" + file.getOriginalFilename())); Map<String, String> map = new HashMap<>(16); map.put("contentType", file.getContentType()); map.put("fileName", file.getOriginalFilename()); map.put("fileSize", file.getSize() + ""); results.add(map); } return results; } @PostMapping("/upload3") @ResponseBody public void upload2(String base64) throws IOException { // TODO BASE64 方式的 格式和名字需要自己控制(如 png 图片编码后前缀就会是 data:image/png;base64,) final File tempFile = new File("C:/Users/asus/Desktop/test.jpg"); // TODO 防止有的传了 data:image/png;base64, 有的没传的情况 String[] d = base64.split("base64,"); final byte[] bytes = Base64Utils.decodeFromString(d.length > 1 ? d[1] : d[0]); FileCopyUtils.copy(bytes, tempFile); } }
测试 http://localhost:8080/uploads
先进入C:/Users/asus/AppData/Local/Temp/目录 全选,能删的都删了(为了能肉眼区分类似tomcat.4445172134953246138.8080这种目录) 启动项目(此时会生成tomcat.4445172134953246138.8080形式目录) 再去此目录的work/Tomcat/localhost/ROOT下逐层建文件夹Users/Winterchen/Desktop 浏览器访问 http://localhost:8080/uploads
base64测试:
修改源码C:/Users/asus/Desktop/test.jpg
http://base64.xpcha.com/pic.html
第十八篇:轻松搞定全局异常:spring-boot-exception
依赖
spring-boot-starter-web
自定义异常
public class CustomException extends RuntimeException { private static final long serialVersionUID = 4564124491192825748L; private int code; public CustomException() { super(); } public CustomException(int code, String message) { super(message); this.setCode(code); } public int getCode() { return code; } public void setCode(int code) { this.code = code; } }
异常信息模板
public class ErrorResponseEntity { private int code; private String message; public ErrorResponseEntity(int code, String message) { this.code = code; this.message = message; } // 省略 get/set }
控制层
@RestController public class ExceptionController { @GetMapping("/test3") public String test3(Integer num) { // TODO 演示需要,实际上参数是否为空通过 @RequestParam(required = true) 就可以控制 if (num == null) { throw new CustomException(400, "num不能为空"); } int i = 10 / num; return "result:" + i; } }
异常处理(关键)
注解概述
@ControllerAdvice捕获Controller层抛出的异常,如果添加@ResponseBody返回信息则为JSON格式。 @RestControllerAdvice相当于@ControllerAdvice与@ResponseBody的结合体。 @ExceptionHandler统一处理一种类的异常,减少代码重复率,降低复杂度。
/** * 全局异常处理 */ @RestControllerAdvice public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { /** * 定义要捕获的异常 可以多个 @ExceptionHandler({}) * * @param request request * @param e exception * @param response response * @return 响应结果 */ @ExceptionHandler(CustomException.class) public ErrorResponseEntity customExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) { response.setStatus(HttpStatus.BAD_REQUEST.value()); CustomException exception = (CustomException) e; return new ErrorResponseEntity(exception.getCode(), exception.getMessage()); } /** * 捕获 RuntimeException 异常 * TODO 如果你觉得在一个 exceptionHandler 通过 if (e instanceof xxxException) 太麻烦 * TODO 那么你还可以自己写多个不同的 exceptionHandler 处理不同异常 * * @param request request * @param e exception * @param response response * @return 响应结果 */ @ExceptionHandler(RuntimeException.class) public ErrorResponseEntity runtimeExceptionHandler(HttpServletRequest request, final Exception e, HttpServletResponse response) { response.setStatus(HttpStatus.BAD_REQUEST.value()); RuntimeException exception = (RuntimeException) e; return new ErrorResponseEntity(400, exception.getMessage()); } /** * 通用的接口映射异常处理方 */ @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if (ex instanceof MethodArgumentNotValidException) { MethodArgumentNotValidException exception = (MethodArgumentNotValidException) ex; return new ResponseEntity<>(new ErrorResponseEntity(status.value(), exception.getBindingResult().getAllErrors().get(0).getDefaultMessage()), status); } if (ex instanceof MethodArgumentTypeMismatchException) { MethodArgumentTypeMismatchException exception = (MethodArgumentTypeMismatchException) ex; logger.error("参数转换失败,方法:" + exception.getParameter().getMethod().getName() + ",参数:" + exception.getName() + ",信息:" + exception.getLocalizedMessage()); return new ResponseEntity<>(new ErrorResponseEntity(status.value(), "参数转换失败"), status); } return new ResponseEntity<>(new ErrorResponseEntity(status.value(), "参数转换失败"), status); } }
测试
http://localhost:8080/test3 http://localhost:8080/test3?num=0 http://localhost:8080/test3?num=5
第十九篇:轻松搞定数据验证(一):spring-boot-validation1
目的是为了轻松的丶验证客户端传来的数据
依赖
spring-boot-starter-web
JSR-303 注解介绍
实体类
public class Book { private Integer id; @NotBlank(message = "name 不允许为空") @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间") private String name; @NotNull(message = "price 不允许为空") @DecimalMin(value = "0.1", message = "价格不能低于 {value}") private BigDecimal price;
//get set }
业务代码(关注注解)
@Validated @RestController public class ValidateController { @GetMapping("/test2") public String test2(@NotBlank(message = "name 不能为空") @Length(min = 2, max = 10, message = "name 长度必须在 {min} - {max} 之间") String name) { return "success"; } @GetMapping("/test3") public String test3(@Validated Book book) { return "success"; } }
测试 http://localhost:8080/test2?name=sasd
第二十篇:轻松搞定数据验证(二)
熟悉 ConstraintValidator 接口并且编写自己的数据验证注解
自定义注解
@Target({FIELD, PARAMETER}) @Retention(RUNTIME) @Constraint(validatedBy = DateTimeValidator.class) public @interface DateTime { String message() default "格式错误"; String format() default "yyyy-MM-dd"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
验证的规则
/** * 日期格式验证 */ public class DateTimeValidator implements ConstraintValidator<DateTime, String> { private DateTime dateTime; @Override public void initialize(DateTime dateTime) { this.dateTime = dateTime; } @Override public boolean isValid(String value, ConstraintValidatorContext context) { // 如果 value 为空则不进行格式验证,为空验证可以使用 @NotBlank @NotNull @NotEmpty 等注解来进行控制,职责分离 if (value == null) { return true; } String format = dateTime.format(); if (value.length() != format.length()) { return false; } SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); try { simpleDateFormat.parse(value); } catch (ParseException e) { return false; } return true; } }
入口
@Validated @RestController public class ValidateController { @GetMapping("/test") public String test(@DateTime(message = "您输入的格式错误,正确的格式为:{format}", format = "yyyy-MM-dd HH:mm") String date) { return "success"; } }
测试
http://localhost:8080/test?date=2020-10-07 15:38
第二十一篇:轻松搞定数据验证(三)
分组验证器
/** * 验证组 */ public class Groups { public interface Update { } public interface Default { } }
实体类
public class Book { @NotNull(message = "id 不能为空", groups = Groups.Update.class) private Integer id; @NotBlank(message = "name 不允许为空", groups = Groups.Default.class) private String name; @NotNull(message = "price 不允许为空", groups = Groups.Default.class) private BigDecimal price; // 省略 GET SET ... }
控制层
@RestController public class ValidateController2 { @GetMapping("/insert") public String insert(@Validated(value = Groups.Default.class) Book book) { return "insert"; } @GetMapping("/update") public String update(@Validated(value = {Groups.Default.class, Groups.Update.class}) Book book) { return "update"; } }
测试
http://localhost:8080/insert?name=springboot&price=3 http://localhost:8080/update?name=springboot&price=3
第二十二篇:轻松搞定重复提交(本地锁):springboot-locallock
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency>
自定义注解
/** * 锁的注解 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface LocalLock { String key() default ""; /** * 过期时间 TODO 由于用的 guava 暂时就忽略这属性吧 集成 redis 需要用到 */ int expire() default 5; }
拦截器(AOP)
/** * 本章先基于 本地缓存来做,后续讲解 redis 方案 */ @Aspect @Configuration public class LockMethodInterceptor { private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder() // 最大缓存 100 个 .maximumSize(1000) // 设置写缓存后 5 秒钟过期 .expireAfterWrite(5, TimeUnit.SECONDS) .build(); @Around("execution(public * *(..)) && @annotation(com.example.springbootlocallock.test.LocalLock)") public Object interceptor(ProceedingJoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); LocalLock localLock = method.getAnnotation(LocalLock.class); String key = getKey(localLock.key(), pjp.getArgs()); if (!StringUtils.isEmpty(key)) { if (CACHES.getIfPresent(key) != null) { throw new RuntimeException("请勿重复请求"); } // 如果是第一次请求,就将 key 当前对象压入缓存中 CACHES.put(key, key); } try { return pjp.proceed(); } catch (Throwable throwable) { throw new RuntimeException("服务器异常"); } finally { } } /** * key 的生成策略,如果想灵活可以写成接口与实现类的方式 * * @param keyExpress 表达式 * @param args 参数 * @return 生成的key */ private String getKey(String keyExpress, Object[] args) { for (int i = 0; i < args.length; i++) { keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString()); } return keyExpress; } }
控制层
/** * BookController */ @RestController @RequestMapping("/books") public class BookController { @LocalLock(key = "book:arg[0]") @GetMapping public String query(@RequestParam String token) { return "success - " + token; } }
测试 http://localhost:8080/books?token=1
第二十三篇:轻松搞定重复提交(分布式锁):springboot-redislock
依赖
spring-boot-starter-web spring-boot-starter-aop spring-boot-starter-data-redis
配置
spring.redis.host=106.75.32.166 spring.redis.port=6379 spring.redis.password=test
CacheLock注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheLock { /** * redis 锁key的前缀 * * @return redis 锁key的前缀 */ String prefix() default ""; /** * 过期秒数,默认为5秒 * * @return 轮询锁的时间 */ int expire() default 5; /** * 超时时间单位 * * @return 秒 */ TimeUnit timeUnit() default TimeUnit.SECONDS; /** * <p>Key的分隔符(默认 :)</p> * <p>生成的Key:N:SO1008:500</p> * * @return String */ String delimiter() default ":"; }
CacheParam注解
/** * 锁的参数 */ @Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited public @interface CacheParam { /** * 字段名称 * * @return String */ String name() default ""; }
Key 生成策略(接口)
/** * key生成器 */ public interface CacheKeyGenerator { /** * 获取AOP参数,生成指定缓存Key * * @param pjp PJP * @return 缓存KEY */ String getLockKey(ProceedingJoinPoint pjp); }
Key 生成策略(实现)
/** * 上一章说过通过接口注入的方式去写不同的生成规则; */ public class LockKeyGenerator implements CacheKeyGenerator { @Override public String getLockKey(ProceedingJoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); CacheLock lockAnnotation = method.getAnnotation(CacheLock.class); final Object[] args = pjp.getArgs(); final Parameter[] parameters = method.getParameters(); StringBuilder builder = new StringBuilder(); // TODO 默认解析方法里面带 CacheParam 注解的属性,如果没有尝试着解析实体对象中的 for (int i = 0; i < parameters.length; i++) { final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class); if (annotation == null) { continue; } builder.append(lockAnnotation.delimiter()).append(args[i]); } if (StringUtils.isEmpty(builder.toString())) { final Annotation[][] parameterAnnotations = method.getParameterAnnotations(); for (int i = 0; i < parameterAnnotations.length; i++) { final Object object = args[i]; final Field[] fields = object.getClass().getDeclaredFields(); for (Field field : fields) { final CacheParam annotation = field.getAnnotation(CacheParam.class); if (annotation == null) { continue; } field.setAccessible(true); builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object)); } } } return lockAnnotation.prefix() + builder.toString(); } }
Lock 拦截器(AOP)
/** * redis 方案 */ @Aspect @Configuration public class LockMethodInterceptor { @Autowired public LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) { this.redisLockHelper = redisLockHelper; this.cacheKeyGenerator = cacheKeyGenerator; } private final RedisLockHelper redisLockHelper; private final CacheKeyGenerator cacheKeyGenerator; @Around("execution(public * *(..)) && @annotation(com.example.demo.test.CacheLock)") public Object interceptor(ProceedingJoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); CacheLock lock = method.getAnnotation(CacheLock.class); if (StringUtils.isEmpty(lock.prefix())) { throw new RuntimeException("lock key don't null..."); } final String lockKey = cacheKeyGenerator.getLockKey(pjp); String value = UUID.randomUUID().toString(); try { // 假设上锁成功,但是设置过期时间失效,以后拿到的都是 false final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit()); if (!success) { throw new RuntimeException("重复提交"); } try { return pjp.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系统异常"); } } finally { // TODO 如果演示的话需要注释该代码;实际应该放开 redisLockHelper.unlock(lockKey, value); } } }
RedisLockHelper 通过封装成 API 方式调用,更灵活
/** * 需要定义成 Bean */ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisLockHelper { private static final String DELIMITER = "|"; /** * 如果要求比较高可以通过注入的方式分配 */ private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10); private final StringRedisTemplate stringRedisTemplate; public RedisLockHelper(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 获取锁(存在死锁风险) * * @param lockKey lockKey * @param value value * @param time 超时时间 * @param unit 过期单位 * @return true or false */ public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) { return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes()
, value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT)); } /** * 获取锁 * * @param lockKey lockKey * @param uuid UUID * @param timeout 超时时间 * @param unit 过期单位 * @return true or false */ public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) { final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds(); boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid); if (success) { stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS); } else { String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid); final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER)); if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) { return true; } } return success; } /** * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a> */ public void unlock(String lockKey, String value) { unlock(lockKey, value, 0, TimeUnit.MILLISECONDS); } /** * 延迟unlock * * @param lockKey key * @param uuid client(最好是唯一键的) * @param delayTime 延迟时间 * @param unit 时间单位 */ public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) { if (StringUtils.isEmpty(lockKey)) { return; } if (delayTime <= 0) { doUnlock(lockKey, uuid); } else { EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit); } } /** * @param lockKey key * @param uuid client(最好是唯一键的) */ private void doUnlock(final String lockKey, final String uuid) { String val = stringRedisTemplate.opsForValue().get(lockKey); final String[] values = val.split(Pattern.quote(DELIMITER)); if (values.length <= 0) { return; } if (uuid.equals(values[1])) { stringRedisTemplate.delete(lockKey); } } }
控制层
/** * BookController */ @RestController @RequestMapping("/books") public class BookController { @CacheLock(prefix = "books") @GetMapping public String query(@CacheParam(name = "token") @RequestParam String token) { return "success - " + token; } }
主函数
@SpringBootApplication public class SpringbootRedislockApplication { public static void main(String[] args) { SpringApplication.run(SpringbootRedislockApplication.class, args); } @Bean public CacheKeyGenerator cacheKeyGenerator() { return new LockKeyGenerator(); } }
测试
一会儿第二次提交
第二十四篇:数据库管理与迁移(Liquibase):springboot-liquibase
数据库重构和迁移,有个开源工具 LiquiBase,通过 changelog 文件形式记录数据库的变更,然后执行 changelog 文件中的修改,将数据库更新或回滚到一致的状态。
支持几乎所有主流的数据库,如MySQL、PostgreSQL、Oracle、Sql Server、DB2等 支持多开发者的协作维护; 日志文件支持多种格式;如XML、YAML、SON、SQL等 支持多种运行方式;如命令行、Spring 集成、Maven 插件、Gradle 插件等
场景
平时测试或者开发环境新增或修改了数据库表字段 + 切换环境 利用 Spring Boot 集成 Liquibase,避免因粗心大意导致环境迁移时缺少字段….
依赖
spring-boot-starter-web spring-boot-starter-jdbc mysql-connector-java liquibase-core
配置
spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.url=jdbc:mysql://106.75.32.166:3310/chapter23?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false spring.datasource.username=root spring.datasource.password=root # 只要依赖了 liquibase-core 默认可以不用做任何配置,但还是需要知道默认配置值是什么 # spring.liquibase.enabled=true # spring.liquibase.change-log=classpath:/db/changelog/db.changelog-master.yaml
其他配置
spring.liquibase.change-log 配置文件的路径,默认值为 classpath:/db/changelog/db.changelog-master.yaml spring.liquibase.check-change-log-location 检查 change log的位置是否存在,默认为true. spring.liquibase.contexts 用逗号分隔的运行环境列表。 spring.liquibase.default-schema 默认数据库 schema spring.liquibase.drop-first 是否先 drop schema(默认 false) spring.liquibase.enabled 是否开启 liquibase(默认为 true) spring.liquibase.password 数据库密码 spring.liquibase.url 要迁移的JDBC URL,如果没有指定的话,将使用配置的主数据源. spring.liquibase.user 数据用户名 spring.liquibase.rollback-file 执行更新时写入回滚的 SQL文件
db.changelog-master.yaml
databaseChangeLog: # 支持 yaml 格式的 SQL 语法 - changeSet: id: 1 author: Levin changes: - createTable: tableName: person columns: - column: name: id type: int autoIncrement: true constraints: primaryKey: true nullable: false - column: name: first_name type: varchar(255) constraints: nullable: false - column: name: last_name type: varchar(255) constraints: nullable: false - changeSet: id: 2 author: Levin changes: - insert: tableName: person columns: - column: name: first_name value: Marcel - column: name: last_name value: Overdijk # 同时也支持依赖外部SQL文件(TODO 个人比较喜欢这种) - changeSet: id: 3 author: Levin changes: - sqlFile: encoding: utf8 path: classpath:db/changelog/sqlfile/test1.sql
test1.sql
INSERT INTO `person` (`id`, `first_name`, `last_name`) VALUES ('3', 'dd', 'cc');
测试
第二十五篇:打造属于你的聊天室(WebSocket):springboot-websocket
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.2.1</version> </dependency>
工具类
package com.example.demo.test; import javax.websocket.RemoteEndpoint; import javax.websocket.Session; import java.io.IOException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public final class WebSocketUtils { /** * 模拟存储 websocket session 使用 */ public static final Map<String, Session> LIVING_SESSIONS_CACHE = new ConcurrentHashMap<>(); public static void sendMessageAll(String message) { LIVING_SESSIONS_CACHE.forEach((sessionId, session) -> sendMessage(session, message)); } /** * 发送给指定用户消息 * * @param session 用户 session * @param message 发送内容 */ public static void sendMessage(Session session, String message) { if (session == null) { return; } final RemoteEndpoint.Basic basic = session.getBasicRemote(); if (basic == null) { return; } try { basic.sendText(message); } catch (IOException e) { e.printStackTrace(); } } }
服务端点
package com.example.demo.test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import javax.websocket.*; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.io.IOException; import static com.example.demo.test.WebSocketUtils.LIVING_SESSIONS_CACHE; import static com.example.demo.test.WebSocketUtils.sendMessage; import static com.example.demo.test.WebSocketUtils.sendMessageAll; /** * 聊天室 */ @RestController @ServerEndpoint("/chat-room/{username}") public class ChatRoomServerEndpoint { private static final Logger log = LoggerFactory.getLogger(ChatRoomServerEndpoint.class); @OnOpen public void openSession(@PathParam("username") String username, Session session) { LIVING_SESSIONS_CACHE.put(username, session); String message = "欢迎用户[" + username + "] 来到聊天室!"; log.info(message); sendMessageAll(message); } @OnMessage public void onMessage(@PathParam("username") String username, String message) { log.info(message); sendMessageAll("用户[" + username + "] : " + message); } @OnClose public void onClose(@PathParam("username") String username, Session session) { //当前的Session 移除 LIVING_SESSIONS_CACHE.remove(username); //并且通知其他人当前用户已经离开聊天室了 sendMessageAll("用户[" + username + "] 已经离开聊天室了!"); try { session.close(); } catch (IOException e) { e.printStackTrace(); } } @OnError public void onError(Session session, Throwable throwable) { try { session.close(); } catch (IOException e) { e.printStackTrace(); } throwable.printStackTrace(); } @GetMapping("/chat-room/{sender}/to/{receive}") public void onMessage(@PathVariable("sender") String sender, @PathVariable("receive") String receive, String message) { sendMessage(LIVING_SESSIONS_CACHE.get(receive), "[" + sender + "]" + "-> [" + receive + "] : " + message); } }
html(static下)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>battcn websocket</title> <!--<script src="jquery-3.2.1.min.js" ></script>--> <script src="/webjars/jquery/3.2.1/jquery.min.js"></script> </head> <body> <label for="message_content">聊 天 室 </label><textarea id="message_content" readonly="readonly" cols="57" rows="10"> </textarea> <br/> <label for="in_user_name">用户姓名 </label><input id="in_user_name" value=""/> <button id="btn_join">加入聊天室</button> <button id="btn_exit">离开聊天室</button> <br/><br/> <label for="in_room_msg">群发消息 </label><input id="in_room_msg" value=""/> <button id="btn_send_all">发送消息</button> <br/><br/><br/> 好友聊天 <br/> <label for="in_sender">发送者 </label><input id="in_sender" value=""/><br/> <label for="in_receive">接受者 </label><input id="in_receive" value=""/><br/> <label for="in_point_message">消息体 </label><input id="in_point_message" value=""/><button id="btn_send_point">发送消息</button> </body> <script type="text/javascript"> $(document).ready(function(){ var urlPrefix ='ws://localhost:8080/chat-room/'; var ws = null; $('#btn_join').click(function(){ var username = $('#in_user_name').val(); var url = urlPrefix + username; ws = new WebSocket(url); ws.onopen = function () { console.log("建立 websocket 连接..."); }; ws.onmessage = function(event){ //服务端发送的消息 $('#message_content').append(event.data+'\n'); }; ws.onclose = function(){ $('#message_content').append('用户['+username+'] 已经离开聊天室!'); console.log("关闭 websocket 连接..."); } }); //客户端发送消息到服务器 $('#btn_send_all').click(function(){ var msg = $('#in_room_msg').val(); if(ws){ ws.send(msg); } }); // 退出聊天室 $('#btn_exit').click(function(){ if(ws){ ws.close(); } }); $("#btn_send_point").click(function() { var sender = $("#in_sender").val(); var receive = $("#in_receive").val(); var message = $("#in_point_message").val(); $.get("/chat-room/"+sender+"/to/"+receive+"?message="+message,function() { alert("发送成功...") }) }) }) </script> </html>
主函数
@EnableWebSocket @SpringBootApplication public class SpringbootWebsocketApplication { public static void main(String[] args) { SpringApplication.run(SpringbootWebsocketApplication.class, args); } @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
测试(两客户端)http://localhost:8080/chat.html
第二十六篇:轻松搞定安全框架(Shiro):springboot-shiro
依赖
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <shiro.version>1.4.0</shiro.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- shiro 相关包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency> <!-- End --> <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>
配置
spring.main.allow-bean-definition-overriding=true
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" name="shiroCache"> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> </ehcache>
代码略(见github)
测试(postman)
http://localhost:8080/login?username=u3&password=p3 http://localhost:8080/users/query http://localhost:8080/users/find
第二十七篇:优雅解决分布式限流:springboot-redislimter
源码见GitHub
测试postman
http://localhost:8080/test
第二十八篇:JDK8 日期格式化:springboot-localdatetime
源码见GitHub
测试
http://localhost:8080/orders