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
View Code

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">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</label><textarea id="message_content" readonly="readonly" cols="57" rows="10">

</textarea>

<br/>


<label for="in_user_name">用户姓名 &nbsp;</label><input id="in_user_name" value=""/>
<button id="btn_join">加入聊天室</button>
<button id="btn_exit">离开聊天室</button>

<br/><br/>

<label for="in_room_msg">群发消息 &nbsp;</label><input id="in_room_msg" value=""/>
<button id="btn_send_all">发送消息</button>


<br/><br/><br/>

好友聊天
<br/>
<label for="in_sender">发送者 &nbsp;</label><input id="in_sender" value=""/><br/>
<label for="in_receive">接受者 &nbsp;</label><input id="in_receive" value=""/><br/>
<label for="in_point_message">消息体 &nbsp;</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+'
');
            };
            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>
View Code

配置

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>
ehcache-shiro.xml

代码略(见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

击石乃有火,不击元无烟!!
原文地址:https://www.cnblogs.com/rain2020/p/13711970.html