应用程序路径必须设置为localhost:8080/
。上下文路径必须为空,因为nginx会将请求映射到localhost:8080/
。
运行前端环境nginx:项目下"other/nginx-1.22.0-tlias/nginx-1.22.0-tlias/nginx.exe"文件,浏览器访问:http://localhost:90,进入前端页面。
nginx需要在一个没有中文的路径下;您应该将nginx压缩包解压到一个没有中文的路径下,而不是解压后再移动到一个没有中文的路径下,以确保nginx能正常启动!
项目基于当前最为主流的前后端分离模式进行开发。
项目实现了员工和部门管理系统的服务端功能开发,前端代码已经实现。前后端开发都严格遵循需求文档(见“other”目录)。
项目是整合springboot、mybatis,使用Maven管理依赖和插件。
项目后期将application.properties
配置文件替换为了更具优势的application.yml
文件,application.properties
配置文件备份到了other
目录中。
项目采用了阿里云OSS技术,所有文件都存储在云端,云上传程序的配置项采用配置注入的方式。
我们的登录功能并非徒有其表,还包含了登录校验的核心功能。
我们还对项目进行了异常处理和事务管理。
最后基于AOP实现了记录业务耗时的功能。
准备数据库表(dept、emp)
SQL语已在
other
目录备好
创建springboot工程,引入对应的起步依赖(web、mybatis、mysql驱动、lombok)
配置文件application.properties中引入mybatis的配置信息,准备对应的实体类
准备对应的Mapper、Service(接口、实现类)、Controllers基础结构
REST(REpresentational State Transfer),表述性状态转换,它是一种软件架构风格。
传统的参数传递方式好吗?我们每个人都有着不一样的命名习惯,这大大加大了后期的维护难度。
规范:URL定位资源,HTTP动词描述操作(即请求方式:GET、POST、PUT、DELETE)。
比如:
REST是风格,是约定方式,约定不是规定,可以打破。
上下文路径中描述模块的功能时通常使用复数,也就是加s的格式,表示此类资源,而非单个资源。如:users、emps、books..
文件上传,是指将本地图片、视频、音频等文件上传到服务器,供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。
要实现文件上传功能,前端需要实现“file”属性的表单,并将提交方式设置为"post",还要指定编码方式为“multipart/form-data”。
“multipart/form-data”会将文件内容一并上传,而默认的“application/x-www-form-urlencoded”只能上传编码后的文件名信息
本地存储存在缺陷,了解接口
/**
* 演示文件从前端上传至服务端
*
* @param username 用户名
* @param age 年龄
* @param image 图片
*/
@PostMapping("/upload")
public Result upload(String username,Integer age,MultipartFile image)throws IOException{
log.info("接收到文件上传:{},{},{}",username,age,image);
/*
* 在表单上传文件或文本到服务端时,通常会先将文件或文本保存在本地临时位置,然后通过HTTP请求发送到服务端。
* 这个临时位置的文件或文本通常会在一定时间后自动删除,但具体时间取决于服务器的配置和操作系统的设置。
* 因此,虽然文件或文本在上传之后可能会被立即删除,但这不是一定的,具体取决于服务器的配置和操作系统的设置。
* */
// 获取原始文件名
String originalFilename=image.getOriginalFilename();
/* public int lastIndexOf(String str)
* 返回指定子字符串最后一次出现的索引。空字符串""的最后一次出现被认为出现在索引值this.length()处。
* */
String newFileName=UUID.randomUUID().toString()+originalFilename.substring(0,originalFilename.lastIndexOf("."));
// 存储文件至本地
image.transferTo(new File("D:\\BaiduSyncdisk\\myCode\\myJavaWeb\\tlias-web-management\\image\\"+newFileName));
return Result.success();
}
MultipartFile
类的常用方法
方法名 | 说明 |
---|---|
String getOriginalFilename(); |
获取原始文件名 |
void transferTo(File dest); |
将接收的文件转存到磁盘文件中 |
long getSjze(); |
获取文件的大小,单位:字节 |
byte[] getBytes(); |
获取文件内容的字节数组 |
InputStream getlnputStream(); |
获取接收到的文件内容的输入流 |
在SpringBoot中,文件上传,默认单个文件允许最大大小为1M。如果需要上传大文件,可以进行如下配置:
#配置单个文件最大上传大小
spring.servlet.multipart.max-file-size=10MB
#配置单个请求最大上传大小(一次请求可以上传多个文件)
spring.servlet.multipart.max-request-size=100MB
如何使用云存储?
准备工作
参照官方SDK编写入门程序
SDK:Software Development Kit 的缩写,软件开发工具包,包括辅助软件开发的依赖(jar包)、代码示例等,都可以叫做SDK。
集成使用
阿里云是阿里巴巴集团旗下全球领先的云计算公司,也是国内最大的云服务提供商。
阿里云对象存储OSS(Object Storage Service),是一款海量、安全、低成本、高可靠的云存储服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种文件。
使用阿里云OOS:
注册阿里云(实名认证)
充值
开通对象存储服务(OSS)
创建bucket
Bucket:存储空间是户用于存储对象(Object,就是文件)的容器,所有的对象都必须隶属于某个存储空间。
获取AccessKey(秘钥)
参照官方SDK编写入门程序
在官网找到OOS的SDK文档 入门程序:
package com.itheima.tliaswebmanagement; import com.aliyun.oss.ClientException; import com.aliyun.oss.OSS; import com.aliyun.oss.OSSClientBuilder; import com.aliyun.oss.OSSException; import com.aliyun.oss.common.auth.CredentialsProviderFactory; import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider; import java.io.ByteArrayInputStream; public class Demo { public static void main(String[] args) throws Exception { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 String endpoint = "https://oss-cn-hangzhou.aliyuncs.com"; // 强烈建议不要把访问凭证保存到工程代码里,否则可能导致访问凭证泄露,威胁您账号下所有资源的安全。本代码示例以从环境变量中获取访问凭证为例。运行本代码示例之前,请先配置环境变量。 EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); String accessKeyId = "LTAI5tSTnrNJSy6fH7XvteWi";//这是我添加的 String secretAccessKey = "CdbXZN8ZWVIRvd2WYP5qxy1r40yzX8";//这是我添加的 // 填写Bucket名称,例如examplebucket。 String bucketName = "web-tlias-falling-dust"; // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。 String objectName = "C:\\Users\\Lenovo\\OneDrive\\图片\\本机照片\\登峰造极.jpeg"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, secretAccessKey);//这是我修改的 try { String content = "Hello OSS"; ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content.getBytes())); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } } }
案例集成OSS
见代码,我们创建了工具类用于传输文件至阿里云OOS,又新建了一个Controller专门用于响应文件类型的参数。
将阿里云的配置信息作为硬编码写在工程类中是不可取的,我们最好将其写在application.properties
等springboot支持的配置文件中(springboot仅支持properties和yml配置文件,不支持xml配置文件):
这些都是我们自己定义的配置信息,key的名称没有固定规则,我们建议见名知义。
springboot已经为我们准备好了一个注解:@Value注解通常用于外部配置的属性注入,具体用法为:@Value("${配置文件中的key}")
,我们需要为工具类的相应属性添加该注解。
# 自定义的阿里云OSS配置信息
aliyun.oss.endpoint=https://oss-cn-hangzhou.aliyuncs.com
aliyun.oss.accessKeyId=LTAI5tSTnrNJSy6fH7XvteWi
aliyun.oss.accessKeySecret=CdbXZN8ZWVIRvd2WYP5qxy1r40yzX8
aliyun.oss.bucketName=web-tlias-falling-dust
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.secretAccessKey}")
private String secretAccessKey;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
springboot除了支持properties配置文件外,还支持yml或yaml配置文件。
对象/Map集合:
# 对象/Map集合:
user:
name: zhangsan
age: 18
password: 123456
数组/List/Set集合:
# 数组/List/Set集合:
hobby:
- java
- c
- game
- sport
我们最开始注入阿里云上传程序的配置信息时是通过@Value
注解来实现的,另一种方式则是通过@ConfigurationProperties
注解实现批量注入。
@Value
注解只能一个一个的进行外部属性的注入。而@ConfigurationProperties
可以批量的将外部的属性配置注入到bean对象的属性中。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
HTTP是一种无状态协议,即服务器不保留与客户交易时的任何状态。
也就是说,上一次的请求对这次的请求没有任何影响,服务端也不会对客户端上一次的请求进行任何记录处理。
为了避免用户使用其他请求参数,直接跳过登录,我们必须实现登录校验功能。
我们接收其他请求参数时必须判断判断用户是否已经登录,因此用户登陆成功后生成一个登录标记。
在接收请求参数时,我们会统一拦截并进行登录校验。
会话:用户打开浏览器,访问web服务器的资源,会话建立,直到有一方断开连接,会话结束。在一次会话中可以包含多次请求和响应。
会话跟踪:一种维护浏览器状态的方法,服务器需要识别多次请求是否来自于==同一浏览器==,以便在同一次会话的多次请求间共享数据。
会话跟踪方案:
客户端会话跟踪技术:Cookie
Cookie技术是指服务器在客户端浏览网站的时候,将一段随机生成的包含信息的小块数据存储在客户端的计算机中,以便客户端在后续访问该网站时,可以快速通过Cookie技术从客户端的硬盘中读取数据的一种技术。 Cookie虽然被广泛的应用,并能做到一些使用其它技术不可能实现的功能,但也存在一些不够完美的方面,给应用带来不便。
服务端会话跟踪技术:Session
令牌技术
CooKie:客户端会话技术,将数据保存到客户端,以后每次请求都携带Cookie数据进行访问。
对于Cookie的实现原理是基于HTTP协议的,其中涉及到到HTTP协议中的两个请求头信息:
跨域:跨域区分三个维度:协议、IP/域名、端口
Session是服务器端技术,利用这个技术,服务器在运行时可以为每一个用户的浏览器创建一个其独享的HttpSession
对象,由于session为用户浏览器独享,所以用户在访问服务器的web资源时,可以把各自的数据放在各自的session中,当用户再去访问服务器中的其它web资源时,其它web资源再从用户各自的session中取出数据为用户服务。
数据存储在服务端,服务端会为每一个客户端浏览器创建一个独享的session;Session也是一个域对象,域的范围是一个会话。
session特点:
session用于存储一次会话的多次请求的数据,存在服务器端
session可以存储任意类型,任意大小的数据(只要内存放得下)
Session和Cookie的主要区别在于:
令牌技术是一种用于授权和身份验证的安全机制。在计算机领域,令牌通常是一种由服务器发放给客户端的加密字符串或数字。客户端可以使用令牌来证明其身份,并获得对特定资源或服务的访问权限。
令牌技术在许多场景中被广泛应用,包括身份验证、单点登录、API访问控制等。当用户成功登录后,服务器会生成一个令牌,并将其返回给客户端。客户端在后续的请求中携带该令牌,以证明其身份。服务器会验证令牌的有效性,并根据权限配置决定是否授予访问权限。
令牌通常具有一定的有效期限制,并且可以通过刷新机制来延长其有效期。此外,令牌还可以包含一些附加信息,如用户角色、权限等,以便服务器在验证令牌时进行更精细的授权。
总而言之,令牌技术提供了一种安全、可扩展的身份验证和授权机制,可用于保护系统中的敏感资源和服务。
全称:JSON Web Token(https://jwt.io/)
定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。
组成:
Header和Payload都是经过Base64编码的。
Base64:是一种基于64个可打印字符(A-Z a-z 0-9 + /)来表示二进制数据的编码方式。
登录认证
依赖配置:
<!--JWT令牌依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
代码实现(测试类):
package com.itheima.tliaswebmanagement;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
//@SpringBootTest//单元测试类添加@SpringBootTest注解后,测试方法运行测试前会自动加载springboot环境
class TliasWebManagementApplicationTests {
@Test
void contextLoads() {
}
/**
* 生成JWT
*/
@Test
public void testGenJwt() {
Map<String, Object> claims = new HashMap<>();
claims.put("name", "Tom");
// Jwts.builder()是Java中的一种构造函数,用于创建一个JWT(JSON Web Token)的构建器对象。该构建器对象可以用于构建和签署JWT。
String jwt = Jwts.builder()
// 接着,signWith()方法用于指定JWT的签名算法和密钥。
.signWith(SignatureAlgorithm.HS256, "itheima")
// 用于在 JWT(JSON Web Token)构建器中设置声明(claims)。声明是 JWT 中包含的关于特定实体的信息,例如用户 ID、用户名、电子邮件地址等。
.setClaims(claims)
// 用于在 JWT(JSON Web Token)构建器中设置过期时间
.setExpiration(new Date(System.currentTimeMillis() + 3600 * 1000))//有效期为1h
// 使用 compact() 方法将所有设置组合成一个紧凑的字符串,该字符串可以发送到接收者。
.compact();
System.out.println(jwt);// eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiZXhwIjoxNjkwMzc1NjU4fQ.KNV_0vrYdiVRMflG7vZZaPpzZayxg-oCyPpbLjXHPcU
}
/**
* 解析JWT令牌
*/
@Test
public void testParseJwt() {
Claims claims = Jwts.parser()
.setSigningKey("itheima")// 匹配生成JWT时的密钥
// 注意!是parseClaimsJws而不是parseClaimsJwt!
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiVG9tIiwiZXhwIjoxNjkwMzc1NjU4fQ.KNV_0vrYdiVRMflG7vZZaPpzZayxg-oCyPpbLjXHPcU")
// 获取我们自定义的信息
.getBody();
// 一旦令牌任意部分被篡改或者令牌过期,都会造成出现异常。
System.out.println(claims);//{name=Tom, exp=1690375658}
}
}
解析JWT并获取部分信息:
概念:Filter过滤器,是JavaWeb三大组件(Servlet、Filter、Listener)之一。
过滤器可以把对资源的请求拦截下来,从而实现一些特殊的功能。
过滤器一般完成一些通用的操作,比如:登录校验、统一编码处理、敏感字符处理等。
Filter
:定义一个类,实现Filter
接口,并重写其所有方法。Filter
:Filter类上加@WebFilter
注解,配置拦截资源的路径。引导类上加@ServletComponentScan
开启Servlet
组件支持。package com.itheima.tliaswebmanagement;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan//开启了对Servlet组件的支持
@SpringBootApplication
public class TliasWebManagementApplication {
public static void main(String[] args) {
SpringApplication.run(TliasWebManagementApplication.class, args);
}
}
package com.itheima.tliaswebmanagement.filter;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import java.io.IOException;
/**
* 过滤器
*/
@WebFilter("/*")// 过滤所有请求
public class DemoFilter implements Filter {
/**
* 初始化方法,只被调用一次
*
* @param filterConfig
* @throws ServletException
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("init: 初始化方法已执行");
}
/**
* 拦截请求后被调用,会被调用多次
*
* @param servletRequest
* @param servletResponse
* @param filterChain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
System.out.println("拦截到了请求");
// 放行
filterChain.doFilter(servletRequest, servletResponse);
}
/**
* 销毁方法,只被调用一次
*/
@Override
public void destroy() {
System.out.println("destroy: 销毁方法执行了");
}
}
疑问:
放行后访问对应资源,资源访问完成后,还会回到Filter中吗?
会
如果回到Filter中,是重新执行还是执行放行后的逻辑呢?
执行放行后的逻辑
所有的请求,拦截到了之后,都需要校验令牌吗?
有一个例外,登录请求
拦截到请求后,什么情况下才可以放行,执行业务操作?
有令牌,且令牌校验通过(合法);否则都返回未登录错误结果
Filter 可以根据需求,配置不同的拦截资源路径:
拦截路径 | urlPatterns值 | 含义 |
---|---|---|
拦截具体路径 | /login | 只有访问/login 路径时,才会被拦截 |
目录拦截 | /emps/* | 访问/emps下的所有资源,都会被拦截 |
拦截所有 | /* | 访问所有资源,都会被拦截 |
介绍:一个web应用中,可以配置多个过滤器,这多个过滤器就形成了一个过滤器链。
顺序:注解配置的Filter,优先级是按照过滤器类名(字符串)的自然排序。
细节尽在代码中
流程:
概念:是一种动态拦截方法调用的机制,类似于过滤器。Spring框架中提供的,用来动态拦截控制器方法的执行。
作用:拦截请求,在指定的方法调用前后,根据业务需要执行预先设定的代码。
定义拦截器,实现Handlerlnterceptor
接口,并重写其所有方法。
package com.itheima.tliaswebmanagement.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; @Component public class LoginCheckInterceptor implements HandlerInterceptor { /** * 目标资源方法执行前执行 * * @param request 请求 * @param response 响应 * @param handler * @return true-放行,false-不放行 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("preHandle..."); return true; } /** * 目标资源方法执行后执行 * * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("postHandle..."); HandlerInterceptor.super.postHandle(request, response, handler, modelAndView); } /** * 视图渲染完毕后执行,最后执行 * * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("afterCompletion..."); HandlerInterceptor.super.afterCompletion(request, response, handler, ex); } }
注册拦截器
package com.itheima.tliaswebmanagement.config; import com.itheima.tliaswebmanagement.interceptor.LoginCheckInterceptor; 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//在Spring Boot中,使用@Configuration注解可以标识一个类作为配置类,用于配置其他组件的属性和关系。 public class WebConfig implements WebMvcConfigurer { @Autowired private LoginCheckInterceptor loginCheckInterceptor; /** * 注册拦截器 * * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginCheckInterceptor).addPathPatterns("/**")//需要两个小星星哦 .excludePathPatterns("/login");//不需要拦截login } }
拦截路径 | 含义 | 举例 |
---|---|---|
/* | 一级路径 | 能匹配/depts,/emps,/login,不能匹配/depts/1 |
/** | 任意级路径 | 能匹配/depts,/depts/1,/depts/1/2… |
/depts/* | /depts下的一级路径 | 能匹配/depts/1,不能匹配/depts/1/2 |
/depts/** | /depts下的任意级路径 | 能匹配/depts,/depts/1,/depts/1/2…不能匹配/emps/1 |
接口规范不同:过滤器需要实现Filter
接口,而拦截器需要实现Handlerlnterceptor
接口。
拦截范围不同:过滤器Filter会拦截所有的资源,而lnterceptor
只会拦截Spring
环境中的资源。
如果Filter 与Interceptor同时存在:
在入门程序的代码基础上添加了类似于过滤器的校验逻辑,详见代码。
程序开发过程中不可避免的会遇到异常现象,此时服务器响应给前端的信息并不符合我们的开发规范。
出现异常,该如何处理?
方案一:在Controller的方法中进行try..catch处理
==代码臃肿,不推荐==
全局异常处理器
package com.itheima.tliaswebmanagement.exception; import com.itheima.tliaswebmanagement.pojo.Result; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * 全局异常处理器 */ @RestControllerAdvice// @RestControllerAdvice = @ControllerAdvice + @ResponseBody 而@ResponseBody可以实现返回值自动转为json public class GlobleExceptionHandler { @ExceptionHandler(Exception.class)//指定异常类型 public Result ex(Exception ex) { ex.printStackTrace(); return Result.error("错误的操作!");// 前段会把msg信息渲染展示 } }
事务是一组操作的集合,它是一个不可分割的工作单位,这些操作要么同时成功,要么同时失败。
操作
删除部门时,如果部门被删除而部门下的员工没有被删除,就会破坏数据的完整性和一致性,所以我们需要添加根据部门删除员工的功能,代码已经实现,详见代码。
但是,如果出现一下情况,阁下当如何应对?
即使程序运行抛出了异常,部门正常删除了,但是部门下的员工却没有删除,还是造成了数据的不一致。
注解:@Transactional
位置:业务(service)层的方法上、类上、接口上
作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务
我们还可以开启springboot事务管理的日志开关:
#spring事务管理日志
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
回滚日志示例:
2023-07-27T19:02:43.059+08:00 DEBUG 17796 --- [io-8080-exec-10] o.s.jdbc.support.JdbcTransactionManager : Initiating transaction rollback
2023-07-27T19:02:43.059+08:00 DEBUG 17796 --- [io-8080-exec-10] o.s.jdbc.support.JdbcTransactionManager : Rolling back JDBC transaction on Connection [com.mysql.cj.jdbc.ConnectionImpl@5beb2b03]
2023-07-27T19:02:43.060+08:00 DEBUG 17796 --- [io-8080-exec-10] o.s.jdbc.support.JdbcTransactionManager : Releasing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@5beb2b03] after transaction
@Transactional
进阶rollbackFor
属性若是出现这种情况,阁下又当如何应对呢?
int i = 1 / 0
会触发ArithmeticException
异常,它是RuntimeException
家族的一员,而此处我们抛出的Exception
是RuntimeException
的父类。
==默认情况下,只有出现RuntimeException
才回滚异常。==
rollbackFor
属性用于控制出现何种异常类型,回滚事务。
@Override
public Dept selectById(Integer id) {
return deptMapper.selectById(id);
}
/**
* 根据id删除部门
*
* <p>
* @Transactional是Spring框架中的注解,用于将事务管理应用于方法或类。
* 它使得开发者可以通过声明性的方式管理事务,而不需要手动处理事务的开启、提交和回滚等细节。
* <br>
* rollbackFor 属性用于指定当发生指定类型的异常时,事务应该进行回滚操作。
* <br>
* 默认情况下,只有出现`RuntimeException`才回滚异常。
* </p>
*
* @param id id号
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id) throws Exception {
deptMapper.delete(id);
// int i = 1 / 0;// 模拟出现异常
if (true) {
throw new Exception("出错啦~");
}
// 为了保证数据的完整性和一致性,必须连带删除部门下的员工
empMapper.deleteByDeptId(id);
}
propagation
属性&新需求事务传播行为:指的就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行事务控制。
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
解散部门时,记录操作日志
需求:解散部门时,无论是成功还是失败,都要记录操作日志。
步骤:
AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。
场景:案例部分功能运行较慢,定位执行耗时较长的业务方法,此时需要统计每一个业务方法的执行耗时,而我们决不能采用依次修改各个业务方法的低级手段来统计时间。
实现:==动态代理==是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。
统计各个业务层方法执行耗时
导入依赖:在pom.xml中导入AOP的依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
编写AOP程序:针对于特定方法根据业务需要进行编程
package com.itheima.aop; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import javax.swing.*; @Slf4j @Aspect// 标示为AOP类 @Component// 交给IOC容器管理 public class TimeAspect { /** * <h3 style="color: #FFA500"> * @Around("execution(* com.itheima.service.*.*(..))") * </h3> * <p style="color: #FFA500"> * 这段代码是Spring框架中的注解,用于定义一个拦截器(Interceptor),用于拦截指定方法。 * <br> * 具体来说,这个注解的含义是: * <li>@Around:表示这个注解用来定义一个环绕通知(Around advice)。</li> * <div> * ("execution(* com.itheima.service.*.*(..))"):表示要拦截的方法的表达式,其中: * <li>*(第一个):任意返回值</li> * <li style="">execution:表示这个表达式使用Java反射机制中的方法名称和参数类型来匹配方法。</li> * <li style="">com.itheima.service:表示要拦截的方法所在的包或类名。</li> * <li style="">.*:分别表示任意类或接口、任意方法名。</li> * <li style="">(..):表示任意参数列表。</li> * </div> * 因此,这段代码的含义是:拦截所有位于com.itheima.service包下的任意方法。 * </p> * * @param joinPoint * @return * @throws Throwable */ @Around("execution(* com.itheima.service.*.*(..))")// 切口表达式 public Object reportTime(ProceedingJoinPoint joinPoint) throws Throwable { // 记录开始时间 long begin = System.currentTimeMillis(); // 调用原始方法运行 Object result = joinPoint.proceed();// 返回值: 原始方法的返回值 // 记录结束时间 long end = System.currentTimeMillis(); log.info(joinPoint.getSignature() + "方法执行耗时: {}ms", end - begin); return result; } }
场景
优势
注意:
当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。
执行顺序
不同切面类中,默认按照切面类的类名字母排序:
==用@Order(数字)
加在切面类上来控制顺序==:
切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知
常见形式:
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符 返回值 包名.类名.方法名(方法参数) throws 异常?)
其中可以省略的部分:
可以使用通配符描述切入点:
*
:
单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
..
:
多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
==两个切入点表达式可以用||
连接==。
书写建议
所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。
如:查询类方法都是find 开头,更新类方法都是update开头。
描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性。
在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用.,使用*匹配单个包。
@annotation 切入点表达式,用于匹配标识有特定注解的方法。
@annotation(自定义注解全类名)
@Before("@annotation(com.itheima.anno.Log)")
public void before(){
log.info("before .…..");
}
这里com.itheima.anno.Log
是我们自定义的注解,添加了这个注解的方法就会被匹配为切入点。
package anno;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
}
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
@Around
通知,获取连接点信息只能使用 ProceedingJoinPoint
JoinPoint
,它是ProceedingJoinPoint
的父类型将案例中增、删、改相关接口的操作日志记录到数据库表中。操作日志日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长。
详见代码。
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。