一、关于Spring Boot
Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。
二、配置环境
- 下载JDK,配置JAVA_HOME、CLASS_PATH和Path
- 下载Apache Maven,配置Maven_Home和Path
- 下载intelij idea,在setting中配置Maven,在Project Struct中配置JDK
注意:学习时使用的是JDK8、Spring Boot2.1.3、Maven32。由于我之前使用的是JDK14,导致编译时报错。更换为JDK8后,要在idea的setting->Java Complier、Project Structre->Project、Project Structre->Moudles中的Sources、Paths、Dependencies将JDK位置、Project Language Level、Target Code Level等换为JDK8。吐槽下JDK的命名方式,由1.5、1.6直接跳到7、8、9、…14也是够迷的。
三、构建Spring Boot项目
一、使用Maven方式构建
- 创建Maven项目
- 在pom.xml中添加相关依赖,然后点击Load Maven change
<!-- 引入Spring Boot依赖 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>
<dependencies>
<!-- 引入Web场景依赖启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
- 编写主程序启动类
//该类的位置在Maven项目中src->java下
@SpringBootApplication //启动类的注解
public class MainApplication { //类名可以自定义
public static void main(String[] args){
SpringApplication.run(MainApplication.class,args);
}
}
- 创建一个用于Web访问的Controller
//该类的位置在Maven项目中src->java->controller下
@RestController //该注解为组合注解,等同于Spring中@Controller+@ResponseBody注解
public class MyController { //类名可以自定义
@GetMapping("/HelloWorld") //等同于Spring框架中@RequestMapping(value="/HelloWorld",Get)注解
public String helloWrold(){
return "hello world";
}
}
注意:启动类和Controller都不能直接放在src->java目录中,要放在这个目录下的包中,并且只能将启动类放在Controller包的上一级,或者将二者放在同一个包中。总之启动类的位置范围应大于或者等于组件所在位置,否则会在访问网页时报错。因为扫描的时候,只扫描项目启动类和它的子包。
二、使用Spring Initializr创建
- 创建Spring boot项目
我使用的是idea的社区版,需要在Setting->Plugins中搜索并安装Spring Assistant。安装完后,点击New->Project就可以创建,创建是需要填写项目名称、位置、JDK版本和Spring boot版本,并要选择Spring boot的组件。创建完成后打开的项目文件目录结构如下:
项目名称
+Debug
+src
++main
++test
+++java
+++resource
+pom.xml - 创建一个用于Web访问的Controller
与用Maven方式创建相同,但是使用这个方法,启动类已经自动创建了。注意启动类和Controller的位置关系。
四、单元测试和热部署
一、单元测试
- 在pom.xml中添加测试启动器。使用Spring Initializr会自动添加。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 编写单元测试类
使用Spring Initializr也会自动添加,在src->test->java中。自己写时主要注意两个注解。在测试类中生成需要测试的类的实例,并输出结果就可以在控制台中看到结果输出。
@RunWith(SpringRunner.class) //测试启动器,加载Spring Boot测试注解
@SpringBootTest //标记测试单元,加载项目的ApplicationContext上下文环境
public class Chapter01ApplicationTests {
@Test
public void contextLoads() {
}
}
二、热部署
- 在pom.xml中添加热部署依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
- 开启idea自动编译功能
依次打开Settings->Build, Execution, Deployment->Compiler->Build project automaticaly,勾选。然后使用快捷键“Ctrl+Shift+Alt+/”打开Maintenance选项框,选中并打开Registry页面,选中compiler.automake.allow.when.app.running。
五、核心配置与注解
- application.properties和application.yaml
- @ConfigurationProperites和@Value
- @PropertySource
- @ImportResource
- 使用@configuration编写自定义类
- Profie文件和@Profie编写自定义配置类
- 随机值和数据间引用
六、数据持久化
将数据写入数据库,这里使用的是Mysql数据库。
首先在pom.xml中添加阿里druid数据源和mysql的依赖。其中mysql的依赖在使用spring boot initilizr创建项目时可以勾选,就不用自己手动添加了。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
然后在application.properties中添加数据源和mysql相关配置信息。
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/userinfo?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.type = com.alibaba.druid.pool.DruidDataSource
spring.datasource.initialSize=20
spring.datasource.minIdle=10
spring.datasource.maxActive=100
完成这些后,建立实体类User和Goods。实体类建立在src->main->java->com.**.->(省略项目名和实体类的包名)。代码中省略了getter、setter和toString。
public class Goods {
Integer id;
String name;
}
public class User {
private Integer id;
private String userName;
private String password;
}
建立数据库和表。
CREATE DATABASE `userinfo`
CREATE TABLE `goods` (
`id` int NOT NULL,
`name` varchar(100) DEFAULT NULL
)
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`userName` varchar(100) NOT NULL,
`password` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
)
持久化的方式就是通过以下三个框架,将实体类存入数据库或将数据库中的值读出。就是建立实体类与数据库之间的联系。
Mybaits
要使用这个框架,首先要在pom.xml中添加依赖,也可以和mysql一样在创建项目时勾选。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
- 注解方式整合MyBatis
首先创建Mapper接口文件。在src->main->java->com.**.**->**下新建一个mapper包,在其中新建接口:
package com.hc.spring_boot_test1.mapper;
import com.hc.spring_boot_test1.domain.User;
import org.apache.ibatis.annotations.*;
@Mapper //标记为mapper
public interface UserMapper {
@Select("select * from user where id = #{id}") //#{}是占位符
public User findById(Integer id);
@Insert("insert into user values(#{id},#{userName},#{password})")
public void addUser(User user);
@Update("update user set password = #{user} where id = #{id}")
public void updateUser(User user);
@Delete("delete from user where id = #{id}")
public void deleteUser(Integer id);
}
在src->test->java->com.**.**下编写测试类。测试。
package com.hc.spring_boot_test1;
import...
@SpringBootTest
class SpringBootTest1ApplicationTests {
@Autowired
private UserMapper userMapper;
@Autowired
private GoodsMapper goodsMapper;
@Test
void contextLoads() {
User user = userMapper.findById(1);
System.out.println("开始打印:");
System.out.println(user.toString());
}
}
- 配置文件方式整合
- 创建接口文件。
和上面一样,但是不需要@select等注解。
package com.hc.spring_boot_test1.mapper;
import com.hc.spring_boot_test1.domain.Goods;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface GoodsMapper {
public Goods findById(Integer id);
}
- 创建xml映射文件。
在src->main->resources的下级包中新建一个与上面mapper同名的xml文件。
<?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.hc.spring_boot_test1.mapper.GoodsMapper">
<!-namespace里写xml文件对应的mapper接口的包位置-!>
<resultMap id="goods" type="com.hc.spring_boot_test1.domain.Goods">
<!-id是与下面resultMap对应的结果名,type是实体类位置-!>
<id property="id" column="id"></id>
<!-id代表主键,result是非主键;property是实体类属性名,column是对应的表中的列名。-!>
<result property="name" column="name"></result>
</resultMap>
<!-下面是查询语句内容,要返回结果就加resultMap,要查询参数的就加parameterType,它的值和接口中函数的参数类型一致-!>
<select id="findById" resultMap="goods" parameterType="int">
select * from goods
</select>
</mapper>
- 配置xml映射
在application.properties中添加
#配置MyBatis的xml配置文件路径
mybatis.mapper-locations=classpath:mapper/xml文件名.xml
#配置XML映射文件中指定的实体类别名路径,如果在xml中写类路径时省略了就要添加这个。
mybatis.type-aliases-package=com.itheima.domain
使xml和mapper接口对应起来。
JPA
- 在pom.xml文件中添加依赖启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
- 编写ORM实体类。
就是编写一个实体类,并将里面的属性通过注解与表一一对应。
package com.hc.spring_boot_test1.ORM;
import javax.persistence.*;
@Entity(name = "user")
public class UserE {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "userName")
private String userName;
@Column(name = "password")
private String password;
}
@Entity表示这个实体类是与一个表映射的,表名就是name的值;@Id表示这个属性是一个主键;@GeneratedValue表示主键是自增的;@Column表示这个属性与一个列名对应。类中还要添加getter、setter和toString。
- 编写Repository接口。
这个接口和Mybaits中的mapper差不多,用来实现查询语句的。
package com.hc.spring_boot_test1.repository;
import...
public interface UserERepositoty extends JpaRepository<UserE,Integer> {
//0.如果自定义接口继承了JpaRepository接口,则默认包含了一些常用的CRUD方法。
//1.直接使用方法名关键字进行查询操作
public List<UserE> findByIdNotNull();
//2.使用@Query分页查询
@Query("SELECT u FROM user u WHERE u.Id = ?1")
public List<UserE> getUserById(Integer id, Pageable pageable);
//3.使用@Query配合原始SQL语句查询
@Query(value = "SELECT * FROM user u WHERE u.Id = ?1",nativeQuery = true)
public List<UserE> getUserById2(Integer id);
//4.增、删、改
@Transactional
@Modifying
@Query("UPDATE user u SET u.password = ?2 WHERE u.userName = ?1")
public int updateUser(String userName,String password);
}
Repository接口继承自JpaRepository接口。自带简单的查询方式(在测试中有演示)。另外还有四种查询方式,分别是通过方法名关键字查询,这种方式不需要写注解和SQL语句,只要使用适当的驼峰命名法把条件写出即可;第二和第三种查询差不多,就是通过SQL查询,但是要使用原生的SQL语句注意使用第三种查法;最后一种是对表增删改用到的,要额外加两个注解,其余和第二、三种一样。
注意:hibernate 会按照驼峰命名规范 将 userName 转成 user_name ,所以在ORE实体类种,要把@Column( name=“userName” ) 里的name 改成 name=“username”)
- 测试
编写测试类
@SpringBootTest
class JpaTest {
@Autowired
private UserERepositoty userERepositoty;
@Test
void Test(){
//0
System.out.println("0:");
Optional<UserE> userE = userERepositoty.findById(1);
System.out.println(userE.toString());
//1
System.out.println("1:");
List<UserE> userE1 = userERepositoty.findByIdNotNull();
for(UserE u:userE1){
System.out.println(u.toString());
}
//4
System.out.println("4:");
userERepositoty.updateUser("1","234");
//3
System.out.println("3:");
List<UserE> userE2 = userERepositoty.getUserById2(1);
for(UserE u:userE1){
System.out.println(u.toString());
}
}
}
注意:使用自带的简单查询,返回值是Optional<T>类型的。同时第二章方法测试出错,考虑到 和第三种差不多,就暂时没有找愿因。
Redis
Redis 是一个开源(BSD许可)的、内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件,并提供多种语言的API。
- 安装Redis和Redis可视化工具 Redis Desktop Manager
- 在pom.xml种添加Spring data redis依赖启动器。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 编写实体类
package com.hc.spring_boot_test1.ORM;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.index.Indexed;
@RedisHash("user") //标记是一个Redis实体类,会在内存中生成相应表
public class UserRedis {
@Id //标记主键,注意是org.springframework.data.annotation.Id包
private Integer id;
@Indexed //生成耳机索引,会用来做查询的最好标记,是org.springframework.data.redis.core.index.Indexed包
private String userName;
@Indexed
private String password;
}
同样是需要加入setter和getter、toString的,这里忽略了。
- 编写Repository接口
package com.hc.spring_boot_test1.repository;
import...
public interface UserRedisRepository extends CrudRepository<UserRedis,Integer> {
//这里不继承JpaRepository,切记。继承上面这个接口,就有基本的Crud。
//JpaRepository也是继承的CrudRepository,但需要JPA的支持。
//Integer是当前主键数据类型,UserRedis是需要存储的类的类型。
List<UserRedis> findById();
List<UserRedis> findByIdAndUsername();
}
- 在application.properties中添加redis数据库连接配置
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
- 测试
@SpringBootTest
public class RedisTest {
@Autowired
private UserRedisRepository userRedisRepository;
@Test
void test(){
UserRedis userRedis = new UserRedis();
userRedis.setId(1);
userRedis.setUsername("黄超");
userRedis.setPassword("123");
userRedisRepository.save(userRedis); //存入内存的非关系数据库
}
}
打开RDM可以看到已经加入数据库。同时可以看到添加了@Index的属性会有二级注解。
七、视图技术
Spring Boot支持的模板引擎
Thymeleaf
- 常用标签
- xmlns:th 引入模板引擎标签
- th:href 引入外联样式文件
- th:text 动态显示标签文本内容
- th:insert 页面片段包含(类似JSP中的include标签)
- th:replace 页面片段包含(类似JSP中的include标签)
- th:each 元素遍历(类似JSP中的c:forEach标签)
- th:if 条件判断,如果为真
- th:unless 条件判断,如果为假条件判断,进行选择性匹配条件判断,进行选择性匹配
- th:object 变量声明
- th:with 变量声明
- th:attr 通用属性修改
- th:attrprepend 通用属性修改,将计算结果追加前缀到现有属性值
- th:attrappend 通用属性修改,将计算结果追加后缀到现有属性值
- th:value 属性值修改,指定标签属性值
- th:href 用于设定链接地址
- th:src 用于设定链接地址
- th:text 用于指定标签显示的文本内容
- th:utext 用于指定标签显示的文本内容,对特殊标签不转义
- th:fragment 声明片段
- th:remove 移除片段
- 标准表达式
- 变量表达式 ${…} 获取上下文中的变量值
- 选择变量表达式 *{…} 用于从被选定对象获取属性值
- 消息表达式 #{…} 用于Thymeleaf模板页面国际化内容的动态替换和展示
- 链接URL表达式 @{…} 用于页面跳转或者资源的引入
- 片段表达式 ~{…} 用来标记一个片段模板,并根据需要移动或传递给其他模板。
Thymeleaf为变量所在域提供了一些内置对象,如下:
#ctx 上下文对象
#vars 上下文变量. #locale: 上下文区域设置
#request (仅限Web Context)HttpServletRequest对象
#response (仅限Web Context)HttpServletResponse对象
#session (仅限Web Context) HttpSession对象
#servletContext (仅限Web Context) ServletContext对象
- Spring Boot整合Thymeleaf
- 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 在全局配置文件中配置Thymeleaf模板的一些参数。
spring.thymeleaf.cache = true #thymeleaf页面缓存设置(默认为true),开发中方便调试应设置为false,上线稳定后应保持默认true
spring.thymeleaf.encoding = UTF-8 #模板编码
spring.thymeleaf.mode = HTML5 模板样式
spring.thymeleaf.prefix = classpath:/templates/ #模板存放路径
spring.thymeleaf.suffix = .html #模板页面后缀名
Spring Boot中静态资源的访问路径
Spring Boot默认将/**所有访问映射到以下目录:
classpath:/META-INF/resources/:项目类路径下的META-INF文件夹下的resources文件夹下的所有文件。
classpath:/resources/:项目类路径下的resources文件夹下的所有文件。
classpath:/static/:项目类路径下的static文件夹下的所有文件
classpath:/public/:项目类路径下的public文件夹下的所有文件。
使用Spring Initializr方式创建的Spring Boot项目,默认生成了一个resources目录,在resources目录中新建public、resources、static三个子目录下,Spring Boot默认会挨个从public、resources、stalic里面查找静态资源。使用时不用创建public、resources,创建一个stalic然后将静态文件放进去就可以了。
- 创建控制类:@Controller
@Controller
public class LoginController { //位置在src->main->java->*.com.*->contorller
@RequestMapping("/Login")
public String login(Model model){
int year = Calendar.getInstance().get(Calendar.YEAR); //调用日历类获取当前年份
model.addAttribute("year",year);
return "login"; //返回要调用的模板名称
};
}
- 创建模板页面并引入静态资源文件
文件名称为login.html,位置为src->main->.com.->resource->templates;静态资源(CSS、JavaScript、.jpg文件)存放在src->main->.com.->resource->static下。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1,shrink-to-fit=no">
<title>用户登录界面</title>
<link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<!-- 用户登录form表单 -->
<form class="form-signin">
<img class="mb-4" th:src="@{/login/img/login.jpg}" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">请登录</h1>
<input type="text" class="form-control"
th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" class="form-control"
th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"> [[#{login.rememberme}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.button}">登录</button>
<p class="mt-5 mb-3 text-muted">© <span th:text="${year}">2018</span>-<span th:text="${year}+1">2019</span></p>
<a class="btn btn-sm" th:href="@{/Login(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/Login(l='en_US')}">English</a>
</form>
</body>
</html>
- 测试
- Thymeleaf配置国际化页面
- 编写多语言国际化文件以及配置文件
文件名称为login.properties、login_zh_CN.properties、login_en_US.properties。位置在src->main->resourc->language。注意这些资源不是静态资源,不能存放在static下。
login.tip=请登录
login.username=用户名
login.password=密码
login.rememberme=记住我
login.button=登录
- 编写核心配置文件
# 配置国际化文件基础名,即国际化文件防止的路径,前面默认为src->main->resource->static
# 如果国际化配置文件命名为message.properties,就只需要写到language。
spring.messages.basename=language.login
- 编写定制区域信息解析器
创建一个用于定制国际化功能区域信息解析器的自定义配置类MyLocalResovel,MyLocalResolver自定义区域解析器配置类实现了LocaleResolver接口,并重写了其中的resolveLocale()方法进行自定义语言解析,最后使用@Bean注解将当前配置类注册成Spring容器中的一个类型为LocaleResolver的Bean组件,这样就可以覆盖默认的LocaleResolver组件。
如果不创建这个解析器,就会使用请求头自带的位置信息。
文件位置为src->main->java->.com.->config
注意使用@Configuration标签,使这个类可以被扫描。
@Configuration
public class MyLocalResovel implements LocaleResolver {
// 自定义区域解析方式
@Override
public Locale resolveLocale(HttpServletRequest httpServletRequest) {
// 获取页面手动切换传递的语言参数l
String l = httpServletRequest.getParameter("l");
// 获取请求头自动传递的语言参数Accept-Language
String header = httpServletRequest.getHeader("Accept-Language");
Locale locale=null;
// 如果手动切换参数不为空,就根据手动参数进行语言切换,否则默认根据请求头信息切换
if(!StringUtils.isEmpty(l)){
String[] split = l.split("_");
locale=new Locale(split[0],split[1]);
}else {
// Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
String[] splits = header.split(",");
String[] split = splits[0].split("-");
locale=new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest httpServletRequest, @Nullable
HttpServletResponse httpServletResponse, @Nullable Locale locale) {
}
// 将自定义的MyLocalResovel类重新注册为一个类型LocaleResolver的Bean组件
@Bean
public LocaleResolver localeResolver(){
return new MyLocalResovel();
}
}
- 页面国际化使用
参见上面的login.html编写方式。测试中出现乱码就更改File Encoding。
八、web开发
Spring MVC整合
- Spring Boot自动配置
引入spring-boot-starter-web依赖后,会自动配置如下:
内置了两个视图解析器:ContentNegotiatingViewResolver和BeanNameViewResolver;
支持静态资源以及WebJars;
自动注册了转换器和格式化器;
支持Http消息转换器;
自动注册了消息代码解析器;
支持静态项目首页index.html;
支持定制应用图标favicon.ico;
自动初始化Web数据绑定器ConfigurableWebBindingInitializer。 - Spring MVC功能拓展
- 视图管理器MVCConfig
对访问的链接进行映射,并添加拦截器。
实现WebMvcConfigurer接口,重写addViewControllers方法添加视图管理。重写addInterceptors方法添加拦截器管理。
注意添加@Configuration注解。
//位置:src->main->java->*.com.*->config
@Configuration
public class MyMVCConfig implements WebMvcConfigurer {
@Autowired
MyInterceptor myInterceptor;
@Override
public void addViewControllers(ViewControllerRegistry registry){
// 请求toLoginPage映射路径或者login.html页面都会自动映射到login.html页面
registry.addViewController("/Login").setViewName("login");
registry.addViewController("login.html").setViewName("login");
}
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login.html");
}
}
- 自定义拦截器Interceptor
对访问进行拦截和预处理。
实现HandlerInterceptor接口,重写preHandle方法、postHandle方法和afterCompletion方法。
preHandle方法的调用时间是Controller方法处理之前;postHandle方法的调用时间是Controller方法处理完之后,DispatcherServlet进行视图的渲染之前。只有在preHandle返回true时生效;afterCompletion方法的调用时间是DispatcherServlet进行视图的渲染之后,只有preHandle返回true时生效。
链式Intercepter情况下,Intercepter按照声明的顺序一个接一个执行。
注意添加@Component注解。
//位置:src->main->java->*.com.*->config
@Component
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,Object handler){
// 用户请求/admin开头路径时,判断用户是否登录
String uri = request.getRequestURI();
Object loginUser = request.getSession().getAttribute("loginUser");
if (uri.startsWith("/admin") && null == loginUser) {
response.sendRedirect("/Login");
return false;
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView){
request.setAttribute("year", Calendar.getInstance().get(Calendar.YEAR));
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse
response, Object handler, @Nullable Exception ex) throws Exception {
}
}
Spring Boot整合Servlet
最初Servlet 开发时,通常首先自定义Servlet、Filter、Listener 三大组件,然后在文件web.xml 中进行配置,而Spring Boot使用的是内嵌式Servlet容器,没有提供外部配置文件web.xml,所以Spring Boot提供了组件注册和路径扫描两种方式整合Servlet 三大组件。
- 组件注册方式
- 创建Servlet组件
位置在src->main->java->.com.->servletComponent。
注意使用@Component注解。
/*servlet*/
@Component
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("Hello World");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request,response);
}
}
/*Filter*/
@Component
public class MyFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("myFilter执行...");
chain.doFilter(request,response);
}
}
/*Listener*/
@Component
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized执行了...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed执行了...");
}
}
- 注册组件
创建组件配置类ServletConfig(名字可以自定义),位置在src->main->java->.com.->config。
注意,类要使用@Configuration注解,方法要使用@Bean注解。
@Configuration
public class ServletConfig {
@Bean
public ServletRegistrationBean getServlet(MyServlet myServlet){
ServletRegistrationBean<MyServlet> myServletServletRegistrationBean = new ServletRegistrationBean<>(myServlet,"/myServlet");
return myServletServletRegistrationBean;
};
@Bean
public FilterRegistrationBean getFilter(MyFilter myFilter){
FilterRegistrationBean<MyFilter> myFilterFilterRegistrationBean = new FilterRegistrationBean<>(myFilter);
//设置拦截的访问路径,现在时http://localhost:8080/Login
myFilterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/Login"));
return myFilterFilterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean getListener(MyListener myListener){
ServletListenerRegistrationBean<MyListener> myListenerServletListenerRegistrationBean = new ServletListenerRegistrationBean<>(myListener);
return myListenerServletListenerRegistrationBean;
}
}
- 路径扫描方式
在Spring Boot 中,使用路径扫描的方式整合内嵌式Servlet容器的 Servlet、Filter、Listener三大组件时,首先需要在自定义组件上分别添加@WebServlet、@WebFilter和@WebListener注解进行声明,并配置相关注解属性,然后在项目主程序启动类上使用@ServletComponentScan注解开启组件扫描即可。
/*servlet*/
@WebServlet("/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("Hello World");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request,response);
}
}
/*Filter*/
@WebFilter(value = {"/Login","/myServlet")
public class MyFilter extends HttpFilter {
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("myFilter执行...");
chain.doFilter(request,response);
}
}
/*Listener*/
@WebListener
public class MyListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("contextInitialized执行了...");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("contextDestroyed执行了...");
}
}
/*项目启动类*/
@SpringBootApplication
@ServletComponentScan
public class SpringBootTest1Application {
public static void main(String[] args) {
SpringApplication.run(SpringBootTest1Application.class, args);
}
}
文件上传与下载
- 文件上传
- 编写文件上传的表单页面
文件名称为upload.html,位于src->main->resource->tamplate
表单中一定要加上“enctype=“multipart/form-data”,否则后端代码获取不到上传的文件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>单文件上传</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="uploadFile" value="请选择文件">
<input type="submit" value="上传">
</form>
</body>
</html>
- 在全局配置文件中添加文件上传的相关配置
# 单个上传文件大小限制(默认1MB)
spring.servlet.multipart.max-file-size=10MB
# 总上传文件大小限制(默认10MB)
spring.servlet.multipart.max-request-size=50MB
- 创建控制类实现文件上传功能
返回“上传成功”的函数需要添加注解@ResponseBody。或者在整个控制器添加注解@RestController。但是要注意,这个控制类没有其他的方法返回的是html页面,这个注解会导致返回的不是页面而是字符串。
@Controller
public class FileContorller {
//设置访问上传页面的映射。
@GetMapping("/toUpload")
public String toUpload(){
return "upload";
}
@PostMapping("/upload")
@ResponseBody
public String upload(MultipartFile[] uploadFile, HttpServletRequest req){
for (MultipartFile file : uploadFile) {
// 获取文件名以及后缀名
String fileName = file.getOriginalFilename();
// 重新生成文件名(根据具体情况生成对应文件名)
fileName = UUID.randomUUID()+"_"+fileName;
// 指定上传文件本地存储目录,不存在需要提前创建
String dirPath = "D:/file/";
File filePath = new File(dirPath);
if(!filePath.exists()){
filePath.mkdirs();
}
try {
file.transferTo(new File(dirPath+fileName));
} catch (Exception e) {
e.printStackTrace();
// 上传失败,返回失败信息
return "上传失败";
}
}
// 携带上传状态信息回调到文件上传页面
return "上传成功";
}
}
- 文件下载
- 添加依赖
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
- 编写下载页面
名称为download.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>文件下载</title>
</head>
<body>
<div style="margin-bottom: 10px">文件下载列表:</div>
<table>
<tr>
<td>头像.jpg</td>
<td><a th:href="@{/download(filename='头像.jpg')}">下载文件</a></td>
</tr>
<tr>
<td>Spring Boot.txt</td>
<td><a th:href="@{/download(filename='Spring Boot.txt')}">
下载文件</a></td>
</tr>
</table>
</body>
</html>
- 编写下载控制类
@Controller
public class FileController {
// 向文件下载页面跳转
@GetMapping("/toDownload")
public String toDownload(){
return "download";
}
// 所有类型文件下载管理
@GetMapping("/download")
public ResponseEntity<byte[]> fileDownload(HttpServletRequest request,
String filename) throws Exception{
// 指定要下载的文件根路径
String dirPath = "D:/file/";
// 创建该文件对象
File file = new File(dirPath + File.separator + filename);
// 设置响应头
HttpHeaders headers = new HttpHeaders();
// 通知浏览器以下载方式打开(下载前对文件名进行转码)
filename=getFilename(request,filename);
headers.setContentDispositionFormData("attachment",filename);
// 定义以流的形式下载返回文件数据
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
try {
return new ResponseEntity<>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
return new ResponseEntity<byte[]>(e.getMessage().getBytes(),HttpStatus.EXPECTATION_FAILED);
}
}
// 根据浏览器的不同进行编码设置,返回编码后的文件名
private String getFilename(HttpServletRequest request, String filename)
throws Exception {
// IE不同版本User-Agent中出现的关键词
String[] IEBrowserKeyWords = {"MSIE", "Trident", "Edge"};
// 获取请求头代理信息
String userAgent = request.getHeader("User-Agent");
for (String keyWord : IEBrowserKeyWords) {
if (userAgent.contains(keyWord)) {
//IE内核浏览器,统一为UTF-8编码显示,并对转换的+进行更正
return URLEncoder.encode(filename, "UTF-8").replace("+"," ");
}
}
//火狐等其它浏览器统一为ISO-8859-1编码显示
return new String(filename.getBytes("UTF-8"), "ISO-8859-1");
}
}
打包和部署
- JAR包
- 添加Maven打包插件
在pom.xml中添加
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 打包与运行
在idea中打开Maven Project窗口,选择lifecycle->pakage,就可以完成打包,在控制台中可以看见打包后文件的存放地址。
运行时,打开cmd,切换到jar所在目录,输入命令java -jar jar完整名称就可以运行。
- WAR包
- 修改默认打包方式
在pom.xml中修改
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
- 声明使用外部提供的Tomcat
在全局配置文件中声明。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
- 提供Spring Boot启动的Servlet初始化器
主程序启动类继承SpringBootServletInitializer类并实现configure()方法,在configure()方法中,sources(MyApplication.class,args)方法的首个参数必须是项目主程序启动类。
需要说明的是,为Spring Boot提供启动的Servlet初始化器SpringBootServletInitializer的典型的做法就是让主程序启动类继承SpringBootServletInitializer类并实现configure()方法;除此之外,还可以在项目中单独提供一个继承SpringBootServletInitializer的子类,并实现configure()方法。
@ServletComponentScan // 开启基于注解方式的Servlet组件扫描支持
@SpringBootApplication
public class MyApplication extends SpringBootServletInitializer {
// 3、程序主类继承SpringBootServletInitializer,并重写configure()方法
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(MyApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
- 打包与执行
打包过程同上。使用时,将WAR包放在webapps下,启动tomcat即可。
九、缓存管理
默认缓存管理
- 开启基于注解的缓存支持
在项目启动类上添加@EnableCaching注解,开启缓存支持
@EnableCaching
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
- 对数据操作方法进行缓存管理
在数据操作方法上添加注解@Cacheable对数据操作方法进行缓存管理。
在Spring Boot中,对连接数据库主要基于三个类:与数据库表相互映射的实体类;封装SQL语句的接口;进行数据操作的service类。
这个注解意味着把查询结果存放在Spring Boot缓存中名为User的缓存空间。
@Cacheable(cacheNames = "comment")
public User findById(int id){
Optional<Comment> optional = userRepository.findById(id);
if(optional.isPresent()){
return optional.get();
}
return null;
}
- 主要注解
-
@EnableCaching是由Spring框架提供的,Spring Boot框架对该注解进行了继承,该注解需要配置在类上(在Spring Boot中,通常配置在项目启动类上),用于开启基于注解的缓存支持。
-
@Cacheable注解也是由Spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于对方法结果进行缓存存储。
@Cacheable注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据。
-
@CachePut注解是由Spring框架提供的,可以作用于类或方法(通常用在数据更新方法上),该注解的作用是更新缓存数据。@CachePut注解的执行顺序是,先进行方法调用,然后将方法结果更新到缓存中。
@CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同。 -
@CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除。
@CacheEvict注解也提供了多个属性,这些属性与@Cacheable注解的属性基本相同,除此之外,还额外提供了两个特殊属性allEntries和beforeInvocation。
allEntries属性表示是否清除指定缓存空间中的所有缓存数据,默认值为false(即默认只删除指定key对应的缓存数据);beforeInvocation属性表示是否在方法执行之前进行缓存清除,默认值为false(即默认在执行方法后再进行缓存清除)。 -
@Caching注解用于针对复杂规则的数据缓存管理,可以作用于类或方法,在@Caching注解内部包含有Cacheable、put和evict三个属性,分别对应于@Cacheable、@CachePut和@CacheEvict三个注解。
@Caching(cacheable={@Cacheable(cacheNames ="comment",key = "#id")},
put = {@CachePut(cacheNames = "comment",key = "#result.author")})
public Comment getComment(int comment_id){
return commentRepository.findById(comment_id).get();
}
- @CacheConfig注解使用在类上,主要用于统筹管理类中所有使用@Cacheable、@CachePut和@CacheEvict注解标注方法中的公共属性,这些公共属性包括有cacheNames、keyGenerator、cacheManager和cacheResolver。
@CacheConfig(cacheNames = "comment")
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
@Cacheable
public Comment findById(int comment_id){
Comment comment = commentRepository.findById(comment_id).get();
return comment; }...}
SpringBoot整合Redis
Spring Boot支持多种缓存组件,按照优先级来使用。Redis组件的优先级大于默认缓存组件。所以使用不同组件时,注解用法一样。
- 基于注解的Redis缓存实现
- 添加Spring Data Redis 依赖启动器
见第六部分。 - Redis服务连接配置
见第六部分。 - 使用@Cacheable、@CachePut、@CacheEvict注解定制缓存管理
与默认缓存管理的用法相同。 - 将缓存对象实现序列化
实体类实现Serializable接口。
- 基于API的Redis缓存实现
- 编写业务处理类
编写一个进行业务处理的类ApiUserService,使用@Autowired注解注入Redis API中常用的RedisTemplate(类似于Java基础API中的JdbcTemplate);然后在数据查询、修改和删除三个方法中,根据业务需求分别进行数据缓存查询、缓存存储、缓存更新和缓存删除。同时,在这个服务类的查询方法中,数据对应缓存管理的key值都手动设置了一个前缀“User_”,这是针对不同业务数据进行缓存管理设置的唯一key,避免与其他业务缓存数据的key重复。
这个类的位置在src->main->java->*.com.*->service。
@Service
public class ApiUserService {
@Autowired
private UserRepository userRepository;
@Autowired
private RedisTemplate redisTemplate;
public User findById(int user_id){
// 先从Redis缓存中查询数据
Object object = redisTemplate.opsForValue().get("user_"+user_id);
if (object!=null){
return (User)object;
}else {
// 缓存中没有,就进入数据库查询
Optional<User> optional = userRepository.findById(user_id);
if(optional.isPresent()){// 如果有数据
User user= optional.get();
// 将查询结果进行缓存,并设置有效期为1天
redisTemplate.opsForValue().set("user_"+user_id, user,1, TimeUnit.DAYS);
// redisTemplate.opsForValue().set("user_"+user_id,user);
// redisTemplate.expire("user_"+user_id,90,TimeUnit.SECONDS);
return user;
}else {
return null;
}
}
}
public User updateUser(User user){
userRepository.updateUser(user.getId(), user.getUserName());
// 更新数据后进行缓存更新
redisTemplate.opsForValue().set("user_"+user.getId(),user);
return user;
}
public void deleteUser(int user_id){
userRepository.deleteById(user_id);
// 删除数据后进行缓存删除
redisTemplate.delete("user_"+user_id);
}
}
-
编写Web访问层Controller类
在类上加入了@RequestMapping(“/api”)注解用于窄化请求,并通过@Autowired注解注入了新编写的ApiCommentService实例对象,然后调用ApiCommentService中的相关方法进行数据查询、修改和删除。 -
相关配置
基于API的Redis缓存实现不需要@EnableCaching注解开启基于注解的缓存支持。
基于API的Redis缓存实现需要在Spring Boot项目的pom.xml文件中引入Redis依赖启动器,并在配置文件中进行Redis服务连接配置,同时将进行数据存储的实体类实现序列化接口。
缓存测试与基于注解的Redis缓存实现的测试完全一样。
- 自定义RedisTemplate
Spring Boot整合Redis进行数据的缓存管理,默认实体类数据使用的是JDK序列化机制,不便于使用可视化管理工具进行查看和管理。所以需要自定义JSON格式的数据序列化机制进行数据缓存管理。
-
Redis API 默认序列化机制
基于Redis API的Redis缓存实现是使用RedisTemplate模板进行数据缓存操作的。具有如下性质:使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable);
使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式defaultSerializer,那么将使用自定义的序列化方式。 -
自定义RedisTemplate序列化机制
在项目中引入Redis依赖后,Spring Boot提供的RedisAutoConfiguration自动配置会生效。打开RedisAutoConfiguration类,查看内部源码中关于RedisTemplate的定义方式可知:在Redis自动配置类中,通过Redis连接工厂RedisConnectionFactory初始化了一个RedisTemplate;该类上方添加了@ConditionalOnMissingBean注解(顾名思义,当某个Bean不存在时生效),用来表明如果开发者自定义了一个名为redisTemplate的Bean,则该默认初始化的RedisTemplate会被覆盖;如果想要使用自定义序列化方式的RedisTemplate进行数据缓存操作,可以参考上述核心代码创建一个名为redisTemplate的Bean组件,并在该组件中设置对应的序列化方式即可。
位置在src->main->java->*.com.*->config下。
@Configuration // 定义一个配置类
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 使用JSON格式序列化对象,对缓存数据key和value进行转换
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 设置RedisTemplate模板API的序列化方式为JSON
template.setDefaultSerializer(jacksonSeial);
return template;
}
}
- 自定义RedisCacheManager
基于Redis APr的 RedisTemplate可以自定义序列化机制,从而实现了相对熟悉的JSON格式缓存数据,但是这种自定义的RedisTemplate,对于基于注解的Redis 缓存实现没有作用。基于注解的Redis 缓存机制和自定义序列化方式需要用到RedisCacheManager。
同RedisTemplate核心源码类似,RedisCacheConfiguration,内部同样通过Redis连接工厂 RedisConnectionFactory定义了一个缓存管理器RedisCacheManager;同时定制RedisCacheManager时,也默认使用了JdkSerializationRedigSerializer.序列化方式。
如果想要使用自定义序列化方式的RedisCacheManager进行数据缓存操作,可以创建一个名为cacheManager的 Bean组件,并在该组件中设置对应的序列化方式即可。
注意,这里是Spring Boot 2.X版本的使用方法,与Spring Boot 1.X版本的实现原理并不相同。
public class RedisConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =
new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
}
上述代码中,在RedisConfig.配置类中使用@Bean注解注入了一个默认名称为方法名的cacheManager,组件。在定义的Bean组件中,通过RedisCacheConfiguration对缓存数据的 key和 value分别进行了序列化方式的定制,其中缓存数据的 key’定制为StringRedisSerializer(即String格式),而 value定制为了Jackson2JsonRedisSerializer(即 JSON格式),同时还使用entryTtl(Duration.ofiDays(1))方法将缓存数据有效期设置为1天。
十、安全管理
Spring Security
引入spring-boot-starter-security启动器后会自动进行用户认证和授权,但是又很多缺陷:只有唯一的默认登录用户user、密码随机生成且过于暴露、登录页面及错误提示页面都是默认的。
MVC Security安全配置
项目引入spring-boot-starter-security依赖启动器,MVC Security安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。
要完全关闭Security提供的Web应用默认安全配置,可以自定义WebSecurityConfigurerAdapter类型的Bean组件以及自定义UserDetailsService、AuthenticationProvider或AuthenticationManager类型的Bean组件。
另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件来覆盖默认访问规则。
WebSecurityConfigurerAdapter类的主要方法如下:
自定义用户认证
这些用户认证都是在引入依赖的情况下进行的。
- 内存身份认证
最简单的身份认证。
- 自定义配置类
这个类继承WebSecurityConfigurerAdapter类
@EnableWebSecurity注解是一个组合注解,主要包括@Configuration注解、@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class})注解和@EnableGlobalAuthentication注解
这个类的位置在src->main->java->*.com.*->config - 使用内存身份认证
在上面的类中,重写configure(AuthenticationManagerBuilder auth)方法,在该方法中使用内存身份认证的方式自定义认证用户信息,设置用户名、密码以及对应的角色信息。
@EnableWebSecurity // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 使用内存用户信息,作为测试使用
auth.inMemoryAuthentication().passwordEncoder(encoder)
.withUser("huangchoa").password(encoder.encode("123456")).roles("common")
.and()
.withUser("admin").password(encoder.encode("123456")).roles("vip");
}
}
- JDBC身份认证
通过JDBC连接数据库的方式来进行已有用户身份认证。使用时首先要添加JDBC和MYSQL依赖,并修改全局配置文件。建立用户表,权限表,和用户-权限关系表。
- 自定义配置类
这个类继承WebSecurityConfigurerAdapter类,位置在src->main->java->*.com.*->config - 重写config方法
@EnableWebSecurity // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Qualifier("dataSource")
@Autowired
private DataSource dataSource;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 使用JDBC用户认证
String userSQL ="select username,password,valid from t_customer " +
"where username = ?"; //查询用户名与密码
String authoritySQL="select c.username,a.authority from t_customer c,t_authority a,"+
"t_customer_authority ca where ca.customer_id=c.id " +
"and ca.authority_id=a.id and c.username =?"; //查询用户权限
auth.jdbcAuthentication().passwordEncoder(encoder)
.dataSource(dataSource)
.usersByUsernameQuery(userSQL)
.authoritiesByUsernameQuery(authoritySQL);
}
}
- UserDetailsService身份认证
对于用户流量较大的项目来说,频繁的使用JDBC进行数据库查询认证不仅麻烦,而且会降低网站登录访问速度。对于一个完善的项目来说,如果某些业务已经实现了用户信息查询的服务,就没必要使用JDBC进行身份认证了。
如果当前项目中已经有用户信息查询的业务方法,就可以在已有的用户信息服务的基础上选择使用UserDetailsService进行自定义用户身份认证。
- 编写服务类
这个类继承自UserDetailsService,重写了loadUserByUsername(String s)方法。借助已有的用户信息查询服务类,将查询记过封装为UserDtails接口的子类User,并返回。
下面代码中的customerService类是一个借助JPA套件查询数据库,并借助redis进行缓存管理的服务类。
这个类的位置在src->main->java->*.com.*->service
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private CustomerService customerService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
// 通过业务方法获取用户及权限信息
Customer customer = customerService.getCustomer(s);
List<Authority> authorities = customerService.getCustomerAuthority(s);
// 对用户权限进行封装
List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());
// 返回封装的UserDetails用户详情类
if(customer!=null){
UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);
return userDetails;
} else {
// 如果查询的用户不存在(用户名不存在),必须抛出此异常
throw new UsernameNotFoundException("当前用户不存在!");
}
}
}
- 编写配置类
继承WebSecurityConfigurerAdapter类,重写config方法。
这个类在src->main->java->*.com.*->config下。
@EnableWebSecurity // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
}
}
自定义用户授权管理
- 自定义用户访问控制
自定义配置类SecurityConfig,继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)方法。位置在src->main->java->*.com.*->config
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated()
.and()
.formLogin();
}
HttpSecurity类主要方法:
authorizeRequests() 开启基于HttpServletRequest请求访问的限制。
formLogin() 开启基于表单的用户登录,
httpBasic() 开启基于HTTP请求的 Basic认证登录
logout() 开启退出登录的支持
sessionManagements() 开启Session管理配置。
rememberMe() 开启记住我功能·
csrf() 配置CSRF跨站请求伪造防护功能。
authorizeRequests() 返回值的方法:
antMatchers(java.lang.String… antPatterns) 开启Ant 风格的路径匹配,
mvcMatchers(java.lang.String… patterns) 开启MVC风格的路径匹配(与Ant 风格类似)
regexMatchers(java.1ang.String… regexPatterns) 开启正则表达式的路径匹配,
and() 功能连接符。
anvRequest() 匹配任何请求。
rememberMe() 开启记住我功能
access(String attribute) 使用基于SpEL表达式的角色现象匹配
hasAnyRole(String… roles) 匹配用户是否有参数中的任意角色,
hasRole(String role) 匹配用户是否有某一个角色。
hasAnyAuthority(String… authorities) 匹配用户是否有参数中的任意权限◎
hasAuthority(String authority) 匹配用户是否有某一个权限,
authenticated() 匹配已经登录认证的用户,
fullyAuthenticated() 匹配完整登录认证的用户(非rememberMe登录用户)
hasIpAddress(String ipaddressExpression) 匹配某IP地址的访问请求,
permitAll() 无条件对请求进行放行,
- 自定义用户登陆
Spring Security默认使用Get方式的“/login”请求用于向登录页面跳转,默认使用Post方式的“/login”请求用于对登录后的数据进行处理。因此,自定义用户登录控制时,需要提供向用户登录页面跳转的方法,且自定义的登录页跳转路径必须与数据处理提交路径一致。
- 自定义用户登陆页面login.html
位置在src->main->resource->templates->login
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用户登录界面</title>
<link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" th:action="@{/userLogin}" th:method="post" >
<img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
<h1 class="h3 mb-3 font-weight-normal">请登录</h1>
<!-- 用户登录错误信息提示框 -->
<div th:if="${param.error}" style="color: red;height: 40px;text-align: left;font-size: 1.1em">
<img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
</div>
<input type="text" name="name" class="form-control" placeholder="用户名" required="" autofocus="">
<input type="password" name="pwd" class="form-control" placeholder="密码" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" name="rememberme"> 记住我
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
<p class="mt-5 mb-3 text-muted">Copyright© 2020-2021</p>
</form>
</body>
</html>
- 自定义用户登陆跳转
示例中,“/userLogin"的Get 请求向登录页跳转,则必须使用路径为“/userLogin”的Post 请求进行登录数据处理。位置在src->main->java->*.com.*->controller。
@GetMapping("/userLogin")
public String toLoginPage() {
return "login/login";
}
- 自定义用户登陆控制
自定义配置类SecurityConfig,继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)
位置在src->main->java->*.com.*->config。
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/login/**").permitAll() //放行对登陆页面的访问
.antMatchers("/detail/common/**").hasRole("common")
.antMatchers("/detail/vip/**").hasRole("vip")
.anyRequest().authenticated();
http.formLogin()
.loginPage("/userLogin").permitAll() //自定义登陆页面
.usernameParameter("name").passwordParameter("pwd") //设置用户名和密码在也页面中对应的变量名。
.defaultSuccessUrl("/") //登陆成功默认跳转路径
.failureUrl("/userLogin?error"); //设置登陆失败页面
}
- 自定义用户退出
- 添加退出表单
Spring Security自带注销功能。HttpSecurity的layout()方法会默认处理/layout路径的Post方式请求,会自动清理session。
在首页中添加退出表单,开启了CRSF后必须以Post方式退出,如果没有开启,可以以任意方式退出。
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
</form>
- 自定义用户控制
自定义配置类SecurityConfig,继承WebSecurityConfigurerAdapter类,重写configure(HttpSecurity http)
位置在src->main->java->*.com.*->config。
@Override
protected void configure(HttpSecurity http){
http.logout()
.logoutUrl("/mylogout") //自定义退出链接
.logoutSuccessUrl("/"); //设置退出后返回的页面
}
- 登陆用户信息获取
这里获取用户信息都是在controller。有两种方式,HttpSession和SecurityContextHolder。都是获取session或context后,在获取其中的用户信息。
- HttpSession
@GetMapping("/getuserBySession")
@ResponseBody
public void getUser(HttpSession session) {
// 从当前HttpSession获取绑定到此会话的所有对象的名称
Enumeration<String> names = session.getAttributeNames();
while (names.hasMoreElements()){
// 获取HttpSession中会话名称
String element = names.nextElement();
// 获取HttpSession中的应用上下文
SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
System.out.println("element: "+element);
System.out.println("attribute: "+attribute);
// 获取用户相关信息
Authentication authentication = attribute.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: "+principal.getUsername());
}
}
- SecurityContextHolder
@GetMapping("/getuserByContext")
@ResponseBody
public void getUser2() {
// 获取应用上下文
SecurityContext context = SecurityContextHolder.getContext();
System.out.println("userDetails: "+context);
// 获取用户相关信息
Authentication authentication = context.getAuthentication();
UserDetails principal = (UserDetails)authentication.getPrincipal();
System.out.println(principal);
System.out.println("username: "+principal.getUsername());
}
- 记住我
- CSRF防护
十一、消息服务
在实际项目开发中,有时候需要与其他系统进行集成完成相关业务功能,这种情况最原始的做法是程序内部相互调用,除此之外,还可以使用消息服务中间件进行业务处理,使用消息服务中间件处理业务能够提升系统的异步通信和扩展解耦能力。Spring Boot对消息服务管理提供了非常好的支持。
RabbitMQ是基于AMQP协议的轻量级、可靠、可伸缩和可移植的消息代理,Spring使用RabbitMQ通过AMQP协议进行通信,在Spirng Boot中对RabbitMQ进行了集成管理。
在大数据业务中推荐kafaka或RocketMQ。
更多
安装RabbitMQ
- 安装Erlang
- 安装RabbitMQ
安装后通过http://localhost:15672查看管理器,账号密码都是guest。如果访问这个网址失败,切花到RabbitMQ安装目录下的sbin目录,在cmd中执行如下代码,启动管理器组件。
# 开启RabbitMQ节点
rabbitmqctl start_app
# 开启RabbitMQ管理模块的插件,并配置到RabbitMQ节点上
rabbitmq-plugins enable rabbitmq_management
# 关闭RabbitMQ节点
rabbitmqctl stop
Spring Boot整合RabbitMQ
- 引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
- 配置全局属性
# 配置RabbitMQ消息中间件连接配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#配置RabbitMQ虚拟主机路径/,默认可以省略
spring.rabbitmq.virtual-host=/
Publish/Subscribe(发布订阅模式)工作模式
Spring Boot整合RabbitMQ中间件实现消息服务,主要围绕三个部分的工作进行展开:定制中间件、消息发送者发送消息、消息消费者接收消息,其中,定制中间件是比较麻烦的工作,且必须预先定制。
- 基于API的方式
- 使用AmqpAdmin定制消息发送组件。
在测试类中定制组件,正式使用时可以在项目启动类中。
/*注入AmqpAdmin*/
@Autowired
private AmqpAdmin amqpAdmin;
/**
* 使用AmqpAdmin管理员API定制消息组件
* 创建了一个交换器fanout_exchange
* 以及连个和它绑定的消息队列
*/
@Test
public void amqpAdmin() {
// 1、定义fanout类型的交换器
amqpAdmin.declareExchange(new FanoutExchange("fanout_exchange"));
// 2、定义两个默认持久化队列,分别处理email和sms
amqpAdmin.declareQueue(new Queue("fanout_queue_email"));
amqpAdmin.declareQueue(new Queue("fanout_queue_sms"));
// 3、将队列分别与交换器进行绑定
amqpAdmin.declareBinding(new Binding("fanout_queue_email",Binding.DestinationType.QUEUE,"fanout_exchange","",null));
amqpAdmin.declareBinding(new Binding("fanout_queue_sms",Binding.DestinationType.QUEUE,"fanout_exchange","",null));
}
- 消息发送者发送消息
进行消息发送过程中,默认使用SimpleMessageConverter转换器进行消息转换存储。所以需要实体类使用jdk的Serializable序列化接口,或者自定义消息转换器。
- 定制转换器
创建一个RabbitMQ消息配置类RabbitMQConfig,并在该配置类中通过@Bean注解自定义了一个Jackson2JsonMessageConverter类型的消息转换器组件,该组件的返回值必须为MessageConverter类型
这个类位于src->main->java->*.com.*->config。
@Configuration
public class RabbitMQConfig {
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
}
定制转化器后,在测试类中添加消息发送者函数。
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 1、Publish/Subscribe工作模式消息发送端
*/
@Test
public void psubPublisher() {
User user=new User();
user.setId(1);
user.setUserName("黄超");
rabbitTemplate.convertAndSend("fanout_exchange","",user);
}
- 消息消费者接收消息
在消费者类中的方法上使用@RabbitListener()标签监听RabbitMQ队列,实现获取消息内容。
创建一个RabbitMQService类,这个类的位置在src->main->java->*.com.*->service中。
@Service
public class RabbitMQService {
/**
* Publish/Subscribe工作模式接收,处理邮件业务
* @param message
*/
@RabbitListener(queues = "fanout_queue_email")
public void psubConsumerEmail(Message message) {
byte[] body = message.getBody();
String s = new String(body);
System.out.println("邮件业务接收到消息: "+s);
}
/**
* Publish/Subscribe工作模式接收,处理短信业务
* @param message
*/
@RabbitListener(queues = "fanout_queue_sms")
public void psubConsumerSms(Message message) {
byte[] body = message.getBody();
String s = new String(body);
System.out.println("短信业务接收到消息: "+s);
}
}
- 基于配置类的方式
使用Spring Boot框架提供的@Configuration注解,编写消息配置类RabbitMQConfig,定义消息转换器、fanout类型的交换器、不同名称的消息队列以及将不同名称的消息队列与交换器绑定。
这个类的位置在src->main->java->*.com.*->config中。
@Configuration
public class RabbitMQConfig {
/**
* 定制JSON格式的消息转换器
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
/**
* 使用基于配置类的方式定制消息中间件
* @return
*/
// 1、定义fanout类型的交换器
@Bean
public Exchange fanout_exchange(){
return ExchangeBuilder.fanoutExchange("fanout_exchange").build();
}
// 2、定义两个不同名称的消息队列
@Bean
public Queue fanout_queue_email(){
return new Queue("fanout_queue_email");
}
@Bean
public Queue fanout_queue_sms(){
return new Queue("fanout_queue_sms");
}
// 3、将两个不同名称的消息队列与交换器进行绑定
@Bean
public Binding bindingEmail(){
return BindingBuilder.bind(fanout_queue_email()).to(fanout_exchange()).with("").noargs();
}
@Bean
public Binding bindingSms(){
return BindingBuilder.bind(fanout_queue_sms()).to(fanout_exchange()).with("").noargs();
}
}
消息生产者和消息接收者与使用API的方式相同,只不过是将交换器和队列的定义放到了配置类中,借助@Bean来实例执行。
- 基于注解方式
在RabbitMQ服务类RabbitMQService中,使用**@RabbitListener注解及其相关属性定制消息组件消费者,改用与发送消息对应的实体类作为消息接收函数的参数**。在@RabbitListener注解中,使用bindings属性来自动创建并绑定交换器和消息队列组件,在定制交换器时将交换器类型设置为fanout。另外,bindings属性的@QueueBinding,注解除了有value、type属性外,还有key属性用于定制路由键routingKey(当前发布订阅模式不需要)。消息的生产者与API方式相同,同时需要在配置了中配置自定义转换器。
@Service
public class RabbitMQService {
/**
* **使用基于注解的方式实现消息服务
* 1.1、Publish/Subscribe工作模式接收,处理邮件业务
* @param user
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("fanout_queue_email"), exchange =@Exchange(value = "fanout_exchange",type = "fanout")))
public void psubConsumerEmailAno(User user) {
System.out.println("邮件业务接收到消息: "+user);
}
/**
* 1.2、Publish/Subscribe工作模式接收,处理短信业务
* @param user
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("fanout_queue_sms"),exchange =@Exchange(value = "fanout_exchange",type = "fanout")))
public void psubConsumerSmsAno(User user) {
System.out.println("短信业务接收到消息: "+user);
}
}
Routing路由工作模式
与上面的工作模式几乎一样,知识在定义交换器时的类型时direct,同时要设置路由键。
下面使用注解方式实现这个工作模式。
消息发送者:
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void routingPublisher() {
rabbitTemplate.convertAndSend("routing_exchange","error_routing_key","routing send error message");
}
消息接收者是一个业务类:
@Service
public class RabbitMQService {
/**
* 2.1、路由模式消息接收,处理error级别日志信息
* @param message
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("routing_queue_error"),exchange =@Exchange(value = "routing_exchange",type = "direct"),key = "error_routing_key"))
public void routingConsumerError(String message) {
System.out.println("接收到error级别日志消息: "+message);
}
/**
* 2.2、路由模式消息接收,处理info、error、warning级别日志信息
* @param message
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("routing_queue_all"),exchange =@Exchange(value = "routing_exchange",type = "direct"),key = {"error_routing_key","info_routing_key","warning_routing_key"}))
public void routingConsumerAll(String message) {
System.out.println("接收到info、error、warning等级别日志消息: "+message);
}
}
注意:这个工作模式中一个消费者可以有多个key。
Topics通配符工作模式
这个工作模式也是交换器类型和路由键的区别。
#代表多个字符,*代表一个字符。
消息生产者如下:
需要记录以下,消息生产者在实际编程中,应该是书写在某个controllor或service中,它的功能就是,用户主动调用时,借助RabbiTemplate类向RabbitMQ消息中间件发送消息。所以一定要注入RabbiTemplate。消息接收者根据这几种方式可知,是一个服务类,通过注解,使Spring自动问询消息中间件中是否有任务,如果有,取出其中的信息并执行任务。
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void topicPublisher() {
// 1、只发送邮件订阅用户消息
// rabbitTemplate.convertAndSend("topic_exchange","info.email","topics send email message");
// 2、只发送短信订阅用户消息
// rabbitTemplate.convertAndSend("topic_exchange","info.sms","topics send sms message");
// 3、发送同时订阅邮件和短信的用户消息
rabbitTemplate.convertAndSend("topic_exchange","info.email.sms","topics send email and sms message");
}
基于注解的消息接收者如下:
@Service
public class RabbitMQService {
/**
* 3.1、通配符模式消息接收,进行邮件业务订阅处理
* @param message
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("topic_queue_email"),exchange =@Exchange(value = "topic_exchange",type = "topic"),key = "info.#.email.#"))
public void topicConsumerEmail(String message) {
System.out.println("接收到邮件订阅需求处理消息: "+message);
}
/**
* 3.2、通配符模式消息接收,进行短信业务订阅处理
* @param message
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("topic_queue_sms"),exchange =@Exchange(value = "topic_exchange",type = "topic"),key = "info.#.sms.#"))
public void topicConsumerSms(String message) {
System.out.println("接收到短信订阅需求处理消息: "+message);
}
}
十二、任务管理
异步任务
Spring框架提供了对异步任务的支持,Spring Boot框架继承了这一异步任务功能,在Spring Boot中整合异步任务时,只需在项目中引入Web模块中的Web依赖可以使用这种异步任务功能。
- 无返回值异步任务调用
- 编写异步调用方法
异步任务方法是一个服务,所以将该类创建在src->main->java->*.com.*->service下。
注意使用注解@Async表示这是一个异步任务。
@Service
public class SMSService{
@Async
public void sendSMS() throws Exception {
System.out.println("调用短信验证码业务方法...");
/*模拟发送短信操作,使进程睡眠*/
Long startTime = System.currentTimeMillis();
Thread.sleep(5000);
Long endTime = System.currentTimeMillis();
System.out.println("短信业务执行完成耗时:" + (endTime - startTime));
}
}
- 开启基于注解的异步任务支持
在项目启动类中添加注解@EnableAsync,使得支持异步任务。
@EnableAsync
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(Chapter09Application.class, args);
}
}
- 编写控制层业务调用方法
编写一个controller用来调用服务,当在浏览器中访问这个类时,就会调用异步任务。
@Controller
public class MyAsyncController{
@Autowired
private MyAsyncService myService;
@GetMapping("/sendSMS")
public String sendSMS() throws Exception {
Long startTime = System.currentTimeMillis();
myService.sendSMS();
Long endTime = System.currentTimeMillis();
System.out.println("主流程耗时: "+(endTime-startTime));
return "success";
}
}
最后会在控制台显示主流程耗时与异步任务耗时。
这个异步方法是没有返回值的,这样主流程在执行异步方法时不会阻塞,而是继续向下执行主流程程序,直接向页面响应结果,而调用的异步方法会作为一个子线程单独执行,直到异步方法执行完成。
- 有返回值异步调用
同样需要开启异步任务支持,这里就省略。
- 创建两个有返回值的异步调用方法
同样是在一个service中创建两个异步方法,只不过这两个方法会返回一个类型的返回值。
@Service
public class SMSService{
@Async
public Future<Integer> processA() throws Exception {
System.out.println("开始分析并统计业务A数据...");
Long startTime = System.currentTimeMillis();
Thread.sleep(4000);
int count=123456;
Long endTime = System.currentTimeMillis();
System.out.println("业务A数据统计耗时:" + (endTime - startTime));
return new AsyncResult<Integer>(count);
}
@Async
public Future<Integer> processB() throws Exception {
System.out.println("开始分析并统计业务B数据...");
Long startTime = System.currentTimeMillis();
Thread.sleep(5000);
int count=654321;
Long endTime = System.currentTimeMillis();
System.out.println("业务B数据统计耗时:" + (endTime - startTime));
return new AsyncResult<Integer>(count);
}
}
- 创建控制层调用方法
创建控制器,用来在浏览器中调用上面的两个方法,同时处理返回值。注意要事先注入服务类。
@Controller
public class MyAsyncController{
@Autowired
private MyAsyncService myService;
@GetMapping("/statistics")
public String statistics() throws Exception {
Long startTime = System.currentTimeMillis();
Future<Integer> futureA = myService.processA();
Future<Integer> futureB = myService.processB();
/*在这里会阻塞,知道上面两个方法返回*/
int total = futureA.get() + futureB.get();
System.out.println("异步任务数据统计汇总结果: "+total);
Long endTime = System.currentTimeMillis();
System.out.println("主流程耗时: "+(endTime-startTime));
return "success";}
}
通过浏览器访问这个路径时,会调用两个异步方法,因为有返回值,主线程会阻塞等待两个子线程执行完再继续。
定时任务
- 开启基于注解的定时任务支持
在项目启动类上添加@EnableScheduling标签开启定时任务支持。 - 编写定时任务业务处理方法
@Scheduled 注解是Spring框架提供的,配置定时任务的执行规则,该注解主要用在定时业务方法上。该注解提供有多个属性,精细化配置定时任务执行规则:
cron 类似于cron的表达式,可以定制定时任务触发的秒、分钟、小时、月中的日、月、周中的日。
zone 指定cron表达式将被解析的时区。默认情况下,该属性是空字符串(即使用服务器的本地时区)。
fixedDelay 表示在上一次任务执行结束后在指定时间后继续执行下一次任务(属性值为long 类型)。
fixedDelayString 表示在上一次任务执行结束后在指定时间后继续执行下一次任务(属性值为long类型的字符串形式)
fixdeRate 表示每隔指定时间执行一次任务(属性值为long类型)
fixdeRateString 表示每隔指定时间执行一次任务(属性值为long类型的字符串形式)
initialDelay 表示在fixedRate或fixedDelay任务第一次执行之前要延迟的毫秒数(属性值为long类型)。
initialDelayString 表示在fixedRate或fixedDelay任务第一次执行之前要延迟的毫秒数(属性值为long类型)。
编写业务类,实现定时任务。这个类在src->main->java->*.com.*->service下。
@Service
public class ScheduledTaskService {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private Integer count1 = 1;
private Integer count2 = 1;
private Integer count3 = 1;
@Scheduled(fixedRate = 60000)
public void scheduledTaskImmediately() {
System.out.println(String.format("fixedRate第%s次执行,当前时间为:%s", count1++, dateFormat.format(new Date())));
}
@Scheduled(fixedDelay = 60000)
public void scheduledTaskAfterSleep() throws InterruptedException {
System.out.println(String.format("fixedDelay第%s次执行,当前时间为:%s", count2++, dateFormat.format(new Date())));
Thread.sleep(10000);
}
@Scheduled(cron = "0 * * * * *") //在整数分钟时间点才会首次执行。
public void scheduledTaskCron(){
System.out.println(String.format("cron第%s次执行,当前时间为:%s",count3++, dateFormat.format(new Date())));
}
}
项目运行后,就会按照设置自动执行定时任务。
邮件任务
- 发送纯文本邮件
- 添加邮件服务的依赖启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
当添加上述依赖后,Spring Boot自动配置的邮件服务会生效,在邮件发送任务时,可以直接使用Spring框架提供的JavaMailSender接口或者它的实现类JavaMailSenderImpl邮件发送。
- 添加邮件服务配置
在全局配置文件中添加如下:
# 发件人邮服务器相关配置
spring.mail.host=smtp.qq.com
spring.mail.port=587
# 配置发件人QQ账户和密码(密码是加密后的授权码)
spring.mail.username=2757826020@qq.com
spring.mail.password=
spring.mail.default-encoding=UTF-8
# 邮件服务超时时间配置
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=3000
spring.mail.properties.mail.smtp.writetimeout=5000
- 定制邮件发送服务
创建一个新的服务类,在src->main->java->*.com.*->service下。
@Service
public class SendEmailService {
@Autowired
private JavaMailSenderImpl mailSender;
@Value("${spring.mail.username}") //注入发件人名称
private String from;
/**
* 发送纯文本邮件
* @param to 收件人地址
* @param subject 邮件标题
* @param text 邮件内容
*/
public void sendSimpleEmail(String to,String subject,String text){
// 定制纯文本邮件信息SimpleMailMessage
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to); //目标邮箱
message.setSubject(subject); //邮件标题
message.setText(text); //邮件文本
try {
// 发送邮件
mailSender.send(message);
System.out.println("纯文本邮件发送成功");
} catch (MailException e) {
System.out.println("纯文本邮件发送失败 "+e.getMessage());
e.printStackTrace();
}
}
}
之后在其他服务类、控制类或者项目启动类中调用这个类就可以了。注意要开启发送人QQ邮箱的SMTP服务。
- 发送带附件和图片的邮件
添加依赖和属性与上面相同,只是服务类的业务处理方式不同。
业务处理方式如下:
*/
@Service
public class SendEmailService {
@Autowired
private JavaMailSenderImpl mailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送复杂邮件(包括静态资源和附件)
* @param to 收件人地址
* @param subject 邮件标题
* @param text 邮件内容
* @param filePath 附件地址
* @param rscId 静态资源唯一标识
* @param rscPath 静态资源地址
*/
public void sendComplexEmail(String to,String subject,String text,String filePath,String rscId,String rscPath){
// 定制复杂邮件信息MimeMessage
MimeMessage message = mailSender.createMimeMessage();
try {
// 使用MimeMessageHelper帮助类,并设置multipart多部件使用为true
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(text, true);
// 设置邮件静态资源
FileSystemResource res = new FileSystemResource(new File(rscPath));
helper.addInline(rscId, res);
// 设置邮件附件
FileSystemResource file = new FileSystemResource(new File(filePath));
String fileName = filePath.substring(filePath.lastIndexOf(File.separator));
helper.addAttachment(fileName, file);
// 发送邮件
mailSender.send(message);
System.out.println("复杂邮件发送成功");
} catch (MessagingException e) {
System.out.println("复杂邮件发送失败 "+e.getMessage());
e.printStackTrace();
}
}
}
上述代码中,sendComplexEmail()方法需要接收的参数除了基本的发送信息外,还包括静态资源唯一标识、静态资源路径和附件路径。在定制复杂邮件信息时使用了MimeMessageHelper,类对邮件信息封装处理,包括设置内嵌静态资源和邮件附件。其中,设置邮件内嵌静态资源的方法为addInline(String contentId,Resource resource),设置邮件附件的方法为add.Attachment(String attachmentFilename, InputSteamSource inputStreamSource)。
上面只是邮件发送服务类,复杂邮件本身还需要编写。测试代码如下:
@Test
public void sendComplexEmailTest() {
String to="2127269781@qq.com";
String subject="【复杂邮件】标题";
// 定义邮件内容
StringBuilder text = new StringBuilder();
text.append("<html><head></head>");
text.append("<body><h1>祝大家元旦快乐!</h1>");
// cid为固定写法,rscId指定一个唯一标识
String rscId = "img001";
text.append("<img src='cid:" +rscId+"'/></body>");
text.append("</html>");
// 指定静态资源文件和附件路径
String rscPath="F:\\email\\newyear.jpg";
String filePath="F:\\email\\元旦放假注意事项.docx";
// 发送复杂邮件
sendEmailService.sendComplexEmail(to,subject,text.toString(),filePath,rscId,rscPath);
}
- 发送模板邮件
模板邮件同样需要添加邮件发送依赖并配置相关属性。除此之外,还要添加Thymeleaf依赖,制作基于Thymeleaf的邮件模板。
- 定制邮件模板
在资源文件中创建模板页面。位置在src->main->resource->templates下。名字为emailTemplate_vercode.html。
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>用户验证码</title>
</head>
<body>
<div><span th:text="${username}">XXX</span> 先生/女士,您好:</div>
<P style="text-indent: 2em">您的新用户验证码为<span th:text="${code}"
style="color: cornflowerblue">123456</span>,请妥善保管。</P>
</body>
</html>
- 编写邮件发送服务类
与上面的区别不大,同样要借助JavaMailSender接口或它的实现类JavaMailSenderImpl发送。
在src->main->java->*.com.*->service下。
*/
@Service
public class SendEmailService {
@Autowired
private JavaMailSenderImpl mailSender;
@Value("${spring.mail.username}")
private String from;
/**
* 发送模板邮件
* @param to 收件人地址
* @param subject 邮件标题
* @param content 邮件内容
*/
public void sendTemplateEmail(String to, String subject, String content) {
MimeMessage message = mailSender.createMimeMessage();
try {
// 使用MimeMessageHelper帮助类,并设置multipart多部件使用为true
MimeMessageHelper helper = new MimeMessageHelper(message, true);
helper.setFrom(from);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
// 发送邮件
mailSender.send(message);
System.out.println("模板邮件发送成功");
} catch (MessagingException e) {
System.out.println("模板邮件发送失败 "+e.getMessage());
e.printStackTrace();
}
}
}
之后在其他服务类、控制类、项目启动类或测试类中调用这个类就可以了。注意添加模板页面引擎依赖。
@Autowired
private SendEmailService sendEmailService;
@Autowired
private TemplateEngine templateEngine;
@Test
public void sendTemplateEmailTest() {
String to="2127269781@qq.com";
String subject="【模板邮件】标题";
// 使用模板邮件定制邮件正文内容
Context context = new Context();
context.setVariable("username", "石头");
context.setVariable("code", "456123");
// 使用TemplateEngine设置要处理的模板页面
String emailContent = templateEngine.process("emailTemplate_vercode", context);
// 发送模板邮件
sendEmailService.sendTemplateEmail(to,subject,emailContent);
}
不难看出,其实发送模板邮件就是向用户发送了一个网页。这个网页提前准备好了,使用时,通过服务类与邮件绑定,在测试类(或者其他调用服务类的类)中,为网页传入了userName和code这两个变量的值,然后发给用户。这就类似于访问一个网页时,在request头中带入变量一样。