这个笔记是在自己学习的过程中根据实际用到的和学到的整理出来的,可能会有缺失,错误等,主要是给激励自己学习,遇到写不下去的情况给自己一个参考,请各位大佬发现问题提出问题时能嘴下留情,也希望多提建议,谢谢。
本笔记长期更新(更新日期2024年9月21日)
使用注意事项:
src/resources
目录下书写规范:
.
换成冒号:
加换行,然后合并spring:
application:
name: 模块或者项目名称
#数据库配置
datasource:
url: jdbc:mysql://localhost:3306/数据库名称
#这个是8.0的配置,如果是5.0需要改成com.mysql.jdbc.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
username: 数据库用户名
password: 数据库密码
#文件上传大小限制
servlet:
multipart:
max-file-size: 单个文件的最大大小
max-request-size: 整个请求所有文件的最大大小
mybatis:
configuration:
#让mybatis每次运行sql代码的时候再控制台打印输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#mybatis大小写驼峰转换
map-underscore-to-camel-case: true
#spring日志打印
logging:
level:
org.springframework.jdbc.support.JdbcTransactionManager: debug
#阿里云OSS地址
aliyun:
oss:
endpoint: 阿里云端点
bucketName: 存储空间名称
#这两个一般不用写,一般配置在环境变量里面了。这两个是配置Access Key ID和Access Key Secret
accessKeyId: 从阿里云获取Access Key Id
accessKeySecret: 从阿里云获取Access Key secret
server:
port: 端口号
address: 地址
书写规范
com.example.pojo.UserMapper
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="">
mapper>
尽量使用yml文件,这个也可以用,并且SpringBoot会优先找这个
spring.application.name=模块或项目名称
#数据库连接
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名称
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=数据库用户名
spring.datasource.password=数据库密码
#让mybatis每次运行sql代码的时候再控制台打印输出
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#让mybatis自动让两个规范的命名转换,小驼峰转下划线
mybatis.configuration.map-underscore-to-camel-case=true
#spring打印日志
logging.level.org.springframework.jdbc.support.JdbcTransactionManager = debug
#阿里云OSS配置
aliyun.oss.endpoint=阿里云端点
aliyun.oss.bucketName=存储空间名称
#设置启动端口
server.port=端口号
一般用于封装,然后将数据发送给前端
注意事项:
前端接收的code也不一定是code,也有可能是flag,接收的也有概率是布尔值等,一定要随机应变,不要粘过去就是用
前端接收的数据不一定是data,也有接收list的,此时就需要把里面的data数据名改成前端接收的
@Data
public class Result {
private Integer code; //编码:1成功,0为失败
private String msg; //错误信息
private Object data; //数据
public static Result success() {
Result result = new Result();
result.code = 1;
result.msg = "success";
return result;
}
public static Result success(Object object) {
Result result = new Result();
result.data = object;
result.code = 1;
result.msg = "success";
return result;
}
public static Result error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
}
一般用于分页功能
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private Long total; //总记录数
private List rows; //当前页数据列表
}
当要使用阿里云OSS存储的时候使用这个工具类来快速完成向云端存储
注意事项:
@Slf4j
public class AliyunOSSUtils {
/**
* 上传文件
* @param endpoint endpoint域名
* @param bucketName 存储空间的名字
* @param content 内容字节数组
*/
public static String upload(String endpoint, String bucketName, byte[] content, String extName) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = UUID.randomUUID() + extName;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new ByteArrayInputStream(content));
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
log.error("Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("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.");
log.error("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
也是用来简化阿里云配置的,这个会通过注解@ConfigurationProperties(prefix = “aliyun.oss”)从配置文件application.properties或者yml中找到并赋值给对应的值
注意事项:
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
}
用于生成和解析TOKEN的,但是必须要用JWT令牌并且引入了正确的依赖
public class JwtUtils {
private static Long expire = 43200000L;//这个是TOKEN的有效期毫秒数,这个是12小时
private static String JWT_KEY = "JWT_SECRET";//如果配置了环境变量就这么写
//如果没配置环境变量就这么写
//private static String JWT_SECRET = "密钥"
//生成JWT令牌TOKEN
public static String generateJwt(Map<String, Object> claims) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
//解析JWT令牌TOKEN
public static Claims parseJWT(String jwt) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
创建时选择Spring Web依赖自动配置一些基本的WEB环境,比如Tomcat服务器的集成,Spring MVC核心组件等
创建时选择Lombok 可以对封装类进行简化注解
如果需要用到MySql数据库操作和MyBatis勾选MySql Driver和MyBatis Framework
**注意事项:**创建好的SpringBoot工程会有一个对应的启动器,这个要和软件包同级,不然Spring会找不到
如果项目中同时存在application.properties,application.yml,application.yaml三个文件,三个的优先级顺序是
application.properties>application.yml>application.yaml
但是项目打包以后,使用cmd命令行运行;或者在启动时添加了虚拟机选项和程序实参,又会引入两个新的属性配置,对应的就是java属性配置和命令行参数,这两个的优先级都比配置文件高,并且命令行参数的优先级会更高一些。
java属性配置格式(虚拟机选项):(-Dkey=value),这个大写D是必须写的,然后就是键值对,例如配置端口号就是
-Dserver.port=9000
命令行参数(程序实参):(–key=value)
--server.port=10000
当然,如果打成jar包以后,可以通过这种方式来设置数据库的各项属性来启动项目
启动jar包的方式:
java (这里可以写java属性配置) -jar XXX(jar包名).jar (之类可以写命令行参数)
启动jar包的时候配置数据库属性:
java -jar XXX.jar --spring.datascoure.url=jdbc://localhost:3306/数据库名称 --spring.datasource.username=名 --spring.datasource.password=密码
所以最终的优先级顺序是:
命令行参数>java属性配置>application.properties>application.yml>application.yaml
主要负责接收用户请求、处理请求参数、调用适当的服务层(Service)方法,并将结果返回给用户。是用户界面与后端服务器的桥梁。
负责业务逻辑的实现,封装了与业务相关的处理和操作,处理业务规则、数据验证、计算、转换等操作。也会去数据访问层拿到需要的数据。
主要是隐藏数据访问的底层细节,提供给上层(服务层)的一个简洁的接口。
通常与MyBatis这样的框架结合使用。通过定义SQL映射来处理数据访问。
通常与ORM框架结合使用。将数据访问操作封装到DAO对象中,这些对象通常对应于数据库中的表或者集合。
不依赖于任何框架或特定API,仅仅是实现了JAVA语言特定的对象。
一般是一些静态方法,用于提供各种通用功能或者简化某些常见的开发任务。这些工具类在整个应用程序中被广泛使用,提高代码的复用性和简洁性。
因为每个项目的不同,还有可能存在异常包exception等。
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Component | 类上 | Spring能识别这个类并将其作为一个bean对象管理 @Controller,@Service,@Repository都集成了这个注解,一般用这几个 |
@Autowried | 类、方法、字段、构造函数或局部变量上 | 当一个类需要另一个类的实例作为依赖时,可以使用此注解来告诉 Spring 容器自动注入正确的实例 |
@Resource(name=“指定注入的bean名称”) | 类、方法、字段、构造函数或局部变量上 | 可以通过名称查找 Bean,如果未指定名称,则默认按照字段名进行装配 不能和Autowried一起使用 处理多实现可以用 |
@Qualifier(value=“制定注入的bean名称”) | 类、方法、字段、构造函数或局部变量上 | 用于解决自动装配时的歧义问题,即当存在多个候选 Bean 时,可以指定具体的 Bean 名称来进行装配 需要和Autowried一起使用 处理多实现可以用 |
@Primary | 类上 | 用于解决在同一个类型下存在多个候选 Bean 时,应该优先使用哪一个 Bean 的问题。 如果一个 Bean 被标记为 @Primary,那么当存在多个候选者时,Spring 会优先选择带有 @Primary 标记的 Bean 进行注入 |
@Slf4j | 类上 | 来自 Lombok 库,用于自动生成一个 Logger实例 |
@Log | 类上 | 可以由不同的日志库提供,例如 Log4j2,用于自动生成一个 Logger实例 |
@Value(“${要引入的值的位置}”) | 字段、方法或方法参数级别上 | 用于直接将配置值注入到 Java 类的字段中。它可以用于注入各种类型的值,如字符串、数字等。 |
@ConfigurationProperties | 类上 | 可以批量注入属性、验证等,并且可以更容易地与配置文件中的复杂属性结构相匹配。 |
@Value和@ConfigurationProperties使用举例:
public class Test {//这里的${property.name}是从Spring的PropertySources中获取的属性值,PropertySources多种来源,如application.properties文件,环境变量,命令行参数等。
@Value("${property.name}")
private String propertyName;
public void test(@Value("${property.name}") String name)
}
@ConfigurationProperties(prefix = "property")
public class Test{//从配置文件中读取property.name和property.age
private String name;
private int age;
}
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Component | 类上 | Spring能识别这个类并将其作为一个bean对象管理 @Controller,@Service,@Repository都集成了这个注解,一般用这几个 |
@Autowried | 类、方法、字段、构造函数或局部变量上 | 当一个类需要另一个类的实例作为依赖时,可以使用此注解来告诉 Spring 容器自动注入正确的实例 |
@Scope | 类上 | 用于指定一个 Bean 的作用域,如单例(singleton)、原型(prototype)、请求(request)、会话(session)等 |
@Bean | 类上 | 注解用于在 @Configuration 类中声明一个方法,该方法返回的对象将被注册为 Spring 容器中的一个 Bean |
@Lazy | 类上 | 用于延迟初始化 Bean。默认情况下,Spring 容器会在启动时立即初始化所有的单例 Bean,而使用 @Lazy 注解可以推迟到第一次使用时才进行初始化。 |
@ComponentScan | 启动器类上 | 用于指定 Spring 应该扫描哪些包来查找组件 |
@Import | 类上 | 用于导入其他配置类,从而使它们定义的 Bean 也被纳入当前配置类的管理范围。 |
@Conditional | 类上或具体的@Bean方法上 | 注解用于根据某些条件决定是否创建某个 Bean。 |
@ConditionalOnClass | 类上或具体的@Bean方法上 | 用于根据类路径上是否存在某个类来决定是否创建 Bean。 |
@ConditionalOnMissingBean | 类上或具体的@Bean方法上 | 用于根据容器中是否已经存在某个类型的 Bean 来决定是否创建新的 Bean。如果指定类型的 Bean 已经存在,则不会创建新的 Bean。 |
@ConditionalOnProperty | 类上或具体的@Bean方法上 | 用于根据属性值来决定是否创建 Bean。它可以检查配置文件中的属性是否存在以及其值是否满足指定条件。 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@RestController | 类上 | 复合注解,集成了@Controller和@ResponseBody的功能 |
@Controller | 类上 | 用来接收HTTP请求,可以根据地址来判断执行哪个功能 |
@ResponseBody | 类上或者方法上 | 和@RequestBody相反,这个是在JAVA向前端发送时将数据转换为JSON数据 |
@RequestMapping(“/路径地址”,method=RequestMethod.请求方式) | 类或者方法上 | 基础的注解,用来指定控制器方法处理哪些HTTP请求,定义一个或者多个URL映射 |
@GetMapping | 类或者方法上 | 专门用于处理Get请求 |
@PostMapping | 类或者方法上 | 专门用于处理POST请求 |
@PutMapping | 类或者方法上 | 专门用于处理Put请求 |
@DeleteMapping | 类或者方法上 | 专门用于处理Delete请求 |
@RequestParam(value=“获取的值的键名”,defaultValue=“如果没有参数的默认值”,required=true/false) | 参数上 | 从HTTP请求的查询字符串中获取参数,可以指定一个请求参数的名字,并且可以指定默认值,是否必须存在等属性。获得的方式类似于map,根据键获得值 |
@RequestBody | 参数上 | 当客户端发送JSON或者其他格式的数据作为请求体时,Spring会尝试将主体的内容转换为相应的JAVA对象 |
@PathVariable() | 参数上 | 用于将URL路径的模板变量绑定到方法参数上。 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Service | 类上 | 表明类是一个服务提供者 |
@Transactional | 类上或者方法上 | 表名这个方法是事务或者整个类下所有方法都是事务 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Mapper | 类上 | 表明类是一个映射接口方法 |
@MapKey(“键名”) | 方法上 | 声明Map的键名,不写也行 |
@Param(“需要传递的参数名”) | 参数上 | SpringBoot一般不用。在MyBatis框架中,这个注解用于在SQL映射文件中的预编译语句中传递多个参数 |
@Options(useGeneratedKeys=true/false,keyProperty=“键名”) | 方法上 | 在完成数据库操作后会获取数据库自动成成的键值并将这个值设置到对应的对象属性上 |
@Result | 方法上 | 描述单个列如何映射到 Java 对象的属性上。 |
@Results | 方法上 | 用于包装一个或多个 @Result 注解,可以用来描述一个完整的映射规则集合。 |
@Select | 方法上 | 用来编写 SELECT 查询语句。可以直接在 Mapper 接口的方法上使用该注解,并提供 SQL 语句。 |
@Update | 方法上 | 用来编写 UPDATE 语句。可以直接在 Mapper 接口的方法上使用该注解,并提供 SQL 语句。 |
@Insert | 方法上 | 用来编写 INSERT 语句。可以直接在 Mapper 接口的方法上使用该注解,并提供 SQL 语句。 |
@Delete | 方法上 | 用来编写 DELETE 语句。可以直接在 Mapper 接口的方法上使用该注解,并提供 SQL 语句。 |
Result和Results的用法示例:
就是在JAVA中使用注解完成应该在XML的resultMap标签的功能
@Results({
@Result(column="user_id",property="userId",id=true),
@Result(column="user_name",property="userName"),
@Result(column="phone",property="phone")
})
public List<User> select();
注解名称 | 注解位置 | 注解租用 |
---|---|---|
@Respository | 类上 | 表名类是一个数据访问层或持久化层的组件 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Data | 类上 | 生成get set hashcode equals toString方法 |
@AllArgsConstructor | 类上 | 生成全参构造 |
@NoArgsConstructor | 类上 | 生成无参构造 |
@Getter | 类上 | 生成Getter方法 |
@Setter | 类上 | 生成Setter方法 |
@DataTimeFormat(pattern = “”) | 成员变量上 | 按照pattern写的规则解析String文件变成LocalDateTime(LocalDate/LocalTime) |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Test | 方法上 | 用于标记一个方法作为测试方法。 |
@BeforeAll | 方法上 | 在所有测试方法之前只执行一次的方法。 |
@AfterAll | 方法上 | 在所有测试方法之后只执行一次的方法。 |
@BeforeEach | 方法上 | 在每个测试方法之前执行一次的方法。 |
@AfterEach | 方法上 | 在每个测试方法之后执行一次的方法。 |
@ParameterizedTest | 方法上 | JUnit5的注解,测试方法可以使用多个参数集来执行多次,每组参数对应一次执行 |
@ValueSource | 方法上 | 提供一个或多个固定值作为参数化测试的参数源。 |
@CsvSource | 方法上 | 提供 CSV 格式的字符串作为参数化测试的参数源。 |
@ParameterizedTest、@ValueSource和@CsvSource示例:
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testNumbers(int number) {
System.out.println(number);//会执行三次,分别打印1,2,3
}
@ParameterizedTest
@CsvSource({"1, one", "2, two", "3, three"})
void testNumbers(String number, String word) {
System.out.println(number + ": " + word);//会执行三次,分别打印1:one,2:two,3:three
}
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@RestControllerAdvice | 类上 | 该类中的方法将用于处理特定的异常 |
@ExceptionHandler(value=“异常类名”) | 方法上 | 用于标记处理特定异常的方法。当应用程序抛出某种异常时,被注解的方法会被调用,并处理该异常。 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@WebFilter(“写拦截路径,如果是/*则代表全部拦截”) | 类上 | 声明一个类作为 Web 应用程序中的过滤器 |
@ServletComponentScan | 类上 | 用于指示容器扫描并自动部署在类路径中的 Servlet、过滤器(Filter)、监听器(Listener)和其他 Servlet 组件。 |
@Configuration | 类上 | 用于声明一个类作为配置类,可以以编程的方式定义和配置 Spring 应用程序中的依赖关系,这种方式称为基于 Java 的配置。 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Aspect | 切面类上 | 告诉Spring这是一个包含切面逻辑的类。 |
@Before | 切面类的方法上 | 定义一个前置通知,在目标方法调用之前执行。 |
@After | 切面类的方法上 | 定义一个后置通知,在目标方法执行完毕之后执行,无论方法是否成功执行。 |
@AfterReturning | 切面类的方法上 | 定义一个返回后通知,在目标方法成功执行并返回结果之后执行。 |
@AfterThrowing | 切面类的方法上 | 定义一个异常抛出后通知,在目标方法抛出异常后执行。 |
@Around | 切面类的方法上 | 定义一个环绕通知,在目标方法调用前后都执行自定义的行为。 |
@annotation | 上面五个的括号里面 | 使用自定义注解来标记需要进行切面。 |
@pointcut | 方法上 | 定义一个可重用的切入点表达式。 |
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@interface | 元注解,在代替类的样式(class/interface) | 是 Java 语言中的一个保留关键字,用于定义注解类型。 |
@Retention | 自定义注解类上 | 指定注解的保留策略,可以是 SOURCE (源码阶段)、CLASS (字节码阶段)或 RUNTIME (运行时阶段)。 |
@Target | 自定义注解类上 | 指定注解的应用目标,如 TYPE (类、接口)、METHOD (方法)、FIELD (字段)等。 |
@Documented | 自定义注解类上 | 指定注解是否应该被包含在 JavaDoc 文档中。 |
@Inherited | 自定义注解类上 | 指定子类是否继承父类上的注解。 |
@Repeatable | 自定义注解类上 | 指定注解是否可以重复使用在同一元素上。 |
分页查询一般是同时包含分页和查询了
有两种接收数据的方法,一种是通过封装类获得,一种是通过一个个接收
建议是通过封装类的方式,这样再传入数据多的时候,不用一个个写接收,让Spring自己对应封装好,但是这样的话需要注意,名字和前端返回的必须完全一致
样例参考:
假如我们可以通过姓名,手机号查询,并且有分页
已知前端发送回来的数据如下
{
"name":"张",
"phone":1234,
"page":1,
"pageSize":5
}
所以后端封装类(UserQueryParam)参考
@Data
public class UserQueryParam {
private Integer page = 1;//前端未传回时默认为第一页
private Integer pageSize = 10;//前端未传回时默认为查找十条数据
private String name;
private String phone;
}
定义返回对象PageBean类
这个类也是要取决于前端写的接收数据的名称来定义,这个total也有写在别的地方的,视情况而定。有的是取res.data.list,也有res.data.data,也有res.data.rows。
下面这个例子是前端接收res.data.total和res.data.rows的例子。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private Long total; //总记录数
private List rows; //当前页数据列表,如果是list或者data成员变量的名要跟着变
}
Controller类
@RestController
@RequestMapping("/users")
public class Controller {
@Autowried
private Service service;
public Result select(UserQueryParam param){
//调用Service层进行数据处理并返回
PageBean pageBean = service.select(param);
return Result.success(pageBean);
}
}
因为同时是做了分页和查询,所以接受的参数写起来也会很麻烦,不建议这么写
查询一般是GET请求,请求参数一般就是跟在路径后面的字符串
例如:/users?page=1&pageSize=10
@RestController
@RequestMapping("/users")
public class Controller{
@Autowried
private Service service;
@GetMapping
public Result select(@RequestParam(defaultValue="1") Integer page,
@RequsetParam(defaultValue="10") Integer Pagesize,//这两个是做分页功能用到的
@RequestParam(required = false) String name,
@RequestParam(required = false) String phone){//这两个是做查询功能用到的
//调用Service层进行数据处理并返回
PageBean pageBean = service.select(page,pageSize,name,phone);
return Resule.success(pageBean);
}
}
定义返回对象PageBean类
这个类也是要取决于前端写的接收数据的名称来定义,这个total也有写在别的地方的,视情况而定。有的是取res.data.list,也有res.data.data,也有res.data.rows。
下面这个例子是前端接收res.data.total和res.data.rows的例子。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean {
private Long total; //总记录数
private List rows; //当前页数据列表,如果是list或者data成员变量的名要跟着变
}
5.1已经完成了从前端获取数据并发送到Service层了,接下来就是根据条件分页和查询了。
不使用插件的分页查询就是使用SQL语句提供的limit功能,Service层的实现类里需要提前计算limit里面的两个值。
Service的实现
@Service
public class Service {
@Autowried
private Mapper mapper;
public PageBean select(Integer page,Integer pageSize,String name,String phone){
//获取总记录数
Long total = mapper.count();
//获取结果列表
Integer start = (page - 1) * pageSize;//这个计算是计算起始值,加上MySql的limit索引是从0开始的
List<User> list = mapper.select(start,pageSize,name,phone);
//封装结果返回
return new PageBean(total,list);
}
}
Mapper的实现
@Mapper
public class Mapper {
public Long count();
public List<User> select(Integer start,Integer pageSize,String name,String phone)
}
Mapper.xml的实现
主要就是倒数第三行那里的limit
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.Mapper">
<select id="count">
select count(*) from user
select>
<select id="select" resultType="com.example.pojo.User">
select * from user
<where>
<if test="name != null and name != ''">and name like concat('%','#{name}','%')if>
<if test="phone != null and phone != ''">and phone like concat('%','#{phone}','%')if>
where>
limit #{start},#{pageSize}
select>
mapper>
5.1已经完成了从前端获取数据并发送到Service层了,接下来就是根据条件分页和查询了。
这里使用了PageHelper工具,需要在POM加入如下依赖
<dependency>
<groupId>com.github.pagehelpergroupId>
<artifactId>pagehelper-spring-boot-starterartifactId>
<version>2.1.0version>
dependency>
Service的实现
@Service
public class Service {
PageHelper.startPage(param.getPage(),param.getPageSize());//这个就是PageHelper提供的分页方法,并且下面就必须写查询方法,仅针对下面生效,务必注意书写位置
List<User> userList = mapper.select(param);
Page<User> list = (Page<User>) userList;//将List类型强制转换为Page类型
return new PageBean(list.getTotal(),list.geResult());//调用Page的获取总页数和数据的方法
}
Mapper的实现
@Mapper
public class Mapper {
List<User> select(UserQueryParam param);
}
Mapper.xml的实现,好处就是PageHelper会自动分页,不需要我们写limit
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.Mapper">
<select id="select" resultType="com.example.pojo.User">
select * from user
<where>
<if test="name != null and name != ''">and name like concat('%','#{param.name}','%')if>
<if test="phone != null and phone != ''">and phone like concat('%','#{param.phone}','%')if>
where>
select>
mapper>
一对多的操作非常常见,例如员工和工作经历,一个员工可能会有0个,也可能一个,也可能多个工作经历。
这种情况一般前端传来的JSON里面还会有一个数组,这个数组里面又是一个JSON
关键点就是我们要插入的工作经历需要员工的ID,但是这个员工是新增,ID是自动增长的主键,导致我们在做添加操作时没有ID,就需要我们添加到数据库以后再获得这个员工的ID。
如果插入后再查询效率难免太低,所以Spring提供了一个注解@Options,这个注解在完成数据库操作后会获取数据库自动成成的键值并将这个值设置到对应的对象属性上。
**使用前提:**这个必须是主键,自增主键
1.第一种方法:在Mapper接口上使用注解
@Options(useGeneratedKeys=true,keyProperty="自增主键")
2.第二种方法:在XML映射文件中使用属性useGeneratedKeys和keyProperty
<insert id="Mapper的方法名" useGeneratedKeys="true" keyProperty="自增主键">
insert>
示例:在这个示例中,当前端发送请求后,传入一个没有id的param,但是在mapper完成add方法后,所有的param都会给ID返回数据库对应的值。
@RestController
@RequestMapper("/users")
public class Controller {
@Autowried
private Service service;
@PostMapping
public Result add(Param param){
service.add(param);
return Result.success();
}
}
@Service
public class Service {
@Autowried
private Mapper mapper;
public void add(Param param){
mapper.add(prarm);
}
}
@Mapper
public class Mapper {
@Option(useGeneratedKeys=true,keyProperty="id")//这个和XML内写两种方法二选一
@Insert("insert into user(name,phone) values(#{name},#{phone})")//这个和XML内写两种方法二选一
void add(Param param);
}
概念: 事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作 要么同时成功,要么同时失败。
事务的四大特性(ACID)
使用位置
位置 | 作用 |
---|---|
方法上 | 将当前方法交给Spring进行事务管理 |
类上 | 当前类中所有方法都交给Spring进行事务管理 |
接口上 | 接口上所有的实现类当中的所有方法都交给Spring进行事务管理 |
属性
属性 | 作用 |
---|---|
rollbackFor | 默认情况下,只有出现RuntimeException(运行时异常)才会回滚事务,这个属性可以定义异常的class,指定出现何种异常才会回滚事务 |
propagation | 用来指定传播行为,具体值见下表 |
propagation值
假设现在有两个事务方法,一个A方法,一个B方法,两个方法都被@Transactional注解了,A方法中又调用了B方法,此时就会出现一个问题,B方法运行时,是加入A方法的事务还是新建一个事务?
属性值 | 含义 |
---|---|
REQUIRED | 【默认值】需要事务,有则加入,无则创建新事务 |
REQUIRES_NEW | 需要新事务,无论有无,总是创建新事务 |
SUPPORTS | 支持事务,有则加入,无则在无事务状态中运行 |
NOT_SUPPORTED | 不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务 |
MANDATORY | 必须有事务,否则抛异常 |
NEVER | 必须没事务,否则抛异常 |
… |
假如说B方法也是操作数据表,比如转账,A是扣钱,B是加钱,A扣完了调用B,B这时候就得加入A这个事务中,出问题全都回滚,此时用默认值就可以;如果说B是日志记录,不管成功与否,都要记录本次操作,那么B就需要单独开启一个事务,A后续出问题不会回滚B,此时B的值就是REQUIRES_NEW。
唯一一个要注意的点就是在不改变rollbackFor的值的情况下只有在出现RuntimeExcrption也就是运行时异常才会回滚,如果出现编译时异常并且异常被触发是不会回滚的
一个项目中功能会有很多,不免就会有很多异常。有编译时异常,也有运行时异常,也有自定义异常。但是每一个异常如果都要try catch处理,难免会导致代码量过大,可读性变差;如果我们只是一味地往上抛异常,又会导致一件事,异常只是抛出,没有做任何处理。因此需要一个对异常做集中处理的方式。
Spring提供了全局异常处理器,只需要我们定义一个类,在这个类上增加一个注解**@RestControllerAdvice**就是定义好了。
最好是新建一个Exception包来存储处理器和自定义的异常。
在全局异常处理器当中,需要定义方法来捕获异常,在方法上需要加上注解**@ExceptionHandler**,可以通过控制该注解的value属性来制定我们要捕获的是哪一类异常,如果什么value都不加,它会默认处理我们所有没有处理的异常,就是没有被try/catch处理的或者没有被本注解处理的异常。
示例:一个自定义异常 CustomerException.java
和全局处理异常 GlobalExceptionHandler.java
public class CustomerException extends RuntimeException {
String msg;
public CustomerException(String msg) {
this.msg = msg;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
public class GlobalExceptionHandler {
@ExceptionHandler
public Result exception(Exception e) {//如果项目内没有任何try/catch那么这个方法将会处理所有非CustomerException的异常
e.printStackTrace();
return Result.error("对不起,操作失败,请联系管理员");
}
@ExceptionHandler(CustomerException.class)
public Result exception(CustomerException e) {//这个方法将会处理所有的CustomerException异常
log.info(e.getMessage());
return Result.error(e.getMsg());
}
}
例如我们遇到的那个问题,删除班级的时候如果班级里面有人抛出异常并且返回前端该班级有人,不能删除就可以这么写:
throw new CustomerException("对不起,该班级下有学生,不能直接删除");
首先,注册,开通服务等在此都不介绍。
阿里云OSS帮助文档SDK参考JAVA:Java_对象存储(OSS)-阿里云帮助中心 (aliyun.com)截止于2024年9月15日,本网站可以正常访问,如果不能正常访问请尝试在阿里云官网自行查找
我们需要在拿到AccessKey后再进行下面的步骤
管理员身份运行CMD命令行,执行如下命令,配置系统的环境变量
set OSS_ACCESS_KEY_ID=从阿里云拿到的AccessKeyId
set OSS_ACCESS_KEY_SECRET=从阿里云拿到的AccessKeySecret
执行如下命令,让更改生效
setx OSS_ACCESS_KEY_ID "%OSS_ACCESS_KEY_ID%"
setx OSS_ACCESS_KEY_SECRET "%OSS_ACCESS_KEY_SECRET%"
验证是否生效,这个的好处是都存在了计算机上,不怕服务器被攻击看到源码
echo %OSS_ACCESS_KEY_ID%
echo %OSS_ACCESS_KEY_SECRET%
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.17.4version>
dependency>
<dependency>
<groupId>javax.xml.bindgroupId>
<artifactId>jaxb-apiartifactId>
<version>2.3.1version>
dependency>
<dependency>
<groupId>javax.activationgroupId>
<artifactId>activationartifactId>
<version>1.1.1version>
dependency>
<dependency>
<groupId>org.glassfish.jaxbgroupId>
<artifactId>jaxb-runtimeartifactId>
<version>2.3.3version>
dependency>
不是所有的都需要添加,按需添加
#阿里云OSS地址
aliyun:
oss:
endpoint: 阿里云端点
bucketName: 存储空间名称
#这两个一般不用写,一般配置在环境变量里面了。这两个是配置Access Key ID和Access Key Secret
accessKeyId: 从阿里云获取Access Key Id
accessKeySecret: 从阿里云获取Access Key secret
@Slf4j
public class AliyunOSSUtils {
/**
* 上传文件
* @param endpoint endpoint域名
* @param bucketName 存储空间的名字
* @param content 内容字节数组
*/
public static String upload(String endpoint, String bucketName, byte[] content, String extName) throws Exception {
// 从环境变量中获取访问凭证。运行本代码示例之前,请确保已设置环境变量OSS_ACCESS_KEY_ID和OSS_ACCESS_KEY_SECRET。
EnvironmentVariableCredentialsProvider credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider();
// 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。
String objectName = UUID.randomUUID() + extName;
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
try {
// 创建PutObjectRequest对象。
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectName, new ByteArrayInputStream(content));
// 创建PutObject请求。
PutObjectResult result = ossClient.putObject(putObjectRequest);
} catch (OSSException oe) {
log.error("Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.");
log.error("Error Message:" + oe.getErrorMessage());
log.error("Error Code:" + oe.getErrorCode());
log.error("Request ID:" + oe.getRequestId());
log.error("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
log.error("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.");
log.error("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return endpoint.split("//")[0] + "//" + bucketName + "." + endpoint.split("//")[1] + "/" + objectName;
}
}
这个类想不写的话也可以不写,只要给每个都要用OSS服务的实现类都配置一下里面的属性就可以了。
参考:
public class Test{
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
//各种实现方法
}
注意事项:
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")//该注解从配置文件application.properties或者yml中找到并赋值给对应的值
public class AliyunOSSProperties {
private String endpoint;
private String bucketName;
}
MultipartFile是Spring框架中的一个接口,用于处理上传的文件。
MultipartFile定义了一下方法,可以轻松读取,验证,和保存上传的文件。
方法名 | 作用 |
---|---|
getOriginalFilename() | 返回上传文件的原始文件名 |
getSize() | 返回上传文件的大小(一字节为单位) |
getContentType() | 返回上传文件的MIME类型(如image/jpg) |
getBytes() | 将上传文件的内容读取为字节数组 |
transferTo(File dest) | 将上传文件的内容保存到指定的目标文件中 |
假如现在要在Controller中实现上传方法
@RestController
public class Controller {
@Autowried
private AliyunOSSProperties aliyunOSSProperties;
@PostMapping("/upload")
public Result upload(MultipartFile file) throws Exception {
String extName = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf("."));//获得文件的后缀,例如.jpg
String endpoint = aliyunOSSProperties.getEndpoint();//获得阿里云端点号
String bucketName = aliyunOSSProperties.getBucketName();//获得阿里云存储空间名称
String url = AliyunOSSUtils.upload(endpoint, bucketName, file.getBytes(), extName);//上传文件,这个方法是自动生成一个UUID名字,所以只需要后缀名就可以了
return Result.success(url);//返回存储的url地址
}
}
JavaWeb中会话技术是用来处理HTTP协议无状态特性的解决方案。HTTP协议是一种无状态协议,这意味着每次客户端向服务器发起请求时,服务器将此请求视为独立的事件,不会记住前一次请求的信息。然而,在构建Web应用程序时,通常需要跨多个请求来维护用户的状态信息,比如用户的登录状态、购物车内容等。这就需要用到会话技术来跟踪用户会话并保持状态信息。
Cookie是在客户端上存储少量信息(最大4KB)的一种方法。
当客户端首次访问服务器时,服务器可能会设置一个Cookie,其中可能包含一个唯一标识符或其他信息。每当客户端向服务器发送请求时,都会自动包含任何已设置的Cookies。服务器可以读取这些Cookies,并根据它们来识别客户端或提供特定于该客户端的服务(如免登录,访问权限等)。
实现示例:
@RestController
public class Controller {
//设置Cookie
@GetMapping("/set")
public Result setCookie(HttpServletResponse response){
response.addCookie(new Cookie("key1","value1"));//只能传两个String类型
response.addCookie(new Cookie("key2","value2"));//可以设置多个cookie
return Result.success();
}
//获得Cookie
@GetMapping("/get")
public Result getCookie(HttpServletRequest request){
Cookie[] cookies = request.getCookies();
for(Cookie cookie : cookies){
System.out.println(cookie.getName()+":"+cookie.getValue());//打印出来的就是key1:value1 key2:value2
//可以进行别的操作,判断等
}
return Result.success();
}
}
优点:
缺点:
Session是服务器端会话跟踪技术,它是存在服务端的。Session就是基于Cookie实现的。
实现示例:
@RestController
public class Controller {
//设置session
@GetMapping("/set")
public Result setSession(HttpSession session){
session.setAttribute("key1","value1");
session.setAttribute("key2","value2");
return Result.success();
}
//获取session
@GetMapping("/get")
public Result getSession(HttpServletRequest request){
HttpSession session = request.getSession();
Object value1 = session.getAttribute("key1");//通过key1拿到了value1
Object value2 = session.getAttribute("key2");//通过key2拿到了value2
//可以进行别的操作
return Result.success();
}
}
优点:
缺点:
JWT是一种用于在网络上安全传输信息的令牌,通过数字签名的方式,以JSON对象为载体,在不同的服务终端之间安全地传输信息。
优势:
缺点:
JWT:(官网:https://jwt.io)
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
似乎新版的jwt更换了新的写法,下面都是按照这个版本写的
分别在管理员身份运行下面三条,前两条是加入环境变量,第三条是验证这个数据是否加入了环境变量 ,这个的好处是都存在了计算机上,不怕服务器被攻击看到源码。
set JWT_SECRET=你想要的密钥
setx JWT_SECRET %JWT_SECRET%
echo %JWT_SECRET%
public class JwtUtils {
private static Long expire = 43200000L;//这个是TOKEN的有效期毫秒数,这个是12小时
private static String JWT_KEY = "JWT_SECRET";//如果配置了环境变量就这么写
//如果没配置环境变量就这么写
//private static String JWT_SECRET = "密钥"
//生成JWT令牌TOKEN
public static String generateJwt(Map<String, Object> claims) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
String jwt = Jwts.builder()
.addClaims(claims)
.signWith(SignatureAlgorithm.HS256, signKey)
.setExpiration(new Date(System.currentTimeMillis() + expire))
.compact();
return jwt;
}
//解析JWT令牌TOKEN
public static Claims parseJWT(String jwt) {
String signKey = System.getenv(JWT_KEY);//如果没配置环境变量就不用写了
Claims claims = Jwts.parser()
.setSigningKey(signKey)
.parseClaimsJws(jwt)
.getBody();
return claims;
}
}
使用上面的工具类生成TOKEN
public String getToken(){
Map<String,Object> data = new HashMap<>();
data.put(key1,value1);
data.put(key2,value2);
String jwt = JwtUtils.generateJwt(data);
return jwt;
}
解析TOKEN
public Result parstJwt(String jwt){
try{
JwtUtils.parstJwt(jwt);
}catch(Exception e){
//如果走到这一步就说明解析出错了
return Result.orrer("解析失败");
}
return Result.success();
}
过滤器Filter是JavaWeb提供的三大组件之一(Servlet程序、Listener监听器、Filter过滤器)
作用:拦截请求,过滤响应,一般用于登录校验,权限检查,日志操作,事务管理,敏感字符处理等。
过滤器和拦截器有一个不同的是过滤器在拦截器前面,相当于在TOMCAT里面,客户端发送的数据需要先经过过滤器,才能再经过拦截器(拦截器见下一章)
优点:
最好是新建一个软件包,名为filter,专门用来存储过滤器。然后在里面定义类,实现Filter接口,并重写其中的方法。
需要在实现Filter接口的类上添加@WebFilter注解,声明这个类作为 Web 应用程序中的过滤器,这个注解后面可以添加拦截的路径,如果是"/*"则是全部拦截。然后在启动器类上添加注解@ServletComponentScan ,这个注解用于指示容器扫描并自动部署在类路径中的 Servlet、过滤器(Filter)、监听器(Listener)和其他 Servlet 组件。
示例:这个示例是拦截了所有的非/login路径请求,如果有合法TOKEN就通过
@WebFilter("/*")
public class TestFilter implements Filter {//应该导入jakarta.servlet里面的Filter
//初始化方法,随着web服务器启动执行一次,因为接口里面写成了默认方法,可以不重写,也可以重写
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
//这个必须重写,每次有请求来的时候都要先执行这一步
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
//转换成HttpServlet
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//获取请求的路径
String url = request.getRequestURL().toString();
System.out.println("url:"+url);
//如果请求是login直接放行
if (url.contains("/login")){
filterChain.doFilter(request, response);
return;
}
//获取请求头里面的token信息
String token = request.getHeader("token");
if (!StringUtils.hasLength(token)){
//设置响应码
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//解析token
try {
JwtUtils.parseJWT(token);
} catch (Exception e) {
//说明token错误,或者过期,解析失败
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return;
}
//放行
filterChain.doFilter(request, response);
}
//销毁方法, web服务器关闭时执行一次, 因为接口里面写成了默认方法,可以不重写,也可以重写
public void destroy() {
Filter.super.destroy();
}
}
拦截器Interceptor用于在请求到达目标控制器之前或之后执行某些操作。虽然 JavaWeb 中通常使用的是过滤器,而非拦截器,但它们的作用有些相似。Filter 可以在请求到达 Servlet 之前或响应离开 Servlet 之后进行干预,常用于执行一些预处理或后处理任务。
优点:
示例步骤:
1.创建一个拦截器Interceptor的软件包,里面存放拦截器类,类实现HandlerInterceptor接口,注意,需要加入容器注解@Component,里面三个接口可以都不重写,用啥写啥。三个方法的执行时机见下表。
@Component
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//获取url
String url = request.getRequestURL().toString();
//判断url中是否包含login,如果包含,就放行,但是下面在配置文件里面也写了,所以这里都注释掉了
//if(url.contains("login")){ //登录请求
// return true; //ture就是放行,false就是不放行
//}
//获得TOKEN
String jwt = request.getHeader("token");
//这个StringUtils.hasLength()是获得里面的String字符串是否非空并且长度大于0,如果没有,就说明token不存在,返回错误结果
if(!StringUtils.hasLength(jwt)){
response.setStatus(HttpStatus.SC_UNAUTHORIZED);//这个返回的是401
return false;
}
//解析token
try {
JwtUtils.parseJWT(jwt);
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}
//走到这一步都没被return说明传过来的请求token也是正确的,放行
return ture;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
方法名 | 执行时机 |
---|---|
preHandle() | 方法在请求处理之前被调用,即在控制器方法执行之前。实现处理器的预处理(如登录检查) |
postHandle() | 方法在控制器方法执行之后,但在视图渲染之前调用。只要preHandle()返回true,这个一定执行。 |
afterCompletion() | 方法在视图渲染完成后调用,即整个请求处理流程的最后阶段。不管preHandle()返回true还是false,这个一定执行。 |
2.创建一个config的软件包,里面存放配置类,类实现WebMvcConfigurer接口,并重写addInterceptors方法,值得注意的是,这个需要加入@Configuration注解,声明一个类作为配置类,通过该注解,可以以编程的方式定义和配置 Spring 应用程序中的依赖关系,这种方式称为基于 Java 的配置。
@Configuration
public class WebConfig implements WebMvcConfigurer {
//需要填充容器
@Autowired
private TokenInterceptor tokenInterceptor;
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(tokenInterceptor)
.addPathPatterns("/**")//addPathPatterns是添加拦截路径,写什么具体参考下表
.excludePathPatterns("/login");//excludePathPatterns是排除路径,添加不需要拦截的路径
}
}
拦截路径 | 意义 |
---|---|
/** | 所有子路径,包括二级,三级路径 |
/* | 只能拦截一集子路径,如果有二级路径则没法拦截 |
AOP,面向切面编程,旨在提高应用内关注点的模块化能力,如日志记录,事务管理,错误处理等。解决了传统面向对象编程中,这些关注点散布在整个项目中,导致代码高度重合和耦合度。
记得在Aspect类上也要加@Component注解。
优点:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Aspect | 切面类上 | 告诉Spring这是一个包含切面逻辑的类。 |
@Before | 切面类的方法上 | 定义一个前置通知,在目标方法调用之前执行。 |
@After | 切面类的方法上 | 定义一个后置通知,在目标方法执行完毕之后执行,无论方法是否成功执行。 |
@AfterReturning | 切面类的方法上 | 定义一个返回后通知,在目标方法成功执行并返回结果之后执行。 |
@AfterThrowing | 切面类的方法上 | 定义一个异常抛出后通知,在目标方法抛出异常后执行。 |
@Around | 切面类的方法上 | 定义一个环绕通知,在目标方法调用前后都执行自定义的行为。 |
@annotation | 上面五个的括号里面 | 使用自定义注解来标记需要进行切面。 |
@pointcut | 方法上 | 定义一个可重用的切入点表达式。 |
目标方法没出现异常执行顺序:
@Before->@Around(环绕开始部分)->目标方法执行->@Around(环绕结束部分)->@AfterReturning->@After
目标方法出现异常执行顺序:
@Before->@Around(环绕开始部分)->目标方法执行->@Around(环绕结束部分)->@AfterThrowing
理论上来讲,这些注解也都可以用一个注解来@Around实现,但是可读性会变差,例如:
@Aspect
@Component
public class Test{
@Around("execution(* com.example.service.*.*(..))")
public Object test(ProceedingJoinPoint pjp) throws Throwable {
//这里可以写@Before注解完成的方法
try {
//这里写@Around(环绕开始部分)
//执行目标方法
Object result = pjp.proceed();
//这里写@Around(环绕结束部分)注解完成的方法(没出异常的情况)
//这里写@AfterReturning注解完成的方法
return result;
} catch (Throwable e) {
//这里写@Around(环绕结束部分)注解完成的方法(出异常的情况)
//这里写@AfterThrowing注解完成的方法
}
//这里写@After注解完成的方法
return null;
}
}
切点位置写在切面类的注解的后面,语法格式:
@注解("execution(访问修饰符 返回值 包名.类或接口.方法名(参数列表) throws 异常")
例如
@Around("execution(public com.example.pojo.User com.example.controller.UserController.findByNameAndPassword(String,String)) throws Exception")
注意:
通配符:
*
:可以表示任意返回值,包名,类名,方法名,任意类型的一个参数,也可以统配包名,类名,方法名的一部分..
:可以表示任意层级的包,或任意类型,任意个数的参数省略方法的修饰符号,省略异常,用*代替返回值类型,用…省略参数,然后查询所有find开头的方法,示例如下:
execution(* com.example.example.controller.UserController.find*(..))
根据业务不同,也可以使用&&、||、!来组成复杂的切入点表达式
基于注解的方式来匹配切入点方法,需要自定义一个注解,需要匹配哪个方法,就在对应的方法上加上对应的注解。
例如我们自定义一个注解,这个注解的名字就是@Record:
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME )
public @interface Record {
}
我们把这个注解拿去使用,例如:
@Aspect
@Component
public class TestAspect {
@Around("@annotation(com.example.anno.Record)")
public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
//这里可以写@Before注解完成的方法
try {
//这里写@Around(环绕开始部分)
//执行目标方法
Object result = pjp.proceed();
//这里写@Around(环绕结束部分)注解完成的方法(没出异常的情况)
//这里写@AfterReturning注解完成的方法
return result;
} catch (Throwable e) {
//这里写@Around(环绕结束部分)注解完成的方法(出异常的情况)
//这里写@AfterThrowing注解完成的方法
}
//这里写@After注解完成的方法
return null;
}
}
连接点(Join Point)是指程序执行过程中的一个特定点,比如方法的调用或异常的抛出。连接点是AOP框架可以插入额外行为的地方。在切面中,可以通过JoinPoint对象来访问这些连接点的信息。
示例:
@Aspect
@Component
public class LogAspect {
// 定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void test() {}
// 前置通知
@Before("test()")
public void logBefore(JoinPoint joinPoint) {
//目标方法被调用前执行的方法
}
// 后置通知
@After("test()")
public void logAfter(JoinPoint joinPoint) {
//目标方法执行完毕后执行
}
// 返回后通知
@AfterReturning("test()")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
//目标方法成功执行后的方法
}
// 异常抛出后通知
@AfterThrowing("test()")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
//目标方法出现异常的方法
}
// 环绕通知
@Around("test()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
//写环绕前的方法
Object result = joinPoint.proceed(); // 继续执行目标方法
//写环绕后的方法
return result;
}
}
方法名称 | 方法作用 |
---|---|
getSignature() | 获得当前连接点的方法签名信息 |
getArgs() | 获得当前连接点的方法参数 |
getTarget() | 获得当前连接点的目标对象 |
getThis() | 获得当前连接点的代理对象 |
getStaticPart() | 获得静态部分的方法签名 |
常用举例:
//获得类名
String className = joinPoint.getTarget().getClass().getName();
//获得操作方法名
String methodName = joinPoint.getSignature().getName();
//获得参数
Object[] args = joinPoint.getArgs();Object[] args = joinPoint.getArgs();
Bean对象,就是被@Compontent修饰的类,如果被继承了@Compontent的类修饰,例如@Controller,@Service,@Repository也是Bean对象。
可以通过@Scope(“value”)注解来设置这个Bean的对象模式,也可以通过@Lazy懒加载注解来设置Bean的延迟加载(第一次使用Bean对象时,才会创建Bean对象并交给IOC容器管理)
@Scope注解中value的值和作用
作用域 | 说明 |
---|---|
singleton | 容器内同名称的bean只有一个实例(单例)(默认) |
prototype | 每次使用该bean时都会创建新的实例(非单例) |
request | 每个请求会创建新的实例(web环境中) |
session | 每个会话都会创建一个新的实例(web环境中) |
application | 每个应用会创建新的实例(web环境中) |
如果只使用注解@Scope或者不添加@Scope注解都是默认单例模式,就是相当于@Scpoe(“singleton”)。
因为除了单例每次使用时都可能会创建一个新的对象,所以@Scope只要值不是Singleton就可以认为是懒加载模式@Lazy
Bean对象也有一个注解@Conditional,这个注解可以通过判断是否满足条件,满足条件才会将对应的Bean对象装载到Spring的IOC容器中。
注解名称 | 注解位置 | 注解作用 |
---|---|---|
@Conditional | 类、方法上 | 判断是否满足条件,满足条件才会将对应的Bean对象装载到Spring的IOC容器中 |
@ConditionalOnClass | 类、方法上 | 判断环境中有对应字节码文件,才注册bean到IOC容器中 |
@ConditionalOnMissingBean | 类、方法上 | 判断环境中都没有对应bean(类型或者名称),才注册Bean到OIC容器中 |
@ConditionalOnProperty | 类、方法上 | 判断配置文件中有对应属性和值,才注册bean到IOC容器中 |
例如:
@Configuration
public class HeaderConfig{
@Bean
@ConditionalOnClass(name="io.jsonwebtoken.Jwts")//如果这个JWT令牌类存在才会加入容器里面
public Test test(){
return new Test();
}
@Bean
@ConditionalOnMissingClass(name="io.jsonwebtoken.Jwts")//如果这个JWT令牌类不!存!在!才会加入容器里面
public Test test(){
return new Test();
}
@Bean
@ConditionalOnProperty(name="aaa",havingValue="bbb")//判断配置文件中是否有一个叫aaa的属性的值是bbb,如果有则加入,没有则不加入,配置文件就是properties,yml,yaml这几个
public Test test(){
return Test();
}
}
从IOC容器中获取到Bean对象只需要使用注解@Autowried就可以拿到了。
IOC容器对象是ApplicationContext接口,如果要使用这个接口,从org.springframework.context包导入
例如:
@Autowried
private ApplicationContext applicationContext;
这个接口提供了三个获取Bean对象的方法
方法名 | |
---|---|
Object getBean(String name) | 通过name获取bean,但是获取的是Object类型的,这个需要强转,一般不用 |
T getBean(Class requiredType) | 根据类型获取bean,没有多个实现类的时候,用这个好 |
T getBean(String name,Class requiredType) | 根据name获取bean(带类型转换),如果有多个实现类,用这个好 |
例如:我们现在有这么一个名叫Test类的Bean对象,我们要通过测试使用以上三种方法打印出他的对象
@Componment
public class BeanTest{
}
@SpringBootTest
public class SpringBootTest {
@Autowried
private ApplicationContext applicationContext;//IOC容器对象
@Test
public void test(){
//根据bean的名称获取
BeanTest bean1 = (BeanTest) applicationContext.getBean("beanTest");
System.out.println(bean1);
//根据bean的类型获取
BeanTest bean2 = applicationContext.getBean(BeanTest.class);
System.out.println(bean2);
//根据bean的名称以及类型获取
BeanTest bean3 = applicationContext.getBean("beanTest",BeanTest.class);
System.out.println(bean3);
}
}
第三方Bean例如我们从maven引入了别的依赖,这个依赖本身可能并没有加@Componment类型的注解,我们无法直接使用@Autowried注解去IOC容器去获取。我们要使用的话不能去修改它的源码,给它加@Componment注解的话就不是原来的依赖了。
例如我们引入了XML的解析jar包,然后我们要把这个jar包下的SAXReader类加入IOC容器中,调用出来。
<dependency>
<groupId>dom4jgroupId>
<artifactId>dom4jartifactId>
<version>1.6.1version>
dependency>
导入方法:
直接在启动器下面加@Bean注解,引入这个类生成这个类对象。但是启动器的主要作用就是启动程序,加这一堆代码就会影响代码可读性,不美观,启动器功能不再单一,所以不推荐。
@SpringBootApplication
@ComponentScan({"这个包名","另一个包名"})//指定要扫描的包
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
@Bean
public SAXReader getSAXReader(){
return new SAXReader();
}
}
单独配置一个配置类,需要在config软件包下(没有就创建一个)专们创建一个类来导入第三方Bean,因为这个是配置类,所以记得需要添加@Configuration注解。
配置类例如:
@Configuration
public class CommonConfig{
@Bean
public SAXReader getSAXReader(){
return new SAXReader;
}
}
@Bean后面是可以加value值的,不加value值获取的值默认是方法名!!,这个方法名就是要用@Qualifier注解调用的名字。加了value后容器里存的就是value的值了,这一般用于多个bean类的情况。
例如刚刚那个,从IOC容器中取出的语句就是:
@Autowried
@Qualifier("getSAXReader")//一般省略不写,这里是演示才写的
private SAXReader saxReader;
假设现在有多个Bean
@Configuration
public class CommonConfig{
@Bean("saxReader1")
public SAXReader getSAXReader1(){
return new SAXReader;
}
@Bean("saxReader2")
public SAXReader getSAXReader2(){
return new SAXReader;
}
}
再从IOC容器中取出这两个Bean的语句就是
@Autowried
@Qualifier("saxReader1")
private SAXReader saxReader1;
@Autowried
@Qualifier("saxReader2")
private SAXReader saxReader2;
假设两个项目都在本地,先去要导入的三方项目的POM里面,找到最开始的地方,没有被框起来的,这个对应的就是依赖的位置
第三方POM:
<groupId>包名groupId>
<artifactId>项目名artifactID>
<version>版本号version>
本地POM添加如下依赖:
<dependency>
<groupId>包名groupId>
<artifactId>项目名artifactId>
<version>版本号version>
dependency>
然后这个项目就作为一个jar包加入自己的项目依赖了。
但是会出现一个问题,引进来的第三方依赖当中的bean没有生效。即使对应的类上已经加了@Component注解。
解决方法
在启动器上添加@ComponentScan注解,里面的value是一个String类型的数组,这个数组就是包名,会把包里面的所有Bean都提取到IOC容器中
@SpringBootApplication
@ComponentScan({"这个包名","另一个包名"})//指定要扫描的包
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
}
不推荐原因:使用繁琐,性能低
不推荐原因:
这个和上面那个都是在引入第三方依赖时,还要知道第三方依赖中有哪些配置类和哪些Bean类对象,对程序员来讲,很不友好,并且比较繁琐。
导入普通类:
依旧是将注解加在启动器上
@SpringBootApplication
@Import(要使用的类名.class)
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
}
导入配置类
配置类就是上面介绍的使用@Configuration注解的类,在这个配置类里面封装一个个@Bean注解引入第三方Bean
例如:
@Configuration
public class Config{
@Bean
public 类名1 方法名1{
return new 类1对象;
}
@Bean
public 类名2 方法名2{
return new 类2对象;
}
}
@SpringBootApplication
@Import(配置类的类名.class)
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
}
导入ImportSelector接口实现类
这个是在第三方项目中实现ImportSelector的一个类。例如:
public class TestImportSelector implements ImportSelector{
public String[] selectImports(AnnotationMetadata importClassMetadata){
return new String[]{"包名.配置类名"};//把我们需要的bean通过这个字符串类名返回
}
}
然后再启动器中调用
@SpringBootApplication
@Import(ImportSelector接口实现类.class)
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
}
这个是为了解决上面两个方法的问题的一种比较常见的解决方式。第三方依赖提供给我们一个注解,这个注解一般都以@EnableXXXX开头的注解,注解中封装的就是@Import注解。并且SpringBoot一般也是这么实现的。
例如第三方项目中有这么一个类:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyImportSelector.class)
public @interface EnableHeaderConfig{
}
那么我们在启动器上就可以直接使用:
@SpringBootApplication
@EnableHeaderConfig
public class 启动器{
public static void main(String[] args){
SpringApplication.run(启动器.class,args);
}
}
Maven 是一个主要用于Java项目的构建工具,它通过使用项目对象模型(Project Object Model, 简称POM)来简化构建过程。POM定义了项目的基本信息、依赖关系、构建配置等,并且这些信息被存储在一个名为pom.xml
的文件中。
为什么要分模块开发?
答:按照功能拆成若干个子模块,方便项目的管理维护、扩展,也方便模块间的相互调用,资源共享。
、
例如:我们的子模块都使用了lombok注解,我们就可以这么写
父项目pom.xml:
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.examplegroupId>
<artifactId>parent-projectartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
...
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.34version>
dependency>
dependencies>
<build>
<plugins>
...
plugins>
build>
...
<modules>
<module>child-module-1module>
<module>child-module-2module>
...
modules>
project>
子模块需要配置如下
<project>
...
<parent>
<groupId>com.examplegroupId>
<artifactId>parent-projectartifactId>
<version>1.0-SNAPSHOTversion>
<relativePath>../parent-project/pom.xmlrelativePath>//这个是相对路径,写这个路径的前提是这两个项目在同一级下
parent>
...
project>
依赖聚合指的是在一个父项目(或称为多模块项目)中集中管理所有子模块的依赖关系。它允许在父项目的POM文件中定义一组依赖项及其版本号,然后让各个子模块根据需要引用这些依赖而无需再次指定版本信息。
可以在
中定义版本号,然后在
或直接在
中引用这些属性。这样做可以进一步简化版本管理,使得更新依赖版本更加方便。如果需要更改某个库的版本,只需在一个地方修改即可。
好处:确保整个项目使用相同版本的库、子模块不需要重复制定依赖的版本号、当需要升级某个依赖库时,只需要修改父POM的一个地方。
例如部分子项目要用jwt令牌,部分要用lombok的注解,我们就可以这么写。
父pom.xml
<project>
<modelVersion>4.0.0modelVersion>
<groupId>com.examplegroupId>
<artifactId>parent-projectartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>pompackaging>
...
<properties>
<lombok.version>1.18.24lombok.version>
<jwt.version>0.9.1jwt.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>${lombok.version}version>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>${jwt.version}version>
dependency>
dependencies>
dependencyManagement>
...
<modules>
<module>child-module-1module>
<module>child-module-2module>
...
modules>
project>
使用lombok的子模块:
<project>
...
<parent>
<groupId>com.examplegroupId>
<artifactId>parent-projectartifactId>
<version>1.0-SNAPSHOTversion>
<relativePath>../parent-project/pom.xmlrelativePath>
parent>
...
<dependencies>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
dependencies>
...
project>
使用jwt的子模块:
<project>
...
<parent>
<groupId>com.examplegroupId>
<artifactId>parent-projectartifactId>
<version>1.0-SNAPSHOTversion>
<relativePath>../parent-project/pom.xmlrelativePath>
parent>
...
<dependencies>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
dependency>
dependencies>
...
project>
这个情况很少见,大部分公司没有自己的本地仓库。主要是改变Maven的Setting.xml来实现。
打开maven的config/setting.xml文件,修改以下元素:
...
<server>
<id>maven-releasesid>
<username>用户名username>
<password>密码password>
server>
<server>
<id>maven-snapshotsid>
<username>用户名username>
<password>密码password>
server>
...
<mirror>
<id>maven-publicid>
<mirrorOf>*mirrorOf>
<url>仓库地址url>
mirror>
...
<profile>
<id>snapshotsid>
<activation>
<activeByDefault>trueactiveByDefault>
activation>
<repositories>
<repository>
<id>maven-publicid>
<url>仓库地址url>
<releases>
<enabled>trueenabled>
releases>
<snapshots>
<enabled>trueenabled>
snapshots>
repository>
repositories>
profile>
...
然后在pom.xml中配置
...
<distributionManagement>
<repository>
<id>maven-releasesid>
<url>稳定版仓库地址url>
repository>
<snapshotRepository>
<id>maven-snapshotsid>
<url>快照仓库地址url>
snapshotRepository>
distributionManagement>
...