Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。用我的话来理解,就是 Spring Boot 其实不是什么新的框架,它默认配置了很多框架的使用方式,就像 Maven 整合了所有的 Jar 包,Spring Boot 整合了所有的框架。
注意:学习时使用的是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依赖 -->
<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);
}
}
//该类的位置在Maven项目中src->java->controller下
@RestController //该注解为组合注解,等同于Spring中@Controller+@ResponseBody注解
public class MyController { //类名可以自定义
@GetMapping("/HelloWorld") //等同于Spring框架中@RequestMapping(value="/HelloWorld",Get)注解
public String helloWrold(){
return "hello world";
}
}
详解Spring Boot中Controller用法
注意:启动类和Controller都不能直接放在src->java目录中,要放在这个目录下的包中,并且只能将启动类放在Controller包的上一级,或者将二者放在同一个包中。总之启动类的位置范围应大于或者等于组件所在位置,否则会在访问网页时报错。因为扫描的时候,只扫描项目启动类和它的子包。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@RunWith(SpringRunner.class) //测试启动器,加载Spring Boot测试注解
@SpringBootTest //标记测试单元,加载项目的ApplicationContext上下文环境
public class Chapter01ApplicationTests {
@Test
public void contextLoads() {
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
将数据写入数据库,这里使用的是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`)
)
持久化的方式就是通过以下三个框架,将实体类存入数据库或将数据库中的值读出。就是建立实体类与数据库之间的联系。
要使用这个框架,首先要在pom.xml中添加依赖,也可以和mysql一样在创建项目时勾选。
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
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());
}
}
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 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>
#配置MyBatis的xml配置文件路径
mybatis.mapper-locations=classpath:mapper/xml文件名.xml
#配置XML映射文件中指定的实体类别名路径,如果在xml中写类路径时省略了就要添加这个。
mybatis.type-aliases-package=com.itheima.domain
使xml和mapper接口对应起来。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
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。
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
Redis 是一个开源(BSD许可)的、内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件,并提供多种语言的API。
<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的,这里忽略了。
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();
}
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的属性会有二级注解。
Thymeleaf为变量所在域提供了一些内置对象,如下:
#ctx 上下文对象
#vars 上下文变量. #locale: 上下文区域设置
#request (仅限Web Context)HttpServletRequest对象
#response (仅限Web Context)HttpServletResponse对象
#session (仅限Web Context) HttpSession对象
#servletContext (仅限Web Context) ServletContext对象
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
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
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"; //返回要调用的模板名称
};
}
<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 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}">2018span>-<span th:text="${year}+1">2019span>p>
<a class="btn btn-sm" th:href="@{/Login(l='zh_CN')}">中文a>
<a class="btn btn-sm" th:href="@{/Login(l='en_US')}">Englisha>
form>
body>
html>
login.tip=请登录
login.username=用户名
login.password=密码
login.rememberme=记住我
login.button=登录
# 配置国际化文件基础名,即国际化文件防止的路径,前面默认为src->main->resource->static
# 如果国际化配置文件命名为message.properties,就只需要写到language。
spring.messages.basename=language.login
@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();
}
}
//位置: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");
}
}
//位置: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 {
}
}
最初Servlet 开发时,通常首先自定义Servlet、Filter、Listener 三大组件,然后在文件web.xml 中进行配置,而Spring Boot使用的是内嵌式Servlet容器,没有提供外部配置文件web.xml,所以Spring Boot提供了组件注册和路径扫描两种方式整合Servlet 三大组件。
/*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执行了...");
}
}
@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;
}
}
/*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);
}
}
<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
出现错误
@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>
<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>头像.jpgtd>
<td><a th:href="@{/download(filename='头像.jpg')}">下载文件a>td>
tr>
<tr>
<td>Spring Boot.txttd>
<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");
}
}
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<description>Demo project for Spring Boot</description>
<packaging>war</packaging>
<properties>
<java.version>1.8</java.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
@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);
}
}
@EnableCaching
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@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(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; }...}
Spring Boot支持多种缓存组件,按照优先级来使用。Redis组件的优先级大于默认缓存组件。所以使用不同组件时,注解用法一样。
@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缓存实现的测试完全一样。
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;
}
}
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-boot-starter-security启动器后会自动进行用户认证和授权,但是又很多缺陷:只有唯一的默认登录用户user、密码随机生成且过于暴露、登录页面及错误提示页面都是默认的。
项目引入spring-boot-starter-security依赖启动器,MVC Security安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。
要完全关闭Security提供的Web应用默认安全配置,可以自定义WebSecurityConfigurerAdapter类型的Bean组件以及自定义UserDetailsService、AuthenticationProvider或AuthenticationManager类型的Bean组件。
另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件来覆盖默认访问规则。
WebSecurityConfigurerAdapter类的主要方法如下:
这些用户认证都是在引入依赖的情况下进行的。
@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");
}
}
@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);
}
}
@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("当前用户不存在!");
}
}
}
@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);
}
}
@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() 无条件对请求进行放行,
<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-2021p>
form>
body>
html>
@GetMapping("/userLogin")
public String toLoginPage() {
return "login/login";
}
@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"); //设置登陆失败页面
}
<form th:action="@{/mylogout}" method="post">
<input th:type="submit" th:value="注销" />
form>
@Override
protected void configure(HttpSecurity http){
http.logout()
.logoutUrl("/mylogout") //自定义退出链接
.logoutSuccessUrl("/"); //设置退出后返回的页面
}
@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());
}
}
@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());
}
在实际项目开发中,有时候需要与其他系统进行集成完成相关业务功能,这种情况最原始的做法是程序内部相互调用,除此之外,还可以使用消息服务中间件进行业务处理,使用消息服务中间件处理业务能够提升系统的异步通信和扩展解耦能力。Spring Boot对消息服务管理提供了非常好的支持。
RabbitMQ是基于AMQP协议的轻量级、可靠、可伸缩和可移植的消息代理,Spring使用RabbitMQ通过AMQP协议进行通信,在Spirng Boot中对RabbitMQ进行了集成管理。
在大数据业务中推荐kafaka或RocketMQ。
更多
# 开启RabbitMQ节点
rabbitmqctl start_app
# 开启RabbitMQ管理模块的插件,并配置到RabbitMQ节点上
rabbitmq-plugins enable rabbitmq_management
# 关闭RabbitMQ节点
rabbitmqctl stop
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
# 配置RabbitMQ消息中间件连接配置
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#配置RabbitMQ虚拟主机路径/,默认可以省略
spring.rabbitmq.virtual-host=/
Spring Boot整合RabbitMQ中间件实现消息服务,主要围绕三个部分的工作进行展开:定制中间件、消息发送者发送消息、消息消费者接收消息,其中,定制中间件是比较麻烦的工作,且必须预先定制。
/*注入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));
}
@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);
}
@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);
}
}
@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来实例执行。
@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);
}
}
与上面的工作模式几乎一样,知识在定义交换器时的类型时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。
这个工作模式也是交换器类型和路由键的区别。
#代表多个字符,*代表一个字符。
消息生产者如下:
需要记录以下,消息生产者在实际编程中,应该是书写在某个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依赖可以使用这种异步任务功能。
@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
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(Chapter09Application.class, args);
}
}
@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
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";}
}
通过浏览器访问这个路径时,会调用两个异步方法,因为有返回值,主线程会阻塞等待两个子线程执行完再继续。
@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
@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="[email protected]";
String subject="【复杂邮件】标题";
// 定义邮件内容
StringBuilder text = new StringBuilder();
text.append("");
text.append("祝大家元旦快乐!
");
// cid为固定写法,rscId指定一个唯一标识
String rscId = "img001";
text.append("");
text.append("");
// 指定静态资源文件和附件路径
String rscPath="F:\\email\\newyear.jpg";
String filePath="F:\\email\\元旦放假注意事项.docx";
// 发送复杂邮件
sendEmailService.sendComplexEmail(to,subject,text.toString(),filePath,rscId,rscPath);
}
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"/>
<title>用户验证码title>
head>
<body>
<div><span th:text="${username}">XXXspan> 先生/女士,您好:div>
<P style="text-indent: 2em">您的新用户验证码为<span th:text="${code}"
style="color: cornflowerblue">123456span>,请妥善保管。P>
body>
html>
*/
@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="[email protected]";
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头中带入变量一样。