前后端传参

三种传参方式

  • x-www-form-urlencoded:只能上传键值对,并且键值对都是间隔分开的

    multipart/form-data:既可以上传文件等二进制数据,也可以上传表单键值对,只是最后会转化为一条信息;

  • x-www-form-urlencoded: Post传参的默认格式,使用js中URLencode转码方法。包括将name、value中的空格替换为加号;将非ascii字符做百分号编码;将input的name、value用‘=’连接,不同的input之间用‘&’连接。这种post格式跟get的区别在于:get把转换、拼接完的字符串用‘?’直接与表单的action连接作为URL使用,所以请求体里没有数据;而post把转换、拼接后的字符串放在了请求体里,不会在浏览器的地址栏显示,因而更安全一些。对于一段utf8编码的字节,用application/x-www-form-urlencoded传输其中的ascii字符没有问题,但对于非ascii字符传输效率就很低了,因此在传很长的字节(如文件)时应用multipart/form-data格式。smtp等协议也使用或借鉴了此格式。

    multipart/form-data:将表单中的每个input转为了一个由boundary分割的小格式,没有转码,直接将utf8字节拼接到请求体中,在本地有多少字节实际就发送多少字节,极大提高了效率,适合传输长字节。

    json:现在很多时候也把它作为请求头,用来告诉服务端消息主体是序列化的JSON字符串,除了低版本的IE,基本都支持。除了低版本的IE都支持JSON.stringify()的方法,服务端也有处理JSON的函数,使用JSON不会有任何麻烦。

  • 当传入参数是x-www-form-urlencoded,接收参数@RequestBody+ String/Map/LinkedHashMap即可接收成功

post和get请求

  • 发送get请求将参数通过?拼接在url后面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
$.ajax({
url: "/order/userPage?page="+page+"&pageSize="+pageSize, //请求的url地址
cache: "false", //设置为false将不会从浏览器中加载请求信息
async: "true", //true所有请求均为异步请求
dataType: "json", //请求返回数据的格式
type:"get", //请求方式
})

//上面等同于==>>
async initData(){
paging: {
page: 1,
pageSize: 5
}
const res = await orderPagingApi(this.paging)
}
function orderPagingApi(data) {
return $axios({
'url': '/order/userPage',
'method': 'get',
//请求参数
params: {...data}
})
}

// 上面等同于==>>
async initData(){
paging: {
page: 1,
pageSize: 5
}
const res = await orderPagingApi(this.paging)
}
function orderPagingApi(data) {
return $axios({
'url': '/order/userPage',
'method': 'get',
'data': data
})
}

// 后端接收参数 @RequestParam 可加可不加
@GetMapping("/xxx")
public R<xxx> userPage(@RequestParam("page")Integer page,Integer pageSize){}
  • 将参数拼接在url中,后台通过占位符接收参数 /{id},前端传递参数直接将参数拼在url中,如 /xxx/1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
async initData(){
const res = await addressFindOneApi(params.id)
}
function addressFindOneApi(id) {
return $axios({
'url': `/addressBook/${id}`,
'method': 'get',
})
}

// 后端接收参数
@GetMapping("/xxx/{id}")
public R<xxx> backList(@PathVariable("id")Long id){}
// @PathVariable:需要接收前端传递的参数必须要加上这个注解
// @PathVariable(name="")和@PathVariable(value="")都可以
// 可以@RequestMapping或@GetMapping。如果用@RequestMapping就应为:@RequestMapping(value = "xxx/{id}", method= RequestMethod.GET)
  • 通过post提交方式将form表单中的数据序列化后传递到后台
1
2
3
4
5
6
7
8
9
10
11
12
13
14
async initData(){
const res =await formAjax();
}
function formAjax() {
$.ajax({
url: "/login",
type: "post",
data: $("#form").serialize(), // 对id为form的表单数据进行序列化并传递到后台
})
}
//后端接收参数
@RequestMapping("/login")
//form表单的数据与User实体类的数据相对应
public String login (User user) {}
  • 通过post提交方式将form表单的类型是 json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async initData(){
const res =await formAjax();
}

function formAjax() {
$.ajax({
url: "/login",
type: "post",
contentType: 'application/json',
})
}
//后端接收参数
@RequestMapping("/login")
//form表单的数据与User实体类的数据相对应
public String login (@RequestBody User user) {}
  • 前台将普通数据转换为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
//后台接收参数 map或对应实体
@GetMapping("/xxx/xxx")
public R<xxx> userPage(@RequesBody Map<Integer,Integer> map){
Integer page = map.get("page");
Integer pageSize = map.get("pageSize");
}async initData(){
paging: {
page: 1,
pageSize: 5
}
const res = await orderPagingApi(this.paging)
}
function orderPagingApi(data) {
return $axios({
'url': '/order/userPage',
'method': 'post',
data: JSON.stringify(data),
})
}
//后台接收参数 map或对应实体
@GetMapping("/xxx/xxx")
public R<xxx> userPage(@RequesBody Map<Integer,Integer> map){
Integer page = map.get("page");
Integer pageSize = map.get("pageSize");
}
  • 接收参数出错案例

    Cannot deserialize instance of java.lang.String out of START_OBJECT token,请求体中的 user 被认为是一个Object对象,不能用String进行解析

1
2
3
4
5
6
//修改前
@PostMapping("/regiest")
public R<String> createUser(@RequestBody Map<String,String> map) {}
//修改后
@PostMapping("/regiest")
public R<String> createUser(@RequestBody Map<String,Object> map) {}
  • 前端传参 {params: params} ,后端用 @RequestParams(“id”) 接参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//前端发送数据
this.$axios
.delete("login/deleteUserInfo",
{
params: {
userId: this.id
}
})
.then(
this.$message({
type: "success",
message: "删除用户信息成功",
}))
.catch(
this.$message({
type: "false",
message: "删除用户信息失败",
})
//后端接收数据
@DeleteMapping("xxx")
public R deleteUserInfo(@RequestParam("userId") String userId){}
  • 前端通过 {data : param} 传参,后端通过 @RequestBody 接参
1
2
3
4
5
6
7
8
9
10
11
//前端
deleteMessage() {
axios.delete('login/deleteUserInfo',
{ data : {
userId: this.id
}}).then((response) => {
});
}
//后端
@DeleteMapping("deleteUserInfo")
public R deleteUserInfo(@RequestBody String userId){}
  • el-select 数据回显问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- :value='1' v-model="1"(数值 1)则匹配(number=number string=string类型要一致),显示label,否则显示value-->
<el-form-item label="角色名称:" label-width="100px">
<el-select
placeholder="请选择角色名称"
v-model="form.roleId"
:label-width="formLabelWidth"
style="width: 200px"
>
<el-option
v-for="item in roleInfo"
:key="item.value"
:label="item.roleName"
:value="item.roleId"
>
</el-option>
</el-select>
</el-form-item>

ajax请求

ajax请求规范

  • axios普通get请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test(value) {
axios({
//请求方式
method:'get',
//后端接口路径
url:'/xxx',
//注意这里使用的是params,该属性负责把属性名和属性值添加到url后面,一般和get配合使用,但也能和post配合使用
params: {
//attributeName为属性名,value为属性值
attributeName:value
}
}).then((response) => {
//response是一个返回的promise对象,该注释所在的这行一般对该promise对象进行处理从而获取数据
}).catch((error) => {
//对error进行处理
})
}
1
2
3
4
5
//普通get请求时对应的接收
@GetMapping("/xxx")
public void test(@RequestParam(value = "attributeName") String attributeName){
//axios使用params进行传值,而Java就需要用到@RequestParam来接收params的值,value后的值要对应上params里的属性名,即attributeName,若不写,则value默认为String后的attributeName这一属性名,而且一个请求能拥有多个@RequestParam
}
  • axios使用 restful 规范的get请求

路径后面有反斜杠’/ ‘,不要写漏了,不然value就会变成路径的一部分,请求路径就会变成/xxx后面跟上value的值,导致404。

1
2
3
4
5
6
7
8
9
10
11
12
function test(value) {
axios({
//请求方式
method:'get',
//后端接口路径+属性值
url:'/xxx/'+value,
}).then((response) => {
//response是一个返回的promise对象,该注释所在的这行一般对该promise对象进行处理从而获取数据
}).catch((error) => {
//对error进行处理
})
}
1
2
3
4
@GetMapping("/xxx/{id}")
public void test(@PathVariable("id") String() attributeName){
//axios使用restful的方式进行传值,而Java就需要用到@PathVariable来接收url后的值,/xxx/{id}里的id必须和@PathVariable("id")里的id名字一致,而且一个请求能拥有多个@PathVariable
}
  • axios使用post+params请求

除了method的请求方式和接收请求用@PostMapping,其他都和普通的get请求一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function test(value) {
axios({
//请求方式
method:'post',
//后端接口路径
url:'/xxx',
//注意这里使用的是params,该属性负责把属性名和属性值添加到url后面,一般和get配合使用,但也能和post配合使用
params: {
//attributeName为属性名,value为属性值
attributeName:value
}
}).then((response) => {
//response是一个返回的promise对象,该注释所在的这行一般对该promise对象进行处理从而获取数据
}).catch((error) => {
//对error进行处理
})
}
1
2
3
4
@PostMapping("/xxx")
public void test(@RequestParam(value = "attributeName") String attributeName){
//axios使用params进行传值,而Java就需要用到@RequestParam来接收params的值,value后的值要对应上params里的属性名,即 attributeName,若不写,则value默认为String后的attributeName这一属性名,而且一个请求能拥有多个@RequestParam
}
  • axios使用post+data请求

注意:obj对象里的属性名要和people实体类里的属性名一样,因为obj对象里的属性名与people实体类里的属性名匹配时,大小写敏感,即区分大小写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//方法名为test,传入js对象obj,js对象放属性和属性值,例如obj:{userName:张三,age:18}
function test(obj) {
axios({
//请求方式
method:'post',
//后端接口路径
url:'/xxx',
//注意这里是data,该属性负责把属性名和属性值添加到请求体里面,一般和post配合使用,而且不要用{}把obj对象包裹起来,不然后端无法解析前端传过来的JSON
data: obj
}).then((response) => {
//response是一个返回的promise对象,该注释所在的这行一般对该promise对象进行处理从而获取数据
}).catch((error) => {
//对error进行处理
})
}
1
2
3
4
5
6
7
8
9
//对应的People实体类
@Data
public class People{
private String userName;
private Integer age;
}
@PostMapping("/xxx")
public void test(@RequestBody People people){
//axios使用post+data进行传值,而Java就需要用到@RequestBody来接收data的值,obj里的属性名与people实体类的属性名相同, obj里的属性名和people的属性名就能进行动态绑定从而把obj的属性值set进people实体类里,而一个请求只能拥有一个@RequestBody; 注意!!!obj对象里的属性名一定得和people实体类里的属性名一模一样,因为obj对象里的属性名与people实体类里的属性名匹配时, 大小写敏感,即区分大小写

传参问题

  • @RequestBody传参

    当接口默认使用@RequestBody传参时,每一个请求必须通过实体对象进行传参,不能使用form-data表单的方式进行传参。

    解决方案有两种:

    ① 改成json方式提交;

    1
    2
    3
    4
    5
    6
    {
    "knowledgeSort" : " ",
    "knowledgeParentId": " ",
    "knowledgeName": " ",
    "chapterId": " "
    }

​ ② 把接口的@RequestBody 注解去掉;

  • Postman传参方式

    第一种是使用 raw:

    img

    第二种是使用 form-data:

    img

    第三种是直接使用变量的方式:

    img


日志

日志框架和日志门面

  • 常见的日志框架

    Log4j:Apache Log4j 是一个广泛使用的日志框架,它提供了丰富的配置选项和多种输出目标。

    Log4j2: Apache Log4j 2 是 Apache Software Foundation 开发的日志框架,它是 Log4j 的升级版本,提供了更好的性能和功能。

    Logback:Logback 是由 Log4j 的创始人设计的日志框架,旨在取代 Log4j。它在性能和灵活性方面表现出色。

    JUL: (Java Util Logging) JUL 是 Java 平台自带的日志框架,位于java.util.logging包下。它是标准的 Java 日志框架,使用简单,但功能相对较弱。

  • 常见的日志门面

    SLF4J:常见的日志门面,它提供了统一的日志 API,并允许你在项目中使用 SLF4J 的接口,而不用直接依赖于具体的日志实现。

    ACL:(Apache Commons Logging)常见的日志门面,类似于 SLF4J,它允许开发者使用通用的 API 来进行日志记录。

注意:日志门面是一个抽象层,它提供了一组通用的日志 API,可以将这些 API 与不同的日志框架进行绑定。这样,你可以在项目中使用统一的日志 API,而无需直接和特定的日志框架耦合。根据项目需求和个人喜好,可以选择使用不同的日志门面和底层的日志实现。其中,SLF4J 是目前使用最广泛的日志门面之一,并且很多日志框架都支持与 SLF4J 的集成

日志如何使用

两个步骤:引入依赖配置日志框架

  • 引入依赖:

    首先,你需要在项目的构建工具(如Maven、Gradle等)中添加对日志门面和具体日志框架的依赖。以 SLF4J 和 Logback 为例,在 Maven 中的依赖配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- SLF4J 日志门面 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>

<!-- Logback 日志框架 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
</dependency>
  • 配置日志框架:

    日志框架的配置方式因框架而异。对于 Logback,需要创建一个 logback.xml配置文件,并放在项目的 classpath 路径下( 通常是src/main/resources 目录下),以下是一个简单的 Logback 配置示例,用于将日志输出到控制台:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- logback.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<root level="debug">
<appender-ref ref="CONSOLE" />
</root>
</configuration>

上述配置定义了一个输出到控制台的 Appender,并将根日志记录器的级别设置为 debug,这意味着会记录所有级别的日志信息。

一旦你完成了依赖的引入和日志框架的配置,你就可以在项目中使用日志门面的 API(例如 SLF4J 提供的 Logger 接口)来记录日志。根据日志门面的 API 设计,你可以使用不同的日志级别(如 debuginfowarnerror 等)和相应的日志方法来记录不同级别的日志信息。

例如,在代码中使用 SLF4J 来记录日志:当项目运行时,具体的日志输出将由你在配置文件中指定的 Appender 和日志级别来决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);

public void doSomething() {
logger.debug("This is a debug message.");
logger.info("This is an info message.");
logger.warn("This is a warning message.");
logger.error("This is an error message.");
}
}

在项目中集成日志门面和日志框架,首先引入相应的依赖(日志门面和具体日志框架的实现),然后配置日志框架以指定日志输出的方式和级别。在代码中,你可以使用日志门面的 API 来记录日志。这样可以在不改变代码的情况下切换不同的日志框架,以满足项目的日志需求


异常

try-with-resource语法

后续补充细节…

网络编程

Socket:套接字,是计算机网络编程中的一个核心概念,它提供了一种跨网络进程间通信(IPC, Inter-Process Communication)的机制。Socket允许两个不同计算机上的应用程序通过网络进行数据交换,就像两个程序在同一条线上“对话”一样。这一概念广泛应用于互联网通信、分布式系统以及各种客户端-服务器模型的程序中。

Socket的基本原理

Socket基于客户端-服务器模型工作,主要涉及以下几个关键点:

  1. 地址家族:指定通信的地址类型,如IPv4、IPv6等。
  2. 套接字类型:主要有流式套接字(Stream Socket,对应TCP协议)和数据报套接字(Datagram Socket,对应UDP协议)两种。流式套接字提供双向的、有序的、无重复的数据流服务,保证数据的可靠传输;而数据报套接字则是无连接的,不保证数据的顺序和可靠性,但传输效率较高。
  3. 通信协议:最常用的是 TCP (Transmission Control Protocol,传输控制协议)和 UDP (User Datagram Protocol,用户数据报协议)。

Socket的典型用途

  • Web服务:HTTP服务器与浏览器之间的通信通常基于TCP Socket。
  • 文件传输:FTP、某些类型的下载和上传服务依赖TCP Socket保证数据的完整传输。
  • 即时通讯:如聊天应用,可能使用TCP Socket保持稳定的连接或使用UDP Socket实现实时性较高的消息传递。
  • 在线游戏:游戏服务器与客户端间的实时交互,可能结合TCP和UDP,TCP用于确保重要指令的可靠传输,UDP用于低延迟的游戏状态更新。
  • 分布式计算:在分布式系统中,不同节点间的数据交换可能通过Socket实现。

使用流程

  1. 创建Socket:客户端和服务器端都需要创建Socket实例。
  2. 绑定与监听:服务器端Socket需要绑定到一个IP地址和端口号上,并开始监听来自客户端的连接请求。
  3. 连接:客户端Socket尝试连接到服务器的指定地址和端口。
  4. 数据交换:连接建立后,双方可以通过输入输出流进行数据的发送和接收。
  5. 关闭Socket:数据交换完毕后,关闭Socket释放资源。

Socket编程为开发者提供了灵活的网络通信能力,是构建网络应用的基础。


集合

List

1
2
3
4
5
// 为什么推荐第一种不推荐第二种
List list = new ArrayList()
ArrayList list = new ArrayList()
// 体现了java语言的多态性
List list = new ArrayList();
  • java是面向对象编程,面向对象一个重要的原则就是“依赖倒置原则”。依赖抽象(接口),而非具体(实现类)

    List是接口,ArrayList是实现类,它允许list可以轻松地在接口的不同实现之间切换

  • List的实现类包括List,Vector,LinkedList , Stack等等,使用List list = new ArrayList()将来如果需要改成线程安全的Vector,只需要直接把ArrayList改成Vector就行了。即List list = new Vector()这种方法实现解耦合,大大提高代码使用的灵活性和通用性,并且在暴露公共接口的时候尤其有用

Map

Map是java中的接口,Map.Entry是Map的一个内部接口。常用方法:keySet()entrySet()

keySet()方法:返回值是Map中key值的集合

entrySet()方法:返回值是一个Set集合,此集合的类型为Map.Entry,它是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示 Map中的一个实体即一个key-value对。接口中有 getKey()getValue()等方法

1
2
3
4
Map<String, String> map = new HashMap<String, String>();
map.put(“key1”, “value1”);
map.put(“key2”, “value2”);
map.put(“key3”, “value3”);

第一种:普遍使用,二次取值。即通过 Map.keySet() 遍历key和value

1
2
3
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}

第二种:通过 Map.entrySet 使用 iterator 遍历key和value

1
2
3
4
5
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

第三种:推荐,尤其是容量大时,即通过 Map.entrySet 遍历key和value

1
2
3
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}

第四种:通过Map.values()遍历所有的value,但不能遍历key

1
2
3
for (String v : map.values()) {
System.out.println("value= " + v);
}

Iterator和Iterable的区别

为什么Collection接口要继承于Iterable接口,而不是Iterator接口?

  • 在jdk 1.5以后,引入了 Iterable ,使用 foreach 语句必须使用 Iterable

  • Java设计者让 Collection 继承于 Iterable 而不是 Iterator 接口

    Iterable 的子类是 Collection,Collection 的子类是List,Set等,这些是数据结构或者说放数据的地方

    Iterator 是定义了迭代逻辑的对象,让迭代逻辑和数据结构分离开来,这样的好处是可以在一种数据结构上实现多种迭代逻辑

  • 更重要的一点是:每一次调用 IterableIterator() 方法,都会返回一个从头开始的 Iterator 对象,各个 Iterator 对象之间不会相互干扰,这样保证了可以同时对一个数据结构进行多个遍历。这是因为每个循环都是用了独立的迭代器 Iterator 对象

img

Stream流

  • 根据条件映射
1
list.stream().map(条件).collect(Collectors.toList());
  • 根据条件遍历
1
list.stream().foreach(条件).collect(Collectors.toList());
  • 根据条件分组
1
list.stream().collect(Collectors.groupingBy(条件));
  • 根据条件排序
1
list.stream().sorted(条件).collect(Collectors.toList());
  • 根据条件筛选、
1
list.stream().filter(条件).collect(Collectors.toList());
  • 根据条件缩减
1
list.stream().reduce(条件)
  • 根据条件比较大小
1
2
list.stream().max(Comparator.comparing(条件));
list.stream().min(Comparator.comparing(条件));
  • 查询集合第一个元素
1
2
list.stream().filter(条件).findFirst();
list.stream().filter(条件).findAny();
  • 查询集合元素数量
1
list.stream().filter(条件).count();
  • 跳过前m个数据,
1
list.stream().skip(m).collect(Collectors.toList());
  • 限制获得前n个数据
1
list.stream().limit(n).collect(Collectors.toList());
  • 去重
1
list.stream().distinct().collect(Collectors.toList());
  • 合并,去重

合并的stream类型必须相同,只能两两合并

1
Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
  • 加字符 “,”
1
stream().map().collect(Collectors.joining(","));

语法

正则表达式

  • 匹配:使用matches()方法来判断一个字符串是否与正则表达式匹配。例如:
1
2
3
String regex = "a*b";
String input = "aab";
boolean isMatch = input.matches(regex);//true
  • 搜索与替换:使用PatternMatcher类来进行搜索和替换操作。例如:
1
2
3
4
5
6
String regex = "\\bcat\\b";
String input = "I have a cat and a dog.";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
boolean found = matcher.find(); // 搜索匹配项 true
String replacedInput = matcher.replaceAll("dog"); // 替换匹配项 I have a dog and a dog.
  • 分割字符串:使用split()方法来根据正则表达式将字符串分割成数组。例如:
1
2
3
String regex = "\\s+"; // 表示一个或多个空格字符
String input = "Hello World";
String[] parts = input.split(regex); //["Hello","World"]
  • 提取内容:使用分组(Grouping)来提取正则表达式中的特定部分。例如:
1
2
3
4
5
6
7
8
9
10
String regex = "(\\d{4})-(\\d{2})-(\\d{2})"; // 匹配日期格式(YYYY-MM-DD)
String input = "Today is 2021-09-30";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(input);
if (matcher.find()) {
String year = matcher.group(1); // "2021"
String month = matcher.group(2); // "09"
String day = matcher.group(3); // "30"
String group = matcher,group();// "2021-09-30"
}
  • Java中的反斜杠 \ 在字符串中需要进行转义,因此正则表达式中的反斜杠需要使用两个反斜杠 \\ 进行表示。
1
2
3
4
5
6
7
8
9
10
*	:匹配前一个元素零次或多次。
. :匹配任意单个字符(除了换行符 \n)。
+ :匹配前一个元素一次或多次。
? :匹配前一个元素零次或一次。
\b :当它是由单词边界包围时才进行匹配 如上例
\s :用于匹配任意连续的空白字符。包括空格、制表符和换行符
\d* :匹配任意数量的数字字符(0 次或多次)。
\. :匹配实际的点号字符。
\d+ :匹配至少一个数字字符。
\? :匹配实际的问号字符。

字符串处理

  • 将字符串转成无 [] 的字符串
1
StringUtils.strip(collect.toString(), "[]")
  • 数字和字母转换
    • For inputString “A”错误
    • 字符串截取操作
1
2
3
4
5
//Intege.parseInt("A")是不能被直接转化成数字的,会报错
//正确做法是将"A"转化成'A',然后直接强制类型转换就行
String code = "A";
int a1 = (int)'A'
int a2 = (int)code.toUpperCase().charAt(0)

转换对象

  • 实体对象
1
BeanUtils.copyProperties(item,voItem);
  • list
1
2
3
4
// list1<a>   	
// list2<b>
BeanUtils.copytoList(list1,list2);
list2<b> = BeanUtils.copytoList(list1,b.class);

遍历

  • foreach()增强for循环
1
2
for(Vo vo : list){
}
  • stream.map
1
2
3
list.stream().map((item)->{
return item;
}).collect(Collectors.toList());
  • list.foreach()list.stream.foreach()
1
2
3
4
5
list.forEach(item->{
});
//使用流处理
lsit.stream.foreach(item->{
});
  • 迭代器 iterator
1
2
3
4
Iterator<xxx> iterator = xxx.iterator();
while (iterator.hasNext()) {
xxx xxx = iterator.next();
}

运算符

  • 自增运算符
1
2
3
int i = 1;
int j = ++i + i++ + ++i + ++i + i++;
System.out.println(j);//18
  • 逻辑运算符
1
2
3
4
5
6
7
8
9
10
11
12
public class HelloWorld {
public static void main(String[] args) {
//长路与 无论第一个表达式的值是true或者false,第二个的值,都会被运算
int i = 2;
System.out.println( i== 1 & i++ ==2 ); //无论如何i++都会被执行,所以i的值变成了3
System.out.println(i);
//短路与 只要第一个表达式的值是false的,第二个表达式的值,就不需要进行运算了
int j = 2;
System.out.println( j== 1 && j++ ==2 ); //因为j==1返回false,所以右边的j++就没有执行了,所以j的值,还是2
System.out.println(j);
}
}

Swagger

Api注解不是Spring自带的,他是swagger里面的,代码编写的时候需要在pom文件中引入相关swagger的依赖.

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>

常用注解

  • @Api:用在请求的类上,表示对类的说明。

    • tags=”说明该类的作用,可以在UI界面上看到的注解”,
    • value=”该参数没什么意义,在UI界面上也看到,所以不需要配置”
  • @ApiOperation:用在请求的方法上,说明方法的用途、作用。

    • value = “接口说明”,
    • httpMethod = “接口请求方式”,
    • response =“接口返回参数类型”,
    • notes = “接口发布说明”;
  • @ApiResponses:用在请求的方法上,表示一组响应

    • @ApiResponse:用在@ApiResponses中,一般用于表达一个错误或正确的响应信息
      • code:数字,例如400
      • message:信息,例如”请求参数没填好”
      • response:抛出异常的类
1
2
3
4
@ApiResponses({
@ApiResponse(code = 0, message = "请求成功"),
@ApiResponse(code = 1, message = "请求失败")
})
  • @ApiModelProperty:用在属性上,描述响应类的属性

    • value-字段说明;
    • name-重写属性名字;
    • dataType-重写属性类型;
    • required-是否必填;
    • example-举例说明;
    • hidden-隐藏
  • @ApiImplicitParams:用在请求的方法上,表示一组参数说明

    • @ApiImplicitParam:用在@ApiImplicitParams注解中,指定一个请求参数的各个方面
      • name:参数名;value:参数的汉字说明、解释;required:参数是否必须传
      • paramType:参数放在哪个地方
      • header –> 请求参数的获取:@RequestHeader
      • query –> 请求参数的获取:@RequestParam
      • path(用于restful接口)–> 请求参数的获取:@PathVariable
      • body(不常用)
      • form(不常用)
      • dataType:参数类型,默认String,其它值dataType=”Integer”
      • defaultValue:参数的默认值
  • @ApiModel:用于响应类上,表示一个返回响应数据的信息。这种一般用在post创建的时候,使用@RequestBody这样的场景,请求参数无法使用@ApiImplicitParam注解进行描述的时候


hutool

ObjectUtil的 isNull和 isEmpty

isNull(Object obj)方法:

  • 该方法用于判断对象是否为 null
  • 如果传入的对象 objnull 或者等于 null(通过调用 equals 方法进行判断),则返回 true,否则返回 false
  • 这个方法只关注对象是否为 null,不考虑对象的具体内容。

isEmpty(Object obj)方法:

  • 该方法用于判断对象是否为空。
  • 如果传入的对象 objnull,则返回 true
  • 如果传入的对象是 CharSequence(如字符串)、Map(如Map集合)、Iterable(如List、Set等可迭代对象)或Iterator(迭代器),则会调用对应的工具类方法来判断对象是否为空。比如,如果是字符串类型,则使用 StrUtil.isEmpty 方法来判断字符串是否为空。
  • 如果传入的对象是数组类型,则使用 ArrayUtil.isEmpty 方法来判断数组是否为空。
  • 如果传入的对象不属于上述类型,那么返回 false,表示对象不为空。

注意:isEmpty() 方法会针对不同类型的对象调用相应的工具类方法来判断对象是否为空;isNull() 方法只关注对象是否为 null


Jsoup

Jsoup官方文档

解析HTML

  • Jsoup.parse(String html): 将HTML字符串解析成一个Document对象。

  • Jsoup.parse(File in, String charsetName): 从文件中解析HTML文档。

  • Jsoup.parse(URL url, int timeoutMillis): 从URL中解析HTML文档。

    遍历和选择元素:

  • getElementsByTag(String tagName): 选择指定标签名的元素,返回一个Elements对象。

  • getElementById(String id): 选择具有指定id属性的元素,返回一个Element对象。

  • getElementsByClass(String className): 选择具有指定CSS类名的元素,返回一个Elements对象。

  • Document.select(String cssQuery): 使用CSS选择器选择元素,返回一个Elements对象。

  • Element.select(String cssQuery): 在当前元素的子元素中使用CSS选择器选择元素,返回一个Elements对象。

  • Elements.first(): 返回Elements集合中的第一个元素。

  • Elements.last(): 返回Elements集合中的最后一个元素。

  • Elements.get(int index): 返回Elements集合中指定索引位置的元素。

  • Elements.hasAttr(String attributeKey): 选择具有指定属性的元素。

  • Elements.not(String cssQuery): 排除符合指定选择器的元素。

  • Element.traverse(NodeVisitor nodeVisitor): 使用自定义的NodeVisitor遍历元素及其子元素。

  • Element.wrap(String html): 将元素用指定的HTML包装起来。

  • Element.unwrap(): 移除元素的父元素,保留元素本身。

过滤元素

  • Elements.filter(NodeFilter nodeFilter): 使用自定义的NodeFilter过滤元素。

操作元素

  • Element.tagName(): 获取元素的标签名。
  • Element.text(): 获取元素的文本内容。
  • Element.html(): 获取元素的HTML内容。
  • Element.attr(String attributeKey): 获取元素指定属性的值。
  • Element.attr(String attributeKey, String attributeValue): 设置元素指定属性的值。
  • Element.append(String html): 在元素的末尾添加HTML内容。
  • Element.prepend(String html): 在元素的开头添加HTML内容。
  • Element.after(String html): 在元素之后插入HTML内容。
  • Element.before(String html): 在元素之前插入HTML内容。
  • Element.remove(): 移除元素。

提取和修改HTML文档结构

  • Document.body(): 获取HTML文档的<body>元素。
  • Element.parent(): 获取元素的父元素。
  • Element.children(): 获取元素的直接子元素。
  • Element.clone(): 克隆元素及其子元素。
  • Element.replaceWith(Node replacement): 用指定的节点替换元素。
  • Element.replaceWith(String html): 用指定的HTML内容替换元素。

操作CSS类

  • Element.hasClass(String className): 检查元素是否具有指定的CSS类。
  • Element.addClass(String className): 为元素添加CSS类。
  • Element.removeClass(String className): 从元素中移除指定的CSS类。

处理表单数据

  • FormElement.formData(): 获取表单元素中的所有表单数据。

获取和设置表单数据

  • Element.val(): 获取或设置元素的值。
  • Element.val(String value): 设置元素的值。

处理相对和绝对URL

  • Element.absUrl(String attributeKey): 获取元素指定属性的绝对URL。

处理特殊字符和编码

  • Entities.escape(String string): 对HTML文本进行转义。
  • Entities.unescape(String string): 解码HTML文本中的特殊字符。

Java数据脱敏

① SQL数据脱敏实现

MYSQL(电话号码,身份证)数据脱敏的实现

1
2
3
4
5
6
7
8
-- CONCAT()、LEFT()和RIGHT()字符串函数组合使用,请看下面具体实现
-- CONCAT(str1,str2,…):返回结果为连接参数产生的字符串
-- LEFT(str,len):返回从字符串str 开始的len 最左字符
-- RIGHT(str,len):从字符串str 开始,返回最右len 字符
-- 电话号码脱敏sql:
SELECT mobilePhone AS 脱敏前电话号码,CONCAT(LEFT(mobilePhone,3), '********' ) AS 脱敏后电话号码 FROM t_s_user
-- 身份证号码脱敏sql:
SELECT idcard AS 未脱敏身份证, CONCAT(LEFT(idcard,3), '****' ,RIGHT(idcard,4)) AS 脱敏后身份证号 FROM t_s_user

② JAVA数据脱敏

可参考:sensitive-plus

数据脱敏插件,目前支持地址脱敏、银行卡号脱敏、中文姓名脱敏、固话脱敏、身份证号脱敏、手机号脱敏、密码脱敏 一个是正则脱敏、另外一个根据显示长度脱敏,默认是正则脱敏,可以根据自己的需要配置自己的规则。

③ mybatis-mate-sensitive-jackson

mybatis-mate-sensitive-jackson

根据定义的策略类型,对数据进行脱敏,当然策略可以自定义。


BeanUtils.copyProperties的坑

我们日常开发中,经常涉及到DO、DTO、VO对象属性拷贝赋值,很容易想到org.springframework.beans.BeanUtilscopyProperties 。它会自动通过反射机制获取源对象和目标对象的属性,并将对应的属性值进行复制。可以减少手动编写属性复制代码的工作量,提高代码的可读性和维护性。

img

第1个坑:类型不匹配

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Data  
public class SourceBean {
private Long age;
}
@Data
public class TargetBean {
private String age;
}
public class Test {
public static void main(String[] args) {
SourceBean source = new SourceBean();
source.setAge(25L);
TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getAge()); //拷贝赋值失败,输出null
}
}

源对象SourceBeanage属性是Long类型,目标对象TargetBeanage属性是String类型。由于类型不匹配,BeanUtils.copyProperties赋值失败

第2个坑:浅拷贝

什么是深拷贝?什么是浅拷贝?

  • 浅拷贝是指创建一个新对象,该对象的属性值与原始对象相同,但对于引用类型的属性,仍然共享相同的引用。

    浅拷贝只复制对象及其引用,而不复制引用指向的对象本身

  • 深拷贝是指创建一个新对象,该对象的属性值与原始对象相同,包括引用类型的属性。

    深拷贝会递归复制引用对象,创建全新的对象,以确保拷贝后的对象与原始对象完全独立

图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Address {  
private String city;
}

public class Person {
private String name;
private Address address;
}

Person sourcePerson = new Person();
sourcePerson.setName("John");
Address address = new Address();
address.setCity("New York");
sourcePerson.setAddress(address);
Person targetPerson = new Person();
BeanUtils.copyProperties(sourcePerson, targetPerson);

sourcePerson.getAddress().setCity("London");

System.out.println(targetPerson.getAddress().getCity()); // 输出为 "London"

源对象Person的属性address是引用类型。使用BeanUtils.copyProperties方法进行属性复制时,只复制了引用。即目标对象targetPersonaddress 属性引用和源对象 sourcePersonaddress 属性引用指向同一个对象。因此,当修改源对象的address对象时,目标对象的address对象也会被修改。

第3个坑:属性名称不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SourceBean {  
private String username;
}
public class TargetBean {
private String userName;
}
SourceBean source = new SourceBean();
source.setUsername("捡田螺的小男孩");

TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);

System.out.println(target.getUserName()); // 输出为 null

源对象SourceBean 的属性名称是username,而目标对象TargetBean的属性名称也是userName。但是,两个 username,一个N是大写,一个n是小写,即属性名称不一致BeanUtils.copyProperties方法无法自动映射这些属性(无法忽略大小写自动匹配),因此目标对象的userName属性值为null

日常开发注意:大小写不一致,差一两个字母等等

第4个坑:Null 值覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data  
public class SourceBean {
private String name;
private String address;
}
@Data
public class TargetBean {
private String name;
private String address;
}

SourceBean source = new SourceBean();
source.setName("John");
source.setAddress(null);

TargetBean target = new TargetBean();
target.setAddress("田螺address");
BeanUtils.copyProperties(source, target);

System.out.println(target.getAddress()); // 输出为 null

源对象 SourceBean 的 address 属性值为 null。默认情况下,BeanUtils.copyProperties 方法会将源对象中的 null 值属性覆盖到目标对象中。因此,目标对象的 address 属性值也为 null。

如果不希望 null 值覆盖目标对象中的属性,可以用 BeanUtils.copyProperties 方法的重载方法,并传入一个自定义的 ConvertUtilsBean 实例来进行配置。

第5个坑:注意引入的包

BeanUtils.copyProperties其实有两个包,分别是spring、apache。这两个包,是有点不一样的:

1
2
3
4
//org.springframework.beans.BeanUtils(源对象在左边,目标对象在右边)  
public static void copyProperties(Object source, Object target) throws BeansException
//org.apache.commons.beanutils.BeanUtils(源对象在右边,目标对象在左边)
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException

注意自己引入的哪个BeanUtils,写对应参数位置。

第6个坑:Boolean类型数据+is属性开头的坑

SourceBean和TargetBean中的都有个属性isTianLuo,它们的数据类型保持不变,但是一个为基本类型boolean,一个为包装类型Boolean

1
2
3
4
5
6
7
8
@Data  
public class SourceBean {
private boolean isTianLuo;
}
@Data
public class TargetBean {
private Boolean isTianLuo;
}

跑测试用里的时候,发现赋值不上:这是因为当属性类型为boolean时,属性名以is开头,属性名会去掉前面的is,因此源对象和目标对象属性对不上。

1
2
3
4
5
6
SourceBean source = new SourceBean();  
source.setTianLuo(true);

TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getIsTianLuo()); // 输出为 null

第7个坑:查找不到字段引用

在某些开发场景呢,如果我们要修改某个字段的赋值,我们可能会全文搜索它的所有set方法,看哪些地方引用到。但是如果使用BeanUtils.copyProperties就不知道是否引用到对应的ste方法啦即查找不到字段引用,这就可能导致你会漏掉修改对应的字段。

图片

图片

第8个坑:不同内部类,即使相同属性,也是赋值失败

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
@Data  
public class CopySource {
public String outerName;
public CopySource.InnerClass innerClass;
@Data
public static class InnerClass {
public String InnerName;
}
}
@Data
public class CopyTarget {
public String outerName;
public CopyTarget.InnerClass innerClass;
@Data
public static class InnerClass {
public String InnerName;
}
}

CopySource test1 = new CopySource();
test1.outerName = "outTianluo";

CopySource.InnerClass innerClass = new CopySource.InnerClass();
innerClass.InnerName = "innerTianLuo";
test1.innerClass = innerClass;

System.out.println(test1);
CopyTarget test2 = new CopyTarget();
BeanUtils.copyProperties(test1, test2);

System.out.println(test2); //输出CopyTarget(outerName=outTianluo, innerClass=null)

CopySourceCopyTarget各自存在一个内部类InnerClass,虽然这个内部类属性也相同,类名也相同,但是在不同的类中,因此Spring会认为属性不同,不会Copy。如果要复制成功,可以让他们指向同一个内部类。

第9个坑:bean对应的属性,没有getter和setter方法,赋值失败

BeanUtils.copyProperties要拷贝属性值成功,需要对应的beangetter和setter方法。因为它是用反射拿到set和get方法再去拿属性值和设置属性值的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data  
public class SourceBean {
private String value;
}
@Getter //没有对应的setter方法
public class TargetBean {
private String value;
}

SourceBean source = new SourceBean();
source.setValue("捡田螺的小男孩");

TargetBean target = new TargetBean();
BeanUtils.copyProperties(source, target);
System.out.println(target.getValue()); //输出null

第10个坑:BeanUtils.copyProperties + 泛型

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
@Data  
public class CopySource {
public String outerName;
public List<CopySource.InnerClass> clazz;
@Data
public static class InnerClass {
public String InnerName;
}
}
@ToString
@Data
public class CopyTarget {
public String outerName;
public List<CopyTarget.InnerClass> clazz;
@Data
public static class InnerClass {
public String InnerName;
}
}

CopySource test1 = new CopySource();
test1.outerName = "outTianluo";

CopySource.InnerClass innerClass = new CopySource.InnerClass();
innerClass.InnerName = "innerTianLuo";

List<CopySource.InnerClass> clazz = new ArrayList<>();
clazz.add(innerClass);
test1.setClazz(clazz);

System.out.println(test1);
CopyTarget test2 = new CopyTarget();
BeanUtils.copyProperties(test1, test2);

System.out.println(test2); //输出CopyTarget(outerName=outTianluo, clazz=null)

BeanUtils.copyProperties方法拷贝包含泛型属性的对象clazzCopyTargetCopySource的泛型属性类型不匹配,因此拷贝赋值失败。

第11个坑:性能问题

由于这些BeanUtils类都是采用反射机制实现的,对程序的效率也会有影响。demo对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SourceBean sourceBean = new SourceBean();  
sourceBean.setName("tianLuoBoy");
TargetBean target = new TargetBean();

long beginTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { //循环10万次
target.setName(sourceBean.getName());
}
System.out.println("common setter time:" + (System.currentTimeMillis() - beginTime));

long beginTime1 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { //循环10万次
BeanUtils.copyProperties(sourceBean, target);
}
System.out.println("bean copy time:" + (System.currentTimeMillis() - beginTime1));

//输出
common setter time:3
bean copy time:331

可以发现,简单的setterBeanUtils.copyProperties对比,性能差距非常大。因此,慎用BeanUtils.copyProperties

替换BeanUtils.copyProperties的方案

第一种,那就是使用原始的setter和getter方法。

使用手动的setter方法进行属性赋值。这种方法可能需要编写更多的代码,但是可以提供更细粒度的控制,并且在性能方面通常比BeanUtils.copyProperties更高效。

1
2
3
Target target = new Target();  
target.setName(source.getName());
target.setAge(source.getAge());

如果实在对象bean的属性比较多的话,可以使用插件GenerateAllSetter,它可以一键生成对象的set方法,挺方便的。

第二种方案,使用映射工具库,如MapStruct、ModelMapper等,它们可以自动生成属性映射的代码。这些工具库可以减少手动编写setter方法的工作量,并提供更好的性能。

1
2
3
4
5
6
7
8
@Mapper  
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class);
@Mapping(source = "name", target = "name")
@Mapping(source = "age", target = "age")
Target mapToTarget(Source source);
}
Target target = SourceTargetMapper.INSTANCE.mapToTarget(source);

导出文件相关

递归输出文件夹里全部文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void listFilesRecursively(File folder) {
File[] contents = folder.listFiles();
if (contents != null) {
for (File item : contents) {
if (item.isFile()) {
System.out.println(item.getAbsolutePath());
} else if (item.isDirectory()) {
listFilesRecursively(item);
}
}
}
}
public static void main(String[] args) {
// 替换为你想要遍历的文件夹路径
String folderPath = "F:\\Typora\\touchfish\\9.PDF books";
File folder = new File(folderPath);
listFilesRecursively(folder);
}

导出为docx或流文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//导出到指定路径的docx文件
File file = new File(templatePath + paperName + ".docx");
try (
InputStream is = new FileInputStream(file);
ServletOutputStream out = response.getOutputStream();
) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
FileUtils.del(file);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 导出为流文件:
// 1.直接返回response的二进制流,这种在response里的流不能携带其他任何信息
// response.setContentType("application/octet-stream");
// response.setHeader("Content-disposition", "attachment;filename=paper.docx");
//2.返回带信息的流,可以封装一个实体包括信息和转为base64格式的二进制流文件
try (
ByteArrayOutputStream os = new ByteArrayOutputStream();
) {
//用Base64转换二级制流导出错误数据,用于前端下载
ExcelUtil.exportTemplate(errorStudentList,"excel/exportStudentTemplate.xlsx", os);
String encodeToString = Base64.getEncoder().encodeToString(os.toByteArray());
} catch (Exception e) {
throw new RuntimeException("导入异常");
}

动态导出表头

image-20230918113035142


调用微信接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
public class WechatTest {

private static final String APP_ID = "xxx";
private static final String APP_SECRET = "xxx";
private static final String GROUP_NAME = "森林之父游戏群";
private static final String NEW_GROUP_NAME = "森林之子游戏群";

public static void main(String[] args) {
try {
// 获取访问凭证
String accessToken = getAccessToken();
if (accessToken == null) {
System.out.println("Failed to get access token.");
return;
}

// 获取微信群列表
String groupListJson = getWeChatGroupList(accessToken);
if (groupListJson == null) {
System.out.println("Failed to get WeChat group list.");
return;
}

// 根据群名称查找群ID
String groupId = findGroupIdByGroupName(groupListJson, GROUP_NAME);
if (groupId == null) {
System.out.println("Target group not found.");
return;
}

// 修改群名称
boolean success = updateGroupName(accessToken, groupId, NEW_GROUP_NAME);
if (success) {
System.out.println("Group name updated successfully.");
} else {
System.out.println("Failed to update group name.");
}
} catch (Exception e) {
e.printStackTrace();
}
}

private static String getAccessToken() throws Exception {
String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="
+ APP_ID + "&secret=" + APP_SECRET;

HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);

if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();

// 解析返回的 JSON 数据,获取访问凭证
// 这里仅作示例,假设返回的 JSON 数据中有一个字段为 "access_token"
// 实际情况中,请根据微信返回的 JSON 数据结构来解析访问凭证
int startIndex = response.indexOf("\"access_token\":\"") + 16;
int endIndex = response.indexOf("\",\"expires_in\"");
return response.substring(startIndex, endIndex);
} else {
System.out.println("Failed to get access token. Response code: " + connection.getResponseCode());
return null;
}
}

private static String getWeChatGroupList(String accessToken) throws Exception {
String url = "https://api.weixin.qq.com/cgi-bin/groups/get?access_token=" + accessToken;

HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);

if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();

return response.toString();
} else {
System.out.println("Failed to get WeChat group list. Response code: " + connection.getResponseCode());
return null;
}
}

private static String findGroupIdByGroupName(String groupListJson, String groupName) {
// 解析微信群列表的 JSON 数据,查找目标群的群ID
// 这里仅作示例,假设返回的 JSON 数据中有一个字段为 "groups",其中包含群列表
// 实际情况中,请根据微信返回的 JSON 数据结构来解析群列表和群ID

String groupId = "YOUR_GROUP_ID";
return groupId;
}

private static boolean updateGroupName(String accessToken, String groupId, String newGroupName) throws Exception {
String url = "https://api.weixin.qq.com/cgi-bin/groups/update?access_token=" + accessToken;

// 构建修改群名称的请求体
String requestBody = String.format("{\"group\":{\"id\":%s,\"name\":\"%s\"}}",
groupId, URLEncoder.encode(newGroupName, "UTF-8"));

HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
connection.setRequestMethod("POST");
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setDoOutput(true);
connection.getOutputStream().write(requestBody.getBytes("UTF-8"));

if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
StringBuilder response = new StringBuilder();
while ((line = reader.readLine()) != null) {
response.append(line);
}
reader.close();

// 解析返回的 JSON 数据,检查修改是否成功
// 这里仅作示例,假设返回的 JSON 数据中有一个字段为 "errcode",为 0 表示修改成功
// 实际情况中,请根据微信返回的 JSON 数据结构来解析修改结果
int errCode = 0;
if (errCode == 0) {
return true;
} else {
System.out.println("Failed to update group name. Error code: " + errCode);
return false;
}
} else {
System.out.println("Failed to update group name. Response code: " + connection.getResponseCode());
return false;
}
}
}

面试

项目中有没有用到AOP?

当时在后台管理系统中,使用了aop的环绕通知+切点表达式,这个表达式就是要找到要记录日志的方法,然后通过环绕通知的参数获取请求方法的参数,比如类信息、注解、请求方式的等,获取到这些参数以后,保存到数据库。