第1章 SpringBoot开发入门
1、SpringBoot概述
- 简介:
- Spring Boot是基于Spring’框架开发的全新框架,设计的目的是简新Spring应用的初始化搭建和开发过程。
- SpringBoot整合了许多框架和第三方库配置,聚会可以达到“开箱即用”
- 优点:
- 快速构建独立的Spring应用
- 之间嵌入Tomcat、Jetty和Undertow服务器,无需部署WAR文件
- 提供依赖启动器简化构建配置
- 极大程度的自动化配置Spring和第三方库
- 梯控生产就绪功能
- 极少的代码生成和XML配置
2、环境准备
保证安装好的软件如下
- JDK 1.8.0_201
- Apache Maven 3.6.0
- Intellij IDEA Ultimate 旗舰版
3、使用Maven方式构建SpringBoot项目
搭建步骤
-
创建Maven项目
-
在pom.xml中添加Spring Boot相关依赖
<!--引入Spring Boot依赖 1.统一进行版本控制 2.让当前项目具有springBoot特性 3.加载指定的配置文件 --> <parent> <artifactId>spring-boot-starter-parent</artifactId> <groupId>org.springframework.boot</groupId> <version>2.1.3.RELEASE</version> </parent> <!--引入Web场景依赖启动器:引入springmvc的相关依赖--> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.2.RELEASE</version> </dependency> </dependencies>
-
编写主程序启动类
package com.itheima; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication //1.该注解的作用:标注该类为主程序启动类 public class ManualChapterApplication { public static void main(String[] args) {//2.创建main方法 SpringApplication.run(ManualChapterApplication.class);//3.加载主程序类 } }
-
创建一个用于Web访问的Controller
package com.itheima.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController //该注解为组合注解:@ResponseBody+@Controller public class HelloController { @GetMapping("/hello") //相当于:@RequestMapping(value="/hello",RequestMethod.Get) public String hello(){ return "hello spring boot"; } }
-
运行项目
- 运行主程序main方法
- 在浏览器输入访问地址(默认为localhost:8080/接口)
4、使用spring Initializr方式构建springBoot项目
搭建步骤
- 创建Spring Boot项目
注意:如果项目无法构建,请检查路径是否带有中文了,或者修改构建源为阿里云的
-
创建一个用于Web访问的Controller
package com.itheima.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello spring Boot"; } }
- 注意:扫描组件只会扫描启动类同目录或下级目录的类
-
运行项目
5、单元测试
搭建步骤:
-
在pom文件中添加spring-boot-starter-test测试启动器(使用spring Initializr方式构建springBoot项目默认添加有)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency>
-
编写单元测试类
-
编写单元测试方法
package com.itheima.chapter01; import com.itheima.controller.HelloController; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class)//测试启动器,并加载springBoot测试注解 @SpringBootTest//标记springBoot单元测试,并加载项目的applicationContext上下环境 class Chapter01ApplicationTests { @Autowired //根据类型进行注入 private HelloController helloController; @Test void contextLoads() { String hello = helloController.hello(); System.out.println(hello); } }
-
运行结果
6、热部署
场景:每当我们修改代码的时候都需要开启启动类,为了方便我们编写代码,则需要用到热部署!
搭建步骤
-
在pom文件中添加spring-boot-devtools热部署依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> </dependency>
-
IDEA中热部署设置
- 开启IDEA的自动编译
-
使用“ctrl+shift+ait+/”打开Maintenance选项框,选中并打开Registry页面勾选:compiler.automake.allow.when.app.running
-
2020版本以后的IDEA没有compiler.automake.allow.when.app.running,需要在设置里面勾上即可
- 热部署测试
7、Spring Boot依赖管理
spring-boot-starter-0parent依赖
作用:spring-boot-starter-0parent是通过标签对一些常用技术框架的依赖进行了统一版本号管理
spring-boot-starter-web依赖
提供web开发创建所需要的底层所有依赖文件,他对Web开发场景所需的依赖文件进行了统一管理
8、Spring Boot自动配置
Spring Boot自动配置的实现
- Spring Boot应用的启动入口时@SpringBootApplication注解标准类中的main()方法;
- @SpringBootApplication能够扫描Spring组件并自动配置SpringBoot。
- @SpringBootApplication注解是一个组合注解,包含了@SpringBootConfiguration @EnableAutoConfiguration @CompenentScan三个核心注解
9、Spring Boot执行流程
整体流程:
流程1 初始化Spring Application实列:
流程2 初始化Spring Boot项目启动
第2章 SpringBoot核心配置与注解
1、application.properties配置文件
配置普通属性
# 应用名称
spring.application.name=chapter02
# 应用服务 WEB 访问端口
server.port=8080
配置对象属性
-
假设有一个person类
package com.itheima.domain; import java.util.Arrays; import java.util.List; import java.util.Map; public class person { private int id; //id private String name; //姓名 private List hobby; //爱好 private String[] family; //家庭成员 private Map map; private Pet pet; //宠物 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List getHobby() { return hobby; } public void setHobby(List hobby) { this.hobby = hobby; } public String[] getFamily() { return family; } public void setFamily(String[] family) { this.family = family; } public Map getMap() { return map; } public void setMap(Map map) { this.map = map; } public Pet getPet() { return pet; } public void setPet(Pet pet) { this.pet = pet; } @Override public String toString() { return "person{" + "id=" + id + ", name='" + name + '\'' + ", hobby=" + hobby + ", family=" + Arrays.toString(family) + ", map=" + map + ", pet=" + pet + '}'; } }
-
宠物Pet类
package com.itheima.domain; public class Pet { private String type; //宠物类型 private String name; //宠物的名字 }
-
配置对象属性
#对象类型 person.id = 1 person.name = zhangsan person.hobby=play,read,sleep person.family=father,mother person.map.k1=v1 person.map.k2=v2 person.pet.type=dog person.pet.name=kity
2、application.yaml配置文件
概念
- yaml文件格式是spring Boot支持的一种JSON超集文件格式。
- 相对于properties位置文件,yanl文件以数据为核心,是一种更为直观且容易配电脑识别的数据序列格式。
- application.yaml文件的工作原理和application.properties一样。
演示
#当value值为普通数据类型的配置
server:
port: 8082
servlet:
context-path: /hello
#当value值为数组或单列集合
#方式1
hobby:
- play
- read
- sleep
#方式2
hobby:[play,read,sleep]
#当value值为map的时候
#方式1
map:
k1: v1
k2: v2
#方式2
map: [k1: v1,k2: v2]
#对实体类person类进行属性配置
person:
id: 2
name: lisi
hobby: [sing,read,sleep]
family: [father,mother]
map: [k1: v1,k2: v2]
pet: [type: cat,name: tom]
3、使用@ConfigurationProperties和@Value注入
@ConfigurationProperties通过配置文件进行注入(用于批量或对象注入)
-
person类
@Component //生成当前类的实列对象存到IOC容器中 @ConfigurationProperties(prefix = "person")//将配置文件中的前缀为person的每个属性的值映射到当前类中的变量上 public class Person { private int id; //id private String name; //姓名 private List hobby; //爱好 private String[] family; //家庭成员 private Map map; private Pet pet; //宠物 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List getHobby() { return hobby; } public void setHobby(List hobby) { this.hobby = hobby; } public String[] getFamily() { return family; } public void setFamily(String[] family) { this.family = family; } public Map getMap() { return map; } public void setMap(Map map) { this.map = map; } public Pet getPet() { return pet; } public void setPet(Pet pet) { this.pet = pet; } @Override public String toString() { return "person{" + "id=" + id + ", name='" + name + '\'' + ", hobby=" + hobby + ", family=" + Arrays.toString(family) + ", map=" + map + ", pet=" + pet + '}'; } }
-
Pet类
package com.itheima.domain; public class Pet { private String type; //宠物类型 private String name; //宠物的名字 public String getType() { return type; } public void setType(String type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Pet{" + "type='" + type + '\'' + ", name='" + name + '\'' + '}'; } }
-
application.yaml
#当value值为普通数据类型的配置 server: port: 8082 servlet: context-path: /hello #当value值为数组或单列集合 #方式1 hobby: - play - read - sleep #方式2 #hobby:[play,read,sleep] #当value值为map的时候 #方式1 map: k1: v1 k2: v2 #方式2 #map: [k1: v1,k2: v2] #对实体类person类进行属性配置 person: id: 2 name: lisi hobby: [sing,read,sleep] family: [father,mother] map: {k1: v1,k2: v2} pet: {type: cat,name: tom}
-
测试类
package com.itheima.chapter02; import com.itheima.domain.Person; //import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import org.junit.Test; @RunWith(SpringRunner.class) @SpringBootTest public class Chapter02ApplicationTests { //将person类进行注入 @Autowired //在容器中取出对象 private Person person; @Test public void contextLoads() { System.out.println(person); } }
@Value注入普通类型数据(用作单个属性注入)
假设把person对象的name属性注入到当前对象的name属性,测试:
package com.itheima.chapter02;
import com.itheima.domain.Person;
//import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.junit.Test;
@RunWith(SpringRunner.class)
@SpringBootTest
public class Chapter02ApplicationTests {
@Value("${person.name}")//注入普通数据类型
private String name;
@Test
public void contextLoads() {
System.out.println("使用@Value注入的数据为:"+name);
}
}
4、两种注解对比分析
5、使用@PropertySource加载配置文件
相关注解
- @PropertySource: 指定自定义配置稳健的位置和名称
- @Configuration: 自定义配置类,Spring容器组件
案列演示
-
创建一个test.propterties
#对实体类对象MyProperties进行属性配置 test.id=100 test.name=test
-
创建配置类
@Configuration //指定当前类为配置类 也可使用@component注解代替 @PropertySource("classpath:test.properties") //指定自己的配置文件的位置和名称 @EnableConfigurationProperties(MyProperties.class) //开启配置了的属性注入功能(使用@component则不需要配置该项) @ConfigurationProperties(prefix = "test") public class MyProperties { private int id; private String name; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "MyProperties{" + "id=" + id + ", name='" + name + '\'' + '}'; } }
-
测试类
@RunWith(SpringRunner.class) @SpringBootTest public class Chapter02ApplicationTests { @Autowired private MyProperties myProperties; @Test public void contextLoads() { System.out.println(myProperties); } }
6、使用@ImportResource加载XML配置文件
- @ImportResource: 指定xml文件位置
使用@ImportResource加载XML配置文件
-
1、在项目下创建一个com.itheima.config包,并在该包里创建MyService,该类不需要写任何代码
public class MyService{ }
-
2、在resources目录下新建一个名为beans.xml的XML自定义配置文件,在该配置文件中通过配置向Spring容器中添加MyService类对象。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="mySerivce" class="com.itheima.config.MyService"></bean> </beans>
-
3、在启动类上添加@ImportResource注解来指定xml文件位置。
@SpringBootApplication @ImportResource("classpath:beans.xml") public class Chapter02Application { public static void main(String[] args) { SpringApplication.run(Chapter02Application.class, args); } }
-
4、在测试类中引入ApplicationContext实体类Bean,并新增一个测试方法进行输出测试。
7、使用@Configuration编写自定义配置类
相关注解
- @Configuration: 自定义一个配置类
- @Bean: 进行组件配置
案列演示
-
在现有的项目基础上新建一个MyConfig,使用@Configuration注解将该类声明为一个配置类
@Configuration public class MyConfig { @Bean(name="myService1") //相当于Bean标签,将方法的返回值存到spring容器中 public MyService myService(){ return new MyService(); } }
-
MyService类
package com.itheima.config; public class MyService { }
-
测试类
@RunWith(SpringRunner.class) @SpringBootTest public class Chapter02ApplicationTests { @Autowired private ApplicationContext applicationContext; @Test public void contextLoads() { MyService mySerivce = (MyService) applicationContext.getBean("myService1"); System.out.println(mySerivce); } }
8、使用Profile文件进行多环境配置(方式1)
为什么要多坏境配置?
在实际开发中,应用程序通常需要部署到不同的运行环境中,如开发环境、测试环境、生产环境等。
多环境配置方式
- Profile文件多环境配置
- @Profile注解多环境配置
使用Profile文件进行多环节配置
-
多环节配置文件格式:application-{profile}.properties
注意:{profile}对应具体的环境标识
-
激活指定环境的方式
- 通过命令行方式激活指定环境的配置文件
- 在全局配置文件spring.profiles.active属性激活
案列演示
-
在resource目录下创建开发环境配置文件application-dev.properties
#修改端口号 server.port=8081
-
在resource目录下创建生产环境配置文件application-prod.properties
#修改端口号 server.port=8081
-
在resource目录下创建测试环境配置文件application-test.properties
#修改端口号 server.port=8081
-
激活指定环境
-
命令行方式
-
将项目打
-
使用命令行(dev代表开发环境,test和prod分别代表测试环境和生产环境)
PS D:\Project\JAVA\manual_chapter01\chapter02> cd target #切换目录 PS D:\Project\JAVA\manual_chapter01\chapter02\target> java -jar chapter02-0.0.1-SNAPSHOT.jar --spring.pro files.active=dev #将项目环境设定为开发环境
-
-
在全局配置文件application.properties中激活
#添加以下命令即可(dev代表开发环境,test和prod分别代表测试环境和生产环境) spring.profiles.active=dev
-
9、使用@Profile注解进行多环节配置(方式2)
-
创建配置接口DBConnector
package com.itheima.config; public class TestDBConnector implements DBConnector{ @Override public void configure() { System.out.println("数据库环境配置-test"); } }
-
创建环境配置DevDBConnector类实现配置接口
package com.itheima.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @Configuration @Profile(value = "dev") //指定多环节配置类的标识 public class DevDBConnector implements DBConnector{ @Override public void configure() { System.out.println("数据库配置环境--dev"); } }
-
在全局配置文件application.properties中激活
#添加以下命令即可(dev代表开发环境,test和prod分别代表测试环境和生产环境) spring.profiles.active=dev
10、随机值设置以及参数间引用
随机数设置
-
语法格式
${random.xx} #xx表示需要指定生产的随机数类型和范围
示例代码
#配置随机数 my.secret=${random.value} #配置随机数为整数 my.number=${random.int} #配置随机数long类型 my.bignumber=${random.long} #配置小于10的随机整数 my.number=${random.int(10)} #配置1024~65536之间的随机整数 my.number=${random.int[1024,65536]}
-
演示:
-
在全局配置文件配置随机数
#配置随机数 my.secret=${random.value}
-
测试
@Value("${my.secret}") //将随机数注入到当前属性中 private String secret; @Test public void randomTest(){ System.out.println("生成的随机数为:"+secret); }
-
参数间引用
-
语法格式
${xx} xx表示先前在配置文件中已经配置过的属性名
-
演示代码
app.name=MyApp #存值 app.description=${app.name} is a Spring Boot applicaiton #取值
实列演示
-
在全局配置文件中配置数据
tom.age=${random.int[10,20]} tom.description=tom的年龄可能是${tom.age}
-
测试:
@Value("${tom.description}") private String description; @Test public void placeholderTest(){ System.out.println(description); }
第3章 SpringBoot数据访问
1、Spring Boot数据访问概述
-
概述:
SpringBoot默认采用整合springData的方式统一处理数据库访问层,通过添加大量自动配置,引入各种数据访问模板xxxTemplate以及统一的repository接口,从而发大大简化数据访问层的操作。
-
常见的数据库依赖启动器
2、集成环境搭建
Spring Boot 整合Mybatis 步骤
-
**数据准备:**创建数据库、数据表并插入一定的数据
#创建数据库 create database springbootdata #创建t_article表 create table t_article( id int(20) not null auto_increment comment '文章id', title varchar(200) default null comment '文章标题', content longtext comment '文章内容', primary key(`id`) )ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; #创建t_comment表 create table t_comment( id int(20) not null auto_increment comment '评论ID', content longtext comment '评论内容', author varchar(200) default null comment '评论作者', a_id int(20) default null comment '关联的文章ID', primary key(`id`) )ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
t_article表内容:
tablet_comment表内容:
-
创建项目,引入相应的启动器:使用Spring Initializr的方式构建项目,选择MySql和MyBatis依赖,编写实体类
- MySQL Driver
- MyBatis Framework
-
编写实体类Comment和Article
-
Comment
package com.itheima.domain; public class Comment { private Integer id; private String content; private String author; private Integer aId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getaId() { return aId; } public void setaId(Integer aId) { this.aId = aId; } @Override public String toString() { return "Comment{" + "id=" + id + ", content='" + content + '\'' + ", author='" + author + '\'' + ", aId=" + aId + '}'; } }
-
Article
package com.itheima.domain; import java.util.List; public class Article { private Integer id; private String title; private String content; private List<Comment> commentList; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public List<Comment> getCommentList() { return commentList; } public void setCommentList(List<Comment> commentList) { this.commentList = commentList; } @Override public String toString() { return "Article{" + "id=" + id + ", title='" + title + '\'' + ", content='" + content + '\'' + ", commentList=" + commentList + '}'; } }
-
-
**编写配置文件:**在配置文件中进行数据库连接配置以及进行第三方数据库源的默认参数覆盖
-
在pom文件中添加阿里的数据源
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.11</version> </dependency>
-
在application.properties配置文件中配置
# 数据库驱动: spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # 数据库连接地址 spring.datasource.url=jdbc:mysql://localhost:3306/blue?serverTimezone=UTC # 数据库用户名&密码: spring.datasource.username=root spring.datasource.password=root #对数据源默认值进行修改 #数据源类型 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource #初始化连接数 spring.datasource.druid.initial-size=20 #最小空闲数 spring.datasource.druid.min-idle=10 #最大连接数 spring.datasource.druid.max-active=100
-
3、使用注解的方式整合MyBatis
-
创建Mapper接口文件:@Mapper
-
CommentMapper接口
package com.itheima.mapper; import com.itheima.domain.Comment; import org.apache.ibatis.annotations.*; @Mapper //表示该类是一个mybatis接口文件,是需要被springboot进行扫描的 public interface CommentMapper { //查询方法 @Select("select * from t_comment where id = #{id}") public Comment findById(Insert id); //添加方法 @Insert("insert into t_comment values(#{id},#{content},#{author},#{aId})") public void insertComment(Comment comment); //修改方法 @Update("update t_comment set content=#{content} where id = #{id}") public void updateComment(Comment comment); //删除方法 @Delete("delete from t_comment where id=#{id}") public void deleteComment(Integer id); }
-
-
编写测试方法进行接口方法测试及整合测试
package com.itheima.chapter03; import com.itheima.domain.Comment; import com.itheima.mapper.CommentMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class Chapter03ApplicationTests { @Autowired //注入 private CommentMapper commentMapper; @Test void contextLoads() { //测试查询方法 Comment comment = commentMapper.findById(1); System.out.println(comment); } }
4、使用配置文件的方式整合MyBatis
-
创建Mapper接口文件:@Mapper
- ArticleMapper
package com.itheima.mapper; import com.itheima.domain.Article; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ArticleMapper { //根据查询id查询文章(包含对应的评论) public Article selectArticle(Integer id); }
-
创建XML映射文件:编写对应的SQL语句
- ArticleMapper.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.itheima.mapper.ArticleMapper"> <resultMap id="ac" type="Article"> <id property="id" column="id"></id> <result property="title" column="title"></result> <result property="content" column="content"></result> <collection property="commentList" ofType="com.itheima.domain.Comment"> <id property="id" column="c_id"></id> <result property="content" column="content"></result> <result property="author" column="author"></result> <result property="aId" column="aId"></result> </collection> </resultMap> <select id="selectArticle" resultMap="ac" parameterType="int"> select * from t_article a,t_comment c where c.a_id=a.id and a.id=#{aid} </select> </mapper>
-
在全局文件中配置XML映射文件路径以及实体类别名映射路径
- application.properties
# 数据库驱动: spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # 数据库连接地址 spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC # 数据库用户名&密码: spring.datasource.username=root spring.datasource.password=root #对数据源默认值进行修改 #数据源类型 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource #初始化连接数 spring.datasource.druid.initial-size=20 #最小空闲数 spring.datasource.druid.min-idle=10 #最大连接数 spring.datasource.druid.max-active=100 #开启驼峰命名匹配映射 mybatis.configuration.map-underscore-to-camel-case=true #配置mybatis的xml配置文件路径 mybatis.mapper-locations=classpath:mapper/ArticleMapper.xml #配置xml映射文件中指定的实体类别名路径 mybatis.type-aliases-package=com.itheima.domain
-
编写测试方法进行接口方法测试及整合
package com.itheima.chapter03; import com.itheima.domain.Article; import com.itheima.domain.Comment; import com.itheima.mapper.ArticleMapper; import com.itheima.mapper.CommentMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class Chapter03ApplicationTests { @Autowired private ArticleMapper articleMapper; @Test void contextLoads() { Article article = articleMapper.selectArticle(2); System.out.println(article); } }
5、Spring Data JPA介绍
Spring Data JPA基本使用
-
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency>
-
编写ORM实体类:实体类与数据表进行映射,并且配置号映射关系。
名为Discuss的实体类:
package com.itheima.domain; import javax.persistence.*; @Entity(name = "t_comment") //该注解表示当前实体类是与表有映射关系的实体类 public class Discuss { @Id //表示该属性对应的字段为属性 @GeneratedValue(strategy = GenerationType.IDENTITY) //表示该自动能够自增长 private Integer id; @Column(name = "content") //将属性与字段进行映射,注解里面的为数据库字段名 private String content; @Column(name = "author") private String author; @Column(name = "aId") private Integer aId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getaId() { return aId; } public void setaId(Integer aId) { this.aId = aId; } @Override public String toString() { return "Discuss{" + "id=" + id + ", content='" + content + '\'' + ", author='" + author + '\'' + ", aId=" + aId + '}'; } }
-
编写Repository接口:正对不同的表数据操作编写各自对应的Repositor接口,并根据需要编写对应的数据操作方法。
在respoitory报下创建名为DiscussRepository的接口:
package com.itheima.respoitory; import com.itheima.domain.Discuss; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import javax.transaction.Transactional; import java.awt.print.Pageable; import java.util.List; //为了曼珠JPA规范,所以接口需要继承JpaRepository,其中第一个参数表示操作的实体类,第二个参数表示主键的数据类型 public interface DiscussRepository extends JpaRepository<Discuss,Integer> { //1.查询author非空的Discuss评论集合 //查询的格式:findBy + 字段 + 条件 (首字母都需要大写) public List<Discuss> findByAuthorNotNull(); //2.根据文章id分页查询Discuss评论激活 @Query("select c from t_comment c where c.aId=?1")//注意:这里的1表示,将第一个参数的值取给它 public List<Discuss> getDiscussPaged(Integer aid, Pageable pageable); //3.使用SQL语句,根据文章id分页查询Discuss评论集合 @Query(value = "select * from t_comment where a_id = ?1",nativeQuery = true)//nativeQuery = true表示我们写的是原生的SQL语句 public List<Discuss> getDiscussPage2(Integer aid,Pageable pageable); //3.根据评论id修改评论作者author @Transactional //事务控制 @Modifying //需要对数据库数据进行改动,则需要使用该注解 @Query("update t_comment c set c.author = ?1 where c.id=?2") public int updateDiscuss(String author,Integer id); //5.根据评论id删除评论 @Transactional @Modifying @Query("delete from t_comment c where c.id=?1") public int deleteDiscuss(Integer id); }
-
测试
package com.itheima.chapter03; import com.itheima.domain.Article; import com.itheima.domain.Discuss; import com.itheima.mapper.ArticleMapper; import com.itheima.respoitory.DiscussRepository; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.Optional; @RunWith(SpringRunner.class) @SpringBootTest class JpaTest { //进行对象注入 @Autowired private DiscussRepository discussRepository; @Test public void test1(){ //测试查询方法 Optional<Discuss> byId = discussRepository.findById(1); System.out.println(byId.get()); } }
6、使用Spring Boot整合JPA
整合步骤(具体步骤请看上面)
- 在pom文件中添加Spring Data JPA依赖启动器
- 编写ORM实体类
- 编写Repository接口
- 编写单元测试进行接口方法测试及整合测试
7、Redis介绍
Redis简介:
Redis是一个开源(BSD许可)的、内存中的数据结构存储系统,它可以用作数据库、缓存、和消息中间件,并提供多种语言的API。
Redis优点:
- 存储速度快
- 支持丰富的数据类型
- 操作具有原子性
- 提供多种功能
Redis安装步骤
- 下载Redis的zip包,下载地址:https://github.com/microsoftarchive/redis/releases
-
打开redis-server.exe(其中redis-server.exe为服务端,redis-cli.exe为客户端)
-
安装redis可视化工具Redis Desktop Manager
-
官方地址:https://redisdesktop.com/download
-
GitHub免费版:https://github.com/uglide/RedisDesktopManager/releases/tag/0.9.3
-
下载完成一直下一步即可安装
-
将可视化工具连接到Redis
-
8、使用Spring Boot整合Redis
整合步骤
-
在pom文件中添加Spring Data Redis依赖启动器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
编写实体类
-
Person类
package com.itheima.domain; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; import javax.persistence.Id; import java.util.List; @RedisHash("persons")//表示在Redis中开放一个名为persons的空间来给Person进行数据操作 public class Person { @Id //用于标识实体类主键 private String id; @Indexed //用于标识该属性会在redis数据库中生成耳机索引,便于查询操作 private String firstname; @Indexed private String lastname; private Address address; private List<Family> familyList; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getFirstname() { return firstname; } public void setFirstname(String firstname) { this.firstname = firstname; } public String getLastname() { return lastname; } public void setLastname(String lastname) { this.lastname = lastname; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } public List<Family> getFamilyList() { return familyList; } public void setFamilyList(List<Family> familyList) { this.familyList = familyList; } @Override public String toString() { return "Person{" + "id='" + id + '\'' + ", firstname='" + firstname + '\'' + ", lastname='" + lastname + '\'' + ", address=" + address + ", familyList=" + familyList + '}'; } }
-
Family类
package com.itheima.domain; import org.springframework.data.redis.core.index.Indexed; public class Family { @Indexed private String type;//家庭成员的关系 @Indexed private String username; public String getType() { return type; } public void setType(String type) { this.type = type; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } @Override public String toString() { return "Family{" + "type='" + type + '\'' + ", username='" + username + '\'' + '}'; } }
-
Address类
package com.itheima.domain; import org.springframework.data.redis.core.index.Indexed; public class Address { @Indexed private String city; @Indexed private String country; public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } @Override public String toString() { return "Address{" + "city='" + city + '\'' + ", country='" + country + '\'' + '}'; } }
-
-
编写Repository接口
-
PersonRepository接口
package com.itheima.respoitory; import com.itheima.domain.Person; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; import java.util.List; public interface PersonRepository extends CrudRepository<Person,String> { List<Person> findByLastname(String lastname);//根据姓氏查询人 Page<Person> findPersonByLastname(String lastname, Pageable page);//根据姓氏查询人,并分页 List<Person> findByFirstnameAndLastname(String firstname,String lastname);//根据姓名查询 List<Person> findByAddress_City(String city);//查询地址里的人 List<Person> findByFamilyList_Username(String username);//根据姓名查询家庭 }
-
-
在全局配置全局配置文件application.properties中添加Redis数据库连接配置
-
application.properties文件
#配置redis连接 #Redis服务器地址 spring.redis.host=127.0.0.1 #redis连接端口(密码没有就不用填) spring.redis.password=
-
-
编写单元测试进行接口方法测试以及整合测试
package com.itheima.chapter03; import com.itheima.domain.Address; import com.itheima.domain.Discuss; import com.itheima.domain.Family; import com.itheima.domain.Person; import com.itheima.respoitory.DiscussRepository; import com.itheima.respoitory.PersonRepository; import org.junit.jupiter.api.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.ArrayList; import java.util.Optional; @RunWith(SpringRunner.class) @SpringBootTest class RedisTest { @Autowired private PersonRepository personRepository; @Test public void savePerson(){ //创建对象 Person person = new Person(); //给对象注入数据 person.setFirstname("有才"); person.setLastname("张"); Address address = new Address(); address.setCity("北京"); person.setAddress(address); Family family = new Family(); family.setType("父亲"); family.setUsername("张三"); Family family2 = new Family(); family.setType("母亲"); family.setUsername("小红"); ArrayList<Family> families = new ArrayList<>(); families.add(family); families.add(family2); person.setFamilyList(families); personRepository.save(person);//保存对象 } }
第4章 SpringBoot视图技术
1、Spring Boot支持的视图技术
Spring Boot 支持的视图技术
- FreeMarker
- Groory
- Thymeleaf
- Mustache
- …
2、Thymeleaf常用标签
Thymeleaf简介
Thymeleaf是一种现代的基于服务器的Java模板引擎技术,也是一个优秀的面向Java的XML、XMTML、HTML5页面模板,它具有丰富的标签语言、函数和表达式,在SpringBoot框架进行页面设计时,一般会选择Thymeleaf模板。
常用标签
3、Thymeleaf标准表达式
- 标准表达式语法
4、Thymeleaf基本使用
- 创建工程,注意选择依赖
当然,也可以在pom文件中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
-
在全局配置文件中配置Thymeleaf模板的一些参数。如设置模板缓存、模板编码、模板样式、指定模板页面存放路径、指定模板页面名称的后缀。
#模板缓存:开启 spring.thymeleaf.cache=true #模板编码 spring.thymeleaf.encoding=UTF-8 #模板样式 spring.thymeleaf.mode=HTML5 #指定模板页面名称的后缀 spring.thymeleaf.prefix=classpath:/templates/ #指定模板页面名称的后缀(注意后缀有个点) spring.thymeleaf.suffix=.html
5、使用Thymeleaf完成数据的页面展示
代码演示:数据的传递和页面的展示
-
创建Spring Boot项目,引入Thymeleaf依赖
-
编写配置文件,对Thymeleaf模板的页面数据缓存进行设置
#模板缓存:开启(开发阶段保持false) spring.thymeleaf.cache=true
-
创建控制类:@Controller
-
LoginController.class
package com.itheima.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.ui.Model; import java.util.Calendar; @Controller public class LoginController { @RequestMapping("/toLoginPage") public String login(Model model){ //获取年份 int i = Calendar.getInstance().get(Calendar.YEAR); model.addAttribute("currentYear",i); return "login"; } }
-
-
创建模板页面并引入静态资源文件
-
templates/login.html模板
<!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">请登录</h1> <input type="text" class="form-control" placeholder="用户名" required="" autofocus=""> <input type="password" class="form-control" placeholder="密码" required=""> <div class="checkbox mb-3"> <label> <input type="checkbox" value="remember-me"> 记住我 </label> </div> <button class="btn btn-lg btn-primary btn-block" type="submit">登录</button> <p class="mt-5 mb-3 text-muted">© <span th:text="${currentYear}">2018</span>-<span th:text="${currentYear}+1">2019</span></p> <a class="btn btn-sm" th:href="@{/toLoginPage(1='zh_CN')}">中文</a> <a class="btn btn-sm" th:href="@{/toLoginPage(1='en_US')}">English</a> </form> </body> </html>
-
其他静态资源请看附录
-
目录结构
-
-
效果测试
-
打开启动类
-
浏览器输入:http://localhost:8080/toLoginPage
-
页面效果:
-
6、使用Thymeleaf配置国际化页面
-
编写多语言国际化文件
-
国际化文件有:login.properties、login_en_US.properties、login_zh_CN.properties
login.properties
login.tip=请登录 login.username=用户名 login.password=密码 login.rememberme=记住我 login.button=登录
login_zh_CN.properties
login.tip=请登录 login.username=用户名 login.password=密码 login.rememberme=记住我 login.button=登录
login_en_US.properties
login.tip=Please sign in login.username=username login.password=password login.rememberme=remember me login.button=login
-
-
编写配置文件
在application.properties中添加国际化文件基础名设置
#配置国际化文件基础名 spring.messages.basename=i18n.login
-
定制区域信息解析器(为了实现手动切换语言)
-
在com.itheima.config下创建MyLocalResovel类
-
MyLocalResovel实现LocaleResolver接口
-
重写接口方法
package com.itheima.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.web.servlet.LocaleResolver; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Locale; /** * @Classname MyLocalResovel * @Description 自定义区域信息解析器配置类 * @Date 2019-3-1 17:24 * @Created by CrazyStone */ @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(); } }
-
-
页面国际化使用
使用 th:text="#{login.tip} 的方法对内容数据进行引用
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">请登录</h1>
-
整合效果测试
第5章 SpringBoot实现web开发
1、Spring Boot的整合支持
Spring Boot 整合Spring MVC的自动化配置功能特性
- 内置了两个视图解析器:ContentNegotiatingViewResolver和BeanNameViewResolver
- 支持静态资源以及WebJar
- 自动注册了转换器和格式化器
- 支持Http消息转换器
- 自动注册了消息代码解析器
- 支持静态项目首页index.html
- 支持定制应用图标favicon.ico
- 自动初始化Web数据绑定器ConfigurableWebBindingInitializer.
Spring MVC功能拓展实现步骤
-
项目基础环境搭建
- 使用spring Initializr方法构建项目,并选择web依赖和Thymeleaf依赖
-
功能拓展实现
-
将chapter04项目的源码拷过来
-
注册视图管理器:在com.itheima.config包下创建一个类去实现WebMvcConfigurer接口
package com.itheima.config; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class MyMVCconfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { //请求toLoginPage映射路径或者login.html页面都会自动映射到login.html页面 registry.addViewController("toLoginPage").setViewName("login"); registry.addViewController("login.html").setViewName("login"); } } //下面的是拦截器部分 @Autowired private MyInterceptor myInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { //拦截所有,放行login.html registry.addInterceptor(myInterceptor).addPathPatterns("/**").excludePathPatterns("login.html"); }
-
注册自定义拦截器MyInterceptor,实现HandlerInterceptor接口,在该类中编写如下方法
package com.itheima.config; import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Calendar; /** * 自定义一个拦截器类 */ @Component public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 用户请求/admin开头路径时,判断用户是否登录 String uri = request.getRequestURI(); Object loginUser = request.getSession().getAttribute("loginUser"); if (uri.startsWith("/admin") && null == loginUser) { response.sendRedirect("/toLoginPage");//重定向 return false; } return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { //让视图能够读取数据 request.setAttribute("currentYear", Calendar.getInstance().get(Calendar.YEAR)); } //不做任何操作 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } }
-
-
效果测试
2、组件注册整合Servlet三大组件
三大组件
servlet、filter、Listener
使用组件注册的方式整合servlet
-
在com.itheima.servletComponent的创建一个自定义servlet类MyServlet
package com.itheima.servletComponent; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().print("hello MyServlet"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } }
-
创建组件配置类:在com.itheima.config下创建ServletConfig类
package com.itheima.config; import com.itheima.servletComponent.MyServlet; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.servlet.ServletRegistration; @Configuration public class ServletConfig { /* * servlet组件的注册 * */ @Bean public ServletRegistrationBean getServlet(MyServlet myServlet){ ServletRegistrationBean<MyServlet> myServletServletRegistrationBean = new ServletRegistrationBean<>(myServlet,"/myServlet"); return myServletServletRegistrationBean; } }
使用组件注册方式整合Filter(与上面类似)
-
自定义Filter类。在com.itheima.servletComponent下创建MyFilter
package com.itheima.servletComponent; import org.springframework.stereotype.Component; import javax.servlet.*; import java.io.IOException; @Component public class MyFilter implements Filter { //重写的这三个方法也就是filter的生命周期,一般功能都写在doFilter里面 @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("MyFilter执行了。。。"); //放行 filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { Filter.super.destroy(); } }
-
在ServletConfig类里进行注册
package com.itheima.config; import com.itheima.servletComponent.MyFilter; import com.itheima.servletComponent.MyServlet; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; @Configuration public class ServletConfig { /* * servlet组件的注册 * */ @Bean public ServletRegistrationBean getServlet(MyServlet myServlet){ ServletRegistrationBean<MyServlet> myServletServletRegistrationBean = new ServletRegistrationBean<>(myServlet,"/myServlet"); return myServletServletRegistrationBean; } @Bean public FilterRegistrationBean getFilter(MyFilter myFilter){ FilterRegistrationBean<MyFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>(myFilter); filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/toLoginPage"));//对该路径进行拦截 return filterFilterRegistrationBean; } }
使用组件注册方式整合Listener
-
在com.itheima.servletComponent下创建MyListener
package com.itheima.servletComponent; import org.springframework.stereotype.Component; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; @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执行了。。。"); } }
-
在配置文件中进行注册
package com.itheima.config; import com.itheima.servletComponent.MyFilter; import com.itheima.servletComponent.MyListener; import com.itheima.servletComponent.MyServlet; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletListenerRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Arrays; @Configuration public class ServletConfig { /* * servlet组件的注册 * */ @Bean public ServletRegistrationBean getServlet(MyServlet myServlet){ ServletRegistrationBean<MyServlet> myServletServletRegistrationBean = new ServletRegistrationBean<>(myServlet,"/myServlet"); return myServletServletRegistrationBean; } @Bean public FilterRegistrationBean getFilter(MyFilter myFilter){ FilterRegistrationBean<MyFilter> filterFilterRegistrationBean = new FilterRegistrationBean<>(myFilter); filterFilterRegistrationBean.setUrlPatterns(Arrays.asList("/toLoginPage"));//对该路径进行拦截 return filterFilterRegistrationBean; } @Bean public ServletListenerRegistrationBean getListener(MyListener myListener){ ServletListenerRegistrationBean<MyListener> myListenerServletListenerRegistrationBean = new ServletListenerRegistrationBean<>(myListener); return myListenerServletListenerRegistrationBean; } }
3、路径扫描整合Servlet三大组件
整合步骤
-
使用路径扫描方式整合Servlet、Filter、Listener
-
在组件类上添加@WebServlet、@WebFilter、@WebListener注解并配置相关注解属性即可
- MyServlet
package com.itheima.servletComponent; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/myServlet") public class MyServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().print("hello MyServlet"); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req,resp); } }
- MyFilter
package com.itheima.servletComponent; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter(value = {"/toLoginPage","/abc"}) //属性即拦截路径 public class MyFilter implements Filter { //重写的这三个方法也就是filter的生命周期,一般功能都写在doFilter里面 @Override public void init(FilterConfig filterConfig) throws ServletException { Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { System.out.println("MyFilter执行了。。。"); //放行 filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() { Filter.super.destroy(); } }
- MyListener
package com.itheima.servletComponent; import org.springframework.stereotype.Component; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @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执行了。。。"); } }
-
在主启动类中添加@ServletComponentScan注解开启组件扫描即可
package com.itheima; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletComponentScan; @SpringBootApplication @ServletComponentScan public class Chapter05Application { public static void main(String[] args) { SpringApplication.run(Chapter05Application.class, args); } }
-
-
效果测试
4、文件上传
整合步骤
-
编写文件上传的表单页面
-
upload.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>动态添加文件上传列表</title> <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet"> <script th:src="@{/login/js/jquery.min.js}"></script> </head> <body> <div th:if="${uploadStatus}" style="color: red" th:text="${uploadStatus}">上传成功</div> <form th:action="@{/uploadFile}" method="post" enctype="multipart/form-data"> 上传文件: <input type="button" value="添加文件" onclick="add()"/> <div id="file" style="margin-top: 10px;" th:value="文件上传区域"> </div> <input id="submit" type="submit" value="上传" style="display: none;margin-top: 10px;"/> </form> <script type="text/javascript"> // 动态添加上传按钮 function add(){ var innerdiv = "<div>"; innerdiv += "<input type='file' name='fileUpload' required='required'>" + "<input type='button' value='删除' οnclick='remove(this)'>"; innerdiv +="</div>"; $("#file").append(innerdiv); // 打开上传按钮 $("#submit").css("display","block"); } // 删除当前行<div> function remove(obj) { $(obj).parent().remove(); if($("#file div").length ==0){ $("#submit").css("display","none"); } } </script> </body> </html>
-
-
在全局配置文件中添加文件上传的相关配置
#单个上传文件大学限制(默认1MB) spring.servlet.multipart.max-file-size=10MB #总上传文件大小限制(默认10MB) spring.servlet.multipart.max-request-size=50MB
-
进行文件上传处理实现文件上传功能
com.itheima.controller.FileController
package com.itheima.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.util.UUID; @Controller public class FileController { //跳转到upload @GetMapping("/toUpload") public String toUpload(){ return "upload"; } //实现文件上传 @PostMapping("/uploadFile") public String uploadFile(MultipartFile[] fileUpload, Model model){ //返回上传成功的状态信息 model.addAttribute("uploadStatus","上传成功"); //上传文件逻辑开始 for(MultipartFile file : fileUpload){ //获取上传文件的名称及后缀名 a.txt String originalFilename = file.getOriginalFilename(); //重新生成文件名 String fileName = UUID.randomUUID()+"-"+originalFilename; //设置存储目录 String dirPath = "D:/file/"; //如果文件不存在,还需要创建 File file1 = new File(dirPath); if (!file1.exists()){ file1.mkdir(); } try{ file.transferTo(new File(dirPath+fileName)); }catch (IOException e){ e.printStackTrace(); model.addAttribute("uploadStatus","上传失败"); } } return "upload"; } }
-
效果测试:localhost:8080/toUpload
5、文件下载
英文名文件下载
-
添加下载工具依赖
<dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> </dependency>
-
编写文件下载页面:templates/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>bloglogo.jpg</td> <td><a th:href="@{/download(filename='bloglogo.jpg')}">下载文件</a></td> </tr> <tr> <td>Spring Boot应用级开发教程.pdf</td> <td><a th:href="@{/download(filename='Spring Boot应用级开发教程.pdf')}"> 下载文件</a></td> </tr> </table> </body> </html>
-
编写下载功能:com.itheima.controller.FileController
// 向文件下载页面跳转 @GetMapping("/toDownload") public String toDownload(){ return "download"; } // // 文件下载管理 // @GetMapping("/download") // public ResponseEntity<byte[]> fileDownload(String filename){ // // 指定要下载的文件根路径 // String dirPath = "F:/file/"; // // 创建该文件对象 // File file = new File(dirPath + File.separator + filename); // // 设置响应头 // HttpHeaders headers = new HttpHeaders(); // // 通知浏览器以下载方式打开 // 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); // } // } // 所有类型文件下载管理 @GetMapping("/download") public ResponseEntity<byte[]> fileDownload(HttpServletRequest request, String filename) throws Exception{ // 指定要下载的文件根路径 String dirPath = "F:/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"); }
-
测试:localhost:8080/toDownload
6、Jar包方式打包部署
-
添加Maven打包插件依赖(一般构建项目会默认添加)
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.7.RELEASE</version> <configuration> <mainClass>com.itheima.chapter05.Chapter05Application</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
-
使用IDEA进行打包
Jar包方式部署
- 在Teminal命令行输入:java -jar target/chapter05-0.0.1-SNAPSHOT.jar
7、War包方式打包部署
- 教程:https://www.cnblogs.com/kendoziyu/p/16085393.html
- 在pom.xml总将打包方式声明为war
-
声明外部Tomcat服务器
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <scope>provided</scope> </dependency>
如图
-
提供spring Boot启动的servlet初始化器:
将启动类继承SpringBootServletInitializer类,并实现configure()方法
package com.itheima; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.servlet.ServletComponentScan; import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; @SpringBootApplication @ServletComponentScan public class Chapter05Application extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(Chapter05Application.class); } public static void main(String[] args) { SpringApplication.run(Chapter05Application.class, args); } }
-
打包,双击package
War包部署方式
将war包拷贝到tomcat的webapps中,执行tomcat的bin目录下startup.bat命令启动war包项目。
第6章 SpringBoot缓存管理
1、集成环境搭建
使用缓存的主要目的是减小数据库的访问压力、提高用户体验,为此,这里结合数据库的访问操作对Spring Boot的缓存管理进行演示说明。
-
准备数据
这里使用第三章创建的springbootdata的数据库,该数据库有个两个表t_article和t_comment,这两个表预先插入几条测试数据。
-
创建项目
- 使用Spring Initializr方式创建项目,选择SQL模块中的JPA、MySQL依赖、Web中的Web依赖
-
编写数据库对应的实体类。
package com.itheima.domain; import javax.persistence.*; @Entity(name = "t_comment") public class Comment { @Id //标识映射对应的主键id @GeneratedValue(strategy = GenerationType.IDENTITY) //设置注解自增策略 private Integer id; private String content; private String author; @Column(name = "a_id") //指定映射的表字段 private Integer aId; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public String getAuthor() { return author; } public void setAuthor(String author) { this.author = author; } public Integer getaId() { return aId; } public void setaId(Integer aId) { this.aId = aId; } }
-
编写操作数据库的Repository接口文件。
package com.itheima.repository; import com.itheima.domain.Comment; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; public interface CommentRepository extends JpaRepository<Comment,Integer> { //根据评论id修改评论作者author @Modifying //变更数据需要使用该注解 @Query("update t_comment c set c.author = ?1 where c.id = ?2") public int updateComment(String author,Integer id); }
-
编写业务操作类Service文件。
package com.itheima.service; import com.itheima.domain.Comment; import com.itheima.repository.CommentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Optional; @Service public class CommentService { @Autowired private CommentRepository commentRepository; //查询:根据id查询 public Comment findById(Integer id){ Optional<Comment> byId = commentRepository.findById(id); if (byId.isPresent()){ return byId.get(); } return null; } //更新 public int updateComment(Comment comment){ int i = commentRepository.updateComment(comment.getAuthor(),comment.getId()); return i; } //删除 public void deleteComment(Integer id){ commentRepository.deleteById(id); } }
-
CommentController评论管理控制类,使用注入的CommentService实列对像4编写对Comment评论数据的查询、修改和删除方法。
package com.itheima.controller; import com.itheima.domain.Comment; import com.itheima.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class CommentController { @Autowired private CommentService commentService; //查询 @GetMapping("/get/{id}") public Comment findById(@PathVariable("id") Integer id){ Comment comment = commentService.findById(id); return comment; } //更新 @GetMapping("/update/{id}/{author}") public int updateComment(int id,String author){ Comment comment = commentService.findById(id); comment.setAuthor(author); int i = commentService.updateComment(comment); return i; } //删除 @GetMapping("/delete/{id}") public void deleteById(@PathVariable("id") Integer id){ commentService.deleteComment(id); } }
-
编写配置文件
在项目全局配置文件application.properties中编写对应的数据库连接配置
# 数据库连接地址 spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC # 数据库用户名&密码: spring.datasource.username=root spring.datasource.password=root # 显示使用JPA进行数据库查询的SQL语句(将sql语句显示在控制台上) spring.jpa.show-sql=true
2、Spring Boot默认缓存体验
-
在项目启动类上使用@EnableCaching注解开启基于注解的缓存支持
@SpringBootApplication @EnableCaching public class Chapter06Application { public static void main(String[] args) { SpringApplication.run(Chapter06Application.class, args); } }
-
在Service类的查询方法上使用@Cacheable注解对数据操作方法进行缓存管理
@Service
public class CommentService {
@Autowired
private CommentRepository commentRepository;
//查询:根据id查询
@Cacheable(cacheNames = "comment")//将查询结果存放到缓存为comment的空间中
public Comment findById(Integer id){
Optional<Comment> byId = commentRepository.findById(id);
if (byId.isPresent()){
return byId.get();
}
return null;
}
//更新
public int updateComment(Comment comment){
int i = commentRepository.updateComment(comment.getAuthor(),comment.getId());
return i;
}
//删除
public void deleteComment(Integer id){
commentRepository.deleteById(id);
}
}
- 测试:localhost:8080/get/1
- 结论:观察控制台发现,没使用缓存时,每次刷新页面都会去数据库查询数据。添加缓存后,刷新页面只有第一次去数据库查询数据。
3、Spring Boot缓存注解介绍
-
@EnableCaching
通常配置在项目启动类上,用于开启基于注解的缓存支持
-
@Cacheable
可以用于类或方法(通常用作查询方法上)。执行顺序,先在缓存中查询,如果为空则将结果进行缓存,如果有数据,这不进行该方法,而是直接使用缓存数据。
注解属性:
-
@CachePut:常用于更新方法上
-
@CacheEvict:作用时删除缓存数据,一般用在删除方法上
4、基于注解的Redis缓存实现
实现步骤
-
添加spring Data Redis依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
在全局配置文件中配置Redis服务连接配置
#Redis服务地址 spring.redis.host=127.0.0.1 #Redis服务器连接端口 spring.redis.port=6379 #Redis服务器连接密码(默认为空) spring.redis.password=
-
使用@Cacheable、@CachePut、@CacheEvict注解定制缓存管理(启动类也需要@EnableCaching注解)
@Service @Transactional public class CommentService { @Autowired private CommentRepository commentRepository; //查询:根据id查询 @Cacheable(cacheNames = "comment",unless = "#result==null")//将查询结果存放到缓存为comment的空间中,当查询结果为空则不进行缓存 public Comment findById(Integer id){ Optional<Comment> byId = commentRepository.findById(id); if (byId.isPresent()){ return byId.get(); } return null; } //更新 @CachePut(cacheNames = "comment",key="#result.id") public Comment updateComment(Comment comment){ int i = commentRepository.updateComment(comment.getAuthor(),comment.getId()); Optional<Comment> byId = commentRepository.findById(comment.getId()); if (byId.isPresent()){ return byId.get(); } return null; } //删除 @CacheEvict(cacheNames = "comment") public void deleteComment(Integer id){ commentRepository.deleteById(id); } }
-
实体类实现序列化接口Serializable
@Entity(name = "t_comment") public class Comment implements Serializable { @Id //标识映射对应的主键id @GeneratedValue(strategy = GenerationType.IDENTITY) //设置注解自增策略 private Integer id; private String content; private String author; @Column(name = "a_id") //指定映射的表字段 private Integer aId; ......
-
Servce类使用@Transactional开启事务管理
@Service @Transactional //开启事务管理 public class CommentService { ......
5、基于API的Redis缓存实现
通过Redis提供的 API调用相关方法实现缓存管理,同时,这种方法还可以手动管理缓存的有效期。
实现步骤
-
在com.itheima.service包下创建业务处理类ApiCommentService
package com.itheima.service; import com.itheima.domain.Comment; import com.itheima.repository.CommentRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Optional; import java.util.concurrent.TimeUnit; @Service @Transactional //事务注解 public class ApiCommentService { @Autowired private CommentRepository commentRepository; @Autowired private RedisTemplate redisTemplate; //查询方法 public Comment findById(Integer id){ Object o = redisTemplate.opsForValue().get("comment_" + id); if (o!=null){ //缓存中有数据 return (Comment) o; }else { Optional<Comment> byId = commentRepository.findById(id); if (byId.isPresent()){ Comment comment = byId.get(); redisTemplate.opsForValue().set("comment_"+id,comment,1, TimeUnit.DAYS); return comment; } } return null; } //更新方法 public Comment updateComment(Comment comment){ int i = commentRepository.updateComment(comment.getAuthor(), comment.getId()); redisTemplate.opsForValue().set("comment_"+comment.getId(),comment); return comment; } //删除方法 public void deleteComment(Integer id){ commentRepository.deleteById(id); redisTemplate.delete("comment_"+id); } }
-
编写Controller文件
package com.itheima.controller; import com.itheima.domain.Comment; import com.itheima.service.CommentService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class CommentController { @Autowired private CommentService commentService; //查询 @GetMapping("/get/{id}") public Comment findById(@PathVariable("id") Integer id){ Comment comment = commentService.findById(id); return comment; } //更新 @GetMapping("/update/{id}/{author}") public Comment updateComment(int id,String author){ Comment comment = commentService.findById(id); comment.setAuthor(author); Comment comment1 = commentService.updateComment(comment); return comment1; } //删除 @GetMapping("/delete/{id}") public void deleteById(@PathVariable("id") Integer id){ commentService.deleteComment(id); } }
-
API的Redis缓存的配置。(所以启动类可以不使用@EnableCaching注解,写了也没事)
-
导入Redis依赖
-
配置连接信息
-
实体类实现序列化接口
以上三点在第5小结已经配置好了
-
-
测试:http://localhost:8080/Api/get/2
6、自定义Redis Template
JDK序列化机制不便于使用和社会管理工具进行查看和管理,我们需要自定义JSON格式的数据序列化机制进行缓存管理。
基于API 实现步骤
-
引入Redis依赖,并做好配置
-
在com.itheima.config下创建自定义配置类RedisConfig
package com.itheima.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; @Configuration //1.声明这是一个配置类 public class RedisConfig { @Bean public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){ RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); //使用json格式序列化对象,对缓存数据key和value进行转换 Jackson2JsonRedisSerializer<Object> JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); //解决查询缓存转换异常问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); JsonRedisSerializer.setObjectMapper(om); //设置redisTemplate模板API的序列化方式为json redisTemplate.setDefaultSerializer(JsonRedisSerializer); return redisTemplate; } }
测试效果:
7、自定义RedisCacheManager
基于Redis注解 实现步骤
-
在Redis配置类RedisConfig中自定义名为cacheManager的Bean组件
配置好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; }
第7章 SpringBoot安全管理
1、Spring Security介绍
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
2、Spring Security快速入门
spring Security的安全管理有两个重要概念,分别是Authentication(认证) 和 Authorization(授权)。认证即确认用户是否登录,并对用户登录进行管控;授权即确定用户拥有的功能权限,并对用户权限进行管控。
基础环境搭建
- 创建Spring Boot项目,并导入依赖
- 引入页面Html资源文件。在resource的templates目录中,引入案列所需要的资源文件
-
编写控制层,在com.itheima.controller下创建页面请求处理的控制类
@Controller public class FilmeController { @GetMapping("/detail/{type}/{path}") public String toDetail(@PathVariable("type") String type,@PathVariable("path") String path){ return "detail/"+type+"/"+path; } }
-
在项目的pom.xml文件中引入Spring Security安全框架的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
-
项目启动测试(启动时会在控制台随机生成一个安全密码)
访问http://localhost:8080/会跳转到以下界面(账户:user 密码:控制台打印出来的随机密码)
3、MVC Security安全配置介绍
4、内存身份认证
实现步骤
-
自定义配置类:com.itheima.config.SecurityConfig
该类需要继承WebSecurityConfigurerAdapter类,并重写configure(AuthenticationManagerBuilder auth)方法
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //设置密码编码器 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //模拟了测试用户 InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer = auth.inMemoryAuthentication().passwordEncoder(bCryptPasswordEncoder); //设置一个名为shitou,密码为123,权限为comment的用户 authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("shitou").password(bCryptPasswordEncoder.encode("123")).roles("comment"); //同上 authenticationManagerBuilderInMemoryUserDetailsManagerConfigurer.withUser("李四").password(bCryptPasswordEncoder.encode("321")).roles("vip"); } }
5、JDBC身份认证
JDBC Authentication(JDBC身份认证)是通过JDBC连接数据库进行已有用户身份认证,这样避免了内存认证的弊端,开源实现对已注册用户进行身份认证。
实现步骤
-
数据准备
在springbootdata数据库创建t_customer用户表、t_authority权限表、t_customer_authority中间表,并预先插入几条测试数据
# 选择使用数据库 USE springbootdata; # 创建表t_customer并插入相关数据 DROP TABLE IF EXISTS `t_customer`; CREATE TABLE `t_customer` ( `id` int(20) NOT NULL AUTO_INCREMENT, `username` varchar(200) DEFAULT NULL, `password` varchar(200) DEFAULT NULL, `valid` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; INSERT INTO `t_customer` VALUES ('1', 'shitou', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1'); INSERT INTO `t_customer` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1'); # 创建表t_authority并插入相关数据 DROP TABLE IF EXISTS `t_authority`; CREATE TABLE `t_authority` ( `id` int(20) NOT NULL AUTO_INCREMENT, `authority` varchar(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8; INSERT INTO `t_authority` VALUES ('1', 'ROLE_common'); INSERT INTO `t_authority` VALUES ('2', 'ROLE_vip'); # 创建表t_customer_authority并插入相关数据 DROP TABLE IF EXISTS `t_customer_authority`; CREATE TABLE `t_customer_authority` ( `id` int(20) NOT NULL AUTO_INCREMENT, `customer_id` int(20) DEFAULT NULL, `authority_id` int(20) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8; INSERT INTO `t_customer_authority` VALUES ('1', '1', '1'); INSERT INTO `t_customer_authority` VALUES ('2', '2', '2'); # 记住我功能中创建持久化Token存储的数据表 create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
-
在项目中添加MySQL连接驱动依赖和JDBC连接依赖
<!-- JDBC数据库连接启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL数据连接驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>
-
在全局配置文件配置数据库连接信息
spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC spring.datasource.username=root spring.datasource.password=root
-
使用JDBC进行身份认证
创建SecurityConfig类去继承WebSecurityConfigurerAdapter类
import javax.sql.DataSource; @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Qualifier("dataSource") @Autowired private DataSource dataSource; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //设置密码编码器 BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); //使用JDBC进行身份认证 JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> authenticationManagerBuilderJdbcUserDetailsManagerConfigurer = auth.jdbcAuthentication().passwordEncoder(bCryptPasswordEncoder); authenticationManagerBuilderJdbcUserDetailsManagerConfigurer.dataSource(dataSource); authenticationManagerBuilderJdbcUserDetailsManagerConfigurer.usersByUsernameQuery("select username,PASSWORD,valid from t_customer where username=?"); authenticationManagerBuilderJdbcUserDetailsManagerConfigurer.authoritiesByUsernameQuery("select c.username,a.authority from t_customer c,t_authority a,t_customer_authority ta where c.id = ta.customer_id and a.id = ta.authority_id and c.username = ?"); } }
6、UserDetailsService身份认证(具体代码chapter7项目)
这节老师复制的内容有点多,具体还得看讲义
对于用户流量较大的项目来说,频繁的使用JDBC进行数据库查询认证不仅麻烦,而且会降低网站登录访问速度。对于一个完善的项目来说,如果某些业务已经实现了用户信息查询的服务,就没必要使用JDBC进行身份认证了。
实现步骤
-
导入依赖
<dependencies> <!-- Security与Thymeleaf整合实现前端页面安全访问控制 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!-- JDBC数据库连接启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- MySQL数据连接驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Redis缓存启动器--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Spring Data JPA操作数据库 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <!-- Spring Security提供的安全管理依赖启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
定义查询用户及教师信息的服务接口
为了案列演示,假设项目中存在一个CustomerService业务处理类,用来通过用户名获取用户及权限信息
。。。。。。。。
7、自定义用户访问控制
- 打开之前创建的MVC Security自定义配置类SecunityConfig,继续重写configure(HttpSecurity http)方法进行用户访问控制
8、自定义用户登录
9、自定义用户退出
10、登录用户信息获取
第8章 SpringBoot消息服务
1、为什么要使用消息服务
使用消息服务可以实现一个高性能,高可用,高扩展的系统。
-
异步处理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f0mn9895-1682519016557)(image-20220927210025186.png)]
-
应用解耦
- 流量削峰
- 分布式事务管理
2、常用消息中间件介绍
指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统集成。常用的有:
-
ActiveMQ
Apache出品,采用Java语言编写的老牌消息中间件,常用于中小型企业。但是它性能相对较弱,不适用于高并发、大数据处理。
-
RabbitMQ
基于Erlang语言开发。对应大规模并发,多用于企业系统内,对数据一致性、稳定性、可靠性要求很高的场景。
-
Kafka
采用Scala和Java语言编写。其特点是最强高吞吐量,适用于产生大量数据的互联网服务。
-
RocketMQ
是阿里开源产品,使用纯Java开发。具有高吞吐量、高可用、适合大规模分布式系统应用的特点。
(实际项目技术选型时,没有特别要求的场景下,通常会选择RocketMQ作为消息中间件)
3、RebbitMQ消息中间件
RocketMQ时基于ANQO协议的轻量级、可靠、可伸缩、可移植的消息代理,再Spring Boot中对RocketMQ进行了集成。
4、安装RabbitMQ(注意版本,否则搞死人)
所需工具:
-
RabbitMQ服务包:Tags · rabbitmq/rabbitmq-server · GitHub或者RabbitMQ Changelog — RabbitMQ
-
Erlang语言包:Downloads - Erlang/OTP
注意:
以上工具均为傻瓜式安装,但Erlang安装好后需要对环境变量进行配置
启动:
rabbitMQ默认提供了两个端口号:5672和15672,访问http://localhost:15672即可查看可视化RabbitMQ
默认登录账户为:guest / guest 或者 admin/admin
解决电脑用户名是中文导致无法启动服务的办法
- 找到rabbitmq的地址(…\RabbitMQ Server\rabbitmq_server-3.7.14\),创建一个data的文件夹。
- 输入rabbitmq-service.bat remove
- set RABBITMQ_BASE=data的路径
- 输入rabbitmq-service.bat install
- 输入rabbitmq-service start
- 输入rabbitmq-plugins enable rabbitmq_management
就能解决用户名是中文的情况了。
解决无法登录的情况
-
打开mq 命令服务
-
输入添加用户命令
rabbitmqctl add_user admin admin
-
输入添加用户命令
rabbitmqctl set_user_tags admin administrator
最后再去测试用admin 账户登录是否成功,第一次失败可以试试重启Mq
5、Spring Boot整合RabbitMQ环境搭建
- 使用Spring Initializr方式构建项目,并勾选web依赖和RabbitMQ依赖
-
编写配置文件,在application.properties全局配置文件中编写RabbitMQ服务对应的连接配置
#配置RabbitMQ消息中间件连接配置 spring.rabbitmq.host=localhost spring.rabbitmq.port=5672 spring.rabbitmq.username=guest spring.rabbitmq.password=guest #配置RabbitMQ虚拟主机路径/,默认可以省略 spring.rabbitmq.virtual-host=/
6、Publish-Subscribe发布-订阅工资模式
基于API的方式
-
使用AmqpAdmin定制消息发送组件
项目中的测试类
@SpringBootTest class Chapter08ApplicationTests { @Autowired private AmqpAdmin amqpAdmin; /** * 使用AmqpAdmin管理员API定制消息组件 */ @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)); } @Test void contextLoads() { } }
-
消息发送者发送消息
在com.itheima.domain下创建User实体类
public class User { private Integer id; private String username; // get/set.... //tostring... }
在测试类中使用Spring框架提供的RabbitTemplate模板实现消息发送
@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); }
在com.itheima.config下创建一个RabbitMQ消息配置类RabbitMQConfig
/** * 定制JSON格式的消息转换器 * @return */ @Bean public MessageConverter messageConverter(){ return new Jackson2JsonMessageConverter(); }
-
消息消费者接收消息
在com.itheima.service下创建RabbitMQService
/** * @Description RabbitMQ消息接收处理的业务类 */ @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); }
基于配置类的方式
-
在com.itheima.config.RabbitMQConfig配置类中进行配置
/** * @Classname RabbitMQConfig * @Description RabbitMQ消息配置类 * @Date 2019-3-8 14:15 * @Created by CrazyStone */ @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(); } }
基于注解的方式
主要使用sping框架提供@RabbitListener注解及相关属性进行消息发送
//消息接收处理的业务类
@Service
public class RabbitMQService {
/**
* **使用基于注解的方式实现消息服务
* 1.1、Publish/Subscribe工作模式接收,处理邮件业务
*/
@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工作模式接收,处理短信业务
*/
@RabbitListener(bindings =@QueueBinding(value =@Queue("fanout_queue_sms"),exchange =@Exchange(value = "fanout_exchange",type = "fanout")))
public void psubConsumerSmsAno(User user) {
System.out.println("短信业务接收到消息: "+user);
}
}
@RabbitListener的bindings属性:用于自动创建并绑定交换器和消息队列组件,在定制交换器时将交换器类型设置为fanout,key属性用于定制路由链routingKey(当前发布订阅模式不需要)
7、Routing路由工资模式
使用基于注解的方式来实现Routing路由模式的整合
-
打开业务类RabbitMQService,使用@RabbitListener注解及相关属性定制Routing路由模式的消息组件
/** * 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); }
-
消息发送者发送消息,打开项目测试类,在测试类中使用TabbitTemplate模板类实现Routing路由模式下的消息发送
/** * 2、Routing工作模式消息发送端 */ @Test public void routingPublisher() { rabbitTemplate.convertAndSend("routing_exchange","error_routing_key","routing send error message"); }
8、Topics通配符工作模式
使用基于注解的方式来实现Topics通配符模式的整合讲解
-
打开进行消息业务类RabbitMQService,在该类中使用@RabbitListener注解及属性定制Topics通配符模式的消息组件
/** * 3.1、通配符模式消息接收,进行邮件业务订阅处理 */ @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、通配符模式消息接收,进行短信业务订阅处理 */ @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); }
其实与路由模式并无太大区别,主要区别于@RabbitListener注解中的type和key属性
-
测试类
/** * 3、Topcis工作模式消息发送端 */ @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"); }
第9章 SpringBoot任务管理
1、无返回值异步任务调用
同步和异步任务
同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。
当处理于第三方系统交互时,容易找出响应迟缓的请看,之前大多数都是使用多线程完成此类任务,此外,还可以使用异步调用的方式完美解决这个问题。
无返回值异步任务调用
实现步骤
-
创建spring boot项目,并选择web模块
-
编写异步调用方法,在com.itheima.service下创业MyAsyncService业务类
@Service public class MyAsyncService { @Async //该注解表示当前方法为异步方法 public void sendSMS() throws Exception { System.out.println("调用短信验证码业务方法。。。"); Long startTime = System.currentTimeMillis();//获取系统当前时间 Thread.sleep(5000); //休眠5秒,用于模拟系统处理业务 Long endTime = System.currentTimeMillis();//获取系统当前时间 System.out.println("短信业务执行耗时:"+(endTime - startTime)); } }
-
开启基于注解的异步任务支持
虽然目标方法已经加上了@Async注解,但还需要在启动类上使用@EnableAsync注解开启注解支持
@EnableAsync @SpringBootApplication public class Chapter09Application { public static void main(String[] args) { SpringApplication.run(Chapter09Application.class, args); } }
-
编写控制层业务调用方法
在com.itheima.controller下创建MyAsyncController类,模拟编写用户短信验证码发送的处理方法
@RestController public class MyAsyncController { @Autowired private MyAsyncService MyAsyncService; @GetMapping("/sendSMS") public String sendSMS() throws Exception { Long startTime = System.currentTimeMillis(); MyAsyncService.sendSMS(); Long endTime = System.currentTimeMillis(); System.out.println("主流程耗时:" + (endTime - startTime)); return "success"; } }
-
测试,启动项目,访问localhost:8080/sendSMS,然后观察控制台
- 总结:执行sendSMS()方法斌调用异步方法处理短信业务时,在很短时间内(5毫秒)完成了主流程的执行,并向页面响应主流程结果,而在主流程打输出方法之前调用的异步方法经过一段时间后才执行完毕。因此发现,主流程时不会等待service而是继续向下执行,案列中无返回值的异步任务调用成功!
2、有返回值异步任务调用
-
service类MyAsyncService
/** * 模拟有返回值的异步任务处理 * @return * @throws Exception */ @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类MyAsyncController
@GetMapping("/statistics") public String statistics() throws Exception { Long startTime = System.currentTimeMillis(); Future<Integer> futureA = MyAsyncService.processA(); Future<Integer> futureB = MyAsyncService.processB(); int total = futureA.get() + futureB.get(); System.out.println("异步任务数据统计汇总结果: "+total); Long endTime = System.currentTimeMillis(); System.out.println("主流程耗时: "+(endTime-startTime)); return "success"; }
-
访问:localhost:8080/statistics控制台结果如下
**总结一下:**通过结果看出,执行statistics()方法时,需要耗费5018毫秒才完成主线程的任务,主线程在打印出结果之前一直在等业务A和业务B执行汇总。这样主流程在执行异步方法时会有短暂阻塞,需要等待并获取异步方法的返回结果。
3、定时任务介绍
顾名思义,按照规定时间去执行相关任务。为了实现这种需求,spring框架提供了Spring scheduling task来实现定时任务。
Spring框架的定时任务调度功能支持配置和注解两种方式。这里我们主要学习注解的方式
相关注解
-
@EnableScheduling
开启基于注解方式的定时任务支持,放在项目启动类上使用
-
@Sheduled
配置定时任务的执行规则,该注解要用在定时业务方法上,下面是它的相关属性
4、定时任务实现
下面案列,配置@Scheduled注解的fixedRate和fixeDelay属性的定时方法会立即执行一次,配置cron属性的定时方法会在整数分钟时间点首次执行,配置fixedRate和cron属性的方法会每隔一分钟重发执行一定的任务,而配置ficedDelay属性的方法是在上一次方法执行完成后再像个一分钟重复执行一次定时任务。
-
开启定时任务注解支持(@EnableScheduling)
@EnableScheduling @EnableAsync @SpringBootApplication public class Chapter09Application { public static void main(String[] args) { SpringApplication.run(Chapter09Application.class, args); } }
-
编写业务处理方法
在com.itheima.servicec下创建业务处理类ScheduledTaskService
package com.itheima.service; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.Date; @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()))); } }
5、发送纯文本邮件
实现步骤
-
添加邮件服务依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>
-
添加邮件服务配置
在application.properties中配置
#发件人服务器配置 spring.mail.host=smtp.qq.com spring.mail.port=587 #配置个人QQ账户和密码(密码是加密和的授权码) spring.mail.username=2127269781@qq.com spring.mail.password=ijdjokspcbnzfbfa 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
-
编写邮件发送服务类,在com.itheima.service下创建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(); } }
-
测试类
@SpringBootTest class Chapter09ApplicationTests { @Autowired private SendEmailService sendEmailService; @Test public void setSendEmailServiceTest(){ String to = "734338421@qq.com";//收件人 String subject = "测试邮件标题";//邮箱标题 String text = "测试内容";//邮箱内容 sendEmailService.sendSimpleEmail(to,subject,text); } }
6、发带附件和图片的文件
-
添加依赖(上小节已经添加过了)
-
编写业务类
/** * 发送复杂邮件(包括静态资源和附件) * @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(); } }
-
测试
//复杂邮箱 @Test public void sendComplexEmailTest() { String to="734338421@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="D:\\test.jpg"; String filePath="D:\\test.txt"; // 发送复杂邮件 sendEmailService.sendComplexEmail(to,subject,text.toString(),filePath,rscId,rscPath); }
7、发送模板邮件
使用场景:动态用户名,验证码,激活码
实现步骤
-
添加Thymeleaf模板依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>
-
定制模板邮件
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>
-
编写邮件发送服务
/** * 发送模板邮件 * @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 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); }
第10章 SpringBoot综合项目实践
1、系统概述
2、系统开发及运行环境
- 操作系统:Windows
- Java开发包:JDK8
- 项目管理工具:Maven 3.6.0
- 项目开发工具: IntelliJ IDEA
- 数据库:MySQL
- 缓存管理工具:Redis 3.2.100
- 浏览器:谷歌浏览器
3、文件组织结构
4、数据库设计
-
文章详情表:t_article
字段名 类型 长度 是否为主键 说明 id int 11 是 文章id title varchar 50 否 文章标题 content longtext 否 文章内容 created date 否 创建时间 modified date 否 修改时间 categories varchar 200 否 文章分类 tags varchar 200 否 文章标签 allow_comment tinyint 1 否 是否允许评论(默认1) thumbnail varchar 200 否 文章缩略图 -
文章评论表:t_comment
字段名 类型 长度 是否为主键 说明 id int 11 是 评论id article_id int 11 否 评论关联的文章id created date 否 创建时间 ip varchar 200 否 评论用户所在ip content text 否 评论内容 status varchar 200 否 评论状态(默认approved) author varchar 200 否 评论作者名 -
文章评论表:t_statistic
字段名 | 类型 | 长度 | 是否为主键 | 说明 |
---|---|---|---|---|
id | int | 11 | 是 | 文章统计id |
article_id | int | 11 | 否 | 文章id |
hits | int | 11 | 否 | 文章点击量 |
comments_num | int | 11 | 否 | 文章评论量 |
-
用户信息表:t_user
字段名 类型 长度 是否为主键 说明 id int 11 是 用户id username varchar 200 否 用户名 password varchar 200 否 用户密码(加密后的密码) email varchar 200 否 用户邮箱 created date 否 创建时间 valid tinyint 1 否 是否为有效用户(默认1) -
用户权限表:authority
字段名 | 类型 | 长度 | 是否为主键 | 说明 |
---|---|---|---|---|
id | int | 11 | 是 | 权限id |
authority | varchar | 200 | 否 | 权限以ROLE_开头 |
- 用户权限关联表:t_user_authority
字段名 | 类型 | 长度 | 是否为主键 | 说明 |
---|---|---|---|---|
id | int | 11 | 是 | 关联表主键id |
article_id | int | 11 | 否 | 文章id |
authority_id | int | 11 | 否 | 权限id |
准备数据库
创建一个名称为blog_system的数据库,并选择该数据库,然后将本书资源中所提供的blog_system.sql文件导入到blog_system数据库中。
blog_system.sql的源码:
/*
Navicat MySQL Data Transfer
Source Server : mysql
Source Server Version : 50720
Source Host : localhost:3306
Source Database : blog_system
Target Server Type : MYSQL
Target Server Version : 50720
File Encoding : 65001
Date: 2018-12-13 16:54:34
*/
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for `t_article`
-- ----------------------------
DROP TABLE IF EXISTS `t_article`;
CREATE TABLE `t_article` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`title` varchar(50) NOT NULL COMMENT '文章标题',
`content` longtext COMMENT '文章具体内容',
`created` date NOT NULL COMMENT '发表时间',
`modified` date DEFAULT NULL COMMENT '修改时间',
`categories` varchar(200) DEFAULT '默认分类' COMMENT '文章分类',
`tags` varchar(200) DEFAULT NULL COMMENT '文章标签',
`allow_comment` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否允许评论',
`thumbnail` varchar(200) DEFAULT NULL COMMENT '文章缩略图',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_article
-- ----------------------------
INSERT INTO `t_article` VALUES ('1', '2018新版Java学习路线图', '    播妞深知广大爱好Java的人学习是多么困难,没视频没资源,上网花钱还老担心被骗。因此专门整理了新版的学习路线图,不管你是不懂电脑的小白,还是已经步入开发的大牛,这套路线图绝对不容错过!12年传智播客黑马程序员分享免费视频教程长达10余万小时,累计下载量3000余万次,受益人数达千万。2018年我们不忘初心,继续前行。 路线图的宗旨就是分享,专业,便利,让喜爱Java的人,都能平等的学习。从今天起不要再找借口,不要再说想学Java却没有资源,赶快行动起来,Java等你来探索,高薪距你只差一步!\r\n\r\n**一、2018新版Java学习路线图---每阶段市场价值及可解决的问题**\r\n![alt](/article_img/roadmap/1.jpg)\r\n\r\n**二、2018新版Java学习路线图---学习大纲及各阶段知识点**\r\n![alt](/article_img/roadmap/2.jpg)\r\n\r\n![alt](/article_img/roadmap/3.jpg)\r\n\r\n**三、2018新版Java学习路线图---升级后新增知识点一览**\r\n![alt](/article_img/roadmap/4.jpg)\r\n\r\n\r\n\r\n', '2018-10-10', null, '默认分类', '2018,Java,学习路线图', '1', null);
INSERT INTO `t_article` VALUES ('2', '2018新版Python学习线路图', '    12年历经风雨,传智播客黑马程序员已免费分享视频教程长达10万余小时,累计下载量超2000万次,受益人数达千万。2018年我们不忘初心,继续前行。学习路线图的宗旨就是分享,专业,便利,让喜爱Python的人,都能平等的学习。据悉,Python已经入驻小学生教材,未来不学Python不仅知识会脱节,可能与小朋友都没有了共同话题~~所以,从今天起不要再找借口,不要再说想学Python却没有资源,赶快行动起来,Python等你来探索,高薪距你只差一步!\r\n\r\n**一、2018新版Python学习路线图---每阶段市场价值及可解决的问题**\r\n![alt](/article_img/phmap/1.jpg)\r\n\r\n![alt](/article_img/phmap/2.jpg)\r\n\r\n**二、2018新版Python学习路线图---学习大纲及各阶段知识点**\r\n![alt](/article_img/phmap/3.jpg)\r\n\r\n![alt](/article_img/phmap/4.jpg)\r\n\r\n**三、2018新版Python学习路线图---升级版Python成长路径**\r\n![alt](/article_img/phmap/5.jpg)\r\n\r\n![alt](/article_img/phmap/6.jpg)', '2018-10-24', null, '默认分类', '2018,Python,学习线路图', '1', null);
INSERT INTO `t_article` VALUES ('3', '2018新版前端与移动开发学习线路图', '    传智播客黑马程序员作为一个IT学习平台,历经12年的成长,免费分享视频教程长达10万余小时,累计下载量超3000万次,受益人数达千万。2018年我们不忘初心,继续前行!路线图的宗旨就是分享,专业,便利,让更多想要学习IT的人都能系统的学习!从今天起不要再找借口,不要再说想学却没有资源,赶快行动起来,前端与移动开发等你来探索,高薪距你只差一步!注:新版前端与移动开发学习大纲于2018年2月26日完成升级,本学习路线图依据最新升级后的规划制作!\r\n\r\n**一、2018新版前端与移动开发学习路线图---每阶段可掌握的能力及可解决的问题**\r\n![alt](/article_img/frmap/1.jpg)\r\n\r\n![alt](/article_img/frmap/2.jpg)\r\n\r\n**二、2018新版前端与移动开发学习路线图---学习大纲及各阶段知识点**\r\n![alt](/article_img/frmap/3.jpg)\r\n\r\n![alt](/article_img/frmap/4.jpg)\r\n\r\n**三、2018新版前端与移动开发学习路线图--升级后新增知识点设计理念**\r\n![alt](/article_img/frmap/5.jpg)', '2018-11-13', null, '默认分类', '2018,前端与移动,学习线路图', '1', null);
INSERT INTO `t_article` VALUES ('4', '2018新版PHP学习线路图', '    传智播客黑马程序员作为一个IT学习平台,历经12年的成长,免费分享视频教程长达10万余小时,累计下载量超3000万次,受益人数达千万。2018年我们不忘初心,继续前行!路线图的宗旨就是分享,专业,便利,让更多喜爱PHP的人都能系统的学习!从今天起不要再找借口,不要再说想学PHP却没有资源,赶快行动起来,PHP等你来探索,高薪距你只差一步!\r\n\r\n**一、2018新版PHP学习路线图---每阶段市场价值及可解决的问题**\r\n![alt](/article_img/phpmap/1.jpg)\r\n\r\n![alt](/article_img/phpmap/2.jpg)\r\n\r\n**二、2018新版PHP学习路线图---学习大纲及各阶段知识点**\r\n![alt](/article_img/phpmap/3.jpg)\r\n\r\n![alt](/article_img/phpmap/4.jpg)\r\n\r\n**三、2018新版PHP学习路线图---升级后新增知识点设计理念**\r\n![alt](/article_img/phpmap/5.jpg)\r\n', '2018-11-16', null, '默认分类', '2018,PHP,学习线路图', '1', null);
INSERT INTO `t_article` VALUES ('5', '2018版Go语言+区块链学习线路图', '    12年传智播客黑马程序员分享免费视频教程长达10万余小时,累计下载量超3000万次,受益人数达千万。2018年我们不忘初心,继续前行。 路线图的宗旨就是分享,专业,便利,让喜爱Go语言+区块链的人,都能平等的学习。从今天起不要再找借口,不要再说想学Go语言+区块链却没有资源,赶快行动起来,Go语言+区块链等你来探索,高薪距你只差一步!\r\n\r\n**一、2018新版Go语言+区块链学习路线图---每阶段市场价值及可解决的问题**\r\n![alt](/article_img/gomap/1.jpg)\r\n\r\n![alt](/article_img/gomap/2.jpg)\r\n\r\n**二、2018新版Go语言+区块链学习路线图---每阶段可掌握的核心能力**\r\n![alt](/article_img/gomap/3.jpg)\r\n\r\n**三、2018新版Go语言+区块链学习路线图---每阶段的设计理念**\r\n![alt](/article_img/gomap/4.jpg)\r\n\r\n**四、2018新版Go语言+区块链学习路线图---学习大纲及各阶段知识点**\r\n![alt](/article_img/gomap/5.jpg)\r\n\r\n![alt](/article_img/gomap/6.jpg)', '2018-11-27', null, '默认分类', '2018,Go语言,区块链,学习线路图', '1', null);
INSERT INTO `t_article` VALUES ('6', 'JDK 8——Lambda表达式介绍', ' Lambda表达式是JDK 8中一个重要的新特性,它使用一个清晰简洁的表达式来表达一个接口,同时Lambda表达式也简化了对集合以及数组数据的遍历、过滤和提取等操作。下面,本篇文章就对Lambda表达式进行简要介绍,并进行演示说明。\r\n\r\n**1. Lambda表达式入门** \r\n\r\n 匿名内部类存在的一个问题是,如果匿名内部类的实现非常简单,例如只包含一个抽象方法的接口,那么匿名内部类的语法仍然显得比较冗余。为此,JDK 8中新增了一个特性Lambda表达式,这种表达式只针对有一个抽象方法的接口实现,以简洁的表达式形式实现接口功能来作为方法参数。 \r\n 一个Lambda表达式由三个部分组成,分别为参数列表、“->”和表达式主体,其语法格式如下:\r\n```js\r\n ([数据类型 参数名,数据类型 参数名,...]) -> {表达式主体}\r\n```\r\n 从上述语法格式上看,Lambda表达式的书写非常简单,下面针对Lambda表达式的组成部分进行简单介绍,具体如下: \r\n (1)([数据类型 参数名,数据类型 参数名,...]):用来向表达式主体传递接口方法需要的参数,多个参数名中间必须用英文逗号“,”进行分隔;在编写Lambda表达式时,可以省略参数的数据类型,后面的表达式主体会自动进行校对和匹配;同时,如果只有一个参数,则可以省略括号“()”。 \r\n (2)->:表示Lambda表达式箭牌,用来指定参数数据指向,不能省略,且必须用英文横线和大于号书写。 \r\n (3){表达式主体}:由单个表达式或语句块组成的主体,本质就是接口中抽象方法的具体实现,如果表达式主体只有一条语句,那么可以省略包含主体的大括号;另外,在Lambda表达式主体中允许有返回值,当只有一条return语句时,也可以省略return关键字。 \r\n 了解了Lambda表达式的语法后,接下来编写一个示例文件对Lambda表达式的基本使用进行介绍,具体代码如下所示。\r\n```js\r\n 1 // 定义动物类接口\r\n 2 interface Animal { \r\n 3 void shout(); // 定义方法shout()\r\n 4 }\r\n 5 public class Example22 {\r\n 6 public static void main(String[] args) {\r\n 7 String name = \"小花\"; \r\n 8 // 1、匿名内部类作为参数传递给animalShout()方法\r\n 9 animalShout(new Animal() { \r\n 10 public void shout() { \r\n 11 System.out.println(\"匿名内部类输出:\"+name+\"喵喵...\");\r\n 12 }\r\n 13 });\r\n 14 // 2、使用Lambda表达式作为参数传递给animalShout()方法\r\n 15 animalShout(()-> System.out.println(\"Lambda表达式输出:\"\r\n 16 +name+\"喵喵...\"));\r\n 17 }\r\n 18 // 创建一个animalShout()静态方法,接收接口类型的参数\r\n 19 public static void animalShout(Animal an) {\r\n 20 an.shout(); \r\n 21 }\r\n 22 }\r\n```\r\n 运行结果下图所示。\r\n![alt](/article_img/lambda/1.jpg)\r\n 上述代码示例中,先定义了只有一个抽象方法的接口Animal,然后分别使用匿名内部类和Lambda表达式的方式实现了接口方法。从图中可以看出,使用匿名内部类和Lambda表达式都能实现接口中方法,但很显然使用Lambda表达式更加简洁和清晰。', '2018-11-27', null, '默认分类', '2018,Lambda表达式', '1', null);
INSERT INTO `t_article` VALUES ('7', '函数式接口', '  虽然Lambda表达式可以实现匿名内部类的功能,但在使用时却有一个局限,即接口中有且只有一个抽象方法时才能使用Lamdba表达式代替匿名内部类。这是因为Lamdba表达式是基于函数式接口实现的,所谓函数式接口是指有且仅有一个抽象方法的接口,Lambda表达式就是Java中函数式编程的体现,只有确保接口中有且仅有一个抽象方法,Lambda表达式才能顺利地推导出所实现的这个接口中的方法。 \r\n  在JDK 8中,专门为函数式接口引入了一个@FunctionalInterface注解,该注解只是显示的标注了接口是一个函数式接口,并强制编辑器进行更严格的检查,确保该接口是函数式接口,如果不是函数式接口,那么编译器就会报错,而对程序运行并没有实质上的影响。 \r\n  接下来通过一个案例来演示函数式接口的定义与使用,示例代码如下所示。\r\n```js\r\n 1 // 定义无参、无返回值的函数式接口\r\n 2 @FunctionalInterface\r\n 3 interface Animal {\r\n 4 void shout();\r\n 5 }\r\n 6 // 定义有参、有返回值的函数式接口\r\n 7 interface Calculate {\r\n 8 int sum(int a, int b);\r\n 9 }\r\n 10 public class Example23 {\r\n 11 public static void main(String[] args) {\r\n 12 // 分别两个函数式接口进行测试\r\n 13 animalShout(() -> System.out.println(\"无参、无返回值的函数式接口调用\"));\r\n 14 showSum(10, 20, (x, y) -> x + y);\r\n 15 }\r\n 16 // 创建一个动物叫的方法,并传入接口对象Animal作为参数\r\n 17 private static void animalShout(Animal animal) {\r\n 18 animal.shout();\r\n 19 }\r\n 20 // 创建一个求和的方法,并传入两个int类型以及接口Calculate类型的参数\r\n 21 private static void showSum(int x, int y, Calculate calculate) {\r\n 22 System.out.println(x + \"+\" + y + \"的和为:\" + calculate.sum(x, y));\r\n 23 }\r\n 24 }\r\n```\r\n  运行结果如下图所示。\r\n\r\n![alt](/article_img/lambda/2.jpg)\r\n  上述代码示例中,先定义了两个函数式接口Animal和Calculate,然后在测试类中分别编写了两个静态方法,并将这两个函数式接口以参数的形式传入,最后在main()方法中分别调用这两个静态方法,并将所需要的函数式接口参数以Lambda表达式的形式传入。从图中可以看出,程序中函数式接口的定义和使用完全正确。\r\n', '2018-12-01', null, '默认分类', '接口,函数式接口', '1', null);
INSERT INTO `t_article` VALUES ('8', 'JDK 8——聚合操作', '  在Java8版本中,JDK包含许多聚合操作(如平均值,总和,最小,最大,和计数),返回一个计算流stream的聚合结果。这些聚合操作被称为聚合操作。JDK除返回单个值的聚合操作外,还有很多聚合操作返回一个collection集合实例。很多的reduce操作执行特定的任务,如求平均值或按类别分组元素。 \r\n\r\n**1. 聚合操作简介**\r\n\r\n 在开发中,多数情况下会涉及到对集合、数组中元素的操作,在JDK 8之前都是通过普通的循环遍历出每一个元素,然后还会穿插一些if条件语句选择性的对元素进行查找、过滤、修改等操作,这种原始的操作方法虽然可行,但是代码量较大并且执行效率较低。 \r\n 为此,JDK 8中新增了一个Stream接口,该接口可以将集合、数组的中的元素转换为Stream流的形式,并结合Lambda表达式的优势来进一步简化集合、数组中元素的查找、过滤、转换等操作,这一新功能就是JDK 8中的聚合操作。 \r\n 在程序中,使用聚合操作没有绝对的语法规范,根据实际操作流程,主要可以分为以下3个步骤: \r\n (1)将原始集合或者数组对象转换为Stream流对象; \r\n (2)对Stream流对象中的元素进行一系列的过滤、查找等中间操作(Intermediate Operations),然后仍然返回一个Stream流对象; \r\n (3)对Stream流进行遍历、统计、收集等终结操作(Terminal Operation),获取想要的结果。 \r\n 接下来,就根据上面聚合操作的3个步骤,通过一个案例来演示聚合操作的基本用法,具体示例代码如下所示。\r\n```js\r\n 1 import java.util.*;\r\n 2 import java.util.stream.Stream;\r\n 3 public class Example31 {\r\n 4 public static void main(String[] args) {\r\n 5 // 创建一个List集合对象\r\n 6 List<String> list = new ArrayList<>(); \r\n 7 list.add(\"张三\");\r\n 8 list.add(\"李四\");\r\n 9 list.add(\"张小明\");\r\n 10 list.add(\"张阳\");\r\n 11 // 1、创建一个Stream流对象\r\n 12 Stream<String> stream = list.stream();\r\n 13 // 2、对Stream流中的元素分别进行过滤、截取操作\r\n 14 Stream<String> stream2 = stream.filter(i -> i.startsWith(\"张\"));\r\n 15 Stream<String> stream3 = stream2.limit(2);\r\n 16 // 3、对Stream流中的元素进行终结操作,进行遍历输出\r\n 17 stream3.forEach(j -> System.out.println(j));\r\n 18 System.out.println(\"=======\");\r\n 19 // 通过链式表达式的形式完成聚合操作\r\n 20 list.stream().filter(i -> i.startsWith(\"张\"))\r\n 21 .limit(2)\r\n 22 .forEach(j -> System.out.println(j));\r\n 23 }\r\n 24 }\r\n```\r\n 运行结果如下图所示。\r\n![alt](/article_img/lambda/3.jpg)\r\n 上述示例代码中,先创建了一个List集合,然后根据聚合操作的3个步骤实现了集合对象的聚合操作,对集合中的元素使用Stream流的形式进行过滤(filter)、截取(limit),并进行遍历输出。其中第12~17行代码分步骤详细展示了聚合操作,而第20~22行代码是使用了链式表达式(调用有返回值的方法时不获取返回值而是直接再调用另一个方法)实现了聚合操作,该表达式的语法格式更简洁、高效,这种链式调用也被称为操作管道流。\r\n\r\n**2. 创建Stream流对象** \r\n 在上一小节中,介绍了聚合操作的主要使用步骤,其中首要解决的问题就是创建Stream流对象。聚合操作针对的就是可迭代数据进行的操作,如集合、数组等,所以创建Stream流对象其实就是将集合、数组等通过一些方法转换为Stream流对象。 \r\n 在Java中,集合对象有对应的集合类,可以通过集合类提供的静态方法创建Stream流对象,而数组数据却没有对应的数组类,所以必须通过其他方法创建Stream流对象。针对不同的源数据,Java提供了多种创建Stream流对象的方式,分别如下: \r\n (1)所有的Collections集合都可以使用stream()静态方法获取Stream流对象; \r\n (2)Stream接口的of()静态方法可以获取基本类型包装类数组、引用类型数组和单个元素的Stream流对象; \r\n (3)Arrays数组工具类的stream()静态方法也可以获取数组元素的Stream流对象。 \r\n 接下来,通过一个案例来学习聚合操作中如何创建Stream流对象,具体示例代码如下所示。\r\n```js\r\n 1 import java.util.*;\r\n 2 import java.util.stream.Stream;\r\n 3 public class Example32 {\r\n 4 public static void main(String[] args) {\r\n 5 // 创建一个数组\r\n 6 Integer[] array = { 9, 8, 3, 5, 2 }; \r\n 7 // 将数组转换为List集合\r\n 8 List<Integer> list = Arrays.asList(array); \r\n 9 // 1、使用集合对象的stream()静态方法创建Stream流对象\r\n 10 Stream<Integer> stream = list.stream();\r\n 11 stream.forEach(i -> System.out.print(i+\" \"));\r\n 12 System.out.println();\r\n 13 // 2、使用Stream接口的of()静态方法创建Stream流对象\r\n 14 Stream<Integer> stream2 = Stream.of(array);\r\n 15 stream2.forEach(i -> System.out.print(i+\" \"));\r\n 16 System.out.println();\r\n 17 // 3、使用Arrays数组工具类的stream()静态方法创建Stream流对象\r\n 18 Stream<Integer> stream3 = Arrays.stream(array);\r\n 19 stream3.forEach(i -> System.out.print(i+\" \"));\r\n 20 }\r\n 21 }\r\n```\r\n 运行结果如下图所示。\r\n![alt](/article_img/lambda/4.jpg)\r\n 上述示例代码中,先创建了一个数组和一个集合,然后通过三种方式实现了Stream流对象的创建,并通过Stream流对象的forEach()方法结合Lambda表达式完成了集合和数组中元素的遍历。 \r\n\r\n**小提示:** \r\n 在JDK 8中,只针对单列集合Collections接口对象提供了stream()静态方法获取Stream流对象,并未对Map集合提供相关方法获取Stream流对象,所以想要用Map集合创建Stream流对象必须先通过Map集合的keySet()、values()、entrySet()等方法将Map集合转换为单列Set集合,然后再使用单列集合的stream()静态方法获取对应键、值集合的Stream流对象。\r\n\r\n', '2018-12-02', null, '默认分类', 'JDK 8,聚合操作', '1', null);
INSERT INTO `t_article` VALUES ('9', '虚拟化容器技术——Docker运行机制介绍', ' Docker是一个开源的应用容器引擎,它基于go语言开发,并遵从Apache2.0开源协议。使用Docker可以让开发者封装他们的应用以及依赖包到一个可移植的容器中,然后发布到任意的Linux机器上,也可以实现虚拟化。Docker容器完全使用沙箱机制,相互之间不会有任何接口,这保证了容器之间的安全性。 \r\n\r\n**1. Docker的引擎介绍**\r\n\r\n Docker Engine(Docker引擎)是Docker的核心部分,使用的是客户端-服务器(C/S)架构模式,其主要组成部分如下图所示。\r\n![alt](/article_img/docker/1.png)\r\n 从上图可以看出,Docker Engine中包含了三个核心组件(docker CLI、REST API和docker daemon),这三个组件的具体说明如下: \r\n ①docker CLI(command line interface):表示Docker命令行接口,开发者可以在命令行中使用Docker相关指令与Docker守护进程进行交互,从而管理诸如image(镜像)、container(容器)、network(网络)和data volumes(数据卷)等实体。 \r\n ②REST API:表示应用程序API接口,开发者通过该API接口可以与Docker的守护进程进行交互,从而指示后台进行相关操作。 \r\n ③docker daemon:表示Docker的服务端组件,他是Docker架构中运行在后台的一个守护进程,可以接收并处理来自命令行接口及API接口的指令,然后进行相应的后台操作。 \r\n 对于开发者而言,既可以使用编写好的脚本文件通过REST API来实现与Docker进程交互,又可以直接使用Docker相关指令通过命令行接口来与Docker进程交互,而其他一些Docker应用则是通过底层的API和CLI进行交互的。\r\n\r\n**2. Docker的架构介绍**\r\n\r\n 了解了Docker内部引擎及作用后,我们还需要通过Docker的具体架构,来了解Docker的整个运行流程。接下来借助Docker官网的架构图来对Docker架构进行详细说明,如下图所示。\r\n![alt](/article_img/docker/2.jpg)\r\n 从图中可以看出,Docker架构主要包括Client、DOCKER_HOST和Register三部分,关于这三部分的具体说明如下。 \r\n  **(1)Client(客户端)** \r\n Client即Docker客户端,也就是上一小节Docker Engine中介绍的docker CLI。开发者通过这个客户端使用Docker的相关指令与Docker守护进程进行交互,从而进行Docker镜像的创建、拉取和运行等操作。 \r\n  **(2)DOCKER_HOST(Docker主机)** \r\n DOCKER_HOST即Docker内部引擎运行的主机,主要指Docker daemon(Docker守护进程)。可以通过Docker守护进程与客户端还有Docker的镜像仓库Registry进行交互,从而管理Images(镜像)和Containers(容器)等。 \r\n  **(3)Registry(注册中心)** \r\n Registry即Docker注册中心,实质就是Docker镜像仓库,默认使用的是Docker官方远程注册中心Docker Hub,也可以使用开发者搭建的本地仓库。Registry中包含了大量的镜像,这些镜像可以是官网基础镜像,也可以是其他开发者上传的镜像。 \r\n 我们在实际使用Docker时,除了会涉及到图中的三个主要部分外,还会涉及到很多Docker Objects(Docker对象),例如Images(镜像)、Containers(容器)、Networks(网络)、Volumes(数据卷)、Plugins(插件)等。其中常用的两个对象Image和Containers的说明如下。 \r\n ①Images(镜像) \r\n Docker 镜像就是一个只读的模板,包含了一些创建Docker容器的操作指令。通常情况下,一个Docker镜像是基于另一个基础镜像创建的,并且新创建的镜像会额外包含一些功能配置。例如:开发者可以依赖于一个Ubuntu的基础镜像创建一个新镜像,并可以在新镜像中安装Apache等软件或其它应用程序。 \r\n ②Containers(容器) \r\n Docker容器属于镜像的一个可运行实例(镜像与容器的关系其实与Java中的类与对象相似),开发者可以通过API接口或者CLI命令行接口来创建、运行、停止、移动、删除一个容器,也可以将一个容器连接到一个或多个网络中,将数据存储与容器进行关联。\r\n\r\n\r\n\r\n', '2018-12-03', null, '默认分类', '虚拟化容器,Docker,运行机制', '1', null);
INSERT INTO `t_article` VALUES ('10', 'Docker常用客户端指令介绍', ' 在使用Docker之前,首先会为对应的项目编写Dockerfile镜像构建文件,然后通过Docker的相关指令进行镜像构建,完成镜像的构建后,就可以使用这些项目镜像进行启动测试了。所以要想知道如何使用Docker来执行这些Dockerfile镜像构建文件,还需要学习Docker客户端的常用指令,本篇文章将对Docker客户端的常用指令进行详细讲解。 \r\n\r\n**1.列出镜像** \r\n 通过docker images指令可以查看本地镜像列表中已有的镜像,具体使用方式如下。\r\n```js\r\n$ docker images\r\n```\r\n 执行上述指令后,系统会将所有本地镜像都展示出来,如下图所示。\r\n![alt](/article_img/docker/3.png)\r\n 从图中可以看出,系统终端将本地镜像列表中的3个镜像分5列进行了展示,每一列的具体含义如下。 \r\n ●REPOSITORY:镜像名称。 \r\n ●TAG:镜像的参数,类似于版本号,默认是latest。 \r\n ●IMAGE ID:镜像ID,是唯一值。此处看到的是一个长度为12的字符串,实际上它是64位完整镜像ID的缩写形式。 \r\n ●CREATED:距今创建镜像的时间。 \r\n ●SIZE:镜像大小。 \r\n\r\n**2.搜索镜像** \r\n 想知道在Docker Hub中包含了哪些镜像,除了可以登录Docker Hub,在官网中心进行查看外,还可以直接在Docker客户端进行查询。例如想要查询Ubuntu镜像,可以使用如下指令。\r\n```js\r\n$ docker search ubuntu\r\n```\r\n 执行上述指令后,系统终端就会将搜索到的有关Ubuntu的镜像展示出来,如下图所示。\r\n![alt](/article_img/docker/4.png)\r\n 从图所示的结果可以看出,系统终端分5列将搜索到的Ubuntu相关镜像都列举出来了,这5列的具体含义如下。 \r\n ●NAME:表示镜像的名称,这里有两种格式的名称,其中不带有“/”的表示官方镜像,而带有“/”的表示其他用户的公开镜像。公开镜像“/”前面是用户在Docker Hub上的用户名(唯一),后面是对应的镜像名;官方镜像与用户镜像,除了从镜像名称上可以区分外,还可以通过第4列的OFFICIAL声明中看出(该列下内容为OK表示官方镜像)。 \r\n ●DESCRIPTION:表示镜像的描述,这里只显示了一小部分。 \r\n ●STARS:表示该镜像的收藏数,用户可以在Docker Hub上对镜像进行收藏,一般可以通过该数字反应出该镜像的受欢迎程度。 \r\n ●OFFICIAL:表示是否为官方镜像。 \r\n ●AUTOMATED:表示是否自动构建镜像。例如,用户可以将自己的Docker Hub绑定到如Github上,当代码提交更新后,可以自动构建镜像。 \r\n \r\n**3.拉取镜像** \r\n 通过docker pull指令可以拉取仓库镜像到本地(默认都是拉取Docker Hub仓库镜像,也可以指定“IP+端口”拉取某个Docker机器上的私有仓库镜像),具体使用方式如下。\r\n```js\r\n$ docker pull ubuntu\r\n```\r\n 执行上述指令后,Docker会自动从Docker Hub上下载最新版本的Ubuntu到本地,当然也可以使用以下指令拉取指定版本的镜像到本地,具体指令如下。\r\n```js\r\n$ docker pull ubuntu:14.04\r\n```\r\n**4.构建镜像** \r\n 除了可以通过docker pull指令从仓库拉取镜像外,还可以通过docker build指令构建Docker镜像,通常情况下都是通过Dockerfile文件来构建镜像的。 \r\n 这里假设linux系统home目录下/shitou/workspace/dockerspace文件夹中编写有对应的Dockerfile文件,则构建镜像直立示例如下所示。 \r\n```js\r\n$ docker build -t hellodocker3 /home/shitou/workspace/dockerspace/.\r\n```\r\n**5.删除镜像** \r\n 当本地存放过多不需要的镜像时,可以通过docker rmi指令将其删除。在删除镜像时,需要指定镜像名称或镜像ID。删除镜像的使用方式如下。\r\n```js\r\n$ docker rmi -f hellodocker2 hellodocker3\r\n```\r\n 上述指令中,docker rmi表示删除镜像,-f表示进行强制删除,而hellodocker2和hellodocker3分别表示需要删除的镜像名称,这里同时删除两个镜像。除了根据名称删除镜像外,还也可以根据镜像ID来删除镜像,只是这里如果指定了删除ID为23c617a866d4的镜像后,会同时删除hellodocker、hellodocker2和hellodocker3三个镜像。 \r\n 需要特别强调的是,在进行镜像删除操作时,如果是通过镜像ID进行镜像删除,那么由该镜像创建的容器必须提前删除或停止。另外,在通过镜像名称操作镜像时,如果出现镜像重名的情况,必须在镜像名称后面指定镜像标签tag参数来确保唯一性。\r\n\r\n**6.创建并启动容器** \r\n Docker镜像主要用于创建容器,可以使用docker run指令创建并启动容器,具体使用方式如下。\r\n```js\r\n$ docker run -d -p 5000:80 --name test hellodocker\r\n```\r\n 上述创建并启动容器的指令略微复杂,具体分析如下。 \r\n ●docker run:表示创建并启动一个容器,而后面的hellodocker就表示要启动运行的镜像名称; \r\n ●-d:表示容器启动时在后台运行; \r\n ●-p 5000:80:表示将容器内暴露的80端口映射到宿主机指定的5000端口,也可以将-p 5000:80更改为-P来映射主机的随机端口(注意p字母的大小写); \r\n ●--name test:表示为创建后的容器指定名称为test,如果没有该参数配置,则生成的容器会设置一个随机名称。 \r\n docker run命令是Docker操作中较为复杂的一个,它可以携带多个参数和参数,我们可以通过docker run --help指令进行查看,其中有些参数如-e、-v和-w等都可以在Dockerfile文件中预先声明。 \r\n \r\n**7.列出容器** \r\n 生成容器后,可以通过docker ps指令查看当前运行的所有容器,具体使用方式如下。\r\n```js\r\n$ docker ps\r\n```\r\n 执行上述命令后,会将所有当前运行的容器都展示出来,具体如下图所示。\r\n![alt](/article_img/docker/5.png)\r\n 从图中可以看出,系统终端通过7列对当前的正在运行的一个容器进行了展示,图中每一列的具体含义如下。 \r\n ●CONTAINER ID:表示生成的容器ID; \r\n ●IMAGE:表示生成该容器的镜像名称; \r\n ●COMMAND:表示启动容器时运行的命令,Docker要求在启动容器时必须运行一个命令; \r\n ●CREATED:表示容器创建的时间; \r\n ●STATUS:表示容器运行状态,例如Up表示运行中,Exited表示已停止; \r\n ●PORTS:表示容器内部暴露的端口映射到主机的端口; \r\n ●NAMES:表示生成容器的名称,由Docker引擎自动生成,可以像上述示例中使用--name参数指定生成容器的名称。 \r\n 另外,docker ps指令运行过程中可以指定多个参数,还可以通过docker ps --help指令对ps指令的其他信息进行查看。\r\n\r\n**8.删除容器** \r\n 当不需要使用容器时,则可以使用docker rm指令删除已停止的容器,具体使用方式如下。\r\n```js\r\n$ docker rm f0c9a8b6e8c5\r\n```\r\n 需要注意的是,上述指令只能删除已经停止运行的容器,而不能删除正在运行的容器。如果想要删除正在运行的容器,则需要添加-f参数强制删除,具体使用方式如下。\r\n```js\r\n$ docker rm -f f0c9a8b6e8c5\r\n```\r\n 当需要删除的容器过多时,如果还一个个的删除就略显麻烦了,此时可以通过如下指令将全部容器删除。\r\n```js\r\n$ docker rm -f $(docker ps -aq)\r\n```\r\n 上述指令中,首先会通过$(docker ps -aq)获取所有容器的ID,然后通过docker rm -f指令进行强制删除。如果开发者有自己特殊的删除需求,可以根据前面docker ps指令进行组装来获取需要删除的容器ID。 \r\n Docker提供的操作指令远不止这些,这里就不一一列举了,想要了解更多Docker的操作指令,可以通过docker --help指令进行查看。\r\n\r\n\r\n', '2018-12-05', null, '默认分类', 'Docker,客户端指令', '1', null);
INSERT INTO `t_article` VALUES ('11', 'Docker数据管理介绍', ' 当我们对容器进行相关操作时,产生的一系列数据都会存储在容器中,而Docker内部又是如果管理这些数据的呢?本篇文章将针对Docker数据管理的一些知识进行介绍。\r\n \r\n**1. Docker数据存储机制** \r\n 使用Docker时,我们操作的都是镜像和由镜像生成的容器,所以想要更好的了解Docker内部的数据存储机制,就必须从镜像、容器与数据存储的关系出发。 \r\n Docker镜像是通过读取Dockerfile文件中的指令构建的,Dockerfile中的每条指令都会创建一个镜像层,并且每层都是只读的,这一系列的镜像层就构成了Docker镜像。接下来以一个Dockerfile文件为例进行说明,具体如下列代码示例所示。\r\n```js\r\n 1 FROM ubuntu:16.04\r\n 2 COPY . /app\r\n 3 RUN make /app\r\n 4 CMD python /app/app.py\r\n```\r\n 上述文件示例中的Dockerfile包含了4条指令,每条指令都会创建一个镜像层,其中每一层与前一层都有所不同,并且是层层叠加的。通过镜像构建容器时,会在镜像层上增加一个容器层(即可写层),所有对容器的更改都会写入容器层,这也是Docker默认的数据存储方式。 \r\n 下面通过一个效果图进行说明,具体如下图所示。\r\n![alt](/article_img/docker/6.png)\r\n 从图中可以看出,Docker容器和镜像之间的主要区别是顶部的容器层,而所有对容器中数据的添加、修改等操作都会被存储在容器层中。当容器被删除时,容器层也会被删除,其中存储的数据会被一同删除,而下面的镜像层却保持不变。 \r\n 由于所有的容器都是通过镜像构建的,所以每个容器都有各自的容器层,对于容器数据的更改就会保存在各自的容器层中。也就是说,由同一个镜像构建的多个容器,它们会拥有相同的底部镜像层,而拥有不同的容器层,多个容器可以访问相同的镜像层,并且有自己的独立数据状态。具体说明如下图所示。 \r\n![alt](/article_img/docker/7.png)\r\n 从图中可以看出,基于同一个镜像构建的多个容器可以共享该镜像层,但是多个容器想要共享相同的数据,就需要将这些数据存储到容器之外的地方,而这种方式就是下一节要提到的Docker volume数据外部挂载机制。 \r\n\r\n**2. Docker数据存储方式** \r\n 在默认情况下,Docker中的数据都是存放在容器层的,但是这样存储数据却有较多的缺陷,具体表现如下。 \r\n ●当容器不再运行时,容器中的数据无法持久化保存,如果另一个进程需要这些数据,那么将很难从容器中获取数据; \r\n ●容器层与正在运行的主机紧密耦合,不能轻易地移动数据; \r\n ●容器层需要一个存储驱动程序来管理文件系统,存储驱动程序提供了一个使用Linux内核的联合文件系统,这种额外的抽象化降低了性能。 \r\n 基于上述种种原因,多数情况下Docker数据管理都不会直接将数据写入容器层,而是使用另一种叫做Docker volume数据外部挂载的机制进行数据管理。 \r\n 针对Docker volume数据外部挂载机制,Docker提供了三种不同的方式将数据从容器映射到Docker宿主机,他们分别为:volumes(数据卷)、bind mounts(绑定挂载)和tmpfs mounts(tmpfs挂载)。这三种数据管理方式的具体选择,需要结合实际情况进行考虑,其中的volumes数据卷是最常用也是官方推荐的数据管理方式。无论选择使用哪种数据管理方式,数据在容器内看起来都一样的,而在容器外则会被被挂载到文件系统中的某个目录或文件中。 \r\n 下面通过一张图来展示数据卷、绑定挂载和tmpfs挂载之间的差异,如下图所示。 \r\n![alt](/article_img/docker/8.png)\r\n 从图中可以看出,Docker提供的三种数据管理方式略有不同,具体分析如下。 \r\n ●volumes:存储在主机文件系统中(在Linux系统下是存在于/var/lib/Docker/volumes/目录),并由Docker管理,非Docker进程无法修改文件系统的这个部分。 \r\n ●bind mounts:可以存储在主机系统的任意位置,甚至可能是重要的系统文件或目录,在Docker主机或容器上的非Docker进程可以对他们进行任意修改。 \r\n ●tmpfs mounts:只存储在主机系统的内存中,并没有写入到主机的文件系统中。\r\n\r\n\r\n', '2018-12-07', null, '默认分类', 'Docker,数据管理', '1', null);
INSERT INTO `t_article` VALUES ('12', 'Spring Boot 2 权威发布', ' 如果这两天登录 [https://start.spring.io/ ](https://start.spring.io/ )就会发现,Spring Boot 默认版本已经升到了 2.1.0。这是因为 Spring Boot 刚刚发布了 2.1.0 版本,我们来看下 Spring Boot 2 发布以来第一个子版本都发布了哪些内容? \r\n\r\n**2.1 中的新特性** \r\n ●将spring-boot-starter-oauth2-oidc-client重命名为spring-boot-starter-oauth2-client命名更简洁 \r\n ●添加 OAuth2 资源服务 starter,OAuth2 一个用于认证的组件 \r\n ●支持 ConditionalOnBean 和 ConditionalOnMissingBean 下的参数化容器 \r\n ●自动配置 applicationTaskExecutor bean 的延迟加载来避免不必要的日志记录 \r\n ●将 DatabaseDriver#SAP 重命名为 DatabaseDriver \r\n ●跳过重启器不仅适用于 JUnit4,也适用于 JUnit5 \r\n ●在 Jest HealthIndicator 中使用集群端点 \r\n ●当 DevTools 禁用重启时添加日志输出 \r\n ●添加注解:@ConditionalOnMissingServletFilter提高 Servlet Filters 的自动装配。\r\n \r\n**2.1 中的组件升级** \r\n ●升级 Hibernate 5.3,Hibernate 的支持升级到了 5.3 \r\n ●升级 Tomcat 9 ,支持最新的 tomcat 9 \r\n ●支持 Java 11,Java 现在更新越来越快,Spring 快赶不上了 \r\n ●升级 Thymeleaf Extras Springsecurity 到 3.0.4.RELEASE ,thymeleaf-extras-springsecurity 是 Thymeleaf 提供集成 Spring Security 的组件 \r\n ●升级 Joda Time 2.10.1,Joda-Time, 面向 Java 应用程序的日期/时间库的替代选择,Joda-Time 令时间和日期值变得易于管理、操作和理解。 \r\n ●升级 Lettuce 5.1.2.RELEASE ,Lettuce 前面说过,传说中 Redis 最快的客户端。 \r\n ●升级 Reactor Californium-SR2 ,Californium 是物联网云服务的 Java COAP 实现。因此,它更专注的是可扩展性和可用性而不是像嵌入式设备那样关注资源效率。不过,Californium 也适合嵌入式的 JVM。 \r\n ●升级 Maven Failsafe Plugin 2.22.1 ,Maven 中的测试插件。 \r\n ●升级 Flyway 5.2.1 , Flyway是一款开源的数据库版本管理工具 \r\n ●升级 Aspectj 1.9.2 ,AspectJ 是 Java 中流行的 AOP(Aspect-oriented Programming)编程扩展框架,是 Eclipse 托管给 Apache 基金会的一个开源项目。 \r\n ●升级 Mysql 8.0.13 ,Mysql 支持到 8。 \r\n ●... \r\n  更多的详细内容可以参考这里:[Spring Boot 2.1 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes)\r\n', '2018-12-12', null, '默认分类', 'Spring Boot 2', '1', null);
-- ----------------------------
-- Table structure for `t_comment`
-- ----------------------------
DROP TABLE IF EXISTS `t_comment`;
CREATE TABLE `t_comment` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '评论id',
`article_id` int(11) NOT NULL COMMENT '关联的文章id',
`created` date NOT NULL COMMENT '评论时间',
`ip` varchar(200) DEFAULT NULL COMMENT '评论用户登录的ip地址',
`content` text NOT NULL COMMENT '评论内容',
`status` varchar(200) NOT NULL DEFAULT 'approved' COMMENT '评论状态',
`author` varchar(200) NOT NULL COMMENT '评论用户用户名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_comment
-- ----------------------------
INSERT INTO `t_comment` VALUES ('1', '12', '2018-12-13', '0:0:0:0:0:0:0:1', '很不错,不过这文章排版不太好看啊', 'approved', '李四');
INSERT INTO `t_comment` VALUES ('2', '11', '2018-12-13', '0:0:0:0:0:0:0:1', '很不错的原理分析,受用了!', 'approved', '李四');
INSERT INTO `t_comment` VALUES ('3', '10', '2018-12-13', '0:0:0:0:0:0:0:1', '关于Docker虚拟容器的讲解挺好的额,学习中', 'approved', '李四');
INSERT INTO `t_comment` VALUES ('9', '1', '2018-12-13', '0:0:0:0:0:0:0:1', '非常不错,赞一个!', 'approved', '李四');
INSERT INTO `t_comment` VALUES ('10', '1', '2018-12-13', '0:0:0:0:0:0:0:1', '博主,这资料怎么弄的?有相关资源和教材推荐吗?', 'approved', '李四');
INSERT INTO `t_comment` VALUES ('11', '1', '2018-12-13', '0:0:0:0:0:0:0:1', '很详细,感谢...', 'approved', '东方不败');
INSERT INTO `t_comment` VALUES ('12', '1', '2018-12-13', '0:0:0:0:0:0:0:1', '很全,努力学习中...', 'approved', '东方不败');
INSERT INTO `t_comment` VALUES ('13', '1', '2018-12-13', '0:0:0:0:0:0:0:1', '好东西,先收藏起来,哈哈', 'approved', 'tom');
INSERT INTO `t_comment` VALUES ('14', '8', '2018-12-13', '0:0:0:0:0:0:0:1', 'very good blog', 'approved', 'tom');
-- ----------------------------
-- Table structure for `t_authority`
-- ----------------------------
DROP TABLE IF EXISTS `t_authority`;
CREATE TABLE `t_authority` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`authority` varchar(200) DEFAULT NULL COMMENT '权限',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_authority
-- ----------------------------
INSERT INTO `t_authority` VALUES ('1', 'ROLE_admin');
INSERT INTO `t_authority` VALUES ('2', 'ROLE_common');
-- ----------------------------
-- Table structure for `t_statistic`
-- ----------------------------
DROP TABLE IF EXISTS `t_statistic`;
CREATE TABLE `t_statistic` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`article_id` int(11) NOT NULL COMMENT '关联的文章id',
`hits` int(11) NOT NULL DEFAULT '0' COMMENT '文章点击总量',
`comments_num` int(11) NOT NULL DEFAULT '0' COMMENT '文章评论总量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_statistic
-- ----------------------------
INSERT INTO `t_statistic` VALUES ('1', '1', '91', '5');
INSERT INTO `t_statistic` VALUES ('2', '2', '3', '0');
INSERT INTO `t_statistic` VALUES ('3', '3', '4', '0');
INSERT INTO `t_statistic` VALUES ('4', '4', '4', '0');
INSERT INTO `t_statistic` VALUES ('5', '5', '4', '0');
INSERT INTO `t_statistic` VALUES ('6', '6', '15', '0');
INSERT INTO `t_statistic` VALUES ('7', '7', '6', '0');
INSERT INTO `t_statistic` VALUES ('8', '8', '23', '1');
INSERT INTO `t_statistic` VALUES ('9', '9', '18', '0');
INSERT INTO `t_statistic` VALUES ('10', '10', '18', '1');
INSERT INTO `t_statistic` VALUES ('11', '11', '10', '1');
INSERT INTO `t_statistic` VALUES ('12', '12', '39', '1');
-- ----------------------------
-- Table structure for `t_user`
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(200) DEFAULT NULL,
`password` varchar(200) DEFAULT NULL,
`email` varchar(200) DEFAULT NULL,
`created` date DEFAULT NULL,
`valid` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES ('1', 'admin', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '2127269781@qq.com', '2018-10-01', '1');
INSERT INTO `t_user` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1768653040@qq.com', '2018-11-13', '1');
INSERT INTO `t_user` VALUES ('3', '东方不败', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '13718391550@163.com', '2018-12-18', '1');
INSERT INTO `t_user` VALUES ('4', 'tom', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', 'asexeees@sohu.com', '2018-12-03', '1');
-- ----------------------------
-- Table structure for `t_user_authority`
-- ----------------------------
DROP TABLE IF EXISTS `t_user_authority`;
CREATE TABLE `t_user_authority` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '关联的用户id',
`authority_id` int(11) NOT NULL COMMENT '关联的权限id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of t_user_authority
-- ----------------------------
INSERT INTO `t_user_authority` VALUES ('1', '1', '1');
INSERT INTO `t_user_authority` VALUES ('2', '2', '2');
INSERT INTO `t_user_authority` VALUES ('3', '3', '2');
INSERT INTO `t_user_authority` VALUES ('4', '4', '2');
文件目录
5、系统环境搭建
搭建步骤
-
创建项目,引入依赖文件
pom.xml
<dependencies> <!-- 阿里巴巴的Druid数据源依赖启动器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.10</version> </dependency> <!-- MyBatis依赖启动器 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.0.0</version> </dependency> <!-- MySQL数据库连接驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Redis服务启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- mail邮件服务启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!-- thymeleaf模板整合security控制页面安全访问依赖 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency> <!-- Spring Security依赖启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Thymeleaf模板引擎启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- Web服务启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis分页插件 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <!-- String工具类包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> <!-- Markdown处理html --> <dependency> <groupId>com.atlassian.commonmark</groupId> <artifactId>commonmark</artifactId> <version>0.11.0</version> </dependency> <!-- Markdown处理表格 --> <dependency> <groupId>com.atlassian.commonmark</groupId> <artifactId>commonmark-ext-gfm-tables</artifactId> <version>0.11.0</version> </dependency> <!-- 过滤emoji表情字符 --> <dependency> <groupId>com.vdurmont</groupId> <artifactId>emoji-java</artifactId> <version>4.0.0</version> </dependency> <!-- devtools热部署工具 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> </dependency> <!-- Spring Boot测试服务启动器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
-
编写配置文件
将application.properties后缀改为yml
server: port: 80 spring: profiles: # 外置jdbc、redis和mail配置文件 active: jdbc,redis,mail # 关闭thymeleaf页面缓存 thymeleaf: cache: false # 配置国际化资源文件 messages: basename: i18n.logo # MyBatis配置 mybatis: configuration: #开启驼峰命名匹配映射 map-underscore-to-camel-case: true #配置MyBatis的xml映射文件路径 mapper-locations: classpath:mapper/*.xml #配置XML映射文件中指定的实体类别名路径 type-aliases-package: com.itheima.model.domain #pagehelper分页设置 pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: true params: count=countSql #浏览器cookie相关设置 COOKIE: # 设置cookie默认时长为30分钟 VALIDITY: 1800
下面是我们创建的配置文件
-
application-jdbc.properties
#添加并配置第三方数据库连接池 spring.datasource.type = com.alibaba.druid.pool.DruidDataSource spring.datasource.initialSize=20 spring.datasource.minIdle=10 spring.datasource.maxActive=100 # Mysql数据库连接配置 spring.datasource.url = jdbc:mysql://localhost:3306/blog_system?serverTimezone=UTC&useSSL=false spring.datasource.username = root spring.datasource.password = root #spring.datasource.driver-class-name = com.mysql.jdbc.Driver
-
application-mail.properties
#QQ邮箱邮件发送服务配置 spring.mail.host=smtp.qq.com spring.mail.port=587 #配置个人QQ账户和密码(密码是加密后的授权码) spring.mail.username=2481742279@qq.com spring.mail.password=tfojbshmqzxwdibi
-
application-redis.properties
# Redis服务器地址,另外主要要开启Redis服务 spring.redis.host=127.0.0.1 # Redis服务器连接端口号 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(负数表示没有限制) spring.redis.jedis.pool.max-active=8 # 连接池最大阻塞等待时间(负数表示没有限制) spring.redis.jedis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8
-
-
前端资源引入
- 后端基础代码引入
注释:
- config下的RedisConfig配置类用于对Redis进行自定义配置,具体内容参照第6章
- model下有domain和ResponseData目录,domain目录对应数据库表实体类,ResponseData目录对应前端请求的响应封装数据(响应状态码、数据、消息等)
- utils目录的Commons工具类用于前端页面的数据转换和展示,MyUtils工具类用于对Markdown文件的处理
6、文章分页展示
实现文章分类展示效果需要同时实现文章查询以及文章同居数据查询,这里先编写文章类Article和统计类Statistic对应的数据访问方法。
持久层
- 创建Dao接口文件
-
ArticleMapper.java
package com.itheima.dao; import com.itheima.model.domain.Article; import org.apache.ibatis.annotations.*; import java.util.List; /** * @Classname ArticleMapper * @Description TODO * @Date 2019-3-14 9:44 * @Created by CrazyStone */ @Mapper public interface ArticleMapper { // 根据id查询文章信息 @Select("SELECT * FROM t_article WHERE id=#{id}") public Article selectArticleWithId(Integer id); // 发表文章,同时使用@Options注解获取自动生成的主键id @Insert("INSERT INTO t_article (title,created,modified,tags,categories," + " allow_comment, thumbnail, content)" + " VALUES (#{title},#{created}, #{modified}, #{tags}, #{categories}," + " #{allowComment}, #{thumbnail}, #{content})") @Options(useGeneratedKeys=true, keyProperty="id", keyColumn="id") public Integer publishArticle(Article article); // 文章发分页查询 @Select("SELECT * FROM t_article ORDER BY id DESC") public List<Article> selectArticleWithPage(); // 通过id删除文章 @Delete("DELETE FROM t_article WHERE id=#{id}") public void deleteArticleWithId(int id); // 站点服务统计,统计文章数量 @Select("SELECT COUNT(1) FROM t_article") public Integer countArticle(); // 通过id更新文章 public Integer updateArticleWithId(Article article); }
-
StatisticMapper.java
package com.itheima.dao; import com.itheima.model.domain.Article; import com.itheima.model.domain.Statistic; import org.apache.ibatis.annotations.*; import java.util.List; /** * @Classname StatisticMapper * @Description TODO * @Date 2019-3-14 9:45 * @Created by CrazyStone */ @Mapper public interface StatisticMapper { // 新增文章对应的统计信息 @Insert("INSERT INTO t_statistic(article_id,hits,comments_num) values (#{id},0,0)") public void addStatistic(Article article); // 根据文章id查询点击量和评论量相关信息 @Select("SELECT * FROM t_statistic WHERE article_id=#{articleId}") public Statistic selectStatisticWithArticleId(Integer articleId); // 通过文章id更新点击量 @Update("UPDATE t_statistic SET hits=#{hits} " + "WHERE article_id=#{articleId}") public void updateArticleHitsWithId(Statistic statistic); // 通过文章id更新评论量 @Update("UPDATE t_statistic SET comments_num=#{commentsNum} " + "WHERE article_id=#{articleId}") public void updateArticleCommentsWithId(Statistic statistic); // 根据文章id删除统计数据 @Delete("DELETE FROM t_statistic WHERE article_id=#{aid}") public void deleteStatisticWithId(int aid); // 统计文章热度信息 @Select("SELECT * FROM t_statistic WHERE hits !='0' " + "ORDER BY hits DESC, comments_num DESC") public List<Statistic> getStatistic(); // 统计博客文章总访问量 @Select("SELECT SUM(hits) FROM t_statistic") public long getTotalVisit(); // 统计博客文章总评论量 @Select("SELECT SUM(comments_num) FROM t_statistic") public long getTotalComment(); }
-
创建MyBatis对应的xml映射文件
resources/mapper 下创建Article文章操作对应的xml映射文件
AriticleMapper.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.itheima.dao.ArticleMapper"> <update id="updateArticleWithId" parameterType="Article"> update t_article <set> <if test="title != null"> title = #{title}, </if> <if test="created != null"> created = #{created}, </if> <if test="modified != null"> modified = #{modified}, </if> <if test="tags != null"> tags = #{tags}, </if> <if test="categories != null"> categories = #{categories}, </if> <if test="hits != null"> hits = #{hits}, </if> <if test="commentsNum != null"> comments_num = #{commentsNum}, </if> <if test="allowComment != null"> allow_comment = #{allowComment}, </if> <if test="thumbnail != null"> thumbnail = #{thumbnail}, </if> <if test="content != null"> content = #{content}, </if> </set> where id = #{id} </update> </mapper>
业务处理层
在com.itheima.service下进行
-
com.itheima.service.IArticleService.java
public interface IArticleService { // 分页查询文章列表 public PageInfo<Article> selectArticleWithPage(Integer page, Integer count); // 统计前10的热度文章信息 public List<Article> getHeatArticles(); }
-
com.itheima.service.impl.ArticleServiceImpl.java
@Service @Transactional public class ArticleServiceImpl implements IArticleService { @Autowired private ArticleMapper articleMapper; @Autowired private StatisticMapper statisticMapper; // 分页查询文章列表 @Override public PageInfo<Article> selectArticleWithPage(Integer page, Integer count) { PageHelper.startPage(page, count); List<Article> articleList = articleMapper.selectArticleWithPage(); // 封装文章统计数据 for (int i = 0; i < articleList.size(); i++) { Article article = articleList.get(i); Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId()); article.setHits(statistic.getHits()); article.setCommentsNum(statistic.getCommentsNum()); } PageInfo<Article> pageInfo=new PageInfo<>(articleList); return pageInfo; } // 统计前10的热度文章信息 @Override public List<Article> getHeatArticles( ) { List<Statistic> list = statisticMapper.getStatistic(); List<Article> articlelist=new ArrayList<>(); for (int i = 0; i < list.size(); i++) { Article article = articleMapper.selectArticleWithId(list.get(i).getArticleId()); article.setHits(list.get(i).getHits()); article.setCommentsNum(list.get(i).getCommentsNum()); articlelist.add(article); if(i>=9){ break; } } return articlelist; } }
请求处理层
-
实现controller控制层处理类
com.itheima.web.client.IndexController.java
@Controller public class IndexController { private static final Logger logger = LoggerFactory.getLogger(IndexController.class); @Autowired private IArticleService articleServiceImpl; // 博客首页,会自动跳转到文章页 @GetMapping(value = "/") private String index(HttpServletRequest request) { return this.index(request, 1, 5); } // 文章页 @GetMapping(value = "/page/{p}") public String index(HttpServletRequest request, @PathVariable("p") int page, @RequestParam(value = "count", defaultValue = "5") int count) { PageInfo<Article> articles = articleServiceImpl.selectArticleWithPage(page, count); // 获取文章热度统计信息 List<Article> articleList = articleServiceImpl.getHeatArticles(); request.setAttribute("articles", articles); request.setAttribute("articleList", articleList); logger.info("分页获取文章信息: 页码 "+page+",条数 "+count); return "client/index"; } }
-
实现自定义拦截器Interceptor
将Commons工具类传递到前端页面中,我们使用自定义拦截器Interceptor的方式,将Commons工具类实列存储在Request域中返回页面使用
在web目录下创建interceptor包,创建BaseInterceptor类实现HandlerInterceptor接口自定义拦截器类
/** * 自定义的Interceptor拦截器类,用于封装请求后的数据类到request域中,供html页面使用 * 注意:自定义Mvc的Interceptor拦截器类 * 1、使用@Configuration注解声明 * 2、自定义注册类将自定义的Interceptor拦截器类进行注册使用 */ @Configuration public class BaseInterceptor implements HandlerInterceptor { @Autowired private Commons commons; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { // 用户将封装的Commons工具返回页面 request.setAttribute("commons",commons); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { } }
通再创建WebMvcConfig类实现WebMvcConfigurer接口进行自定义拦截器注册
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private BaseInterceptor baseInterceptor; @Override // 重写addInterceptors()方法,注册自定义拦截器 public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(baseInterceptor); } }
-
实现前端页面功能
-
测试:访问localhost
-
账户:user
-
密码:打印再控制台了
-
7、文章详情查看
-
评论类Comment对应的接口:com.itheima.dao.CommentMapper.java
package com.itheima.dao; import com.itheima.model.domain.Comment; import org.apache.ibatis.annotations.*; import java.util.List; /** * @Classname CommentMapper * @Description TODO * @Date 2019-3-14 10:12 * @Created by CrazyStone */ @Mapper public interface CommentMapper { // 分页展示某个文章的评论 @Select("SELECT * FROM t_comment WHERE article_id=#{aid} ORDER BY id DESC") public List<Comment> selectCommentWithPage(Integer aid); // 后台查询最新几条评论 @Select("SELECT * FROM t_comment ORDER BY id DESC") public List<Comment> selectNewComment(); // 发表评论 @Insert("INSERT INTO t_comment (article_id,created,author,ip,content)" + " VALUES (#{articleId}, #{created},#{author},#{ip},#{content})") public void pushComment(Comment comment); // 站点服务统计,统计评论数量 @Select("SELECT COUNT(1) FROM t_comment") public Integer countComment(); // 通过文章id删除评论信息 @Delete("DELETE FROM t_comment WHERE article_id=#{aid}") public void deleteCommentWithId(Integer aid); }
-
业务处理层
-
打开业务接口文件IArticleService,编一个根据文章ID查询文章详情的接口方法
// 根据文章id查询单个文章详情 public Article selectArticleWithId(Integer id);
-
再com.itheima.servicec包下分别创建评论业务处理和博客站点业务处理的Serivice接口文件
ICommentService.java
//@Description 文章评论业务处理接口 public interface ICommentService { // 获取文章下的评论 public PageInfo<Comment> getComments(Integer aid, int page, int count); }
ISiteService.java
//@Description 博客站点统计服务 public interface ISiteService { // 最新收到的评论 public List<Comment> recentComments(int count); // 最新发表的文章 public List<Article> recentArticles(int count); // 获取后台统计数据 public StaticticsBo getStatistics(); // 更新某个文章的统计数据 public void updateStatistics(Article article); }
-
擦婚嫁Service层接口实现类
ArticleServiceImpl类中添加该方法
@Autowired private RedisTemplate redisTemplate; // 根据id查询单个文章详情,并使用Redis进行缓存管理 public Article selectArticleWithId(Integer id){ Article article = null; Object o = redisTemplate.opsForValue().get("article_" + id); if(o!=null){ article=(Article)o; }else{ article = articleMapper.selectArticleWithId(id); if(article!=null){ redisTemplate.opsForValue().set("article_" + id,article); } } return article; }
-
ICommentService接口
/**
* @Description 文章评论业务处理接口
*/
public interface ICommentService {
// 获取文章下的评论
public PageInfo<Comment> getComments(Integer aid, int page, int count);
}
ISiteService接口
/**
* @Description 博客站点统计服务
*/
public interface ISiteService {
// 最新收到的评论
public List<Comment> recentComments(int count);
// 最新发表的文章
public List<Article> recentArticles(int count);
// 获取后台统计数据
public StaticticsBo getStatistics();
// 更新某个文章的统计数据
public void updateStatistics(Article article);
}
CommentServiceImpl实现类
@Service
@Transactional
public class CommentServiceImpl implements ICommentService {
@Autowired
private CommentMapper commentMapper;
// 根据文章id分页查询评论
@Override
public PageInfo<Comment> getComments(Integer aid, int page, int count) {
PageHelper.startPage(page,count);
List<Comment> commentList = commentMapper.selectCommentWithPage(aid);
PageInfo<Comment> commentInfo = new PageInfo<>(commentList);
return commentInfo;
}
}
SiteServiceImpl实现类
@Service
@Transactional
public class SiteServiceImpl implements ISiteService {
@Autowired
private CommentMapper commentMapper;
@Autowired
private ArticleMapper articleMapper;
@Autowired
private StatisticMapper statisticMapper;
@Override
public void updateStatistics(Article article) {
Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId());
statistic.setHits(statistic.getHits()+1);
statisticMapper.updateArticleHitsWithId(statistic);
}
@Override
public List<Comment> recentComments(int limit) {
PageHelper.startPage(1, limit>10 || limit<1 ? 10:limit);
List<Comment> byPage = commentMapper.selectNewComment();
return byPage;
}
@Override
public List<Article> recentArticles(int limit) {
PageHelper.startPage(1, limit>10 || limit<1 ? 10:limit);
List<Article> list = articleMapper.selectArticleWithPage();
// 封装文章统计数据
for (int i = 0; i < list.size(); i++) {
Article article = list.get(i);
Statistic statistic = statisticMapper.selectStatisticWithArticleId(article.getId());
article.setHits(statistic.getHits());
article.setCommentsNum(statistic.getCommentsNum());
}
return list;
}
@Override
public StaticticsBo getStatistics() {
StaticticsBo staticticsBo = new StaticticsBo();
Integer articles = articleMapper.countArticle();
Integer comments = commentMapper.countComment();
staticticsBo.setArticles(articles);
staticticsBo.setComments(comments);
return staticticsBo;
}
}
请求处理层
再用户首页请求处理类IndexController中提那就用于查询文章详情处理的方法
@Controller
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private IArticleService articleServiceImpl;
@Autowired
private ICommentService commentServiceImpl;
@Autowired
private ISiteService siteServiceImpl;
// 博客首页,会自动跳转到文章页
@GetMapping(value = "/")
private String index(HttpServletRequest request) {
return this.index(request, 1, 5);
}
// 文章页
@GetMapping(value = "/page/{p}")
public String index(HttpServletRequest request, @PathVariable("p") int page, @RequestParam(value = "count", defaultValue = "5") int count) {
PageInfo<Article> articles = articleServiceImpl.selectArticleWithPage(page, count);
// 获取文章热度统计信息
List<Article> articleList = articleServiceImpl.getHeatArticles();
request.setAttribute("articles", articles);
request.setAttribute("articleList", articleList);
logger.info("分页获取文章信息: 页码 "+page+",条数 "+count);
return "client/index";
}
// 文章详情查询
@GetMapping(value = "/article/{id}")
public String getArticleById(@PathVariable("id") Integer id, HttpServletRequest request){
Article article = articleServiceImpl.selectArticleWithId(id);
if(article!=null){
// 查询封装评论相关数据
getArticleComments(request, article);
// 更新文章点击量
siteServiceImpl.updateStatistics(article);
request.setAttribute("article",article);
return "client/articleDetails";
}else {
logger.warn("查询文章详情结果为空,查询文章id: "+id);
// 未找到对应文章页面,跳转到提示页
return "comm/error_404";
}
}
// 查询文章的评论信息,并补充到文章详情里面
private void getArticleComments(HttpServletRequest request, Article article) {
if (article.getAllowComment()) {
// cp表示评论页码,commentPage
String cp = request.getParameter("cp");
cp = StringUtils.isBlank(cp) ? "1" : cp;
request.setAttribute("cp", cp);
PageInfo<Comment> comments = commentServiceImpl.getComments(article.getId(),Integer.parseInt(cp),3);
request.setAttribute("cp", cp);
request.setAttribute("comments", comments);
}
}
}
8、文章评论管理
重点讲解文章评论发布功能的实现
业务处理层
-
编写Service接口方法
在评论业务接口文件IcommentService中编写一个发布文章评论的方法
// 用户发表评论 public void pushComment(Comment comment);
-
编写Sevice层接口实现类方法
在CommentServiceImpl中实现新增的评论发布方法
@Autowired private StatisticMapper statisticMapper; // 用户发表评论 @Override public void pushComment(Comment comment){ commentMapper.pushComment(comment); // 更新文章评论数据量 Statistic statistic = statisticMapper.selectStatisticWithArticleId(comment.getArticleId()); statistic.setCommentsNum(statistic.getCommentsNum()+1); statisticMapper.updateArticleCommentsWithId(statistic); }
请求处理层
在com.itheima.web.client下穿运河评论管理控制类CommentController
CommentController
@Controller
@RequestMapping("/comments")
public class CommentController {
private static final Logger logger = LoggerFactory.getLogger(CommentController.class);
@Autowired
private ICommentService commentServcieImpl;
// 发表评论操作
@PostMapping(value = "/publish")
@ResponseBody
public ArticleResponseData publishComment(HttpServletRequest request,@RequestParam Integer aid, @RequestParam String text) {
// 去除js脚本
text = MyUtils.cleanXSS(text);
text = EmojiParser.parseToAliases(text);
// 获取当前登录用户
User user=(User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// 封装评论信息
Comment comments = new Comment();
comments.setArticleId(aid);
comments.setIp(request.getRemoteAddr());
comments.setCreated(new Date());
comments.setAuthor(user.getUsername());
comments.setContent(text);
try {
commentServcieImpl.pushComment(comments);
logger.info("发布评论成功,对应文章id: "+aid);
return ArticleResponseData.ok();
} catch (Exception e) {
logger.error("发布评论失败,对应文章id: "+aid +";错误描述: "+e.getMessage());
return ArticleResponseData.fail();
}
}
}
9、数据展示
主要对最新的文章、评论和数据量进行统计展示,业务功能在之前的系统通缉令SiteSericeImpl中已经实现了,只需从controller开始写
在com.itheima.web下创建后台控制管理的admin包,并在该包下创建用于后台控制的控制类AdminController,并编写响应的请求控制方法
AdminController.java
// 后台管理模块
@Controller
@RequestMapping("/admin")
public class AdminController {
private static final Logger logger = LoggerFactory.getLogger(AdminController.class);
@Autowired
private ISiteService siteServiceImpl;
// 管理中心起始页
@GetMapping(value = {"", "/index"})
public String index(HttpServletRequest request) {
// 获取最新的5篇博客、评论以及统计数据
List<Article> articles = siteServiceImpl.recentArticles(5);
List<Comment> comments = siteServiceImpl.recentComments(5);
StaticticsBo staticticsBo = siteServiceImpl.getStatistics();
// 向Request域中存储数据
request.setAttribute("comments", comments);
request.setAttribute("articles", articles);
request.setAttribute("statistics", staticticsBo);
return "back/index";
}
}
10、文章发布
业务处理层
-
编写Service层接口方法
IArticleService接口中添加下面方法
// 发布文章 public void publish(Article article);
-
编写接口实现类
ArticleServiceImpl实现类
// 发布文章 @Override public void publish(Article article) { // 去除表情 article.setContent(EmojiParser.parseToAliases(article.getContent())); article.setCreated(new Date()); article.setHits(0); article.setCommentsNum(0); // 插入文章,同时插入文章统计数据 articleMapper.publishArticle(article); statisticMapper.addStatistic(article); }
请求处理层
在后台管理控制类AdminController中添加文章发布到页面跳转方法、发布文章的请求处理方法、文章发布处理成功后跳转到文章管理列表的方法
@Controller
@RequestMapping("/admin")
public class AdminController {
private static final Logger logger = LoggerFactory.getLogger(AdminController.class);
@Autowired
private ISiteService siteServiceImpl;
@Autowired
private IArticleService articleServiceImpl;
// 管理中心起始页
@GetMapping(value = {"", "/index"})
public String index(HttpServletRequest request) {
// 获取最新的5篇博客、评论以及统计数据
List<Article> articles = siteServiceImpl.recentArticles(5);
List<Comment> comments = siteServiceImpl.recentComments(5);
StaticticsBo staticticsBo = siteServiceImpl.getStatistics();
// 向Request域中存储数据
request.setAttribute("comments", comments);
request.setAttribute("articles", articles);
request.setAttribute("statistics", staticticsBo);
return "back/index";
}
// 向文章发表页面跳转
@GetMapping(value = "/article/toEditPage")
public String newArticle( ) {
return "back/article_edit";
}
// 发表文章
@PostMapping(value = "/article/publish")
@ResponseBody
public ArticleResponseData publishArticle(Article article) {
if (StringUtils.isBlank(article.getCategories())) {
article.setCategories("默认分类");
}
try {
articleServiceImpl.publish(article);
logger.info("文章发布成功");
return ArticleResponseData.ok();
} catch (Exception e) {
logger.error("文章发布失败,错误信息: "+e.getMessage());
return ArticleResponseData.fail();
}
}
// 跳转到后台文章列表页面
@GetMapping(value = "/article")
public String index(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "count", defaultValue = "10") int count,
HttpServletRequest request) {
PageInfo<Article> pageInfo = articleServiceImpl.selectArticleWithPage(page, count);
request.setAttribute("articles", pageInfo);
return "back/article_list";
}
}
11、文章修改
业务处理层
编写Service层接口
在文章业务接口IArticleService中添加文章修改的方法
// 根据主键更新文章
public void updateArticleWithId(Article article);
编写实现类ArticleServiceImpl
// 更新文章
@Override
public void updateArticleWithId(Article article) {
article.setModified(new Date());
articleMapper.updateArticleWithId(article);
redisTemplate.delete("article_" + article.getId());
}
请求处理层
在后台管理控制类AdminController中添加文章编辑页面跳转和文章修改处理的请求方法
// 向文章修改页面跳转
@GetMapping(value = "/article/{id}")
public String editArticle(@PathVariable("id") String id, HttpServletRequest request) {
Article article = articleServiceImpl.selectArticleWithId(Integer.parseInt(id));
request.setAttribute("contents", article);
request.setAttribute("categories", article.getCategories());
return "back/article_edit";
}
// 文章修改处理
@PostMapping(value = "/article/modify")
@ResponseBody
public ArticleResponseData modifyArticle(Article article) {
try {
articleServiceImpl.updateArticleWithId(article);
logger.info("文章更新成功");
return ArticleResponseData.ok();
} catch (Exception e) {
logger.error("文章更新失败,错误信息: "+e.getMessage());
return ArticleResponseData.fail();
}
}
12、文章删除
业务处理层
-
编写Service层接口方法
在文章业务接口IArticleService中添加文章删除的方法
// 根据主键删除文章 public void deleteArticleWithId(int id);
-
编写接口实现类的方法
在接口实现类ArticleServiceImpl中添加删除方法
// 删除文章 @Override public void deleteArticleWithId(int id) { // 删除文章的同时,删除对应的缓存 articleMapper.deleteArticleWithId(id); redisTemplate.delete("article_" + id); // 同时删除对应文章的统计数据 statisticMapper.deleteStatisticWithId(id); // 同时删除对应文章的评论数据 commentMapper.deleteCommentWithId(id); }
请求处理层
在系统后台管理控制类AdminController中添加删除的处理方法
// 文章删除
@PostMapping(value = "/article/delete")
@ResponseBody
public ArticleResponseData delete(@RequestParam int id) {
try {
articleServiceImpl.deleteArticleWithId(id);
logger.info("文章删除成功");
return ArticleResponseData.ok();
} catch (Exception e) {
logger.error("文章删除失败,错误信息: "+e.getMessage());
return ArticleResponseData.fail();
}
}
13、用户登录控制
前面演示过程都需要预先使用spring Security提供的默认的能力页面和默认登录用户user登录认证。这种默认安全控制在实际开发中肯定是不合适的,所以我们需要整合Spring Security进行自定义用户登录控制功能的实现。
请求处理层
创建用户登录管理控制类:com.itheima.web.client.LoginController.java
@Controller
public class LoginController {
// 向登录页面跳转,同时封装原始页面地址
@GetMapping(value = "/login")
public String login(HttpServletRequest request, Map map) {
// 分别获取请求头和参数url中的原始访问路径
String referer = request.getHeader("Referer");
String url = request.getParameter("url");
System.out.println("referer= "+referer);
System.out.println("url= "+url);
// 如果参数url中已经封装了原始页面路径,直接返回该路径
if (url!=null && !url.equals("")){
map.put("url",url);
// 如果请求头本身包含登录,将重定向url设为空,让后台通过用户角色进行选择跳转
}else if (referer!=null && referer.contains("/login")){
map.put("url", "");
}else {
// 否则的话,就记住请求头中的原始访问路径
map.put("url", referer);
}
return "comm/login";
}
// 对Security拦截的无权限访问异常处理路径映射
@GetMapping(value = "/errorPage/{page}/{code}")
public String AccessExecptionHandler(@PathVariable("page") String page, @PathVariable("code") String code) {
return page+"/"+code;
}
}
编写Security认证授权配置类
在com.itheima.config下船舰一个用于整合Security进行安全控制的配置类SecurityConfig,并重写自定义用户认证和授权方法。
@EnableWebSecurity // 开启MVC security安全支持
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Value("${COOKIE.VALIDITY}")
private Integer COOKIE_VALIDITY;
/**
* 重写configure(HttpSecurity http)方法,进行用户授权管理
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// 1、自定义用户访问控制
http.authorizeRequests()
.antMatchers("/","/page/**","/article/**","/login").permitAll()
.antMatchers("/back/**","/assets/**","/user/**","/article_img/**").permitAll()
.antMatchers("/admin/**").hasRole("admin")
.anyRequest().authenticated();
// 2、自定义用户登录控制
http.formLogin()
.loginPage("/login")
.usernameParameter("username").passwordParameter("password")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
String url = httpServletRequest.getParameter("url");
// 获取被拦截的原始访问路径
RequestCache requestCache = new HttpSessionRequestCache();
SavedRequest savedRequest = requestCache.getRequest(httpServletRequest,httpServletResponse);
if(savedRequest !=null){
// 如果存在原始拦截路径,登录成功后重定向到原始访问路径
httpServletResponse.sendRedirect(savedRequest.getRedirectUrl());
} else if(url != null && !url.equals("")){
// 跳转到之前所在页面
URL fullURL = new URL(url);
httpServletResponse.sendRedirect(fullURL.getPath());
}else {
// 直接登录的用户,根据用户角色分别重定向到后台首页和前台首页
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
boolean isAdmin = authorities.contains(new SimpleGrantedAuthority("ROLE_admin"));
if(isAdmin){
httpServletResponse.sendRedirect("/admin");
}else {
httpServletResponse.sendRedirect("/");
}
}
}
})
// 用户登录失败处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest,HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
// 登录失败后,取出原始页面url并追加在重定向路径上
String url = httpServletRequest.getParameter("url");
httpServletResponse.sendRedirect("/login?error&url="+url);
}
});
// 3、设置用户登录后cookie有效期,默认值
http.rememberMe().alwaysRemember(true).tokenValiditySeconds(COOKIE_VALIDITY);
// 4、自定义用户退出控制
http.logout().logoutUrl("/logout").logoutSuccessUrl("/");
// 5、针对访问无权限页面出现的403页面进行定制处理
http.exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
// 如果是权限访问异常,则进行拦截到指定错误页面
RequestDispatcher dispatcher = httpServletRequest.getRequestDispatcher("/errorPage/comm/error_403");
dispatcher.forward(httpServletRequest, httpServletResponse);
}
});
}
/**
* 重写configure(AuthenticationManagerBuilder auth)方法,进行自定义用户认证
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 密码需要设置编码器
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 使用JDBC进行身份认证
String userSQL ="select username,password,valid from t_user where username = ?";
String authoritySQL ="select u.username,a.authority from t_user u,t_authority a," +
"t_user_authority ua where ua.user_id=u.id " +
"and ua.authority_id=a.id and u.username =?";
auth.jdbcAuthentication().passwordEncoder(encoder)
.dataSource(dataSource)
.usersByUsernameQuery(userSQL)
.authoritiesByUsernameQuery(authoritySQL);
}
}
此时重启项目进行访问,则只需要输入数据库中已有的用户信息就可以登录认证。
14、定时邮件发送
嵌入定时邮件任务,动态获取个人网站的流量数据。如每周、每月进行博客系统的访问量和评论量统计从而让博主更好了解个人网站的热度情况。下面将实现每月统计博客网站总访问量和总评论的功能。
邮件发送工具类实现
在com.itheima.utils下创建用于邮件发送服务的工具类MaiUtils并编写一个发送简单邮件的方法
/**
* @Description 邮件发送工具类
*/
@Component
public class MailUtils {
@Autowired
private JavaMailSenderImpl mailSender;
@Value("${spring.mail.username}")
private String mailfrom;
// 发送简单邮件
public void sendSimpleEmail(String mailto, String title, String content) {
// 定制邮件发送内容
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(mailfrom);
message.setTo(mailto);
message.setSubject(title);
message.setText(content);
// 发送邮件
mailSender.send(message);
}
}
邮件定时发送调度实现
邮件发送服务写完后,为了实现定时调度管理,需要编写一个定时任务管理类。
在com.itheima.web包下创建scheduletask包,并在该包下创建定时任务管理类ScheduleTask
在里面编写一个方法调用邮件工具类实现定时邮件发送任务
@Component
public class ScheduleTask {
@Autowired
private StatisticMapper statisticMapper;
@Autowired
private MailUtils mailUtils;
@Value("${spring.mail.username}")
private String mailto;
/**
* 定时邮件发送任务,每月1日中午12点整发送邮件
*/
@Scheduled(cron = "0 0 12 1 * ?")
// @Scheduled(cron = "0 */3 * * * ? ")
public void sendEmail(){
// 定制邮件内容
long totalvisit = statisticMapper.getTotalVisit();
long totalComment = statisticMapper.getTotalComment();
StringBuffer content = new StringBuffer();
content.append("博客系统总访问量为:"+totalvisit+"人次").append("\n");
content.append("博客系统总评论量为:"+totalComment+"人次").append("\n");
mailUtils.sendSimpleEmail(mailto,"个人博客系统流量统计情况",content.toString());
}
}
开启基于注解的定时任务
在项目启动类上使用@EnableScheduling注解
@EnableScheduling //开启定时任务注解功能支持
@SpringBootApplication
public class BlogSystemApplication {
public static void main(String[] args) {
SpringApplication.run(BlogSystemApplication.class, args);
}
}
登录账户密码
账户:admin
密码:123456