运行DEMO可以在百度网盘中获取:通过网盘分享的文件:https://pan.baidu.com/s/1Ut9-STQL_8ColkAa4-kMkQ?pwd=edar
资料来源:https://heuqqdmbyk.feishu.cn/wiki/space/7413668442156498972?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home
Spring 官网:https://spring.io
SpringBootWeb快速入门 需求:使用 SpringBoot 开发一个web应用,浏览器发起请求/hello后,给浏览器返回字符串”Hello World”
创建SprintBoot工程,并勾选web开发相关依赖
定义HelloController类,添加方法hello,并添加注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package app.netlify.norlcyan.controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;@RestController public class HelloController { @RequestMapping("/hello") public String hello () { System.out.println("Hello World" ); return "Hello World" ; } }
运行测试
在浏览器中输入网址:127.0.0.1:8080/hello,网页中显示Hello World表示启动成功。
HTTP协议 概念:超文本传输协议,规定了浏览器和服务器之间数据传输的规则
特点:
基于TCP协议:面向连接,安全
基于请求-响应模型的:一次请求对应一次响应
HTTP协议是无状态的协议:对于事物处理没有记忆能力。每次请求-响应都是独立的
请求协议 请求行:请求数据第一行(请求方式、资源路径、协议)
请求头:第二行开始,格式:key : value
Key
说明
Host
请求的主机名
User-Agent
浏览器版本
Accept
表示浏览器能够接收的资源类型,如test/*,image/*或者*/*表示所有
Accept-Language
表示浏览器偏好的语言,服务器可以据此返回不同语言的网页
Accept-Encoding
表示浏览器可以支持的压缩类型
Content-Type
请求主体的数据类型
Content-Length
请求主体的大小(单位:字节)
请求体:POST请求,存放请求参数
请求方式-GET:请求参数在请求行中,没有请求体,GET请求大小是有限制的
请求方式-POST:请求参数在请求体中,POST请求大小是没有限制的
响应协议 响应行:响应数据第一行(协议、状态码、描述)
状态码
说明
1xx
响应中-临时状态码,表示请求已经接收,告诉客户端应该继续请求或者如果它已经完成则忽略它
2xx
成功-表示请求已经被成功接收,处理已完成
3xx
重定向-重定向到其他地方;让客户端再发起一次请求以完成整个处理
4xx
客户端错误-处理发生错误,责任在客户端。如:请求了不存在的资源、客户端未被授权、禁止访问等
5xx
服务器错误-处理发生错误,责任在服务端。如:程序抛出异常等
响应头:第二行开始,格式:key : value
Key
说明
Content-Type
表示该响应内容的类型,例如:text/html,application/json
Content-Length
表示该响应内容的长度(字节数)
Content-Encoding
表示该响应压缩算法,如:gzip
Cache-Control
指示客户端应如何缓存,例如:max-age=300表示可以最多缓存300秒
Set-Cookie
告诉浏览器为当前页面所在的域设置cookie
响应体:最后一部分,存放响应数据
Web服务器 Web服务器是一个软件程序,对HTTP协议的操作进行封装,使得程序员不必直接对协议进行操作,让Web开发更加便捷。主要功能是“提供上网信息浏览服务”
Tomcat
概念:Tomcat是Apache软件基金会的一个核心项目,是一个开源免费的轻量级Web服务器,支持Servlet/JSP少量JavaEE规范
Tomcat也被称为Web容器、Servlet容器。Servlet程序需要依赖于Tomcat才能运行
官网:https://tomcat.apache.org
Tomcat部署项目:将项目放置到webapps目录下,即部署完成
请求响应
请求(HTTPServletRequest):获取请求数据
响应(HTTPServletResponse):设置响应数据
BS架构:Browser/Server ,浏览器/服务器架构模式。客户端只需要浏览器,应用程序的逻辑和数据都存储在服务端
简单参数
原始方式:在原始的Web程序中,获取请求参数,需要通过HttpServletRequest对象手动获取
1 2 3 4 5 6 7 8 @RequestMapping("/simpleParam") public String simpleParam (HttpServletRequest request) { String name = request.getParameter("name" ); String agestr = request.getParameter("age" ); int age = Integer.parseInt(ageStr); System.out.println(name + ":" + age); return "OK" ; }
SpringBoot方式:参数名与形参变量名相同,定义形参即可接收参数(会自动转换数据类型)
1 2 3 4 5 @RequestMapping("/simpleParam") public String simpleParam (String name,Integer age) { System.out.println(name + ":" + age); return "OK" ; }
如果方法形参名称与请求参数名称不匹配,可以使用@RequestParam
完成映射
1 2 3 4 5 @RequestMapping("/simpleParam") public String simpleParam (@RequestParam(name="name") String username,Integer age) { System.out.println(username + ":" + age); return "OK" ; }
注意:@RequestParam
中的required
属性默认为true
,代表该请求参数必须传递,如果不传递将报错。如果该参数是可选的,可以将required属性设置为false
1 2 3 4 5 @RequestMapping("/simpleParam") public String simpleParam (@RequestParam(name="name",required=false) String username,Integer age) { System.out.println(username + ":" + age); return "OK" ; }
实体参数 简单实体参数
简单实体参数:请求参数与形参对象属性名相同,定义POJO
接收即可
示例:
生成一个User类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package app.netlify.norlcyan.POJO;public class User { private String name; private Integer age; public User () {}; public User (String name, Integer age) { this .name = name; this .age = age; } @Override public String toString () { return "User{" + "name='" + name + '\'' + ", age=" + age + '}' ; } public String getName () { return name; } public void setName (String name) { this .name = name; } public Integer getAge () { return age; } public void setAge (Integer age) { this .age = age; } }
保证类中有正确的”getter”和”setter”
编写处理函数:
1 2 3 4 5 6 @RequestMapping("/simplePojo") public String simplePojo (User u) { System.out.println(u); return "OK" ; }
其中形参是之前创建的User类
发送GET请求:
1 http://127.0.0.1:8080/simplePojo?name=张三&age=20
控制台打印以下结果:
复杂实体参数 复杂实体参数:请求参数名与形参对象属性名相同,按照对象层次结构关系即可接收嵌套POJO属性参数
以简单实体参数为例,只需要额外添加一个Address
对象即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package app.netlify.norlcyan.POJO;public class Address { private String province; private String city; public String getProvince () { return province; } public void setProvince (String province) { this .province = province; } public String getCity () { return city; } public void setCity (String city) { this .city = city; } @Override public String toString () { return "Address{" + "province='" + province + '\'' + ", city='" + city + '\'' + '}' ; } }
处理函数没有区别
发送GET请求:
1 http://127.0.0.1:8080/complexPojo?name=张三GET&age=44&address.province=江苏&address.city=苏州
数组集合参数 数组 数组参数:请求参数名与形参数组名称相同 且请求参数为多个,定义数组类型形参即可接收参数
1 2 3 4 5 @RequestMapping("/arrayParam") public String arrayParam (String[] hobby) { System.out.println(Arrays.toString(hobby)); return "OK" ; }
发送GET请求:
1 http://127.0.0.1:8080/arrayParam?hobby=打游戏&hobby=敲代码&hobby=看电影
集合 集合参数:请求参数名与形参集合名称相同且请求参数为多个,@RequestParam
绑定参数关系(不加这个注解会默认将参数添加到数组中而不是集合中)
1 2 3 4 5 @RequestMapping("/listParam") public String listParam (@RequestParam List<String> hobby) { System.out.println(hobby); return "OK" ; }
发送GET请求:
1 http://127.0.0.1:8080/listParam?hobby=打游戏&hobby=敲代码&hobby=看电影
日期参数 日期参数:使用@DateTimeFormat
注解完成日期参数格式转换
1 2 3 4 5 @RequestMapping("/dateParam") public String dateParam (@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") LocalDateTime updateTime) { System.out.println(updateTime); return "OK" ; }
发送GET请求:
1 http://127.0.0.1:8080/dateParam?updateTime=2024-10-07 21:50:30
JSON参数 GET请求并不能直接使用JSON参数
JSON参数:JSON数据键名与形参对象属性名相同,定义POJO类型形参即可接收参数,需要使用@RequestBody
注解
1 2 3 4 5 @RequestMapping("/jsonParam") public String jsonParam (@RequestBody User user) { System.out.println(user); return "OK" ; }
User对象和之前的复杂实体参数是一样的
发送POST请求:
1 http://127.0.0.1:8080/jsonParam
在Postman中选择body➡raw,选择JSON格式,JSON数据如下
1 2 3 4 5 6 7 8 { "name" : "Tom" , "age" : 21 , "address" : { "province" : "江苏" , "city" : "苏州" } }
路径参数 路径参数:通过请求URL直接传递参数,使用{...}
来标识该路径参数,需要使用@PathVariable
获取路径参数
1 2 3 4 5 @RequestMapping("/path/{id}") public String pathParam (@PathVariable Integer id) { System.out.println(id); return "OK" ; }
发送GET请求:
1 http://127.0.0.1:8080/path/1
URL中的1
可以变为其他整数,并且发送后控制台可以获取到该数值
响应 ResponseBody @ResponseBody注解:
类型:方法注解、类注解
位置:Controller方法上/类上
作用:将方法返回值直接响应,如果返回值类型是实体对象/集合,将会转换为JSON格式响应
说明:@RestController = @Controller + @ResponseBody;
返回对象:
1 2 3 4 5 @RequestMapping("/getUserInfo") public User getUserInfo (User u) { System.out.println(u); return u; }
返回集合:
1 2 3 4 5 6 @RequestMapping("/getList") public List<String> getList (@RequestParam List<String> hobby) { System.out.println(hobby); return hobby; }
统一响应结果 使用一个实体对象,将响应的结果进行统一处理
1 2 3 4 5 6 7 8 public static Result{ private Integer code; private String msg; private Object data; }
分层解耦 三层架构
controller:控制层,接收前端发送的请求,对请求进行处理,并响应数据
service:业务逻辑层,处理具体的业务逻辑
dao:数据访问层(Data Access Object)(持久层)。负责数据访问操作,包括数据的增删改查
分层解耦 耦合:衡量软件中各个层/各个模块的依赖关联程度
内聚:软件中各个功能模块内部的功能联系
控制反转:Inversion Of Control,简称IOC。对象的创建控制权由程序自身转移到外部(容器),这种思想称为控制反转
依赖注入:Dependecy Injection,简称DI。容器为应用程序提供运行时,所依赖的资源,称之为依赖注入
Bean对象:IOC容器中创建、管理的对象,称之为Bean
实现思路 实现思路如下:
将项目中的类交给IOC容器管理(IOC,控制反转)
应用程序运行时需要什么对象,直接依赖容器为其提供(DI,依赖注入)
核心注解 @Component:
@Component
注解用于将一个类标记为Spring的组件,使其成为Spring容器管理的Bean。Spring会自动扫描标有@Component
注解的类,并将其实例化、配置并注册到应用程序上下文中。
@Autowired
@Autowired
注解用于自动注入Spring容器中的Bean。这意味着Spring会自动将需要的Bean注入到标注了@Autowired
的字段、构造函数或方法中。
IOC详解
要把某个对象交给IOC容器管理,需要在对应的类上加上如下注解之一:
注解
说明
位置
@Component
声明bean的基础注解
不属于以下三类时,用此注解
@Controller
@Component的衍生注解
标注在控制层类上
@Service
@Component的衍生注解
标注在业务层类上
@Repository
@Component的衍生注解
标注在数据访问层类上(由于与mybatis整合,用的较少)
前面声明bean的四大注解,要想生效,还需要被组件扫描注解@ComponentScan扫描
该注解虽然没有显式配置,但是实际上已经包含在了启动类声明注解@SpringBootApplication中,默认扫描的范围是启动类所在包及其子包
DI详解 基于@Autowired进行依赖注入的常见方式有如下三种:
属性注入
1 2 3 4 5 6 @RestController public class UserController { @Autowired private UserService userService; }
构造函数注入
1 2 3 4 5 6 7 8 @RestController public class UserController { private final UserService userService; @Autowired public UserController (UserService userService) { this .userService = userService; } }
setter注入
1 2 3 4 5 6 7 8 @RestController public class UserController { private final UserService userService; @Autowired public void setUserController (UserService userService) { this .userService = userService; } }
属性注入——优点:代码简洁、方便快速开发
属性注入——缺点:隐藏了类之间的依赖关系、可能会破坏类的封装性
构造函数注入优点——能清晰地看到类地依赖关系、提高了代码的安全性
构造函数注入缺点——代码繁琐、如果构造参数过多,可能会导致构造函数臃肿
构造函数注入注意点——如果只有一个构造函数,@Autowired注解可以省略
setter注入优点——保持了类的封装性,依赖关系更清晰
setter注入缺点——需要额外编写setter方法,增加了代码量
解决方案一:@Primary注解,表示优先注入的对象
解决方案二:@Qualifier注解,搭配@Autowired注解,并在@Qualifier中填写指定的类名,如@Qualifier(“xxx”)
解决方案三:@Resource注解,用法和@Qualifier类似,@Resource(name = “xxx”)
JDBC JDBC:(Java DataBase Connectivity),就是使用Java语言操作关系型数据库的一套API
本质:
Sun公司官方定义的一套操作所有关系型数据库的规范,即接口
各个数据库厂商去实现这套接口,提供数据库驱动jar包
用户可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类
入门使用 修改数据 案例需求:基于JDBC程序,执行update语句(update user set age = 25 where id = 1);
步骤:
准备工作:创建一个Maven项目,引入依赖;并准备数据库表user
代码实现:编写JDBC程序,操作数据库
1 2 3 4 5 <dependency > <groupId > com.mysql</groupId > <artifactId > mysql-connector-j</artifactId > <version > 8.0.33</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 create table user ( id int unsigned primary key auto_increment comment 'ID,主键' , username varchar (20 ) comment '用户名' , password varchar (32 ) comment '密码' , name varchar (10 ) comment '姓名' , age tinyint unsigned comment '年龄' ) comment '用户表' ; insert into user (id, username, password, name, age) values (1 , 'daqiao' , '123456' , '大乔' , 22 ), (2 , 'xiaoqiao' , '123456' , '小乔' , 18 ), (3 , 'diaochan' , '123456' , '貂蝉' , 24 ), (4 , 'lvbu' , '123456' , '吕布' , 28 ), (5 , 'zhaoyun' , '12345678' , '赵云' , 27 );
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Class.forName("com.mysql.cj.jdbc.Driver" ); String url = "jdbc:mysql://localhost:3306/web01" ;String username = "root" ;String password = "1234" ;Connection connection = DriverManage.getConnect(url,username,password);Statement statement = connection.createStatement();int i = statement.executeUpdate("Update user set age = 25 where id = 1" );statement.close(); Connection.close();
查询数据 案例需求:基于JDBC执行如下select
语句,将查询结果封装到User
对象中
1 select * from user where username = 'daqiao' and password = '123456'
案例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public void testSelect () { String url = "jdbc:mysql://localhost:3306/web01" ; String dbUser = "root" ; String dbPassword = "1234" ; String sql = "SELECT id, username, password, name, age FROM user WHERE username = ? AND password = ?" ; List<User> userList = new ArrayList <>(); Connection conn = null ; PreparedStatement stmt = null ; ResultSet rs = null ; try { Class.forName("com.mysql.cj.jdbc.Driver" ); conn = DriverManager.getConnection(url, dbUser, dbPassword); stmt = conn.prepareStatement(sql); stmt.setString(1 , "daqiao" ); stmt.setString(2 , "123456" ); rs = stmt.executeQuery(); while (rs.next()) { User userObj = new User (); userObj.setId(rs.getInt("id" )); userObj.setUsername(rs.getString("username" )); userObj.setPassword(rs.getString("password" )); userObj.setName(rs.getString("name" )); userObj.setAge(rs.getInt("age" )); userList.add(userObj); } } catch (Exception e) { e.printStackTrace(); } finally { try { if (rs != null ) rs.close(); } catch (SQLException se) { se.printStackTrace(); } try { if (stmt != null ) stmt.close(); } catch (SQLException se) { se.printStackTrace(); } try { if (conn != null ) conn.close(); } catch (SQLException se) { se.printStackTrace(); } } for (User user : userList) { System.out.println(user); } }
ResultSet(结果集对象) ResultSet rs = statement.executeQuery()
:
next():将光标从当前位置向前移动一行,并判断当前行是否为有效行,返回值为boolean
true:有效行,当前行有数据
false:无效行,当前行没有数据
getXxx(…):获取数据,可以根据列的标号获取,也可以根据列名获取(推荐)
预编译SQL 静态SQL(参数硬编码):
1 2 3 Statement statement = connection.createStatement();int i = statement.executeUpdate("update user set age = 25 where id = 1" );System.out.println("SQL执行完毕,影响的记录数为:" + i);
预编译SQL(参数动态传递)
1 2 3 4 PreparedStatement pstmt = conn.prepareStatement("Select * FROM user WHERE username = ? AND password = ?" );pstmt.setString(1 ,"daqiao" ); pstmt.setString(2 ,"123456" ); ResultSet resultset = pstmt.executeQuery();
优势
安全:
防止SQL注入:SQL注入即通过控制输入来修改事先定义好的SQL语句,以达到执行代码对服务器进行攻击的方法
性能更高
MyBatis MyBatis是一款优秀的持久层(dao层)框架,用于简化JDBC的开发
查看之前的案例
对比使用MyBatis:
1 2 3 4 5 6 @Mapper public interface UserMapper { // 查询全部 @Select("select * from user"); public List<User> findAll(); }
MyBatis官网:https://mybatis.org/mybatis-3/zh_CN/index.html
入门程序
辅助配置 SQL语句识别
默认在MyBatis中配置的SQL语句是不识别的。可以做如下配置:
做完以上配置,user会报错:
产生原因:idea和数据库没有建立连接,不识别表信息
解决方式:在idea中配置MySQL数据库连接
MyBatis日志输出
默认情况下,在MyBatis中,SQL语句执行时,用户并不能直接看到SQL语句的执行日志。加入如下配置,即可查看日志:
1 2 3 4 5 6 7 8 9 10 spring.application.name =Mybatis_Demo spring.datasource.url =jdbc:mysql://localhost:3306/web01 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.username =root spring.datasource.password =1234 mybatis.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl
数据库连接池
数据库连接池是个容器,负责分配、管理数据库连接(Connection)
它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个
释放空间时间超过最大空闲时间的连接,来避免因为没有释放连接而引起的数据库连接遗漏
优势:
资源重用
提升系统响应速度
避免数据库连接遗漏
标准接口:DataSource
官方(sun)提供的数据库连接池接口,由第三方组织实现此接口
功能:获取连接
1 Connection getConnetion () throws SQLException;
常见产品:
C3P0
DBCP
Druid(阿里巴巴开源的数据库连接池项目)
Hikari(SpringBoot默认)
切换数据库连接池 1 2 3 4 5 <dependency > <groupId > com.alibaba</groupId > <artifactId > druid-spring-boot-starter</artifactId > <version > 1.2.19</version > </dependency >
1 2 3 4 5 spring.datasource.type =com.alibaba.druid.pool.DruidDataSource spring.datasource.url =jdbc:mysql://localhost:3306/web01 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.username =root spring.datasource.password =1234
增删改查操作 删除
需求:根据ID删除用户信息
SQL:delete from user where id = 5;
Mapper接口:
1 2 3 @Delete("delete from user where id = #{id}") public void deleteById (Integer id) ;
1 2 @Delete("delete from user where id = #{id}") public Integer deleteById (Integer id) ;
MyBatis中的#号与$号:
符号
说明
场景
优缺点
#{…}
占位符。执行时,会将#{…}替换为?,生成预编译SQL
参数值传递
安全、性能高(推荐)
${…}
拼接符。直接将参数拼接在SQL语句中,存在SQL注入问题
表名、字段名动态设置时使用
不安全、性能低
增加
需求:添加一个用户
SQL:insert into user(username,password,name,age) values('zhouyu','123456','周瑜',20);
Mapper接口:
1 2 3 @Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})") ;public void insert (String username,String password,String name,Integer age) ;
假设参数过多,可以使用对象封装起来
1 2 @Insert("insert into user(username,password,name,age) values(#{username},#{password},#{name},#{age})") ;public void insert (User user) ;
修改
需求:根据ID更新用户信息
SQL:update user set username = 'zhouyu',password = '123456', name = '周瑜', age = 20 where id = 1
Mapper接口:
1 2 @Update("update user set username = #{username},password = #{password}, name = #{name}, age = #{age} where id = #{id}") public void update (User user) ;
查询
需求:根据用户名和密码查询用户信息
SQL:select * from user where username = 'zhouyu' and password = '666888'
Mapper接口:
1 2 3 4 @Select("select * from user where username = #{username} and password = #{password}") public User findByUsernameAndPassword (String username,String password) ;
@Param注解的作用是为接口的方法形参起名字的
XML映射配置
在Mybatis中,既可以通过注解配置SQL语句,也可以通过XML配置文件配置SQL语句
默认规则:
XML映射文件的名称与Mapper接口名称一致,并且将XML映射文件和Mapper接口放置在相同包下(同包同名)
XML映射文件的namespace属性为Mapper接口全限定名一致
XML映射文件中sql语句的id与Mapper接口中的方法名一致,并保持返回类型一致
Mapper接口:
1 2 3 4 @Mapper public interface UserMapper { public List<User> findAll () ; }
XML:
1 2 3 4 5 <mapper namespace ="app.netlify.norlcyan.mapper.UserMapper" > <select id ="findAll" resultType ="app.netlify.norlcyan.pojo.User" > select id, username, password, name, age from user </select > </mapper >
辅助配置 现在XML文件位置如下:
配置XML映射文件的位置:
application.properties:
1 2 mybatis.mapper-location =classpath:mapper/*.xml
对于XML映射和注解的选择
SpringBoot配置文件
SpringBoot项目提供了多种属性配置方式(properties、yaml、yml)
application.properties:
1 2 3 4 5 6 7 8 spring.application.name =Mybatis_Demo spring.datasource.type =com.alibaba.druid.pool.DruidDataSource spring.datasource.url =jdbc:mysql://localhost:3306/web01 spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.username =root spring.datasource.password =1234
缺点:
application.yaml/application.yml:
1 2 3 4 5 6 spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/web01 username: root password: 1234
优点:
yml配置文件 格式:
数值前边必须有空格,作为分隔符
使用缩进表示层级关系,缩进时,不允许使用Tab键,只能用空格(idea中会自动将Tab转换为空格)
缩进的空格数码不重要,只要相同层级的元素左侧对齐即可
# 表示注释,从这个字符一直到行尾,都会被解析器忽略
定义对象/Map集合:
1 2 3 4 user: name: 张三 age: 18 password: 123456
定义数组/List/Set集合:
1 2 3 4 hobby: - java - game - sport
注意:在yml格式的配置文件中,如果配置项的值是以0开头的,值需要使用' '
引用他起来,因为以0开头在yml中表示8进制的数据
日志技术 Java中的日志技术提供了多种选择,开发人员可以根据项目需求选择适合的日志框架,记录和分析应用程序的运行状态,提高代码质量和维护效率
优势:
主流的日志框架:
JUL(java.util.logging):这是JavaSE平台提供的官方日志框架,配置相对简单,但不够灵活,性能较差
Log4j:主流的日志框架,提供了灵活的配置选项,支持多种输出目标
Logback:基于Log4j升级而来,提供了更多的功能和配置选项,性能也优于Log4j(推荐)
Slf4j(Simple Logging Facade For Java):简单日志门面,提供了一套日志操作的标准接口及抽象类,允许应用程序使用不同的底层日志框架
快速入门
准备工作:引入Logback的依赖(springboot项目中该依赖已传递 )、配置文件Logback.xml
记录日志:定义日志记录对象Logger,记录日志
1 2 3 4 5 <dependency > <groupId > ch.qos.logback</groupId > <artifactId > logback-classid</artifactId > <version > 1.4.11</version > </dependency >
Logback.xml文件具体如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <configuration > <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder class ="ch.qos.logback.classic.encoder.PatternLayoutEncoder" > <pattern > %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern > </encoder > </appender > <root level ="debug" > <appender-ref ref ="STDOUT" /> </root > </configuration >
测试案例:
1 2 3 4 5 6 7 8 9 10 11 12 private static final Logger log = LoggerFactory.getLogger(LogTest.class);@Test public void testLog () { log.debug("开始计算..." ); int sum = 0 ; int [] nums = {1 ,5 ,3 ,2 ,1 ,4 ,5 ,4 ,6 ,7 ,4 ,34 ,2 ,23 }; for (int i = 0 ;i <= nums.length;i++) { sum += nums[i]; } log.info("计算结果为:" + sum); log.debug("结束计算..." ); }
配置文件
配置文件名:logback.xml
该配置文件是对Logback日志框架输出的日志进行控制的,可以来配置输出的格式、位置及日志开关等
常用的两种输出日志的位置:控制台、系统文件
1 2 <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > ...</appender >
1 2 <appender name ="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > ...</appender >
详细的配置修改查询AI即可
日志级别
日志级别指的是日志信息的类型,日志都会分级别,常见的日志级别如下(级别由低到高):
日志级别
说明
记录方式
trace
追踪,记录程序运行轨迹【使用很少】
log.trace(“…”)
debug
调试,记录程序调试过程中的信息,实际应用中一般将其视为最低级别【使用较多】
log.debug(“…”)
info
记录一般信息,描述程序运行的关键时间,如:网络连接、IO操作等【使用较多】
log.info(“…”)
warn
警告信息,记录潜在有害的情况【使用较多】
log.warn(“…”)
error
错误信息【使用较多】
log.error(“…”)
可以在配置文件中,灵活的控制输出哪些类型的日志
1 2 3 4 <root level ="info" > <appender-ref ref ="STDOUT" /> <appender-ref ref ="FILE" /> </root >
事务管理
概念:事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败
比如在WebAi实战项目中的添加员工信息,假设添加员工基本信息成功,但是添加工作经历的代码出现了错误,会导致数据库数据的不完整(只有员工基本信息,没有工作经历信息)
默认MySQL的事务是自动提交的,也就是说,当执行一条DML语句,MySQL会立即隐式的提交事务
操作
事务控制主要三步操作:开启事务、提交事务/回滚事务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 start transaction; / begin ;insert into emp values (39 ,'Tom' ,'123456' ,'汤姆' ,1 ,'13300001111' ,1 ,4000 ,'1.jpg' ,'2023-11-01' ,1 ,now(),now());insert into emp_expr(emp_id, begin , end , company, job) values (37 ,'2020-01-01' , '2021-01-01' , '百度' , 'Java开发' ), (37 ,'2022-01-01' , '2023-01-01' , '阿里巴巴' , '运维' ) commit ; rollback ;
Spring事务管理——控制事务
注解:@Transactional
作用:将当前方法交给Spring进行事务管理,方法执行前,开启事务;成功执行则提交事务;出现异常则回滚事务
位置:在业务(Service)层的方法上、类上、接口上
方法(推荐):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Transactional @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); List<EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(expr -> expr.setEmpId(emp.getId())); empExprMapper.insertBatch(exprList); } }
接口:
1 2 @Transactional public interface EmpService {}
类:
1 2 3 @Transactional @Service public class EmpServiceImpl implements EmpService {}
配置日志信息,查看Spring事务管理的底层日志
1 2 3 logging: level: org.springframework.jdbc.support.JdbcTransactionManage: debug
事务进阶——rollbackFor
rollbackFor
属性用于控制出现何种异常类型,回滚事务
Transactional
默认为出现运行时异常(RuntimeException)才会回滚
以下代码,保存员工工作经历不会执行,但是并不会回滚
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Transactional @Override public void save(Emp emp) throws Exception { // 设置默认值 emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); // 保存基本员工信息 empMapper.insert(emp); if (true) { throw new Exception("出错信息!!!"); } // 保存员工工作经历 List<EmpExpr> exprList = emp.getExprList(); if(!CollectionUtils.isEmpty(exprList)) { // 遍历集合,为empId赋值 exprList.forEach(expr -> expr.setEmpId(emp.getId())); empExprMapper.insertBatch(exprList); } }
修改后就可以正常使用了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Transactional(rollbackFor = {Exception.class}) @Override public void save (Emp emp) { emp.setCreateTime(LocalDateTime.now()); emp.setUpdateTime(LocalDateTime.now()); empMapper.insert(emp); if (true ) { throw new Exception ("出错信息!!!" ); } List<EmpExpr> exprList = emp.getExprList(); if (!CollectionUtils.isEmpty(exprList)) { exprList.forEach(expr -> expr.setEmpId(emp.getId())); empExprMapper.insertBatch(exprList); } }
事务进阶——propagation
事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制
例如:
1 2 3 4 5 6 7 8 9 10 11 @Transactional public void a () { userService.b(); } @Transactional public void b () { }
在a方法中的b方法的事务行为(加入、新建)可以通过propagation
属性指定
如:
1 2 3 4 @Transactional(propagation = Propagation.REQUIRED) public void b () { }
其中propagation的常见属性值有如下几种
属性值
含义
REQUIRED
【默认值】需要事务,有则加入,无则创建新书屋
REQUIRES_NEW
需要新事物,无论有无,总是创建新事物
SUPPORTS
支持事务,有则加入,无则在无事务状态
中运行
NOT_SUPPORTED
不支持事务,在无事务状态
下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY
必须有事务,否则抛出异常
NEVER
必须没事务,否则抛出异常
四大特性
原子性:事务是不可分割的最小单元,要么全部成功,要么全部失败
一致性:事务完成时,必须使所有的数据都保持一致状态
隔离性:数据库系统提供的隔离机制,保证事务在不受外部并发影响的独立环境下运行
持久性:事务一旦提交或回滚,它对数据库中的数据的改变就是永久的
文件上传
文件上传:是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程
前端:
1 2 3 4 5 6 <form action ="/uploda" method ="post" enctype ="multipart/form-data" > 姓名:<input type ="text" name ="name" > <br > 年龄:<input type ="text" name ="age" > <br > 图像:<input type ="file" name ="file" > <br > <input type ="submit" value ="上传文件" name ="submit" > </form >
后端:
1 2 3 4 5 6 7 8 @RestController public class UploadController { @PostMapping("/upload") public Result handleFileUpload (String name,Integer age,MultipartFile file) { log.info("文件上传:{}" ,file); return Result.success; } }
接收到前端发来的数据后,后端会将其保存为临时文件xxx.tmp
本地存储 对之前的后端代码进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("/upload") public Result upload (String name, Integer age, MultipartFile file) throws IOException { log.info("接收参数:{},{},{}" ,name,age,file); String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf("." )); String newFileName = UUID.randomUUID().toString() + extension; file.transferTo(new File ("E:/files/" + originalFilename)); return Result.success(); }
注意:
为了确保上传的文件名不能与本地存储的文件名重复,需要使用UUID
生成文件名
在 Spring Boot 中,默认的文件上传大小限制为 1MB ,如果上传文件过大,需要在application.yaml中修改配置:
1 2 3 4 5 6 7 spring: servlet: multipart: max-file-size: 10MB max-request-size: 100MB
阿里云OSS 阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件
使用教程09-后端Web实战(员工管理) - 飞书云文档
登录校验
sequenceDiagram
participant User as 用户
participant Frontend as 前端
participant Backend as 后端
participant Database as 数据库
User->>Frontend: 输入用户名和密码
Frontend->>Backend: 发送登录请求 (POST /login)
Backend->>Backend: 验证参数 (非空、格式等)
alt 参数验证不通过
Backend-->>Frontend: 返回错误信息 (400 Bad Request)
Frontend-->>User: 显示错误信息
else 参数验证通过
Backend->>Database: 查询用户信息 (SELECT * FROM users WHERE username = ?)
Database-->>Backend: 返回用户信息
alt 用户不存在
Backend-->>Frontend: 返回错误信息 (401 Unauthorized)
Frontend-->>User: 显示错误信息
else 用户存在
Backend->>Backend: 验证密码 (compare password)
alt 密码验证失败
Backend-->>Frontend: 返回错误信息 (401 Unauthorized)
Frontend-->>User: 显示错误信息
else 密码验证成功
Backend->>Backend: 生成Token (JWT)
Backend-->>Frontend: 返回登录成功信息及Token (200 OK)
Frontend-->>User: 跳转到主页
end
end
end
会话技术
Cookie 案例来自黑马程序员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package com.itheima.controller;import com.itheima.pojo.Result;import jakarta.servlet.http.Cookie;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@Slf4j @RestController public class SessionController { @GetMapping("/c1") public Result cookie1 (HttpServletResponse response) { response.addCookie(new Cookie ("login_username" ,"itheima" )); return Result.success(); } @GetMapping("/c2") public Result cookie2 (HttpServletRequest request) { Cookie[] cookies = request.getCookies(); for (Cookie cookie : cookies) { if (cookie.getName().equals("login_username" )){ System.out.println("login_username: " +cookie.getValue()); } } return Result.success(); } }
Cookie的优点:HTTP协议中支持的技术
Cookie的缺点:
移动端APP无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域
Session 案例来自黑马程序员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package com.itheima.controller;import com.itheima.pojo.Result;import jakarta.servlet.http.Cookie;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import jakarta.servlet.http.HttpSession;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@Slf4j @RestController public class SessionController { @GetMapping("/s1") public Result session1 (HttpSession session) { log.info("HttpSession-s1: {}" , session.hashCode()); session.setAttribute("loginUser" , "tom" ); return Result.success(); } @GetMapping("/s2") public Result session2 (HttpSession session) { log.info("HttpSession-s2: {}" , session.hashCode()); Object loginUser = session.getAttribute("loginUser" ); log.info("loginUser: {}" , loginUser); return Result.success(loginUser); } }
Session的优点:存储在服务端,安全
Session的缺点:
服务器集群环境下无法直接使用Session
包括所有Cookie中的缺点
令牌 令牌的优点:
支持PC端、移动端
解决集群环境下的认证问题
减轻服务端存储压力
令牌的缺点:
JWT令牌
全称:JSON Web Token(http://jwt.io)
定义了一种简洁的、自包含的格式,用于在通信双方以JSON数据格式安全的传输信息
组成:
第一部分:Header(头),记录令牌类型、算法签名等,例如:{"alg":"HS256", "type":"JWT"}
第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"Tom"}
第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload融入,并加入指定密钥,通过指定签名算法计算而来
使用步骤:
引入jjwt
的依赖
调用官方提供的工具类Jwts
来生成或解析jwt令牌
1 2 3 4 5 <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > <version > 0.9.1</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 @Test public void testGenerateJwt () { Map<String, Object> dataMap = new HashMap <>(); dataMap.put("id" , 1 ); dataMap.put("username" , "norlcyan" ); String jwt = Jwts.builder().signWith(SignatureAlgorithm.HS256, "bm9ybGN5YW4=" ) .addClaims(dataMap) .setExpiration(new Date (System.currentTimeMillis() + 3600 * 1000 )) .compact(); System.out.println(jwt); }
1 2 3 4 5 6 7 @Test public void testParseJwt () { String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJub3JsY3lhbiIsImV4cCI6MTczOTA5NTAzOH0.grGPR6b2Dib5qW74VIHPSujQec-TuKRTMduoTSnBEis" ; Claims claims = Jwts.parser().setSigningKey("bm9ybGN5YW4=" ).parseClaimsJws(token).getBody(); System.out.println(claims); }
过滤器Filter
概念:Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等
快速入门
定义Filter:定义一个类,实现Filter接口,并实现其所有方法
配置Filter:Filter类上加@WebFilter
注解,配置拦截路径。引导类上加@ServletComponentScan
开启Servlet组件支持
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package app.netlify.norlcyan.filter;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import java.io.IOException;@WebFilter("/*") public class DemoFilter implements Filter { @Override public void init (FilterConfig filterConfig) throws ServletException { System.out.println("init..." ); } @Override public void doFilter (ServletRequest servletRequest, ServletResponse servletResponse,FilterChain chain) throws ServletException, IOException { System.out.println("拦截到了请求..." ); chain.doFilter(servletRequest, servletResponse); } @Override public void destroy () { System.out.println("destroy..." ); } }
1 2 3 4 @servletComponentScan @SpringBootApplication public class TliasManagementApplication {...}
注意:
拦截到请求后需要执行放行操作,否则服务器不会返回数据
并不是所有请求都需要校验令牌,如登录请求、注册请求
当有令牌,且令牌校验通过后,放行;否则返回未登录错误结果
令牌校验Filter流程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package app.netlify.norlcyan.filter;import app.netlify.norlcyan.utils.JwtUtils;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;@WebFilter(urlPatterns = "/*") public class TokenFilter implements Filter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TokenFilter.class); @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 { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestURI = request.getRequestURI(); if (requestURI.contains("/login" )) { log.info("登录请求,放行" ); filterChain.doFilter(servletRequest,servletResponse); return ; } String token = request.getHeader("token" ); if (token == null || token.isEmpty()) { log.info("令牌为空,返回401状态码" ); response.setStatus(401 ); return ; } try { JwtUtils.parseJwt(token); log.info("解析Token成功" ); } catch (Exception e) { log.info("解析Token失败,返回401状态码" ); response.setStatus(401 ); return ; } log.info("校验通过,放行" ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { Filter.super .destroy(); } }
Filter拦截路径
拦截路径
urlPatterns值
含义
拦截具体路径
/login
只有访问/login路径时,才会被拦截
目录拦截
/emps/*
访问/emps下的所有资源,都会被拦截
拦截所有
/*
访问所有资源,都会被拦截
Filter过滤器链
一个Web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链
注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序
拦截器Interceptor
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,主要用于动态拦截控制器方法的执行
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码
快速入门
定义拦截器,实现HandlerInterceptor接口,并实现其所有方法
注册拦截器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package app.netlify.norlcyan.interceptor;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.springframework.web.servlet.HandlerInterceptor;import org.springframework.web.servlet.ModelAndView;@Component public class DemoInterceptor implements HandlerInterceptor { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(DemoInterceptor.class); @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle..." ); return true ; } @Override public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("postHandle..." ); } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("afterCompletion..." ); } }
1 2 3 4 5 6 7 8 9 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private DemoInterceptor demoInterceptor @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(demoInterceptor).addPathPatterns("/**" ); } }
令牌校验Interceptor 流程与Filter类似
TokenInterceptor:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 package app.netlify.norlcyan.filter;import app.netlify.norlcyan.utils.JwtUtils;import jakarta.servlet.*;import jakarta.servlet.annotation.WebFilter;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import java.io.IOException;public class TokenFilter implements Filter { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(TokenFilter.class); @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 { HttpServletRequest request = (HttpServletRequest) servletRequest; HttpServletResponse response = (HttpServletResponse) servletResponse; String requestURI = request.getRequestURI(); if (requestURI.contains("/login" )) { log.info("登录请求,放行" ); filterChain.doFilter(servletRequest,servletResponse); return ; } String token = request.getHeader("token" ); if (token == null || token.isEmpty()) { log.info("令牌为空,返回401状态码" ); response.setStatus(401 ); return ; } try { JwtUtils.parseJwt(token); log.info("解析Token成功" ); } catch (Exception e) { log.info("解析Token失败,返回401状态码" ); response.setStatus(401 ); return ; } log.info("校验通过,放行" ); filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy () { Filter.super .destroy(); } }
WebConfig:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package app.netlify.norlcyan.config;import app.netlify.norlcyan.interceptor.DemoInterceptor;import app.netlify.norlcyan.interceptor.TokenInterceptor;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private TokenInterceptor tokenInterceptor; @Override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(tokenInterceptor).addPathPatterns("/**" ); } }
拦截路径 拦截器可以根据需求,配置不同的拦截路径
1 2 3 4 public void addInterceptors(InterceptorRegistry registry) { @Override registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/login"); }
excludePathPatterns:指定哪些请求不拦截
拦截路径
含义
举例
/*
一级路径
能匹配/depts,/emps等,不能匹配/depts/1
/**
任意级路径
所有路径都能匹配
/depts/*
/depts下的一级路径
能匹配/depts/1,不能匹配/depts/1/2,/depts
/depts/**
/depts下的任意级路径
/depts下的所有路径,如/depts/1,/depts/1/2
执行流程 优先Filter,其次才是拦截器(拦截器是在Spring框架下的)
实际项目中,二选一即可
AOP
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),可以简单理解为就是面向特定方法编程
场景:案例中部分业务方法运行较慢,定位执行耗时较长的方法,此时需要统计每个业务方法的执行耗时
原始方式:
1 2 3 4 5 6 7 public List<Dept> list () { long beginTime = System.currentTimeMillis(); List<Dept> deptList = deptMapper.list(); long endTime = System.currentTimeMillis(); log.info("执行耗时:{}" ,endTime - beginTime); return deptList; }
AOP方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Slf4j @Aspect @Component public class RecordTimeAspect { @Around("execution(* xxx.xxx.service.*.*(..))") public Object recordTime (ProceedingJointPoint pjp) { long beginTime = System.currentTimeMillis(); Object result = pjp.proceed(); long endTime = System.currentTimeMillis(); log.info("执行耗时:{}" ,endTime - beginTime); return result; } }
优势:
减少重复代码
代码无侵入
提高开发效率
维护方便
提示:AOP是一种思想
,而在Spring框架中对这种思想进行的实现,就是Spring AOP
个人理解:相当于给方法装了个插件,方法本身不会被修改
AOP基础 快速入门 需求:统计所有业务层方法的执行耗时
导入依赖:在pom.xml中引入AOP的依赖
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-aop</artifactId > </dependency >
编写AOP程序:针对特定的方法根据业务需要进行编程
1 2 3 4 5 6 7 8 9 @Aspect @Component public class RecordTimeAspect { @Around("execution(* xxx.xxx.service.impl.*.*(..))") public Object recordTime (ProceedingJoinPoint pjp) throws Throwable { } }
AOP核心概念
连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指那些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
AOP进阶 通知类型
根据通知方法执行时机的不同,将通知类型分为以下常见的五类:
@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
@Before:前置通知,此注解标注的通知方法在目标方法前被执行
@After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行
注意:
@Around环绕通知需要自己调用ProceedingJoinPoint.proceed()
来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值
@PointCut 该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可
1 2 3 4 5 @PointCut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void pt () {};@Around("pt()") public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable {...}
通知顺序
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行
执行顺序:
不同切面类中,默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
用@Order(数字)
加在切面类上来控制顺序
目标方法前的通知方法:数字小的先执行
目标方法前的通知方法:数字小的后执行
1 2 3 4 5 @Slf4j @Order(5) @Aspect @Component public class RecordTimeAspect {...}
切入点表达式
介绍:描述切入点方法的一种表达式
作用:用来决定项目中的哪些方法需要加入通知
常见形式:
execution(...)
:根据方法的签名来匹配
1 2 @Before("execution(public void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))") public void before (JoinPoint joinPoint) {..}
@annotation(...)
:根据注解匹配
1 2 @Before("@annotation(com.itheima.anno.Log)") public void before () {}
execution 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
1 execution(访问修饰符? 返回值 + 包名.类名.?方法名(方法参数) throws 异常?)
其中带?
的表示可以省略的部分
访问修饰符:可省略(比如:public、protected)
包名.类名:可省略(不建议省略)
throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
可以使用通配符描述切入点
*
:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
1 execution(* com.*.service.*.update*(*))
..
:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
1 execution(* com.itheima..DeptService.*(..))
书写建议:
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:findXxx
、updateXxx
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名尽量不使用..
,使用*
匹配单个包
@annotation
@annotation
切入点表达式,用于匹配标识有特定注解的方法
1 2 3 4 5 @Around("@annotation(com.itheima.anno.LogOperation)") public Object logAround (ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); }
1 2 3 4 5 6 7 @LogOperation @DeleteMapping public Result delete (Integer id) { System.out.println("根据ID删除部门数据:" + id); deptService.delete(id); return Result.success(); }
注:@LogOperation
该注解为用户自定义注解,关于创建用户自定义注解在Java | Norlcyan’s Blog 提及
连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于@Around通知,获取连接点信息只能 使用ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能 使用JoinPoint,它是ProceedingJoinPoint的父类型
1 2 3 4 5 6 7 8 9 @Around("execution(* com.itheima.service.DeptService.*(..))") public Object around (ProceedingJoinPoint joinPoint) throws Throwable { String className = joinPoint.getTarget().getClass().getName(); Signature signature = joinPoint.getSignature(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); Object res = joinPoint.proceed(); return res; }
1 2 3 4 5 6 7 @Before("execution(* com.itheima.service.DeptService.*(..))") public void before (JoinPoint joinPoint) { String className = joinPoint.getTarget().getClass().getName(); Signature signature = joinPoint.getSignature(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); }
AOP案例
将Tlias智能学习辅助系统案例中增删改接口的操作日志记录到数据库中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长
创建操作日志表:
1 2 3 4 5 6 7 8 9 10 11 create table operate_log( id int unsigned primary key auto_increment comment 'ID' , operate_emp_id int unsigned comment '操作人ID' , operate_time datetime comment '操作时间' , class_name varchar (100 ) comment '操作的类名' , method_name varchar (100 ) comment '操作的方法名' , method_params varchar (2000 ) comment '方法参数' , return_value varchar (2000 ) comment '返回值' , cost_time bigint unsigned comment '方法执行耗时, 单位:ms' ) comment '操作日志表' ;
创建AOP类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 package app.netlify.norlcyan.aop;import app.netlify.norlcyan.mapper.OperateLogMapper;import app.netlify.norlcyan.pojo.OperateLog;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.time.LocalDateTime;import java.util.Arrays;@Aspect @Component public class LogAspect { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogAspect.class); @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(app.netlify.norlcyan.anno.Log)") public Object logOperation (ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); long costTime = endTime - startTime; OperateLog olog = new OperateLog (); olog.setOperateEmpId(1 ); olog.setOperateTime(LocalDateTime.now()); olog.setClassName(joinPoint.getTarget().getClass().getName()); olog.setMethodName(joinPoint.getSignature().getName()); olog.setMethodParams(Arrays.toString(joinPoint.getArgs())); olog.setReturnValue(result != null ? result.toString() : "void" ); olog.setCostTime(costTime); log.info("日志记录:{}" , olog); operateLogMapper.insert(olog); return result; } }
由于只记录增删改操作,所以使用@annotation
注解,创建自定义注解:
1 2 3 4 5 6 7 8 9 10 11 package app.netlify.norlcyan.anno;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Log {}
根据数据库结构创建实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 package app.netlify.norlcyan.pojo;import lombok.Data;import java.time.LocalDateTime;public class OperateLog { private Integer id; private Integer operateEmpId; private LocalDateTime operateTime; private String className; private String methodName; private String methodParams; private String returnValue; private Long costTime; public OperateLog () { } public OperateLog (Integer id, Integer operateEmpId, LocalDateTime operateTime, String className, String methodName, String methodParams, String returnValue, Long costTime) { this .id = id; this .operateEmpId = operateEmpId; this .operateTime = operateTime; this .className = className; this .methodName = methodName; this .methodParams = methodParams; this .returnValue = returnValue; this .costTime = costTime; } public Integer getId () { return id; } public void setId (Integer id) { this .id = id; } public Integer getOperateEmpId () { return operateEmpId; } public void setOperateEmpId (Integer operateEmpId) { this .operateEmpId = operateEmpId; } public LocalDateTime getOperateTime () { return operateTime; } public void setOperateTime (LocalDateTime operateTime) { this .operateTime = operateTime; } public String getClassName () { return className; } public void setClassName (String className) { this .className = className; } public String getMethodName () { return methodName; } public void setMethodName (String methodName) { this .methodName = methodName; } public String getMethodParams () { return methodParams; } public void setMethodParams (String methodParams) { this .methodParams = methodParams; } public String getReturnValue () { return returnValue; } public void setReturnValue (String returnValue) { this .returnValue = returnValue; } public Long getCostTime () { return costTime; } public void setCostTime (Long costTime) { this .costTime = costTime; } }
创建Mapper对数据库进行操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package app.netlify.norlcyan.mapper;import app.netlify.norlcyan.pojo.OperateLog;import org.apache.ibatis.annotations.Insert;import org.apache.ibatis.annotations.Mapper;@Mapper public interface OperateLogMapper { @Insert("insert into operate_log (operate_emp_id, operate_time, class_name, method_name, method_params, return_value, cost_time) " + "values (#{operateEmpId}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});") public void insert (OperateLog log) ; }
对需要记录的操作加上@annotation
注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 package app.netlify.norlcyan.controller;import app.netlify.norlcyan.anno.Log;import app.netlify.norlcyan.pojo.*;import app.netlify.norlcyan.service.EmpService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.format.annotation.DateTimeFormat;import org.springframework.web.bind.annotation.*;import java.time.LocalDate;import java.time.LocalDateTime;import java.util.Arrays;import java.util.List;@RequestMapping("/emps") @RestController public class EmpController { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(EmpController.class); @Autowired private EmpService empService; @GetMapping public Result page (EmpQueryParam empQueryParam) { log.info("分页查询,参数:{}" ,empQueryParam); PageResult<Emp> pageResult = empService.page(empQueryParam); return Result.success(pageResult); } @Log @PostMapping public Result save (@RequestBody Emp emp) { log.info("保存员工信息,参数:{}" ,emp); empService.save(emp); return Result.success(); } @Log @DeleteMapping public Result delete (@RequestParam List<Integer> ids) { log.info("删除员工信息,参数:{}" , ids); empService.delete(ids); return Result.success(); } @GetMapping("/{id}") public Result getInfo (@PathVariable Integer id) { log.info("查询回显,参数:{}" , id); Emp emp = empService.getInfo(id); return Result.success(emp); } @Log @PutMapping public Result update (@RequestBody Emp emp) { log.info("修改员工信息,参数:{}" , emp); empService.update(emp); return Result.success(); } @GetMapping("/list") public Result list () { log.info("查询所有员工信息" ); List<Emp> list = empService.list(); return Result.success(list); } }
在之前创建的AOP类代码中,获取员工数据是写死的:
想要获取员工动态数据,可以从令牌的Token中解析得到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 package app.netlify.norlcyan.interceptor;import app.netlify.norlcyan.utils.JwtUtils;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;@Component public class TokenInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(DemoInterceptor.class); @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); if (requestURI.contains("/login" )) { log.info("登录请求,放行" ); return true ; } String token = request.getHeader("token" ); if (token == null || token.isEmpty()) { log.info("令牌为空,返回401状态码" ); response.setStatus(401 ); return false ; } try { JwtUtils.parseJwt(token); log.info("解析Token成功" ); } catch (Exception e) { log.info("解析Token失败,返回401状态码" ); response.setStatus(401 ); return false ; } log.info("校验通过,放行" ); return true ; } }
想要将其中的参数发送给AOP、Controller、Serivce,需要通过ThreadLocal
来实现
ThreadLocal
ThreadLocal并不是一个Thread,而是Thread的局部变量
ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离效果,不同的线程之间不会相互干扰
ThreadLocal常用方法:
public void set(T value):设置当前线程的线程局部变量
public T get():返回当前线程所对应的线程局部变量
public void remove():移除当前线程的线程局部变量
获取当前登录员工 具体操作步骤:
定义ThreadLocal操作的工具类,用于操作当前登录员工ID
在拦截器类中,解析当前完成登录员工ID,将其存入ThreadLocal(用完之后需要将其删除)
在AOP程序中,从ThreadLocal中获取当前登录员工的ID
ThreadLocal工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package app.netlify.norlcyan.utils;public class CurrentHolder { private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal <>(); public static void setCurrentId (Integer employeeId) { CURRENT_LOCAL.set(employeeId); } public static Integer getCurrentId () { return CURRENT_LOCAL.get(); } public static void remove () { CURRENT_LOCAL.remove(); } }
修改拦截器相关代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 package app.netlify.norlcyan.interceptor;import app.netlify.norlcyan.utils.CurrentHolder;import app.netlify.norlcyan.utils.JwtUtils;import io.jsonwebtoken.Claims;import jakarta.servlet.http.HttpServletRequest;import jakarta.servlet.http.HttpServletResponse;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.stereotype.Component;import org.springframework.web.servlet.HandlerInterceptor;@Component public class TokenInterceptor implements HandlerInterceptor { private static final Logger log = LoggerFactory.getLogger(DemoInterceptor.class); @Override public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI = request.getRequestURI(); if (requestURI.contains("/login" )) { log.info("登录请求,放行" ); return true ; } String token = request.getHeader("token" ); if (token == null || token.isEmpty()) { log.info("令牌为空,返回401状态码" ); response.setStatus(401 ); return false ; } try { Claims claims = JwtUtils.parseJwt(token); Integer empId = Integer.valueOf(claims.get("id" ).toString()); CurrentHolder.setCurrentId(empId); log.info("当前用户id为:{}" ,empId); log.info("解析Token成功" ); } catch (Exception e) { log.info("解析Token失败,返回401状态码" ); response.setStatus(401 ); return false ; } log.info("校验通过,放行" ); return true ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { CurrentHolder.remove(); } }
AOP动态获取当前登录员工数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package app.netlify.norlcyan.aop;import app.netlify.norlcyan.mapper.OperateLogMapper;import app.netlify.norlcyan.pojo.OperateLog;import app.netlify.norlcyan.utils.CurrentHolder;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import java.time.LocalDateTime;import java.util.Arrays;@Aspect @Component public class LogAspect { private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(LogAspect.class); @Autowired private OperateLogMapper operateLogMapper; @Around("@annotation(app.netlify.norlcyan.anno.Log)") public Object logOperation (ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); long endTime = System.currentTimeMillis(); long costTime = endTime - startTime; OperateLog olog = new OperateLog (); olog.setOperateEmpId(CurrentHolder.getCurrentId()); olog.setOperateTime(LocalDateTime.now()); olog.setClassName(joinPoint.getTarget().getClass().getName()); olog.setMethodName(joinPoint.getSignature().getName()); olog.setMethodParams(Arrays.toString(joinPoint.getArgs())); olog.setReturnValue(result != null ? result.toString() : "void" ); olog.setCostTime(costTime); log.info("日志记录:{}" , olog); operateLogMapper.insert(olog); return result; } }