Spring Boot学习笔记(五):Spring Boot 企业级开发

Spring Boot 在企业级开发中的常用技术介绍。

全部章节传送门:
Spring Boot学习笔记(一):Spring Boot 入门基础
Spring Boot学习笔记(二):Spring Boot 运行原理
Spring Boot学习笔记(三):Spring Boot Web开发
Spring Boot学习笔记(四):Spring Boot 数据访问
Spring Boot学习笔记(五):Spring Boot 企业级开发
Spring Boot学习笔记(六):Spring Boot 应用监控

安全控制 Spring Security

Spring Security 是专门针对基于 Spring 的项目的安全框架,充分利用了依赖注入和 AOP 来实现安全的功能。

安全框架有2个重要的概念,即认证(Authentication)和授权(Authorization)。认证即认证用户可以访问当前系统;授权即确定用户在当前系统下所拥有的功能权限。

准备环境

在数据库中创建3张表,分别是用户表、角色表和用户角色关联表,并在表中插入几条数据。

create table t_sys_user(
    id int primary key auto_increment,
    username varchar(10),
    password varchar(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into t_sys_user(id, username, password) values(1, 'wyk','wyk');
insert into t_sys_user(id, username, password) values(2, 'gjj','gjj');

create table t_sys_role(
    id int primary key auto_increment,
    name varchar(15) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into t_sys_role(id, name) values(1, 'ROLE_ADMIN');
insert into t_sys_role(id, name) values(2, 'ROLE_USER');

create table sys_user_roles(
    sys_user_id int,
    roles_id int,
    foreign key(sys_user_id) references t_sys_user(id),
    foreign key(roles_id) references t_sys_role(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into sys_user_roles(sys_user_id, roles_id) values(1, 1);
insert into sys_user_roles(sys_user_id, roles_id) values(2, 2);

新建 Spring Boot 项目

新建Spring Boot 的 Web项目,添加相关依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wyk</groupId>
    <artifactId>securitydemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot Security</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- themeleaf针对spring security的扩展包-->
        <dependency> <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在 application.properties 中添加相关配置。

# 数据库相关
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springstudy?serverTimezone=GMT%2B8
spring.datasource.username=spring
spring.datasource.password=spring
spring.datasource.dirverClassName=com.mysql.jdbc.Driver
# 根据实体类自动维护数据库表结构的功能
spring.jpa.hibernate.ddl-auto=none
# 设置hibernate操作的时候在控制台显示真实的SQL语句
spring.jpa.show-sql=true
# 安全日志级别
logging.level.org.springframework.security=INFO
# 关掉thymeleaf缓存
spring.thymeleaf.cache=false

创建用户和角色

根据数据表定义用户和角色类。

用户:

package com.wyk.securitydemo.domain;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Entity
@Table(name="t_sys_user")
public class SysUser implements UserDetails {

    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue
    private Long id;
    private String username;
    private String password;

    //用户和角色的映射关系
    @ManyToMany(cascade = {CascadeType.REFRESH}, fetch=FetchType.EAGER)
    @JoinTable(name="sys_user_roles", joinColumns = {@JoinColumn(name="sys_user_id", referencedColumnName = "id")},
            inverseJoinColumns={@JoinColumn(name="roles_id", referencedColumnName = "id")})
    private List<SysRole> roles;

    //重写,将用户角色作为权限
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        List<GrantedAuthority> auths = new ArrayList<GrantedAuthority>();
        List<SysRole> roles = this.getRoles();
        for(SysRole role : roles) {
            auths.add(new SimpleGrantedAuthority(role.getName()));
        }
        return auths;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    public boolean isEnabled() {
        return true;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public List<SysRole> getRoles() {
        return roles;
    }

    public void setRoles(List<SysRole> roles) {
        this.roles = roles;
    }
}

角色:

package com.wyk.securitydemo.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="t_sys_role")
public class SysRole {
    @Id
    @GeneratedValue
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

另外添加一个用来展示数据的类:

package com.wyk.securitydemo.domain;

public class Msg {
    private String title;
    private String content;
    private String extraInfo;

    public Msg(String title, String content, String extraInfo) {
        this.title = title;
        this.content = content;
        this.extraInfo = extraInfo;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getExtraInfo() {
        return extraInfo;
    }

    public void setExtraInfo(String extraInfo) {
        this.extraInfo = extraInfo;
    }
}

数据访问

本项目的数据访问很简单。

package com.wyk.securitydemo.dao;

import com.wyk.securitydemo.domain.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;

public interface SysUserRepository extends JpaRepository<SysUser, Long> {
    //根据用户名查询用户
    SysUser findByUsername(String username);
}

自定义 UserDetailService

UserDetailsService接口用于返回用户相关数据。它有loadUserByUsername()方法,根据username查询用户实体,可以实现该接口覆盖该方法,实现自定义获取用户过程。该接口实现类被DaoAuthenticationProvider 类使用,用于认证过程中载入用户信息。

package com.wyk.securitydemo.security;

import com.wyk.securitydemo.dao.SysUserRepository;
import com.wyk.securitydemo.domain.SysUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;

public class CustomUserService implements UserDetailsService {
    @Autowired
    SysUserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String s) {
        SysUser user = userRepository.findByUsername(s);
        if(user == null) {
            throw new UsernameNotFoundException("用户名不存在");
        }

        return user;
    }
}

配置

Spring MVC 配置:

package com.wyk.securitydemo.config;


import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    /**
     * 注册访问login转向login.html
     * @param registry
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }

    /**
     * 注册静态资源根目录是在static目录
     * 估计是收spring security影响,不加此方法无法正确找到静态资源
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        // TODO Auto-generated method stub
        registry.addResourceHandler("/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }
}

Spring Security 配置:

package com.wyk.securitydemo.config;

import com.wyk.securitydemo.security.CustomPasswordEncoder;
import com.wyk.securitydemo.security.CustomUserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;

@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    UserDetailsService customUserService() {
        return new CustomUserService();
    }
    
    //通过@Bean注入指定的PasswordEncoder
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new CustomPasswordEncoder();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //添加自定义user detail service 认证
        auth.userDetailsService(customUserService());
    }

    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.authorizeRequests()
                .anyRequest().authenticated() // 所有请求需要认证后访问
                .and()
                .formLogin()
                    .loginPage("/login")
                    .failureUrl("/login?error")
                    .permitAll() // 定制登录行为,登录页面可任意访问
                .and()
                .logout().permitAll(); // 定义注销行为,注销页面可任意访问
    }
    
    @Override
    public void configure(WebSecurity webSecurity) throws Exception {
        //忽略静态资源
        webSecurity.ignoring().antMatchers("/css/**","/js/**");
    }

}

其中使用的加密算法是明文加密,即不加密,如下所示。

package com.wyk.securitydemo.security;

import org.springframework.security.crypto.password.PasswordEncoder;

public class CustomPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

前端页面

添加 bootstrap 静态资源,创建登陆页面:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta content="text/html;charset=UTF-8" />
        <title>登录页面</title>
        <link th:href="@{css/bootstrap.min.css}" rel="stylesheet" />
        <style type="text/css">
            body {
                padding-top: 50px;
            }
            .starter-template {
                padding: 40px 15px;
                text-align: center;
            }
        </style>
    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Spring Security 演示</a>
                </div>
                <div>
                    <ul class="nav navbar-nav">
                        <li><a th:href="@{/}"> 首页 </a></li>
                    </ul>
                </div>
            </div>
        </nav>
        <div class="container">
            <div class="starter-template">
                <p th:if="${param.logout}" class="bg-warning">已成功注销</p>
                <p th:if="${param.error}" class="bg-danger">有错误,请重试</p>
                    <h2>使用帐号密码登录</h2>
                    <form name="form" th:action="@{/login}" action="/login" method="POST">
                        <div class="form-group">
                            <label for="username">帐号</label>
                            <input type="text" class="form-control" name="username" id="username"
                                   value="" placeholder="账号" />
                        </div>
                        <div class="form-group">
                            <label for="password">密码</label>
                            <input type="password" class="form-control" name="password" id="password"
                                   value="" placeholder="密码" />
                        </div>
                        <input type="submit" id="login" value="Login" class="btn btn-primary" />
                    </form>
            </div>
        </div>
    </body>
</html>

首页,需要注意,虽然maven引入的是thymeleaf-extras-springsecurity5,但是这里的命名空间要使用thymeleaf-extras-springsecurity4。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
    <head>
        <meta content="text/html;charset=UTF-8" />
        <title sec:authentication="name"></title>
        <link th:href="@{css/bootstrap.min.css}" rel="stylesheet" />
        <style type="text/css">
            body {
                padding-top: 50px
            }
            .starter-template {
                padding: 40px 15px;
                text-align: center;
            }
        </style>
    </head>
    <body>
        <nav class="navbar navbar-inverse navbar-fixed-top">
            <div class="container">
                <div class="navbar-header">
                    <a class="navbar-brand" href="#">Spring Security 演示</a>
                </div>
                <div>
                    <ul class="nav navbar-nav">
                        <li><a th:href="@{/}"> 首页 </a></li>
                    </ul>
                </div>
            </div>
        </nav>
        <div class="container">
            <div class="starter-template">
                <h1 th:text="${msg.title}"></h1>
                <p class="bg-primary" th:text="${msg.content}"></p>

                <div sec:authorize="hasRole('ROLE_ADMIN')">
                    <p class="bg-info" th:text="${msg.extraInfo}"></p>
                </div>

                <div sec:authorize="hasRole('ROLE_USER')">
                    <p class="bg-info">无更多信息显示</p>
                </div>

                <form th:action="@{/logout}" method="post">
                    <input type="submit" class="btn btn-primary" value="注销" />
                </form>
            </div>
        </div>
    </body>
</html>

测试程序

运行程序,进入登陆界面。

springsecurity-login.png

使用普通用户登录。

springsecurity-user.png

注销后在使用管理员登录。

springsecurity-admin.png

批处理 Spring Batch

批处理是一种处理模式,它涉及一系列自动复杂作业的执行而无需用户交互。批处理过程处理批量数据并运行很长时间。一些企业应用程序需要处理大量数据来执行操作,如

  • 基于时间的事件,如周期性计算。
  • 在大型数据集上重复处理的定期应用程序。
  • 处理和验证交易方式中可用数据的应用程序。

Spring Batch 是一个轻量级框架,用于开发在企业应用程序中使用的批处理应用程序。

Spring Batch 结构

Spring Batch 框架架构如下所示。

springbatch框架.png

每个作业Job(需要执行的任务)有1个或者多个作业步Step;每个Step对应一个ItemReader(读取数据的接口)、ItemProcessor(处理数据的接口)、ItemWriter(输出数据的接口); 通过JobLauncher(启动接口)可以启动Job,启动Job时需要从JobRepository(注册的容器)获取存在的JobExecution; 当前运行的Job及Step的结果及状态会保存在JobRepository中。

准备环境

以将一个csv文件中的数据使用JDBC批处理方式插入数据库为例进行讲解。

在MySQL中创建一个数据表。

create table t_person_info (
    id int not null primary key,
    name varchar(20),
    age int,
    nation varchar(20),
    address varchar(20)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

alter table t_person_info modify id int auto_increment;

新建 Spring Boot 项目

创建 Spring Boot 项目并添加依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wyk</groupId>
    <artifactId>batchdemo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>batchdemo</name>
    <description>Demo project for Spring Boot Batch</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
            <!--去除默认加载的hsqldb驱动 -->
            <exclusions>
                <exclusion>
                    <groupId>org.hsqldb</groupId>
                    <artifactId>hsqldb</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 校验数据 -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

在 application.properties 中添加配置。由于Spring Batch默认会创建数据表,这里要允许创建。

# 数据库相关
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/springstudy?serverTimezone=GMT%2B8
spring.datasource.username=spring
spring.datasource.password=spring
spring.datasource.dirverClassName=com.mysql.jdbc.Driver
# 允许覆盖注册
spring.main.allow-bean-definition-overriding=true
# 在数据库里面创建默认的数据表
spring.batch.initialize-schema=ALWAYS
# 显示sql
spring.jpa.show-sql=true

在类路径 src/main/resources 下添加 people.csv 。

王某某,21,汉族,合肥
张某某,33,汉族,北京
李某某,22,藏族,拉萨
赵某某,13,壮族,上海

添加领域模型类

根据数据表添加领域模型类。

package com.wyk.batchdemo.domain;

import javax.validation.constraints.Size;

public class Person {

    @Size(max=4, min=2)
    private String name;
    private int age;
    private String nation;
    private String address;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getNation() {
        return nation;
    }

    public void setNation(String nation) {
        this.nation = nation;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }
}

添加数据处理和校验

数据处理类:

package com.wyk.batchdemo.batch;

import com.wyk.batchdemo.domain.Person;
import org.springframework.batch.item.validator.ValidatingItemProcessor;
import org.springframework.batch.item.validator.ValidationException;

//数据处理
public class CsvItemProcessor extends ValidatingItemProcessor<Person> {
    @Override
    public Person process(Person item) throws ValidationException {
        //调用自定义校验器
        super.process(item);
        //校验数据
        if(item.getNation().equals("汉族")) {
            item.setNation("01");
        } else {
            item.setNation("02");
        }

        return item;
    }
}

数据校验类:

package com.wyk.batchdemo.batch;

import org.springframework.batch.item.validator.ValidationException;
import org.springframework.batch.item.validator.Validator;
import org.springframework.beans.factory.InitializingBean;


import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidatorFactory;
import java.util.Set;

public class CsvBeanValidator<T> implements Validator<T>, InitializingBean {
    private javax.validation.Validator validator;

    @Override
    public void validate(T t) throws ValidationException {
        Set<ConstraintViolation<T>> constraintViolations = validator.validate(t);//校验数据

        if(constraintViolations.size() > 0) {
            StringBuilder message = new StringBuilder();
            for(ConstraintViolation<T> constraintViolation : constraintViolations) {
                message.append(constraintViolation.getMessage() + "\n");
            }
            throw new ValidationException(message.toString());
        }
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        //校验器初始化
        ValidatorFactory validatorFactory = Validation.buildDefaultValidatorFactory();
        validator = validatorFactory.usingContext().getValidator();
    }
}

Job 监听

package com.wyk.batchdemo.batch;

import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.JobExecutionListener;

public class CsvJobListener implements JobExecutionListener {
    long startTime;
    long endTime;

    @Override
    public void beforeJob(JobExecution jobExecution) {
        startTime = System.currentTimeMillis();
        System.out.print("任务开始处理");
    }

    @Override
    public void afterJob(JobExecution jobExecution) {
        endTime = System.currentTimeMillis();
        System.out.print("任务处理结束");
        System.out.println("耗时:" + (endTime - startTime) + "ms");
    }
}

添加配置

package com.wyk.batchdemo.batch;

import com.wyk.batchdemo.domain.Person;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.batch.core.launch.support.SimpleJobLauncher;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.support.JobRepositoryFactoryBean;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.batch.item.ItemReader;
import org.springframework.batch.item.ItemWriter;
import org.springframework.batch.item.database.BeanPropertyItemSqlParameterSourceProvider;
import org.springframework.batch.item.database.JdbcBatchItemWriter;
import org.springframework.batch.item.file.FlatFileItemReader;
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
import org.springframework.batch.item.file.mapping.DefaultLineMapper;
import org.springframework.batch.item.file.transform.DelimitedLineTokenizer;
import org.springframework.batch.item.validator.Validator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.transaction.PlatformTransactionManager;

import javax.sql.DataSource;

@Configuration
@EnableBatchProcessing
public class CsvBatchConfig {

    @Bean
    public ItemReader<Person> reader() throws Exception {
        FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>();
        reader.setResource(new ClassPathResource("people.csv"));// 设置文件路径
        reader.setLineMapper(new DefaultLineMapper<Person>() { // csv文件和领域模型进行映射
            {
                setLineTokenizer(new DelimitedLineTokenizer() {
                    {
                        setNames(new String[]{"name", "age", "nation", "address"});
                    }
                });
                setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {
                    {
                        setTargetType(Person.class);
                    }
                });
            }
        });
        return reader;
    }

    @Bean
    public ItemProcessor<Person, Person> processor() {
        CsvItemProcessor processor = new CsvItemProcessor();
        processor.setValidator(csvBeanValidator()); //指定校验器
        return processor;
    }

    @Bean
    public ItemWriter<Person> writer(DataSource dataSource) {
        JdbcBatchItemWriter<Person> writer = new JdbcBatchItemWriter<Person>();
        writer.setItemSqlParameterSourceProvider(new BeanPropertyItemSqlParameterSourceProvider<Person>());
        String sql = "insert into t_person_info " + "(name,age,nation,address) "
                + "values(:name, :age, :nation, :address)";
        writer.setSql(sql); // 设置需要批处理的SQL
        writer.setDataSource(dataSource);
        return writer;
    }

    @Bean
    public JobRepository jobRepository(DataSource dataSource,
                                       PlatformTransactionManager transactionManager)
            throws Exception{
        JobRepositoryFactoryBean jobRepositoryFactoryBean = new JobRepositoryFactoryBean();
        jobRepositoryFactoryBean.setDataSource(dataSource);
        jobRepositoryFactoryBean.setTransactionManager(transactionManager);
        jobRepositoryFactoryBean.setDatabaseType("mysql");
        return jobRepositoryFactoryBean.getObject();
    }

    @Bean
    public SimpleJobLauncher jobLauncher(DataSource dataSource,
                                         PlatformTransactionManager transactionManager)
        throws Exception {
        SimpleJobLauncher jobLauncher = new SimpleJobLauncher();
        jobLauncher.setJobRepository(jobRepository(dataSource, transactionManager));
        return jobLauncher;
    }

    @Bean
    public Job importJob(JobBuilderFactory jobs, Step s1) {
        return jobs.get("importJob")
                .incrementer(new RunIdIncrementer())
                .flow(s1)
                .end()
                .listener(csvJobListener())
                .build();
    }

    @Bean
    public Step step1(StepBuilderFactory stepBuilderFactory, ItemReader<Person> reader,
                      ItemWriter<Person> writer, ItemProcessor<Person, Person> processor) {
        return stepBuilderFactory
                .get("step1")
                .<Person, Person>chunk(65000) //批处理数据条数
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .build();

    }

    @Bean
    public CsvJobListener csvJobListener() {
        return new CsvJobListener();
    }

    @Bean
    public Validator<Person> csvBeanValidator() {
        return new CsvBeanValidator<Person>();
    }
}

添加玩配置后已经可以运行程序,执行批量,但如果我们想手动触发任务,还要添加一些设置。

手动触发任务

修改 CsvBatchConfig 类中的 reader 方法。

@Bean
@StepScope
public FlatFileItemReader<Person> reader(@Value("#{jobParameter['input.file.name']}") String pathToFile)
    throws Exception {
    FlatFileItemReader<Person> reader = new FlatFileItemReader<Person>();
    reader.setResource(new ClassPathResource("people.csv"));// 设置文件路径
    reader.setLineMapper(new DefaultLineMapper<Person>() { // csv文件和领域模型进行映射
        {
            setLineTokenizer(new DelimitedLineTokenizer() {
                {
                    setNames(new String[]{"name", "age", "nation", "address"});
                }
            });
            setFieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {
                {
                    setTargetType(Person.class);
                }
            });
        }
    });
    return reader;
}

然后添加控制器。

package com.wyk.batchdemo.controller;

import org.springframework.batch.core.Job;
import org.springframework.batch.core.JobParameters;
import org.springframework.batch.core.JobParametersBuilder;
import org.springframework.batch.core.launch.JobLauncher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {
    @Autowired
    JobLauncher jobLauncher;
    @Autowired
    Job importJob;
    public JobParameters jobParameters;

    @RequestMapping("/imp")
    public String imp(String fileName) throws Exception{
        String path = fileName + ".csv";
        jobParameters = new JobParametersBuilder()
                .addLong("time", System.currentTimeMillis())
                .addString("input.file.name", path)
                .toJobParameters();
        jobLauncher.run(importJob, jobParameters);
        return "ok";
    }
}

运行程序,打开 http://localhost:8080/imp?fileName=people ,即可成功运行批量。

异步消息

异步消息主要用于系统间的通讯。所谓异步消息即消息发送者无需等待消息接收者的处理及返回,甚至无需关心消息是否发送成功。

其中,消息代理(message broker)用来接管发送后的消息。异步消息的2种形式的目的地(destination):队列(queue)用于点对点的通讯,主题(topic)用于发布/订阅式的消息通讯。

企业级消息代理

JMS(Java Message Service)即Java消息服务,而ActiveMQ是一个JMS消息代理的实现。

ActiveMQ 安装

在 Docker 之中安装 ActiveMQ, 首先打开 Docker,执行命令。

docker search activemq

然后安装其中的 ActiveMQ 镜像。

docker pull webcenter/activemq

运行 ActiveMQ 。

docker run -d --name myactivemq -p 61617:61616 -p 8162:8161 webcenter/activemq:latest

访问 http://localhost:8161 ,可以查看 ActiveMQ 的管理界面。

activemq-interface.png

使用 ActiveMQ

新建 Spring Boot 项目,并添加 ActiveMQ 的依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

在 applicaton.properties 中配置 ActiveMQ 消息代理的地址。

spring.activemq.broker-url=tcp://localhost:61616

定义消息,实现 MessageCreator 接口,并重写 createMessage 方法。

public class Msg implements MessageCreator {
    @Override
    public Message createMessage(Session session) throws JMSException {
        return session.createTextMessage("这是一个测试消息");
    }
}

在运行类中定义消息发送以及目的地,需要实现CommandLineRunner接口。

@SpringBootApplication
public class ActivemqdemoApplication implements CommandLineRunner { // CommandLineRunner接口用于程序启动后执行的代码

    @Autowired
    JmsTemplate jmsTemplate;

    public static void main(String[] args) {
        SpringApplication.run(ActivemqdemoApplication.class, args);
    }

    public void run(String... args) throws Exception {
        //向mt-destination目的地发送Msg的消息
        jmsTemplate.send("my-destination", new Msg());
    }
}

定义消息监听的类,通过 @JmsListener 注解监听消息。

@Component
public class Receiver {

    @JmsListener(destination = "my-destination")
    public void receiveMessage(String message) {
        System.out.println("接收到: <" + message + ">");
    }
}

运行程序,会自动向目的地发送消息,控制台会监听到消息。

activemq-receive.png

在 ActiveMQ 的管理端也可以查看目的地的相关信息。

activemq-brokers.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342