MybatisPlus二级映射和关联对象ResultMap

MybatisPlus二级映射和关联对象ResultMap

在我们的教程中,我们设计了一个课程内容的数据结构,包含章节和相关资源。这种结构非常适合在线教育平台或电子学习系统,其中课程内容需要被组织成不同的章节和子章节,每个子章节可能关联特定的学习资源。

这将是一个很好的示例来展示 MyBatis 中如何使用一对多(<collection>)和一对一(<association>)映射。

一、业务背景

1. 数据库表结构

  1. 章节表 (chapter)
    这张表包含所有章节的信息,其中包括大章节和小章节。大章节作为容器,可以包含多个小章节。
    • id (章节ID)
    • parent_id (父章节ID,用于区分大章节和小章节)
    • name (章节名称)
    • courseId (课程ID)
language-java
1
2
3
4
5
6
7
public class Chapter {

private Long id;
private Long parentId;
private String name;
private Long courseId;
}
  1. 资源表 (resource)
    这张表包含与小章节相关联的资源信息。
    • id (资源ID)
    • section_id (章节ID,关联到章节表)
    • name (资源名称)
language-java
1
2
3
4
5
6
public class Resource {

private Long id;
private Long sectionId;
private String name;
}

2. 需求

要求根据courseId查询出指定课程的信息,包括大章节、小章节、资源,并以一定结构返回,比如

language-json
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
[
{

"id": 1,
"parentId": null,
"name": "Chapter 1",
"courseId": 100,
"subChapters": [
{

"id": 11,
"parentId": 1,
"name": "Section 1.1",
"courseId": 100,
"resource": {

"id": 101,
"sectionId": 11,
"name": "Introduction Video"
}
},
{

"id": 12,
"parentId": 1,
"name": "Section 1.2",
"courseId": 100,
"resource": null
}
],
"resource": null
}
// other...
]

所以我们定义一个Dto如下

language-java
1
2
3
4
5
6
7
public class ChapterDto extends Chapter {

private List<ChapterDto> subChapters;
private Resource resource;

// 构造器、getter和setter
}

二、使用映射直接得到指定结构

ChapterMapper.xml 文件中,我们定义 SQL 查询以及结果映射。

language-xml
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
<mapper namespace="com.example.mapper.ChapterMapper">

<resultMap id="ChapterDtoMap" type="com.example.dto.ChapterDto">
<id column="chapter_id" property="id" />
<result column="parent_id" property="parentId" />
<result column="name" property="name" />
<result column="courseId" property="courseId" />
<collection property="subChapters" ofType="com.example.dto.ChapterDto">
<id column="sub_chapter_id" property="id" />
<result column="sub_parent_id" property="parentId" />
<result column="sub_name" property="name" />
<result column="sub_courseId" property="courseId" />
<association property="resource" javaType="com.example.model.Resource">
<id column="resource_id" property="id" />
<result column="section_id" property="sectionId" />
<result column="resource_name" property="name" />
</association>
</collection>
</resultMap>

<select id="selectChaptersWithResources" resultMap="ChapterDtoMap">
SELECT
c.id AS chapter_id, c.parent_id, c.name, c.courseId,
sc.id AS sub_chapter_id, sc.parent_id AS sub_parent_id, sc.name AS sub_name, sc.courseId AS sub_courseId,
r.id AS resource_id, r.section_id, r.name AS resource_name
FROM
chapter c
LEFT JOIN
chapter sc ON c.id = sc.parent_id
LEFT JOIN
resource r ON sc.id = r.section_id
WHERE
c.courseId = #{courseId} AND c.parent_id IS NULL
</select>

</mapper>

三、其他文件

1. Mapper

language-java
1
2
3
4
public interface ChapterMapper {

List<ChapterDto> selectChaptersWithResources(Long courseId);
}

2. Service

language-java
1
2
3
4
5
6
7
8
9
10
11
@Service
public class ChapterService {

@Autowired
private ChapterMapper chapterMapper;

public List<ChapterDto> getChaptersWithResources(Long courseId) {

return chapterMapper.selectChaptersWithResources(courseId);
}
}

3. Controller

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("/chapters")
public class ChapterController {

@Autowired
private ChapterService chapterService;

@GetMapping("/{courseId}")
public ResponseEntity<List<ChapterDto>> getChapters(@PathVariable Long courseId) {

List<ChapterDto> chapters = chapterService.getChaptersWithResources(courseId);
return ResponseEntity.ok(chapters);
}
}

四、概念理解

一级映射

在提供的 resultMap 中,一级映射是针对 ChapterDto类的直接属性的映射。这意味着数据库中的列(如 chapter_id, parent_id等)直接映射到 ChapterDto类的相应属性(如 id, parent_id等),这部分映射是非常直接的。

二级映射

二级映射用于处理复杂的对象关系,比如当一个对象包含其他对象或对象的集合时。这通常在处理一对多关系时出现,例如,一个章节结构(ChapterDto)可能包含多个子章节。

聚合

这种聚合是根据您在 <collection> 标签中定义的规则进行的。MyBatis 会识别哪些行应该被映射为独立的实例,哪些行应该作为子元素聚合到其他实例中。

五、标签使用

1. 标签

用途:用于映射一对多关系。在这个例子中,ChapterDto类包含一个 Chapter 类型的列表,这代表了大章节和小章节之间的一对多关系。

常用属性

  • property:指定要映射到的目标属性名称。
  • ofType:指定集合中元素的类型。

2. 标签

用途:用于映射一对一关系。在您的例子中,ChapterDto包含一个 Resource 类型的属性,这代表了小章节和资源之间的一对一关系。
常用属性

  • property:指定要映射到的目标属性名称。
  • javaType:指定关联对象的类型。
spring-boot-starter-validation常用注解

spring-boot-starter-validation常用注解

一、使用

要使用这些注解,首先确保在你的 Spring Boot 应用的 pom.xml 文件中添加了 spring-boot-starter-validation 依赖。然后,你可以将这些注解应用于你的模型类字段上。在你的控制器方法中,你可以使用 @Valid@Validated 注解来触发验证,例如:

language-java
1
2
3
4
5
6
@PostMapping("/users")
public ResponseEntity<?> createUser(@Valid @RequestBody User user) {

// 如果存在验证错误,会抛出异常
// 正常业务逻辑
}

在这个例子中,如果 User 对象的字段不满足注解定义的验证规则,Spring 将抛出一个异常,你可以通过全局异常处理或控制器层的异常处理来处理这些异常,并向用户返回适当的响应。

二、常用注解

spring-boot-starter-validation 依赖包引入了 Java Bean Validation API(通常基于 Hibernate Validator 实现),提供了一系列注解来帮助你对 Java 对象进行验证。以下是一些常用的验证注解及其含义和使用方式:

  1. @NotNull : 确保字段不是 null

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotNull(message = "用户名不能为空")
    private String username;
    // 其他字段和方法
    }
  2. @NotEmpty : 确保字段既不是 null 也不是空(对于字符串意味着长度大于0,对于集合意味着至少包含一个元素)。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotEmpty(message = "密码不能为空")
    private String password;
    // 其他字段和方法
    }
  3. @NotBlank : 确保字符串字段不是 null 且至少包含一个非空白字符。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @NotBlank(message = "邮箱不能为空且不能只包含空格")
    private String email;
    // 其他字段和方法
    }
  4. @Size: 确保字段(字符串、集合、数组)符合指定的大小范围。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Size(min = 2, max = 30, message = "用户名长度必须在2到30之间")
    private String username;
    // 其他字段和方法
    }
  5. @Min@Max: 对数值类型字段设置最小值和最大值。

    language-java
    1
    2
    3
    4
    5
    6
    7
    public class User {

    @Min(value = 18, message = "年龄必须大于等于18")
    @Max(value = 100, message = "年龄必须小于等于100")
    private int age;
    // 其他字段和方法
    }
  6. @Email: 确保字段是有效的电子邮件地址。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Email(message = "无效的邮箱格式")
    private String email;
    // 其他字段和方法
    }
  7. @Pattern: 确保字符串字段匹配正则表达式。

    language-java
    1
    2
    3
    4
    5
    6
    public class User {

    @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户名只能包含字母和数字")
    private String username;
    // 其他字段和方法
    }
  8. @Positive@PositiveOrZero: 确保数值字段是正数或者正数和零。

    language-java
    1
    2
    3
    4
    5
    6
    public class Product {

    @Positive(message = "价格必须是正数")
    private BigDecimal price;
    // 其他字段和方法
    }

三、@Valid or @Validated ?

@Valid@Validated 注解都用于数据验证,但它们在使用和功能上有一些差异:

  1. @Valid:

    • 来源于 JSR 303/JSR 380 Bean Validation API。
    • 可以用在方法参数上,以触发对传递给该方法的对象的验证。这通常在 Spring MVC 中用于验证带有 @RequestBody@ModelAttribute 注解的参数。
    • 不支持验证组的概念,这意味着不能控制验证的顺序或验证特定的子集。

    示例:

    language-java
    1
    2
    3
    4
    5
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@Valid @RequestBody User user) {

    // 业务逻辑
    }
  2. @Validated(推荐):

    • 是 Spring 的特有注解,不是 JSR 303/JSR 380 的一部分。
    • 支持验证组,允许您更灵活地指定在特定情况下应用哪些验证约束。例如,可以根据不同的操作(如创建、更新)定义不同的验证规则。
    • 可以用在类型级别(在类上)和方法参数上。在类型级别使用时,它会触发该类中所有带有验证注解的方法的验证。

    示例:

    language-java
    1
    2
    3
    4
    5
    @PostMapping("/users")
    public ResponseEntity<?> createUser(@Validated @RequestBody User user) {

    // 业务逻辑
    }

在实际使用中,如果你需要简单的验证功能,@Valid 是一个很好的选择。如果你需要更复杂的验证逻辑,比如验证组,那么 @Validated 更适合。此外,@Validated 可以应用在类级别,从而对一个类的多个方法进行验证,这在使用 Spring 服务层时非常有用。

四、分组校验

分组校验(Group Validation)是一种在 Java Bean Validation 中用于在不同上下文中应用不同验证规则的方法。这对于那些在不同情况下(例如,创建 vs 更新)需要不同验证规则的对象特别有用。

1. 分组校验的基本概念

在分组校验中,你可以定义多个接口(通常为空)来表示不同的验证组。然后,你可以在验证注解中指定这些接口,以表明该注解仅在验证特定组时应用。

例如,你可能有一个User类,其中某些字段在创建用户时是必需的,但在更新用户时可能是可选的。

2. 定义验证组

首先,定义两个空接口作为验证组:

language-java
1
2
3
4
public interface OnCreate {
}
public interface OnUpdate {
}

3. 应用分组到模型

然后,在你的模型类中使用这些接口作为验证注解的参数:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class User {


@NotNull(groups = OnCreate.class)
private Long id;

@NotBlank(groups = {
OnCreate.class, OnUpdate.class})
private String username;

@Email(groups = {
OnCreate.class, OnUpdate.class})
private String email;

// 其他字段和方法
}

在这个例子中,id 字段仅在创建用户时需要验证(OnCreate组),而 usernameemail 字段在创建和更新用户时都需要验证。

4. 在控制器中使用分组

最后,在你的控制器方法中,使用 @Validated 注解指定要应用的验证组:

language-java
1
2
3
4
5
6
7
8
9
10
11
@PostMapping("/users")
public ResponseEntity<?> createUser(@Validated(OnCreate.class) @RequestBody User user) {

// 创建用户的业务逻辑
}

@PutMapping("/users")
public ResponseEntity<?> updateUser(@Validated(OnUpdate.class) @RequestBody User user) {

// 更新用户的业务逻辑
}

在这个例子中,createUser 方法只会验证属于 OnCreate 组的字段,而 updateUser 方法则只会验证属于 OnUpdate 组的字段。这样,你就可以根据不同的操作自定义验证逻辑了。

5. 默认组如何总是被校验

当字段没有指定groups时,属于默认组,当@Validated(OnUpdate.class)指定了特定的组后,属于默认组的字段将不再被校验,一个个都加上groups太麻烦,如何让默认组字段总是被校验呢?
如下

language-java
1
2
3
4
5
import jakarta.validation.groups.Default;
public interface OnCreate extends Default{
}
public interface OnUpdate extends Default{
}

6. 总结

通过使用分组校验,你可以为同一个对象的不同操作设置不同的验证规则,这在复杂应用中非常有用。这种方法提高了代码的灵活性和可维护性。

SpringBoot不同的@Mapping使用

SpringBoot不同的@Mapping使用

一、介绍

一般@Mapping类注解在Spring框架中用于将HTTP请求映射到对应的处理器方法。它们各自对应于不同类型的HTTP方法,主要用于RESTful Web服务中。以下是每个注解的作用:

  1. @GetMapping: 用于映射HTTP GET请求到处理器方法。通常用于读取操作。

  2. @PostMapping: 用于映射HTTP POST请求到处理器方法。通常用于创建操作。

  3. @PutMapping: 用于映射HTTP PUT请求到处理器方法。通常用于更新操作,其中更新操作是幂等的,意味着多次执行同一个请求会得到相同的结果。

  4. @DeleteMapping: 用于映射HTTP DELETE请求到处理器方法。通常用于删除操作。

  5. @PatchMapping: 用于映射HTTP PATCH请求到处理器方法。PATCH与PUT类似,但通常用于部分更新资源。

  6. @RequestMapping: 是一个通用的注解,可以用于映射任何HTTP方法,通过指定method属性来明确映射类型。如果不指定method,它将映射到所有HTTP方法。这是最早的注解,后来为了方便使用,根据HTTP方法细分出了上述的专用注解。

二、使用

在Spring框架中,不同的@Mapping注解通常与不同的数据获取方式结合使用,以适应各种HTTP请求的特点:

  1. @GetMapping:

    • 用于获取数据,通常与@RequestParam一起使用来接收查询参数。

    • 示例: 获取URL中查询参数的值。

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/users")
      public String getUsers(@RequestParam String name) {

      // ...
      }
    • 也可以用@PathVariable来获取URL中的路径变量。

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/users/{id}")
      public String getUserById(@PathVariable Long id) {

      // ...
      }
  2. @PostMapping:

    • 用于创建数据,通常与@RequestBody结合使用来接收请求体中的内容,如JSON或XML。

    • 示例: 从请求体中获取JSON对象。

      language-java
      1
      2
      3
      4
      5
      @PostMapping("/users")
      public String addUser(@RequestBody User user) {

      // ...
      }
    • 有时也与@RequestParam结合使用来处理表单数据。

  3. @PutMapping:

    • 用于更新数据,通常与@RequestBody结合使用来接收请求体中的内容。

    • 示例: 从请求体中获取JSON对象来更新数据。

      language-java
      1
      2
      3
      4
      5
      @PutMapping("/users/{id}")
      public String updateUser(@PathVariable Long id, @RequestBody User user) {

      // ...
      }
  4. @DeleteMapping:

    • 用于删除数据,通常与@PathVariable结合使用来指定要删除的资源的ID。

    • 示例: 删除指定ID的用户。

      language-java
      1
      2
      3
      4
      5
      @DeleteMapping("/users/{id}")
      public String deleteUser(@PathVariable Long id) {

      // ...
      }
  5. @PatchMapping:

    • 用于部分更新资源,与@RequestBody结合使用来接收部分数据。

    • 示例: 部分更新用户信息。

      language-java
      1
      2
      3
      4
      5
      @PatchMapping("/users/{id}")
      public String patchUser(@PathVariable Long id, @RequestBody Map<String, Object> updates) {

      // ...
      }
  6. @RequestMapping:

    • 这是一个通用注解,可以用来处理任何HTTP方法,其具体方法由method属性指定。
    • 数据获取方式取决于具体的HTTP方法。

每个注解的使用依赖于HTTP请求的语义,以及你如何打算处理请求数据。总体而言,@RequestParam@PathVariable用于从URL获取数据,而@RequestBody用于处理复杂的请求体内容。

SpringBoot注解@GetMapping处理Get请求

SpringBoot注解@GetMapping处理Get请求

一、如何从URL中获取参数

当你想从URL中获取名为courseId的参数时,可以使用@GetMapping("/course/{courseId}")@GetMapping("/course")两种办法,主要体现在URL模式和如何获取参数上。

  1. 使用@GetMapping("/course/{courseId}"):

    • 这种方式表示你正在定义一个REST风格的API,其中{courseId}是URL的一部分,通常被称为路径变量(path variable)。

    • 你可以通过在方法的参数中加上@PathVariable注解来获取这个路径变量。例如:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course/{courseId}")
      public String getCourse(@PathVariable String courseId) {

      // 使用courseId
      }
    • 这种方式适合于当courseId是必需的,并且每个课程的URL都是唯一的情况。

  2. 使用@GetMapping("/course"):

    • 这种方式下,URL不直接包含courseId。相反,courseId可以作为请求参数(query parameter)来传递。

    • 你可以通过在方法的参数中加上@RequestParam注解来获取这个请求参数。例如:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course")
      public String getCourse(@RequestParam String courseId) {

      // 使用courseId
      }
    • 这种方式适合于当你想要让courseId作为一个可选参数或者你希望从一组标准的URL中筛选特定课程的情况。

二、获取多个参数

当URL中有多个参数时,@GetMapping("/course/{courseId}")@GetMapping("/course")的使用方式和它们之间的区别仍然基于路径变量(Path Variable)和请求参数(Request Parameter)的概念。这两种方法可以根据参数的性质和用途灵活组合使用。

  1. 使用路径变量(Path Variables):

    • 当你使用@GetMapping("/course/{courseId}")并且URL中有多个参数时,这些参数通常是URL路径的一部分,并且每个参数都是资源定位的关键部分。

    • 例如,如果你有一个URL像这样:/course/{courseId}/module/{moduleId},你可以这样使用:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course/{courseId}/module/{moduleId}")
      public String getModule(@PathVariable String courseId, @PathVariable String moduleId) {

      // 使用courseId和moduleId
      }
    • 在这个例子中,courseIdmoduleId都是路径的一部分,用来定位特定的资源。

  2. 使用请求参数(Request Parameters):

    • 当你使用@GetMapping("/course")并且URL中有多个参数时,这些参数通常作为URL的查询字符串(query string)。

    • 例如,URL可能是这样的:/course?courseId=123&moduleId=456,你可以这样使用:

      language-java
      1
      2
      3
      4
      5
      @GetMapping("/course")
      public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

      // 使用courseId和moduleId
      }
    • 在这个例子中,courseIdmoduleId是作为查询字符串的一部分传递的,通常用于过滤、排序或其他非资源定位的操作。

三、用一个类接收多个参数

对于URL中包含多个请求参数(如 /course?courseId=123&moduleId=456),你可以使用一个Java类来接收所有这些参数。这种方法可以使代码更加整洁,尤其是当处理具有多个参数的复杂请求时。

这里是如何使用一个类来接收所有请求参数的示例:

  1. 定义一个类来表示请求参数 :
    首先,你需要定义一个类,其属性对应于URL中的请求参数。例如,对于参数courseIdmoduleId,你可以定义如下类:

    language-java
    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
    public class CourseQuery {

    private String courseId;
    private String moduleId;

    // 标准的getter和setter方法
    public String getCourseId() {

    return courseId;
    }

    public void setCourseId(String courseId) {

    this.courseId = courseId;
    }

    public String getModuleId() {

    return moduleId;
    }

    public void setModuleId(String moduleId) {

    this.moduleId = moduleId;
    }

    // 可以添加构造函数、toString()等
    }
  2. 在控制器方法中使用这个类 :
    接下来,在你的控制器方法中,你可以将这个类作为参数,并使用@ModelAttribute注解(或者不使用任何注解,因为Spring MVC会自动将请求参数绑定到类的属性上):

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(@ModelAttribute CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

    或者,如果你不使用@ModelAttribute注解,Spring MVC默认也会按照相同的方式处理这个参数:

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

使用这种方式,你可以轻松地处理包含许多参数的复杂请求,同时保持代码的清晰和可维护性。这也让你能够在需要时对参数进行验证,比如使用Spring的验证框架来确保传入的参数满足特定条件。

四、疑问

Q:定义一个类来表示请求参数时,不需要使用@RequestParam吗?还是说@RequestParam用于简单的类型获取单个参数,比如String?

在定义一个类来表示请求参数时,通常不需要在类的属性上使用@RequestParam注解。当你使用一个类来接收请求参数,Spring MVC的数据绑定机制会自动将请求中的参数名称映射到类的属性上。这是基于类的属性名和请求参数名的匹配来完成的。

使用类接收参数:

  • 当你有多个请求参数,并且想要将它们组织在一个类中时,你可以定义一个类,其属性与请求参数的名称相对应。

  • 在控制器方法中,你可以使用这个类的实例作为参数,而无需为每个属性单独使用@RequestParam。Spring会自动处理参数到类属性的映射。

  • 例如:

    language-java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class CourseQuery {

    private String courseId;
    private String moduleId;

    // getter和setter省略
    }

    @GetMapping("/course")
    public String getCourse(CourseQuery query) {

    // 使用query.getCourseId()和query.getModuleId()
    }

使用@RequestParam:

  • @RequestParam通常用于获取单个请求参数,特别是当这个参数是一个简单类型(如Stringint等)时。

  • 当你只需要从请求中获取一个或少数几个参数,而不是一个复杂对象时,@RequestParam是一个很好的选择。

  • 例如:

    language-java
    1
    2
    3
    4
    5
    @GetMapping("/course")
    public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

    // 直接使用courseId和moduleId
    }

总结来说,当处理包含多个参数的复杂请求时,使用一个类来封装这些参数可以使代码更加清晰和易于维护。而@RequestParam适用于处理更简单或直接的参数获取需求。在实际的应用中,你可以根据具体情况选择最适合的方法。

Q:假如我已经有一个在/course上的controller了,如下
@PostMapping("/course") public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto dto)
我还能继续写
@GetMapping("/course") public String getCourse(@RequestParam String courseId, @RequestParam String moduleId)
吗?

是的,你完全可以在同一个控制器(Controller)中同时定义一个@PostMapping和一个@GetMapping方法,即使它们都映射到了相同的路径(如/course)。这是因为HTTP请求的方法类型(GET、POST等)作为映射的一部分,允许区分不同的操作。在你的例子中,一个方法用于处理POST请求,另一个用于处理GET请求。

  1. @PostMapping("/course") 用于创建课程(通常是添加新资源),接收一个请求体(RequestBody)。

  2. @GetMapping("/course") 用于获取课程信息(通常是读取资源),接收URL中的请求参数(RequestParam)。

你的控制器可能看起来像这样:

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@RestController
@RequestMapping("/path-to-controller")
public class CourseController {


@PostMapping("/course")
public CourseBaseInfoDto createCourseBase(@RequestBody AddCourseDto dto) {

// 创建课程的逻辑
}

@GetMapping("/course")
public String getCourse(@RequestParam String courseId, @RequestParam String moduleId) {

// 获取课程信息的逻辑
}
}

在这种配置中,当一个POST请求发送到/course时,createCourseBase方法将被调用;而当一个GET请求发送到/course时,带有请求参数的getCourse方法将被调用。

这种方法是RESTful API设计中的常见实践,它允许你在同一路径上对资源进行不同类型的操作,同时保持了代码的清晰和逻辑的分离。

SpringBoot自定义全局异常处理器

SpringBoot自定义全局异常处理器

一、介绍

Springboot框架提供两个注解帮助我们十分方便实现全局异常处理器以及自定义异常

  • @ControllerAdvice@RestControllerAdvice(推荐)
  • @ExceptionHandler

二、实现

1. 定义全局异常处理器

定义GlobalExceptionHandler类,拦截所有异常。
@RestControllerAdvice注解使得你可以在GlobalExceptionHandler 中处理异常,@ExceptionHandle注解用于将指定异常绑定到处理的函数上。如下使用@ExceptionHandler(Exception.class)即对所有异常进行捕获处理。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e){

//record log
log.error("系统异常{}", e.getMessage(),e);
//decode errorException
String errMessage = "系统异常";
return new RestErrorResponse(errMessage);
}
}
language-java
1
2
3
4
5
6
@Data
@AllArgsConstructor
public class RestErrorResponse implements Serializable {

private String errMessage;
}

事实上,写到这里已经可以用了,RestErrorResponse 用来承载错误信息到前端,因为@RestControllerAdvice已经包含了@ResponseBody

2. 自定义异常类

继承RuntimeException 异常类写一个自定义的异常类。这么做主要是能够使用自定义的枚举类来更优雅的抛出错误。

language-java
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
@Data
public class XueChengPlusException extends RuntimeException {


private String errMessage;

public XueChengPlusException() {

super();
}

public XueChengPlusException(String errMessage) {

super(errMessage);
this.errMessage = errMessage;
}

public static void cast(CommonError commonError){

throw new XueChengPlusException(commonError.getErrMessage());
}
public static void cast(String errMessage){

throw new XueChengPlusException(errMessage);
}

}
language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Getter
public enum CommonError {

UNKOWN_ERROR("执行过程异常,请重试。"),
PARAMS_ERROR("非法参数"),
OBJECT_NULL("对象为空"),
QUERY_NULL("查询结果为空"),
REQUEST_NULL("请求参数为空");

private String errMessage;

private CommonError( String errMessage) {

this.errMessage = errMessage;
}
}

同时,对于GlobalExceptionHandler 也要做一些修改,一方面处理自定义异常,另一方处理其余异常。

language-java
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
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


@ExceptionHandler(XueChengPlusException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse customException(XueChengPlusException e){

//record log
log.error("系统异常{}", e.getErrMessage(),e);
//decode errorException
String errMessage = e.getErrMessage();
return new RestErrorResponse(errMessage);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public RestErrorResponse exception(Exception e){

//record log
log.error("系统异常{}", e.getMessage(),e);
//decode errorException
String errMessage = CommonError.UNKOWN_ERROR.getErrMessage();
return new RestErrorResponse(errMessage);
}
}

三、使用

在程序中任意地方抛出异常,controllerservicedao层都可以,比如

language-java
1
throw new RuntimeException("价格不能为空且必须大于0");

这时走的就是

language-java
1
2
@ExceptionHandler(Exception.class)
public RestErrorResponse exception(Exception e)

除此之外,可以这样抛出自定义异常,比如

language-java
1
XueChengPlusException.cast(CommonError.PARAMS_ERROR);
language-java
1
XueChengPlusException.cast("其他的消息");
language-java
1
throw new XueChengPlusException(CommonError.OBJECT_NULL.getErrMessage());
language-java
1
throw new XueChengPlusException("其他的消息");

这时走的就是

language-java
1
2
@ExceptionHandler(XueChengPlusException.class)
public RestErrorResponse customException(XueChengPlusException e)

四、疑问

Q:疑问,XueChengPlusException异常类继承自RuntimeException ,而RuntimeException 继承自Exception,为什么触发customException而不是exception?

在这个全局异常处理器中,当抛出一个XueChengPlusException异常时,它会被customException(XueChengPlusException e)方法处理,而不是exception(Exception e)方法。

这是因为Spring框架的异常处理机制会优先匹配最具体的异常类型。在您的代码中,XueChengPlusExceptionRuntimeException(以及Exception)的子类,因此它更具体。所以,当抛出一个XueChengPlusException异常时,Spring会优先调用处理XueChengPlusException的方法,而不是处理Exception的方法。

这种行为确实表明全局异常处理器有一定的优先级和覆盖逻辑。具体来说,处理器会优先处理更具体的异常类型,如果没有找到匹配的处理器,那么它会寻找处理更一般异常类型的处理器。

SpringBoot3整合OpenAPI3(Swagger3)

SpringBoot3整合OpenAPI3(Swagger3)

swagger2更新到3后,再使用方法上发生了很大的变化,名称也变为OpenAPI3

官方文档

一、引入依赖

language-xml
1
2
3
4
5
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi.version}</version>
</dependency>
language-yml
1
2
3
4
5
6
7
8
9
10
server:
servlet:
context-path: /content
springdoc:
api-docs:
enabled: true
path: /v3/api-docs
swagger-ui:
enabled: true
path: /swagger-ui.html

openapi3使用十分方便,做到这里后,你可以直接通过以下网址访问swagger页面。

language-html
1
http://<ip>:<port>/content/swagger-ui/index.html

二、使用

1. @OpenAPIDefinition + @Info

用于定义整个 API 的信息,通常放在主应用类上。可以包括 API 的标题、描述、版本等信息。

language-java
1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
@Slf4j
@OpenAPIDefinition(info = @Info(title = "内容管理系统", description = "对课程相关信息进行管理", version = "1.0.0"))
public class ContentApplication {

public static void main(String[] args) {

SpringApplication.run(ContentApplication.class, args);
}
}

2. @Tag

用于对 API 进行分组。可以在控制器类或方法级别上使用。

language-java
1
2
3
4
5
6
@Tag(name = "课程信息编辑接口")
@RestController("content")
public class CourseBaseInfoController {

}

3. @Operation

描述单个 API 操作(即一个请求映射方法)。可以提供操作的摘要、描述、标签等。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Operation(summary = "课程查询接口")
@PostMapping("/course/list")
public PageResult<CourseBase> list(
PageParams params,
@RequestBody(required = false) QueryCourseParamsDto dto){


CourseBase courseBase = new CourseBase();
courseBase.setCreateDate(LocalDateTime.now());

return new PageResult<CourseBase>(new ArrayList<CourseBase>(List.of(courseBase)),20, 2, 10);
}

4. @Parameter

用于描述方法参数的额外信息,例如参数的描述、是否必需等。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@Operation(summary = "课程查询接口")
@PostMapping("/course/list")
public PageResult<CourseBase> list(
@Parameter(description = "分页参数") PageParams params,
@Parameter(description = "请求具体内容") @RequestBody(required = false) QueryCourseParamsDto dto){


CourseBase courseBase = new CourseBase();
courseBase.setCreateDate(LocalDateTime.now());

return new PageResult<CourseBase>(new ArrayList<CourseBase>(List.of(courseBase)),20, 2, 10);
}

5. @Schema

描述模型的结构。可以用于类级别(标注在模型类上)或字段级别。

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageParams {

//当前页码
@Schema(description = "页码")
private Long pageNo = 1L;

//每页记录数默认值
@Schema(description = "每页条目数量")
private Long pageSize =10L;
}

6. @ApiResponse

描述 API 响应的预期结果。可以指定状态码、描述以及返回类型。

language-java
1
2
3
4
@ApiResponse(responseCode = "200", description = "Successfully retrieved user")
public User getUserById(@PathVariable Long id) {

}
JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

JavaWeb 若依RuoYi-Vue3框架将Mybatis切换成MybatisPlus

这里是官方的做法

mybatisSqlSessionFactoryBean,而mybatis-plusMybatisSqlSessionFactoryBean,所以一般最好是项目中使用一个最好,当然想要共存也可以,mybatis-plus的版本最好要高。这里只讲如何切换成MyBatisPlus。

一、修改yml

application.yml中将mybatis配置注释并写上新的mybatisplus,如下所示

language-yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
## MyBatis配置
#mybatis:
# # 搜索指定包别名
# typeAliasesPackage: com.ruoyi.**.domain
# # 配置mapper的扫描,找到所有的mapper.xml映射文件
# mapperLocations: classpath*:mapper/**/*Mapper.xml
# # 加载全局的配置文件
# configLocation: classpath:mybatis/mybatis-config.xml
mybatis-plus:
# 配置要扫描的xml文件目录,classpath* 代表所有模块的resources目录 classpath 不加星号代表当前模块下的resources目录
mapper-locations: classpath*:mapper/**/*Mapper.xml
# 实体扫描,*通配符
typeAliasesPackage: com.ruoyi.**.domain

二、maven导入mybatisplus包,如下

language-xml
1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>最新版本</version>
</dependency>

三、注释SqlSessionFactoryBean

找到com.ruoyi.framework.config.MyBatisConfig,注释public SqlSessionFactory sqlSessionFactory(DataSource dataSource)如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//    @Bean
// public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
// {

// String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
// String mapperLocations = env.getProperty("mybatis.mapperLocations");
// String configLocation = env.getProperty("mybatis.configLocation");
// typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
// VFS.addImplClass(SpringBootVFS.class);
//
// final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
// sessionFactory.setDataSource(dataSource);
// sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
// sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
// sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
// return sessionFactory.getObject();
// }

到这里就完成了切换,快去试试 吧。

资料参考
若依框架集成mybatis换成mybatis-plus记录

JavaWeb项目开发流程

JavaWeb项目开发流程

1. 需求分析

收集客户需求,明确项目功能,设计较为详细的实体关系图。

推荐:ProcessOn

2. 技术选型

确定开发框架、数据库、服务器等技术选型,这些选择应该与项目需求相匹配,同时也要考虑团队成员技术能力和经验。

推荐:RuoYi-Vue3

3. 数据库设计

根据需求分析结果,设计数据库模型,表结构,表关系。

推荐:dbdiagram.io

4. 模块划分和接口设计

将项目划分为多个小模块,并为每个模块的前后端设计api。

推荐:Apifox

4. UI设计

根据需求分析结果,设计原型模型,包括UI界面设计等。

推荐:Pixso

5. 编码

根据需求分析、技术选型、原型设计和数据库设计等结果,开始编写代码,包括前端代码和后端代码等。

推荐:前端Vue3+ElementPlus开发,后端SpringBoot+MybatisPlus开发

6. 调试和测试

在编码过程中,需要不断进行代码调试和测试,以确保程序的正确性和稳定性。

推荐:Junit

7. 部署和上线

完成测试后,将程序部署到服务器上,并进行上线运行,同时需要进行系统监控和数据备份等工作。

推荐:云服务器+Docker

8. 运维和维护

程序上线后,需要进行运维和维护工作,包括性能监控、安全维护、bug修复等。

(例子待完善)

JavaSE-网络通信

JavaSE-网络通信

一、概述

基本的通信架构有2种形式:

  • CS架构( Client客户端/Server服务端)
  • BS架构(Browser浏览器/Server服务端) IPV4共32bit,4字节,形如192.168.000.001
    IPV6共128bit,16字节,形如2001:0db8:0000:0023:0008:0800:200c:417a
    IPV4形为4位十进制数,每个数字基于8bit,范围0~255
    IPV6形为8位十六进制数,每个数基于16bit,范围0000~FFFF(0 ~ 65535)
    使用Java自带的java.net.InetAddress 可以互动IP 前两个为构造器,后三个为方法

二、端口和协议

UDP

不事先建立连接,发送方直接发送,不管对面状态,是不可靠连接。但是通信效率高,适合诸如视频直播、语音通话等。单个数据包最大限制为64KB。

TCP

事先通过三次握手建立连接,可靠连接,四次挥手断开连接,在不可靠信道做到可靠连接

三次握手是要Client和Server知道对方可以’收’和’发’
第一次握手:S知道C可以发
第二次握手:C知道S可以收发
第三次握手:S知道C可以收发
之后传输数据时,C也不断向S发送确认消息,S需要回复确认,如果没有,那么C会采取重发等措施。

三、UDP编程

S和C都需要先创建数据包类,用来接收信息。
S代码如下

language-cpp
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
public class Server {

public static void main(String[] args) throws Exception {

//1. create server socket
DatagramSocket socket = new DatagramSocket(8889);
//2. create packet
byte[] bytes = new byte[1024 * 64];
DatagramPacket packet = new DatagramPacket(bytes, bytes.length);
while (true) {

//3.receive
socket.receive(packet);

System.out.println("服务端收到了长度为 "+packet.getLength()+" bytes的数据");
System.out.println("数据为: "+new String(bytes,0, packet.getLength()));
System.out.println("消息来自:\nIP->"+packet.getAddress().getHostAddress());
System.out.println("Name->"+packet.getAddress().getHostName());
System.out.println("Port->"+packet.getPort());
System.out.println("-----------------------------");
}

// socket.close();
}
}

S会在socket.receive一直等待C的消息,同时通过packet也可以查看发送方的一些信息。
C代码如下

language-cpp
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
public class Client {

public static void main(String[] args) throws Exception {

//1. create client
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while (true){

System.out.println("请说:");
String s = sc.nextLine();
if ("exit".equals(s)){

System.out.println("欢迎下次使用~");
break;
}
//2. create data package
byte[] bytes = s.getBytes();
DatagramPacket packet = new DatagramPacket(bytes, bytes.length, InetAddress.getLocalHost(),8889);
//3.send
socket.send(packet);
System.out.println("客户端发送了长度为 "+bytes.length+" bytes的数据");
System.out.println("数据为: "+new String(bytes));
System.out.println("----------------------");
}
socket.close();
}
}

你可能会好奇为什么S可以在循环外创建packet重复使用而C每次循环都要创建新的?
因为C每次发送的信息长度是未知的,而packet创建需要给出length所以C每次发送都需要重新创建,同时S对C发送的消息长度也是未知的,但是由于UDP对数据包有64KB上限制约,所以我们可以直接创建个64KB大的packet来接收,然后截断字节就可以。

四、TCP编程

TCP中C由Socket类创建,而S由ServerSocket类创建,然后通过accept方法再次获得Socket类。

S代码如下

language-cpp
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
    public static void main(String[] args) throws Exception {

//1.create serversocket
ServerSocket serverSocket = new ServerSocket(8889);
//2.wait client link request
Socket socket = serverSocket.accept();
//3.get input is for receive message
InputStream is = socket.getInputStream();
//4.package low is to high is
DataInputStream dis = new DataInputStream(is);
while (true) {

try {

//5.receive message
System.out.println("消息来自: "+socket.getRemoteSocketAddress());
String rs = dis.readUTF();
System.out.println("消息为:"+rs);
} catch (IOException e) {

System.out.println("客户端离线了");
break;
}
}
//close
dis.close();
socket.close();
}
}

S会在accept等待C
C代码如下

language-cpp
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
public class Client {

public static void main(String[] args) throws Exception {

//1.create socket and send link request to server
Socket socket = new Socket(InetAddress.getLocalHost().getHostAddress(), 8889);
//2.get socket output for write message to server
OutputStream os = socket.getOutputStream();
//3.package low os to high os
DataOutputStream dos = new DataOutputStream(os);

Scanner sc = new Scanner(System.in);
while (true) {

System.out.println("请说");
String s = sc.nextLine();
if (s.equals("exit")){

break;
}
//4.write message
dos.writeUTF(s);
dos.flush();//立刻发送
System.out.println("已发送~");
}

//close
dos.close();
socket.close();
}
}

通过高级流包装原始流,方便编程,这里用的是数据流,将字符串以UTF-8的编码形式传输。

五、QA

TCP怎么实现多个C对一个S?

一个S应对多个C的TCP通信,S需要线程池,每个通信都抛给子线程处理即可。在ServerSocket.accept到一个C的适合,就把这个socket抛到子线程。

TCP怎么实现群聊?

C除了需要发送信息,还需要接收信息,并且两个行为需要持续开启,所以C需要多线程,一个从用户键盘读取信息发送,一个从S接收其他C发送的信息。
S除了线程池外,还需要一个子线程都能访问共享数组,来保存所有socket,子线程收到对应的C的消息,就抄一份发给所有socket。

JavaSE-高级特性

JavaSE-高级特性

一、单元测试

使用Junit框架

二、反射

反射可以破坏封装性,得到一个类的所有成分,最重要的途径是用来参与Java框架制作。
获取Class对象的三种方式

  • Class c1 = 类名.class
  • 调用Class提供方法: public static Class forName(String package);
  • Object提供的方法: public Class getClass(); Class c3 = 对象.getClass();

反射案例

需求:对于任意对象,将对象的字段名和对应的值保存到文件中去
假设我们有Teacher类和Student类,那么代码如下

language-java
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
public class Demo01 {

private String dirPath = "src/main/resources/P13/";

@Test
public void test01() {

Student s1 = new Student("淇",22,'女',167.56,"swim");
Teacher t1 = new Teacher("柳",5433.2);

work(s1, dirPath+"s.txt");
work(t1, dirPath+"t.txt");
}

public void work(Object object, String filePath) {

//1.create fileIO
try (
// FileWriter fileWriter = new FileWriter(filePath);
PrintWriter printWriter = new PrintWriter(filePath);
) {

//2.get Object fields info
Class c = object.getClass();
Field[] declaredFields = c.getDeclaredFields();
printWriter.println("--------"+c.getSimpleName()+"--------");
for (Field f : declaredFields) {

f.setAccessible(true);
printWriter.println(f.getName()+" --> "+f.get(object));
}
} catch (Exception e) {

throw new RuntimeException(e);
}
}
}

三、注解

我的注解一如下

language-java
1
2
3
4
5
6
7
public @interface MyAnnotation {

String aaa();
boolean bbb() default true;
String[] ccc();
}

注解二如下

language-java
1
2
3
4
public @interface MyAnnotation2 {

String value();
}

注解使用如下

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package org.kmo.d08_Annotation.Test01;

import org.junit.Test;

@MyAnnotation(aaa="牛魔王", ccc={
"Python","Java"})
public class MyTest01 {

@MyAnnotation(aaa="铁山",bbb=false,ccc={
"C++","C"})
public void test1(){

}

@MyAnnotation2("孙悟空")
public void test2(){

}
}

解析注解案例

需求:
定义注解MyTest4。
定义一个Demo类,在类中定义一个test1方法,并在该类和其方法上使用MyTest4注解。
定义AnnotationTest3测试类,解析Demo类中的全部注解。

这是一个注解

language-java
1
2
3
4
5
6
7
8
9
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest4 {

String value();
double aaa() default 100;
String[] bbb();
}

这是Demo类

language-java
1
2
3
4
5
6
7
8
9
10
11
12
@MyTest4(value = "vv",aaa = 99.9, bbb={
"b","bb","bbb"})
public class Demo {


@MyTest4(value = "我是至尊宝",aaa = 100.9, bbb = {
"小淇","爱你"})
public void test01(){


}
}

这是解析类,解析Demo类上的注解以及Demo类方法的注解

language-java
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
public class AnnotationTest3 {


@Test
public void test01(){

Class c = Demo.class;
if (c.isAnnotationPresent(MyTest4.class)){

MyTest4 myTest4 = (MyTest4) c.getDeclaredAnnotation(MyTest4.class);
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(Arrays.toString(myTest4.bbb()));
}
}
@Test
public void test02() throws Exception {

Class c = Demo.class;
Method method = c.getDeclaredMethod("test01");
if (method.isAnnotationPresent(MyTest4.class)){

MyTest4 myTest4 = (MyTest4) method.getDeclaredAnnotation(MyTest4.class);
System.out.println(myTest4.value());
System.out.println(myTest4.aaa());
System.out.println(Arrays.toString(myTest4.bbb()));
}
}
}

注解案例-简易Junit

这是一个注解

language-java
1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {


}

这是运行代码

language-java
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
public class Demo {

@MyTest()
public void test01(){

System.out.println("===Test01====");
}

public void test02(){

System.out.println("===Test02====");
}

@MyTest
public void test03(){

System.out.println("===Test03====");
}
public void test04(){

System.out.println("===Test04====");
}

public static void main(String[] args) throws Exception {

Demo demo = new Demo();
Class c = Demo.class;
Method[] methods = c.getDeclaredMethods();
for (Method method : methods) {

if (method.isAnnotationPresent(MyTest.class)){

method.invoke(demo);
}
}
}
}

四、动态代理

相当于把相同的地方集合到一起了,只要写一遍,且灵活。

动态代理案例

需求:对一个类的所有方法前后都加上计时,当类的方法被调用时,输出时间。

一个UserService接口

language-java
1
2
3
4
5
6
public interface UserService {

void login(String loginName, String password) throws Exception;
void deleteUsers() throws Exception;
String[] selectUsers() throws Exception;
}

UserServicelmpl实现类

language-java
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
public class UserServiceImpl implements UserService {

@Override
public void login(String loginName, String passWord) throws Exception {

if ("admin".equals(loginName) && "123456".equals(passWord)) {

System.out.println("您登录成功,欢迎光临本系统~");
} else {

System.out.println("您登录失败,用户名或密码错误~");
}
Thread.sleep(1000);
}

@Override
public void deleteUsers() throws Exception {

System.out.println("成功删除了1万个用户~");
Thread.sleep(1500);
}

@Override
public String[] selectUsers() throws Exception {

System.out.println("查询出3个用户");
String[] names = {
"张全蛋","李二狗","牛爱花"};
Thread.sleep(500);
return names;
}
}

ProxyUtil代理类

language-java
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
public class ProxyUtil {

public static UserService createProxy(UserService userService){

UserService userServiceProxy = (UserService) Proxy.newProxyInstance(
ProxyUtil.class.getClassLoader(),
new Class[]{
UserService.class},
new InvocationHandler() {

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if (method.getName().equals("login") || method.getName().equals("deleteUsers") || method.getName().equals("selectUsers")){

long startTime = System.currentTimeMillis();
Object res = method.invoke(userService, args);
long endTime = System.currentTimeMillis();
System.out.println(method.getName()+"方法执行耗时: " + (endTime - startTime) / 1000.0 + "s");
return res;
}else {

Object res = method.invoke(userService, args);
return res;
}
}
}
);

return userServiceProxy;
}
}

在这个代理中,被代理的类传入,Proxy.newProxyInstance中重写的invoke方法里,就是在重写被代理类的所有方法,可以理解为在调用(被代理的)类的方法时,其实是在调用这里的invoke。如果不想做更改,直接执行method.invoke就行。

运行代码

language-java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Tests {

public static void main(String[] args) throws Exception {

// UserService userService = new UserServiceImpl();
UserService userService = ProxyUtil.createProxy(new UserServiceImpl());

userService.login("admin","123456");
System.out.println("--------------------");
userService.deleteUsers();
System.out.println("--------------------");
String[] strings = userService.selectUsers();
System.out.println(Arrays.toString(strings));
System.out.println("--------------------");

}
}