拦截器(Interceptor)和过滤器(Filter)都是用于在请求道道目标资源的之前或之后进行处理的组件。主要区别有以下几点:
跨域问题指的是不同站点之间,使用ajax无法互相调用的问题。跨域问题本质是浏览器的一种保护机制,它的初衷是为了保证用户的安全,防止恶意网站盗取数据。但是这个保护机制也带来了新的问题,它的问题是给不同站点之间的正常调用的正常调用,也带来了阻碍,那么如何解决这三种问题呢?
在请求时,如果出现以下情况的的任意一种,那么它就是跨域请求:
协议不同:
http://example.com
发起请求到 https://api.example.com/data
域名不同:
example.com
的网页请求另一个域名为 api.example.com
的资源http://www.example.com
请求到 http://api.example.net/data
端口不同:
http://www.example.com
请求到 http://www.example.com:8080/data
或者类似以下这几种情况:
接下来,我们使用两个 Spring Boot 项目来演示跨域的问题,其中一个是端口号为 8080 的前端项目,另一个端口号为 9090 的后端接口项目
Frontend Project
跨域测试
后端接口项目首先先在 application.properties 配置文件中,设置项目的端口号为 9090,如下所示:
server.port=9090
// BackendController.java
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BackendController {
@GetMapping("/api/data")
public String getData() {
return "Hello";
}
}
在这个示例中:
/api/data
接口,并将返回的数据显示在页面上。/api/data
的GET接口,当收到请求时返回一条简单的消息。Spring Boot中跨域问题会有很多解决方案,如下:
使用@CrossOrigin注解可以轻松实现跨域,此注解即可以修饰类,也可以修饰方法。当修饰类时,表示此类中的所有接口都是可以跨域的,实现方式如下:
// BackendController.java
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class BackendController {
@CrossOrigin(origins = "http://localhost:8080") // 允许来自端口为8080的源的跨域请求
@GetMapping("/api/data")
public String getData() {
return "Hello";
}
}
使用此方式只能实现局部跨域,当一个项目中存在多个类的话,使用此方式就会比较麻烦(需要给所有类上都添加此注解)
接下来我们通过设置配置文件的方式就可以实现全局跨域了,它的实现步骤如下:
实现代码如下:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration // 一定不要忽略此注解
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 所有接口
.allowCredentials(true) // 是否发送 Cookie
.allowedOriginPatterns("*") // 支持域
.allowedMethods(new String[]{"GET", "POST", "PUT", "DELETE"}) // 支持方法
.allowedHeaders("*")
.exposedHeaders("*");
}
}
此实现方式和上一种实现方式类似,它也可以实现全局跨域,它的具体实现代码如下:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration // 一定不能忽略此注解
public class MyCorsFilter {
@Bean
public CorsFilter corsFilter() {
// 1.创建 CORS 配置对象
CorsConfiguration config = new CorsConfiguration();
// 支持域
config.addAllowedOriginPattern("*");
// 是否发送 Cookie
config.setAllowCredentials(true);
// 支持请求方式
config.addAllowedMethod("*");
// 允许的原始请求头部信息
config.addAllowedHeader("*");
// 暴露的头部信息
config.addExposedHeader("*");
// 2.添加地址映射
UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();
corsConfigurationSource.registerCorsConfiguration("/**", config);
// 3.返回 CorsFilter 对象
return new CorsFilter(corsConfigurationSource);
}
}
此方式是解决跨域问题最原始的方式,但它可以支持任意的 Spring Boot 版本(早期的 Spring Boot 版本也是支持的)。但此方式也是局部跨域,它应用的范围最小,设置的是方法级别的跨域,它的具体实现代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
@RestController
public class TestController {
@RequestMapping("/test")
public HashMap test(HttpServletResponse response) {
// 设置跨域
response.setHeader("Access-Control-Allow-Origin", "*");
return new HashMap() {{
put("state", 200);
put("data", "success");
put("msg", "");
}};
}
}
通过重写 ResponseBodyAdvice 接口中的 beforeBodyWrite(返回之前重写)方法,我们可以对所有的接口进行跨域设置,它的具体实现代码如下:
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {
/**
* 内容是否需要重写(通过此方法可以选择性部分控制器和方法进行重写)
* 返回 true 表示重写
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 方法返回之前调用此方法
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
// 设置跨域
response.getHeaders().set("Access-Control-Allow-Origin", "*");
return body;
}
}
此实现方式也是全局跨域,它对整个项目中的所有接口有效
为什么通过以上方法设置之后,就可以实现不同项目之间的正常交互呢? 这个问题的答案也很简单,我们之前在说跨域时讲到:“跨域问题本质是浏览器的行为,它的初衷是为了保证用户的访问安全,防止恶意网站窃取数据”,那想要解决跨域问题就变得很简单了,只需要告诉浏览器这是一个安全的请求,“我是自己人”就行了,那怎么告诉浏览器这是一个正常的请求呢?
只需要在返回头中设置“Access-Control-Allow-Origin”参数即可解决跨域问题,此参数就是用来表示允许跨域访问的原始域名的,当设置为“*”时,表示允许所有站点跨域访问,如下图所示:
所以以上 5 种解决跨域问题的本质都是给响应头中加了一个 Access-Control-Allow-Origin 的响应头而已
@Transactional时Spring框架中用于声明事务的的注解。它可以应用于方法级别,表示该方法应该在事务的范围内执行。当方法被调用时,Spring会在方法开始时开启一个事务,在方法结束时根据方法执行的结果来决定事务的提交或回滚。例如:
@Transactional
public void addUser(User user) {
userRepository.save(user);
}
当使用@Transactional注解标注一个方法的时候,Spring Boot在运行时会生成一个代理对象,该代理对象拦截被注解的方法调用,并在方法调用前后进行事务的管理。事务管理包括开始十五、提交事务或回滚事务等操作
导致@Trancsctional失效的常见场景有以下几种:
当使用@Transactional修饰非public方法,事务会失效,失效的原因主要是两种:
浅层原因:浅层原因是@Transactional源码限制了必须是public才能执行后续的代理流程,它的源码如下:
protected TransactionAttribute computeTransactionAttribute(Method method, Class> targetClass) {
// 不允许非public方法作为必需的方法。
// 如果只允许public方法,并且方法不是public,则设置为null。
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// 这里可以添加更多的逻辑来处理事务属性,例如根据方法名、参数等决定事务的隔离级别、只读性等。
// 下面是一个示例,仅作参考。
// 根据方法的名称或其他属性来决定事务的隔离级别。
Isolation isolationLevel = Isolation.READ_COMMITTED;
if ("someSpecificMethod".equals(method.getName())) {
isolationLevel = Isolation.READ_UNCOMMITTED;
}
// 设置事务的其他属性,如只读、超时等。
TransactionAttribute transactionAttribute = new Propagation(Propagation.REQUIRED)
.and(isolationLevel)
.and(Transactional.timeout(10)) // 设置事务超时时间为10秒
.and(new ReadOnly()) // 设置事务为只读模式
.build();
return transactionAttribute;
}
深层次原因:深层次原因是Spring Boot的动态代理只能代理公告方法,而不能代理私有方法或受保护方法。这是因为欸Spring的动态代理是基于Java的接口代理机制或者基于CGLib库来实现的,而这两种代理方式都是能代理公共方法:
如果在@Transactional方法内部捕获了所有可能抛出的异常,但是没有将它们重新抛出,那么十五就不会发现异常的存在了,从而就不会回滚事务
从 @Transactiona 实现源码也可以看出,@Transactional 注解只有执行方式捕获到异常了才会回滚,反之则不会,核心源码如下:
protected Object invokeWithinTransaction(Method method, Class> targetClass, final Invocation invocation) throws Throwable {
// 如果事务属性为null,则方法为非事务性方法
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass);
// 前半部分代码补全
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 使用 getTransaction 和 commit/rollback 方法进行标准事务划分
// 自动开启事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 这是环绕通知:调用链中的下一个拦截器。
// 正常情况下会调用目标对象的方法
// 反射调用业务方法
retVal = invocation.proceedWithInvocation();
} catch (Throwable ex) {
// 目标调用异常
// 异常时,在 catch 逻辑中,自动回滚事务
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
} finally {
cleanupTransactionInfo(txInfo);
// 自动提交事务
commitTransactionAfterReturning(txInfo);
}
return retVal;
} else {
// 补全部分
}
}
在Spring中,@Transactional
注解通常与动态代理结合使用来实现事务管理。当你在一个类的方法上标注了 @Transactional
注解时,Spring会为该类创建一个代理对象,并在代理对象上添加事务管理的逻辑。因此,只有通过代理对象调用的方法才能被事务管理器所控制。
如果你在类的内部调用一个带有 @Transactional
注解的方法,而是直接通过 this
对象来调用,那么事务管理器就无法介入,因为调用不经过代理对象。这样的调用会绕过代理,导致事务管理失败。
如果事务传播机制设置为 Propagation.NEVER 以非事务方式运行,或者是 Propagation.NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起的传播机制,则当前 @Transactional 修饰的方法也是不会正常执行事务的
Spring Boot/Spring 框架内之所以能使用事务是因为它们连接的数据库支持事务,这是前提条件,所以当数据库层面不支持事务时,那么框架中的代码无论怎么写都不会存在事务的
Spring Boot事务传播机制是指,包含多个事务的方法在相互调用的时候,事务是如何在这些方法间传播的
Spring Boot事务传播机制可以使用@Transactional(propagation=Propagation.REQUIREN)来定义,事务传播机制级别重要是包含以下七种:
设计模式是编写高质量、可维护和可扩展软件的重要工具。设计模式主要有以下几种:
MyBatis是一款优秀的基于Java的持久层框架,它内部封装了JDBC,使开发者只需要关注SQL语句本身,而不需要花费精力去处理加载驱动、创建连接等的过程,MyBatis的执行流程如下:
${}
和#{}
有什么区别?什么情况下一定要使用${}
?${}和#{}都是MyBatis种用来代替参数的特殊标识,如下:
但是二者的区别二还是很大的,主要区别如下:
也就是说,为了防止安全问题,所以大部分场景都要使用 #{} 替换参数,但是如果传递的是 SQL 关键字,例如order by xxx asc/desc 时(传递 asc 后 desc),一定要使用 $,因为它需要在执行时就被替换成关键字,而不能使用占位符替代(占位符不用应用于 SQL 关键字,否则会报错)。
在传递 SOL 关键字时,一定要使用 ${},但使用前,一定要进行过滤和安全检査,以防止 SQL 注入。
SQL注入就是指应用程序对用户输入的数据的合法性没有判断或过滤不严,攻击者可以在应用程序中事先定义好查询语句的结尾上添加额外的SQL语句,在管理员不知情的情况下实现非法操作,依次来实现欺骗数据库服务器执行非授权的热议操作,从而进一步得到相应的数据信息
也就是说所谓的 SQL 注入指的是,使用某个特殊的 SQL 语句,利用 SQL 的执行特性,绕过 SQL 的安全检查,查询到本不该查询到的结果
如以下代码:
从上述结果可以看出,以上程序在应用程序不知情的情况下实现非法操作,以此来实现欺骗数据库服务器执行非授权的敏感数据
预编译语句与参数化查询:使用 PreparedStatement 可以有效防止 SQL 注入,因为它允许你先定义 SQL 语句的结构,然后将变量作为参数传入,数据库驱动程序会自动处理这些参数的安全性,确保它们不会干扰 SQL 语句的结构,如下代码所示:
String sql ="SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt= connection.prepareStatement(sql);
pstmt.setString(1,userInputUsername);
pstmt.setstring(2,userInputPassword);
ResultSet rs=pstmt.executeQuery();
输入验证和过滤:对用户输入的信息进行验证和过滤,确保其符合预期的类型和格式
MyBatis二级缓存是用于提高MyBatis查询数据库的性能和减少访问数据库的机制。MyBatis的二级缓存总共由两个缓存机制:
二级缓存默认是不开启的,手动开启MyBatis步骤如下:
完整示例如下:
编写单元测试代码:
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class StudentMapperTest {
@Autowired
private StudentMapper studentMapper;
@Test
void testGetStudentCount() {
int count = studentMapper.getStudentCount();
System.out.println("查询结果:" + count);
int count2 = studentMapper.getStudentCount();
System.out.println("查询结果2:" + count2);
}
}
从以上结果可以看出,两次查询虽然使用了不同的 SqlSession,但第二次查询使用了缓存,并未查询数据库