1、概述
1.1、介绍
Spring 诞生时是 Java 企业版(Java Enterprise Edition,JEE,也称 J2EE)的轻量级代替品。无需开发重量级的 Enterprise JavaBean(EJB),Spring 为企业级Java 开发提供了一种相对简单的方法,通过依赖注入和面向切面编程,用简单的Java 对象(Plain Old Java Object,POJO)实现了 EJB 的功能。
1.2、配置演变过程
(1)第一阶段:xml配置
在Spring 1.x时代,使用Spring开发满眼都是xml配置的Bean,随着项目的扩大,我们需要把xml配置文件放到不同的配置文件里,那时需要频繁的在开发的类和配置文件之间进行切换
(2)第二阶段:注解配置
在Spring 2.x 时代,随着JDK1.5带来的注解支持,Spring提供了声明Bean的注解(例如@Component、@Service),大大减少了配置量。主要使用的方式是应用的基本配置(如数据库配置)用xml,业务配置用注解。
(3)第三阶段:java配置
Spring 3.0 引入了基于 Java 的配置能力,这是一种类型安全的可重构配置方式,可以代替 XML。我们目前刚好处于这个时代,Spring4.x和Spring Boot都推荐使用Java配置。
(4)第四阶段:SpringBoot
Spring Boot 简化了基于Spring的应用开发,只需要“run”就能创建一个独立的、生产级别的Spring应用。Spring Boot为Spring平台及第三方库提供开箱即用的设置(提供默认设置),这样我们就可以简单的开始。多数Spring Boot应用只需要很少的Spring配置。
- 注意事项:可以使用SpringBoot创建java应用,并使用java –jar 启动它,或者采用传统的war部署方式。
- Spring Boot 主要目标是:
- 为所有 Spring 的开发提供一个从根本上更快的入门体验
- 开箱即用,但通过自己设置参数,即可快速摆脱这种方式。
- 提供了一些大型项目中常见的非功能性特性,如内嵌服务器、安全、指标,健康检测、外部化配置等
- 绝对没有代码生成,也无需 XML 配置。
2、项目架构
2.1、项目架构搭建
- pojo包:表示一个很普通的对象,不需要实现什么接口,没有任何的逻辑,有的只是属性,和空参构造函数还有getter/setter方法,其实就是获取和设置每一个属性值的方法。
- dao层:表示数据访问层,其中包含了你对数据库的所有操作,所以只要是你有对象要对数据库进行操作,都把这些方法放在dao层里,最好先写一个抽象类(interface)来封装所有的方法。
- 新建一个impl文件夹(implement)即来实现这个抽象类
- service层:处理完数据访问层,就应该处理对应的系统业务逻辑,同理也可以先构造一个抽象类,
- 新建一个impl文件夹:实现这个抽象类里面的方法。
- controller层次:在web包下面新建controller包,所以这肯定是要和前端页面打交道的。接收用户提交请求的代码,所以该层是会调用service里面的接口来实现业务流程。
2.2、POJO文件夹 - 创建相关实体类
- 业务需求:用户查看课程章节下的视频。
// 1、定义章节实体类
public class Chapter;
// 2、定义用户实体类
public class User;
// 3、定义视频实体类
public class Video implements Serializable;
2.3、DAO文件夹 - 静态数据调用接口
- 业务需求:用户登录数据和视频播放数据
// 1、模拟静态用户数据
public class UserMapper(){};
// 2、模拟静态视频数据
public class VideoMapper(){};
3、GET请求实战 - 查询功能实现
- 业务需求:用户查询课程的视频列表
3.1、DAO文件夹 - 定义读取数据
public class VideoMapper {
private static Map<Integer, Video> videoMap = new HashMap<>();
static {
videoMap.put(5,new Video(5,"小滴课堂面试专题第一季,300道大厂连环问"));
}
// 定义数据接口
public List<Video> listVideo(){
List<Video> list = new ArrayList<>();
list.addAll(videoMap.values());
return list;
}
}
3.2、Service文件夹 - 实现数据的业务逻辑处理
public interface VideoService {
List<Video> listVideo();
}
@Service
public class VideoServiceImpl implements VideoService {
@Autowired
private VideoMapper videoMapper;
@Override
public List<Video> listVideo() {
return videoMapper.listVideo();
}
}
3.3、Controller文件夹 - 读取数据
@RestController
@RequestMapping("/api/v1/pub/video")
public class VideoController {
@Autowired
private VideoService videoService;
//@RequestMapping(value = "list",method = RequestMethod.GET)
@GetMapping("list")
public JsonData list() throws JsonProcessingException {
List<Video> list = videoService.listVideo();
return JsonData.buildSuccess(list);
}
}
- 统一接口返回工具类
public class JsonData
3.4、启动项目
http://localhost:8080/api/v1/pub/video/list
4、POST请求实战
- 业务场景:表单提交,用户注册登录。
4.1、Controller文件夹 - 读取数据
@RestController
@RequestMapping("api/v1/pub/user")
public class UserController {
@Autowired
public UserService userService;
/**
* 登录接口
* @param user
* @return
*/
@PostMapping("login")
public JsonData login(@RequestBody User user){
System.out.println("user=" + user.toString());
String token = userService.login(user.getUsername(), user.getPwd());
return token !=null ? JsonData.buildSuccess(token) : JsonData.buildError("账号密码错误");
}
}
4.2、Service文件夹 - 实现数据的业务逻辑处理
public interface UserService {
String login(String username, String pwd);
List<User> listUser();
}
// 定义用户注册登录逻辑
@Service
public class UserServiceImpl implements UserService {
private static Map<String, User> sessionMap = new HashMap<>();
@Autowired
private UserMapper userMapper;
@Override
public String login(String username, String pwd) {
User user = userMapper.login(username,pwd);
if(user==null){
return null;
}else {
String token = UUID.randomUUID().toString();
System.out.println(token);
sessionMap.put(token,user);
return token;
}
}
@Override
public List<User> listUser() {
return userMapper.listUser();
}
}
4.3、DAO文件夹 - 定义读取用户数据接口
@Repository
public class UserMapper {
private static Map<String , User> userMap = new HashMap<>();
static {
userMap.put("jack",new User(1,"jack","123"));
userMap.put("xdclass-lw",new User(2,"xdclass-lw","123456"));
userMap.put("tom",new User(3,"tom","123456789"));
}
// 比对用户名是否存在 & 用户密码是否正确
public User login(String username, String pwd){
User user = userMap.get(username);
if(user == null){
return null;
}
if(user.getPwd().equals(pwd)){
return user;
}
return null;
}
public List<User> listUser(){
List<User> list = new ArrayList<>();
list.addAll(userMap.values());
return list;
}
}
4.4、启动项目查询结果
(1)POST注册用户
http://localhost:8080/api/v1/pub/user/login
(2)SET查询用户
http://localhost:8080/api/v1/pub/user/list
4.5、POST的数组提交
- 业务需求:新增视频json对象,章数组提交。
- 业务场景:json对象映射,数组对象提交接口开发。
@RestController
@RequestMapping("/api/v1/pub/video")
public class VideoController {
......
@PostMapping("save_video_chapter")
public JsonData saveVideoChapter(@RequestBody Video video){
System.out.println(video.toString());
return JsonData.buildSuccess(video);
}
}
- 调试运行
http://localhost:8080/api/v1/pub/video/save_video_chapter
5、SpringBoot定制JSON字段
5.1、JavaBean序列化为json常见框架
(1)性能:Jackson > FastJson > Gson > Json-lib 同个结构
(2)特点:空间换时间,时间换空间
5.2、jsckjson处理相关自动
(1)指定字段不返回:@JsonIgnore
(2)指定⽇期格式:@JsonFormat(pattern=“yyyy-MM-dd hh:mm:ss”,locale=“zh”,timezone=“GMT+8”)
(3)空字段不返回:@JsonInclude(Include.NON_NULL)
(4)指定别名:@JsonPropert
5.3、实战案例
(1)业务实现:过滤⽤户敏感信息。
@RestController
@RequestMapping("api/v1/pub/user")
public class UserController {
......
/**
* 列出全部用户
* @return
*/
@GetMapping("list")
public JsonData listUser(){
return JsonData.buildSuccess(userService.listUser());
}
}
// 过滤密码字段,即不返回密码
public class User {
......
@JsonIgnore
private String pwd;
}
(2)业务需求:视频创建时间返回⾃定义格式;
// 指定返回时间格式
public class Video implements Serializable {
......
@JsonProperty("create_time")
@JsonFormat(pattern="yyyy-MM-dd hh:mm:ss",locale="zh",timezone="GMT+8")
private Date createTime;
}
(3)业务需求:空字段不返回
// 指定返回时间格式
public class Video implements Serializable {
......
@JsonInclude(JsonInclude.Include.NON_NULL)
private List<Chapter> chapterList;
}
6、SpringBoot中的配置文件
6.1、SpringBoot的配置文件形式
(1)常⻅的配置⽂件格式
xml、properties、json、yaml
(2)Springboot⾥⾯常⽤xx.yml
- YAML(Yet Another Markup Language)
- 写 YAML 要⽐写 XML 快得多(⽆需关注标签或引号) 使⽤空格 Space 缩进表示分层,不同层次 之间的缩进可以使⽤不同的空格数⽬
- 注意:key后⾯的冒号,后⾯⼀定要跟⼀个空格,树状结构 xml、properties、json、yaml
server:
port: 8080 //设置启动端⼝号为8080
house:
family:
name: Doe
parents:
- John
- Jane
children:
- Paul
- Mark
- Simone
address:
number: 34
street: Main Street
city: Nowheretown
zipcode: 12345
(3)Springboot⾥⾯常⽤ xx.properties(推荐)
- Key=Value格式
- 语法简单,不容易出错
server.port=8082
#session失效时间,30m表示30分钟
server.servlet.session.timeout=30m
# Maximum number of connections that the server accepts and processes at
any given time.
server.tomcat.max-connections=10000
# Maximum size of the HTTP post content.
server.tomcat.max-http-post-size=2MB
server.tomcat.max-http-form-post-size=2MB
# Maximum amount of worker threads
server.tomcat.max-threads=200
6.2、SpringBoot注解配置⽂件映射属性和实体类实战
配置⽂件加载
(1)⽅式⼀
- ①Controller上⾯配置 @PropertySource({“classpath:resource.properties”})
- ②增加属性 @Value(“${test.name}”) private String name;
(2)⽅式⼆:实体类配置⽂件
- ①添加 @Component 注解;
- ②使⽤ @PropertySource 注解指定配置⽂件位置;
- ③使⽤ @ConfigurationProperties 注解,设置相关属性;
- ④必须 通过注⼊IOC对象Resource 进来 , 才能在类中使⽤获取的配置⽂件值。
- @Autowired private ServerSettings serverSettings;
6.3、properties文件配置实战案例
- properties文件
#微信支付的appid
wxpaay.appid=w18434343
#支付密钥
wxpay.sercret=dasjdkj1k
#微信支付商户号
wx.mechid=128138
- 配置文件注入
@RestController
@RequestMapping("api/v1/test")
@PropertySource({"classpath:pay.properties"})
public class TestController {
@Value("${wxpaay.appid}")
private String payAppid;
@Value("${wxpaay.appid}")
private String paySecret;
@GetMapping("get_conf")
public JsonData testConfig(){
Map<String,String> map = new HashMap<>();
map.put("payappid",payAppid);
map.put("paysecret",paySecret);
return JsonData.buildSuccess(map);
}
}
- 运行结果
http://localhost:8080/api/v1/test/get_conf
7、单元测试
- 单元测试:完成最小的软件设计单元验证工作,目标是确保模块被正确编码。
7.1、单元测试概述
(1)单元测试依赖
<!--springboot程序测试依赖,如果是⾃动创建项⽬默认添加-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency
(2)单元测试相关注解
序号 | 注解 | 概述 |
1 | @Runwith(SpringRunner.class) | 底层用junit |
2 | @SpringBootTest(Class=入口类) | 启动整个SpringBoot工程 |
3 | before | |
4 | test | |
5 | after | |
6 | TestCase.assertXXX | 断言,判断程序结果是否符合预期 |
7.2、入门案例
- 含义:选中测试类执行哪个测试类,选中所有测试类,则会先后执行。
@RunWith(SpringRunner.class)
@SpringBootTest(classes={HeimaApplication.class})
class HeimaApplicationTests {
@Before
void testOne() {
System.out.println("测试Before");
}
@Test
void testTwo() {
System.out.println("测试Two");
}
@Test
void testThree() {
System.out.println("测试Three");
}
@After
void testFour() {
System.out.println("测试After");
}
}
- 验证结果
7.3、实战案例之调用Controller-Service层接口
(1)Controller层接口单元测试
@RunWith(SpringRunner.class) //底层用junit SpringJUnit4ClassRunner
@SpringBootTest(classes={DemoProjectApplication.class})//启动整个springboot工程
public class UserTest {
@Autowired
private UserController userController;
@Test
public void loginTest(){
User user = new User();
user.setUsername("jack");
user.setPwd("1234");
JsonData jsonData = userController.login(user);
System.out.println(jsonData.toString());
// 断言:判断Code状态码是否为1
TestCase.assertEquals(0,jsonData.getCode());
}
}
- 验证结果
(2)Service层接口测试
@RunWith(SpringRunner.class) //底层用junit SpringJUnit4ClassRunner
@SpringBootTest(classes={DemoProjectApplication.class})//启动整个springboot工程
public class VideoTest {
@Autowired
private VideoService videoService;
@Before
public void testOne(){
System.out.println("这个是测试 before");
}
@Test
public void testVideoList(){
List<Video> videoList = videoService.listVideo();
TestCase.assertTrue(videoList.size()>0);
}
}
- 验证结果
8、SpringBoot全局异常处理
- 本质:在SpringBoot自带异常上进行封装返回JSON数据/页面使用户可读异常
8.1、全局异常处理概述
(1)引入案例
- Contoller异常逻辑
@RestController
@RequestMapping("api/v1/test")
@PropertySource({"classpath:pay.properties"})
public class TestController {
@GetMapping("list")
public JsonData testExt(){
int i = 1/0;
return JsonData.buildSuccess("");
}
}
- 测试结果
- 说明事项:
(2)全局异常介绍
- 配置全局异常应用场景:1/0、空指针等
- 配置好处:统一的错误页面或错误码、对用户更友好
(3)配置方式
- 第一步:类添加注解
- 返回异常页面:@ControllerAdvice,如果需要返回json数据,则⽅法需要加@ResponseBody
- 返回异常JSON数据:@RestControllerAdvice, 默认返回json数据,⽅法不需要加@ResponseBody
- 第二步:方法添加处理器
- 捕获全局异常,处理所有不可知的异常,@ExceptionHandler(value=Exception.class)
8.2、全局异常处理 - 返回JSON数据
①添加自定义异常处理器
/**
* 标记这个是一个异常处理类
*/
@RestControllerAdvice
public class CustomExtHandler {
@ExceptionHandler(value = Exception.class)
JsonData handlerException(Exception e, HttpServletRequest request){
return JsonData.buildError("服务端出问题了", -2);
}
}
②Contoller异常逻辑
@RestController
@RequestMapping("api/v1/test")
@PropertySource({"classpath:pay.properties"})
public class TestController {
@GetMapping("list")
public JsonData testExt(){
int i = 1/0;
return JsonData.buildSuccess("");
}
}
③测试结果
8.2、全局异常处理 - 返回页面
(1)概述
- 返回自定义异常界面,需要引入thymeleaf依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(2)实战案例
①自定义异常处理类
/**
* 标记这个是一个异常处理类
*/
//@RestControllerAdvice
@ControllerAdvice
public class CustomExtHandler {
@ExceptionHandler(value = Exception.class)
Object handlerException(Exception e, HttpServletRequest request){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error.html");
System.out.println(e.getMessage());
modelAndView.addObject("msg",e.getMessage());
return modelAndView;
}
}
②自定义html.xml文件
<!DOCTYPE html>
<html xmlns:th="http://www/thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
欢迎学习 这个是自定义异常界面
<p th:text="${msg}"> </p>
</body>
</html>
③测试结果
9、SpringBoot过滤器和拦截器
- 本质:拦截器和过滤器都是AOP思想体现,都可以实现权限检查、日志记录
context-param (由外到内)
-->listener(监听器)
-->filter(过滤器)
-->Servlet
-->interceptor(拦截器)
-->Controller(控制器)
8.1、过滤器介绍
过滤器是在**web应用启动的时候初始化一次**, 在web应用停止的时候销毁,可以**对请求的URL进行过滤,** **对敏感词过滤**,挡在拦截器的外层
- 执行顺序
- ①用户/服务器请求/返回servlet或者html会先经过Filter
- ②执行Filter过滤代码,进行终端或者放行浏览器
- 应用场景
- 浏览器对服务器的请求:先经过过滤器,再到达服务器。
- 服务器对浏览器的响应:先经过过滤器,最后响应给浏览器。
8.2、过滤器实战
- 创建自定义过滤器方式:使⽤Servlet3.0的注解进⾏配置步骤如下
第一步:启动类⾥⾯增加 @ServletComponentScan,进⾏扫描
第二步:新建⼀个Filter类,继承Filter并实现相关方法,并实现对应的接⼝
第三步:@WebFilter 标记⼀个类为filter,被spring进⾏扫描
- urlPatterns:拦截规则,⽀持正则
- 处理逻辑:控制chain.doFilter的⽅法的调⽤,来实现是否通过放⾏ ,不放⾏,web应⽤resp.sendRedirect(“/index.html”) 或者 返回json字符串
- 实战案例
①启动类⾥⾯增加 @ServletComponentScan
@SpringBootApplication
@ServletComponentScan
public class DemoProjectApplication {
public static void main(String[] args) {
SpringApplication.run(DemoProjectApplication.class, args);
}
}
②新建所需要拦截URL,即对应Controller类
@RestController
@RequestMapping("api/v1/pri/order")
public class VideoOrderController {
@RequestMapping("save")
public JsonData saveOrder(){
return JsonData.buildSuccess("下单成功");
}
}
③新建Filter类,拦截对应的URL
@WebFilter(urlPatterns = "/api/v1/pri/*", filterName = "loginFilter")
public class LoginFilter implements Filter {
/**
* 容器加载的时候
* @param filterConfig
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init LoginFilter======");
}
// 过滤处理逻辑;
/**
* 容器销毁的时候
*/
@Override
public void destroy() {
System.out.println("destroy LoginFilter======");
}
}
- 测试结果:无结果显示
- IDEA响应结果
8.3、拦截器介绍
Interceptor 在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截然后在之前或之后加入某些操作。比如日志,安全等。一般拦截器方法都是通过动态代理的方式实现。可以通过它来进行权限验证,或者判断用户是否登陆,或者是像12306 判断当前时间是否是购票时间。
- 拦截器执行流程:在 preHandle 中如果返回 false,那么后续的流程将不被执行,这可能也是拦截器命名的由来。
8.4、拦截器实战
(1)第一步:定义拦截器配置类实现WebMvcConfigurer类
(2)第二步:自定义拦截器:HandlerInterceptor
①preHandle:调用Controller某个方法之前
②postHandle:Controller之后调用,视图渲染之前,如果控制器Controller出现了异常,则不会执行此方法
③afterCompletion:不管有没有异常,这个afterCompletion都会被调用,用于资源清理。
(3)第三步:将自定义拦截器配置到拦截器配置类中。
- 实战案例
①拦截器配置类:用于注册拦截器
/**
* 拦截器配置类
*/
@Configuration
class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
}
}
②自定义拦截器
class LoginIntercepter implements HandlerInterceptor {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("LoginIntercepter preHandle =====");
String token = request.getHeader("token");
if(StringUtils.isEmpty(token)){
token = request.getParameter("token");
}
if(!StringUtils.isEmpty(token)){
//判断token是否合法
User user = UserServiceImpl.sessionMap.get(token);
if(user!=null){
return true;
}else {
JsonData jsonData = JsonData.buildError("登录失败,token无效",-2);
String jsonStr = objectMapper.writeValueAsString(jsonData);
renderJson(response,jsonStr);
return false;
}
}else {
JsonData jsonData = JsonData.buildError("未登录",-3);
String jsonStr = objectMapper.writeValueAsString(jsonData);
renderJson(response,jsonStr);
return false;
}
//return HandlerInterceptor.super.preHandle(request,response,handler);
}
private void renderJson(HttpServletResponse response,String json){
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
try(PrintWriter writer = response.getWriter()){
writer.print(json);
}catch (Exception e){
e.printStackTrace();
}
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("LoginIntercepter postHandle =====");
HandlerInterceptor.super.postHandle(request,response,handler,modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("LoginIntercepter afterCompletion =====");
HandlerInterceptor.super.afterCompletion(request,response,handler,ex);
}
}
③在拦截器配置类中添加拦截器
/**
* 拦截器配置类
*/
@Configuration
class CustomWebMvcConfigurer implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(getLoginInterceptor()).addPathPatterns("/api/v1/pri/**");
registry.addInterceptor(new TwoIntercepter()).addPathPatterns("/api/v1/pri/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
@Bean
public LoginIntercepter getLoginInterceptor(){
return new LoginIntercepter();
}
}
- 验证结果
8.5、过滤器和拦截器区别
- 主要区别:
过滤器 | 拦截器 | |
关注点 | web请求 | Action(部分web请求) |
实现方式 | 函数回调 | 动态代理 |
级别 | 系统级 | 非系统级 |
深度 | Servlet前后 | 方法前后 |
(1)触发时机
- 过滤器:只能在请求的前后使用
- 拦截器:可以详细到每个方法。
(2)实现原理
- 过滤器:回调函数实现,通过Servlet容器回调完成
- 拦截器:基于反射实现,通过动态代理完成
(3)生命周期
- 过滤器:由Servlet容器管理
- 拦截器:由IOC容器来管理
(4)应用场景
- 过滤器:可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息,筛选出request中的部分
- 登录验证:判断用户是否登录。
- 权限验证:判断用户是否有权限访问资源,如校验token
- 日志记录:记录请求操作日志(用户ip,访问时间等),以便统计请求访问量。
- 性能监控:监控请求处理时长等
- 拦截器:可以拿到你请求的控制器和方法,却拿不到请求方法的参数。,主要用于安全方面,比如终止一些流程
- (1)过滤敏感词汇(防止sql注入)
- (2)设置字符编码
- (3)URL级别的权限访问控制
- (4)压缩响应信息
10、SpringBoot整合模板引擎thymeleaf和Fk
10.1、Starter & 模板引擎介绍
(1)Starter介绍
starter主要简化依赖⽤的,spring-boot-starter-web->⾥⾯包含多种依赖 查看 pom⽂件 spring-boot-starter-parent -> spring-boot-dependencies ⾥⾯综合的 很多依赖包
(2)常见模板引擎
①JSP(后端渲染,消耗性能)
Java Server Pages 动态⽹⻚技术,由应⽤服务器中的JSP引擎来编译和执⾏,再将⽣成的整个页面返回给客户端,不再使用。
②Freemarker
FreeMarker Template Language(FTL) ⽂件⼀般保存为 xxx.ftl,严格依赖MVC模式,不依赖Servlet容器(不占⽤JVM内存)。
③Thymeleaf (主推)
轻量级的模板引擎(复杂逻辑业务的不推荐,解析DOM或者XML会占⽤多的内存) ,可以直接在浏览器中打开且正确显示模板⻚⾯ 直接是html结尾,直接编辑xdlcass.net/user/userinfo.html。
10.2、Springboot引入Freemarker模板引擎
(1)pom文件添加依赖
<!-- 引⼊freemarker模板引擎的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
(2)application文件中配置Freemarker
# 是否开启thymeleaf缓存,本地为false,⽣产建议为true
spring.freemarker.cache=false
spring.freemarker.charset=UTF-8
spring.freemarker.allow-request-override=false
spring.freemarker.check-template-location=true
#类型
spring.freemarker.content-type=text/html
spring.freemarker.expose-request-attributes=true
spring.freemarker.expose-session-attributes=true
#⽂件后缀
spring.freemarker.suffix=.ftl
#路径
spring.freemarker.template-loader-path=classpath:/templates/
(3)建立文件夹
1)src/main/resources/templates/fm/user/
2)建⽴⼀个index.ftl
3)user⽂件夹下⾯建⽴⼀个user.html
- HTML文件内容如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
这是freemaker整合index.html页面
<h1>payAppid ${setting.payAppid}</h1>
<h1>paySecret ${setting.paySecret}</h1>
</body>
</html>
(4)创建调用测试类
@Controller
@RequestMapping("freemaker")
class FreemakerController {
@Autowired
private WXConfig wxConfig;
@GetMapping("test")
public String index(ModelMap modelMap){
modelMap.addAttribute("setting",wxConfig);
//不用加后缀,因为配置文件里面已经指定了后缀
return "user/fm/index";
}
}
- 测试结果
10.3、Springboot引入Thymeleaf模板引擎
(1)官网地址
https://www.thymeleaf.org/doc/articles/thymeleaf3migration.html
(2)pom文件添加thymeleaf依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
(3)application文件添加thymeleaf配置
#开发时关闭缓存,不然没法看到实时⻚⾯
spring.thymeleaf.cache=false
spring.thymeleaf.mode=HTML5
#前缀
spring.thymeleaf.prefix=classpath:/templates/
#编码
spring.thymeleaf.encoding=UTF-8
#类型
spring.thymeleaf.content-type=text/html
#名称的后缀
spring.thymeleaf.suffix=.html
(4)建立文件夹
1)src/main/resources/templates/tl/
2)建⽴⼀个index.html
(5)创建调用测试类
@Controller
@RequestMapping("tpl")
class TemplateController {
@Autowired
private WXConfig wxConfig;
@GetMapping("thymeleaf")
public String index2(ModelMap modelMap){
modelMap.addAttribute("setting",wxConfig);
//不用加后缀,因为配置文件里面已经指定了后缀
return "tl/index";
}
}
- 测试结果
11、SpringBoot使用slf4j进行日志管理
11.1、slf4j介绍
只需要按统一的方式写记录日志的代码,而无需关心日志是通过哪个日志系统,以什么风格输出的。因为它们取决于部署项目时绑定的日志系统。例如,在项目中使用了 slf4j 记录日志,并且绑定了 log4j(即导入相应的依赖),则日志会以 log4j 的风格输出;后期需要改为以 logback 的风格输出日志,只需要将 log4j 替换成 logback 即可,不用修改项目中的代码。这对于第三方组件的引入的不同日志系统来说几乎零学习成本,况且它的优点不仅仅这一个而已,还有简洁的占位符的使用和日志级别的判断
- 使用方式:直接使用 LoggerFactory 创建即可
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Test {
private static final Logger logger = LoggerFactory.getLogger(Test.class);
// ……
}
11.2、 application.yml 中对日志的配置
logging:
config: logback.xml
level:
com.itcodai.course03.dao: trace
(1)logging.config 是用来指定项目启动的时候,读取哪个配置文件,这里指定的是日志配置文件是根路径下的 logback.xml 文件,关于日志的相关配置信息,都放在 logback.xml 文件中了。
(2)logging.level 是用来指定具体的 mapper 中日志的输出级别,上面的配置表示 com.itcodai.course03.dao 包下的所有 mapper 日志输出级别为 trace,会将操作数据库的 sql 打印出来,开发时设置成 trace 方便定位问题,在生产环境上,将这个日志级别再设置成 error 级别即可。
常用的日志级别按照从高到低依次为:ERROR、WARN、INFO、DEBUG。
11.3、logback.xml配置文件解析
(1)定义日志输出格式和存储路径
<configuration>
<property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" />
<property name="FILE_PATH" value="D:/logs/course03/demo.%d{yyyy-MM-dd}.%i.log" />
</configuration>
- 首先定义一个格式,**命名为 “LOG_PATTERN”,**该格式中 %date 表示日期,%thread 表示线程名,%-5level 表示级别从左显示5个字符宽度,%logger{36} 表示 logger 名字最长36个字符,%msg 表示日志消息,%n 是换行符。
- 然后再定义一下名为 “FILE_PATH” 文件路径,日志都会存储在该路径下。%i 表示第 i 个文件,当日志文件达到指定大小时,会将日志生成到新的文件里,这里的 i 就是文件索引,日志文件允许的大小可以设置,下面会讲解。这里需要注意的是,不管是 windows 系统还是 Linux 系统,日志存储的路径必须要是绝对路径。
(2)定义控制台输出
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- 按照上面配置的LOG_PATTERN来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
</configuration>
- 使用节点设置个控制台输出(
class="ch.qos.logback.core.ConsoleAppender"
)的配置,定义为 “CONSOLE”。使用上面定义好的输出格式(LOG_PATTERN)来输出,使用${}
引用进来即可。
(3)定义日志文件相关参数
<configuration>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 按照上面配置的FILE_PATH路径来保存日志 -->
<fileNamePattern>${FILE_PATH}</fileNamePattern>
<!-- 日志保存15天 -->
<maxHistory>15</maxHistory>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<!-- 单个日志文件的最大,超过则新建日志文件存储 -->
<maxFileSize>10MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
</rollingPolicy>
<encoder>
<!-- 按照上面配置的LOG_PATTERN来打印日志 -->
<pattern>${LOG_PATTERN}</pattern>
</encoder>
</appender>
</configuration>
- 使用 定义一个名为 “FILE” 的文件配置,主要是配置日志文件保存的时间、单个日志文件存储的大小、以及文件保存的路径和日志的输出格式。
(4)定义日志输出级别
<configuration>
<logger name="com.itcodai.course03" level="INFO" />
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="FILE" />
</root>
</configuration>
- 使用 来定义一下项目中默认的日志输出级别,这里定义级别为 INFO,然后针对 INFO 级别的日志,使用 引用上面定义好的控制台日志输出和日志文件的参数。这样 logback.xml 文件中的配置就设置完了。
12、SpringBoot的定时任务和异步任务
12.1、定时任务
- 定时任务场景:定时消息提醒、订单通知
(1)相关注解
序号 | 注解 | 备注 |
1 | @EnableScheduling | 作用在启动类上,开启基于注解的定时任务 |
2 | @Component | 表示该类被容器扫描 |
3 | @Scheduled | 作用在方法上,表示该方法为定时方法 |
(2)入门案例
- 定时任务
@Component
public class ScheduledServiceImpl {
@Scheduled(fixedRate = 2000) // 每 4 秒执行一次
public void hello(){
System.out.println("hello ... ");
}
}
- 启动类开启定时任务
@SpringBootApplication(scanBasePackages = "com.springboot.heima")
@EnableScheduling // 启动定时任务
public class HeimaApplication {
public static void main(String[] args) {
SpringApplication.run(HeimaApplication.class, args);
}
}
- 验证结果
(3)多种定时表达式
序号 | 任务表达式 | 备注 |
1 | cron=“*/1 * * * * *” | 定时任务表达式 |
2 | fixedRate | 定时多久执⾏⼀次(上⼀次开始执⾏时间点后xx秒再次执⾏) |
3 | fixedDelay | 上⼀次执⾏结束时间点后xx秒再次执⾏ |
- 在线cron表达式生成器:https://cron.qqe2.com/
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期天凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
12.2、异步任务
- 异步任务场景:处理Log、发送邮件短信等
(1)相关注解
序号 | 注解 | 备注 |
1 | @EnableAsync | 作用在启动类上,开启基于注解的异步任务 |
2 | @Component | 表示该类被容器扫描 |
3 | @Async | 作用在方法上,说明该方法是异步方法;作用在类上,表示该类的所有方法都是异步方法 |
(2)入门案例
- 异步任务定义
@Component
public class AsyncServiceImpl {
@Async
public Future<String> execTaskA() throws InterruptedException {
System.out.println("TaskA开始");
long star = new Date().getTime();
Thread.sleep(5000);
long end = new Date().getTime();
System.out.println("TaskA结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskA结束");
}
@Async
public Future<String> execTaskB() throws InterruptedException {
System.out.println("TaskB开始");
long star = new Date().getTime();
Thread.sleep(3000);
long end = new Date().getTime();
System.out.println("TaskB结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskB结束");
}
@Async
public Future<String> execTaskC() throws InterruptedException {
System.out.println("TaskC开始");
long star = new Date().getTime();
Thread.sleep(4000);
long end = new Date().getTime();
System.out.println("TaskC结束,耗时毫秒数:" + (end - star));
return new AsyncResult<>("TaskC结束");
}
}
- 启动类添加异步注解
@SpringBootApplication(scanBasePackages = "com.springboot.heima")
@EnableScheduling // 启动定时任务
@EnableAsync // 启动异步任务
public class HeimaApplication {
public static void main(String[] args) {
SpringApplication.run(HeimaApplication.class, args);
}
}
- 进行异步测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes={HeimaApplication.class})
class AsyncTaskApplicationTests {
// 重点:异步任务封装到类⾥⾯,不能直接写到Controller
@Autowired
AsyncServiceImpl asyncTask;
@Test
public void testAsyncTask() throws InterruptedException {
long star = new Date().getTime();
System.out.println("任务开始,当前时间" +star );
Future<String> taskA = asyncTask.execTaskA();
Future<String> taskB = asyncTask.execTaskB();
Future<String> taskC = asyncTask.execTaskC();
//间隔一秒轮询 直到 A B C 全部完成
while (true) {
if (taskA.isDone() && taskB.isDone() && taskC.isDone()) {
break;
}
Thread.sleep(1000);
}
long end = new Date().getTime();
System.out.println("任务结束,当前时间" + end);
System.out.println("总耗时:"+(end-star));
}
}
- 验证结果
(3)注意事项
- 要把异步任务封装到类⾥⾯,不能直接写到Controller
- 增加Future 返回结果 AsyncResult(“task执⾏完成”)
- 如果需要拿到结果 需要判断全部的 task.isDone()
12.3、异步任务 & 自定义线程池
- 业务场景:为异步任务规划自定义线程池
(1)默认线程池
- 线程池作用:
- ①防止资源占用无限的扩张
- ②调用过程省去资源的创建和销毁所占用的时间
在springboot配置文件中加入上面的配置,即可实现ThreadPoolTaskExecutor 线程池,如果没有配置线程池的话,springboot会自动配置一个ThreadPoolTaskExecutor 线程池到bean当中。
# 核心线程数
spring.task.execution.pool.core-size=8
# 最大线程数
spring.task.execution.pool.max-size=16
# 空闲线程存活时间
spring.task.execution.pool.keep-alive=60s
# 是否允许核心线程超时
spring.task.execution.pool.allow-core-thread-timeout=true
# 线程队列数量
spring.task.execution.pool.queue-capacity=100
# 线程关闭等待
spring.task.execution.shutdown.await-termination=false
spring.task.execution.shutdown.await-termination-period=
# 线程名称前缀
spring.task.execution.thread-name-prefix=task-
(2)自定义线程池
- 业务场景:我们希望将系统内的一类任务放到一个线程池,另一类任务放到另外一个线程池,所以使用Spring Boot自带的任务线程池就捉襟见肘了。
// 创建一个线程池配置类TaskConfiguration,并配置一个任务线程池对象taskExecutor
@Configuration
public class TaskConfiguration {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
return executor;
}
}
(3)实战案例 - 使用自定义线程池改造上个案例
- 自定义线程池
@Configuration
public class TaskConfiguration {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(200);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("taskExecutor-");
executor.setRejectedExecutionHandler(new CallerRunsPolicy());
return executor;
}
}
- 使用自定义线程进行异步任务
@Component
public class AsyncServiceImpl {
@Async("taskExecutor")
public Future<String> execTaskA() throws InterruptedException {
System.out.println("TaskA开始");
long star = new Date().getTime();
Thread.sleep(5000);
long end = new Date().getTime();
System.out.println("TaskA结束,耗时毫秒数:" + (end - star) + Thread.currentThread().getName());
return new AsyncResult<>("TaskA结束");
}
@Async("taskExecutor")
public Future<String> execTaskB() throws InterruptedException {
System.out.println("TaskB开始");
long star = new Date().getTime();
Thread.sleep(3000);
long end = new Date().getTime();
System.out.println("TaskB结束,耗时毫秒数:" + (end - star) + Thread.currentThread().getName());
return new AsyncResult<>("TaskB结束");
}
@Async("taskExecutor")
public Future<String> execTaskC() throws InterruptedException {
System.out.println("TaskC开始");
long star = new Date().getTime();
Thread.sleep(4000);
long end = new Date().getTime();
System.out.println("TaskC结束,耗时毫秒数:" + (end - star) + Thread.currentThread().getName());
return new AsyncResult<>("TaskC结束");
}
}
- 验证结果