开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

看了同事写的代码,我竟然开始默默的模仿了。。。(一)

发表于 2021-11-11

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

背景

事情是这样的,目前我正在参与 XXXX 项目的搭建,需要与第三方对接接口。在对方的接口中存在几个异步通知,为了接口的安全性,需要对接口的参数进行验签处理。

为了方便大家对异步通知返回参数的处理,Z 同事提出要将该验签功能进行统一封装,到时候大家只需要关注自己的业务逻辑即可。

Z同事的解决方案

Z 同事选择的是“自定义参数解析器”的解决方案,接下来我们通过代码来了解一下。

自定义注解

在自定义注解中定义一个方法:是否启用验签功能,默认验签。

1
2
3
4
5
6
7
8
9
10
java复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.PARAMETER})
public @interface RsaVerify {

/**
* 是否启用验签功能,默认验签
*/
boolean verifySign() default true;
}

自定义方法参数解析器

创建自定义方法参数解析器 RsaVerifyArgumentResolver 实现 HandlerMethodArgumentResolver 接口,并实现里边的方法。

  1. supportsParameter:此方法用来判断本次请求的接口是否需要解析参数,如果需要返回 true,然后调用下面的 resolveArgument 方法,如果不需要返回 false。
  2. resolveArgument:真正的解析方法,将请求中的参数值解析为某种对象。
    • parameter 要解析的方法参数
    • mavContainer 当前请求的 ModelAndViewContainer(为请求提供对模型的访问)
    • webRequest 当前请求
    • WebDataBinderFactory 用于创建 WebDataBinder 的工厂
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
java复制代码@AllArgsConstructor
@Component
public class RsaVerifyArgumentResolver implements HandlerMethodArgumentResolver {

private final SecurityService securityService;

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RsaVerify.class);
}


@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
RsaVerify parameterAnnotation = parameter.getParameterAnnotation(RsaVerify.class);
if (!parameterAnnotation.verifySign()) {
return mavContainer.getModel();
}

//对参数进行处理并验签的逻辑
......

//返回处理后的实体类参数
return ObjectMapperFactory
.getDateTimeObjectMapper("yyyyMMddHHmmss")
.readValue(StringUtil.queryParamsToJson(sb.toString()), parameter.getParameterType());
}

}

创建配置类

创建配置类 PayTenantWebConfig 实现 WebMvcConfigurer 接口,将自定义的方法参数解析器加入到配置类中。

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
@AllArgsConstructor
public class PayTenantWebConfig implements WebMvcConfigurer {

private final RsaVerifyArgumentResolver rsaVerifyArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(rsaVerifyArgumentResolver);
}
}

这样我们就完成了自定义参数解析器解决方案的基础搭建,至于该怎么来使用它,我们将在下一节中进行讲解。如果你有不同的意见或者更好的idea,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

动态代理的实现原理,JDK Proxy 和 CGLib的区别

发表于 2021-11-11

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」

动态代理的常用实现方式是反射。反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及类对象中包含的属性及方法。

但动态代理不止有反射一种实现方式,例如,动态代理可以通过 CGLib 来实现,而 CGLib 是基于 ASM(一个 Java 字节码操作框架)而非反射实现的。简单来说,动态代理是一种行为方式,而反射或 ASM 只是它的一种实现手段而已。

JDK Proxy 和 CGLib 的区别主要体现在以下几个方面:

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;
  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新 JDK Proxy,例如 Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
  • JDK Proxy 是通过拦截器加反射的方式实现的;
  • JDK Proxy 只能代理继承接口的类;
  • JDK Proxy 实现和调用起来比较简单;
  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
  • CGLib 无需通过接口来实现,它是通过实现子类的方式来完成调用的。

分析

  1. JDK Proxy 和 CGLib 的使用及代码分析

JDK Proxy 动态代理实现

JDK Proxy 动态代理的实现无需引用第三方类,只需要实现 InvocationHandler 接口,重写 invoke() 方法即可,整个实现代码如下所示:

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
java复制代码import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
​
/**
 * JDK Proxy 相关示例
 */
public class ProxyExample {
    static interface Car {
        void running();
    }
​
    static class Bus implements Car {
        @Override
        public void running() {
            System.out.println("The bus is running.");
        }
    }
​
    static class Taxi implements Car {
        @Override
        public void running() {
            System.out.println("The taxi is running.");
        }
    }
​
    /**
     * JDK Proxy
     */
    static class JDKProxy implements InvocationHandler {
        private Object target; // 代理对象
​
        // 获取到代理对象
        public Object getInstance(Object target) {
            this.target = target;
            // 取得代理对象
            return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                    target.getClass().getInterfaces(), this);
        }
​
        /**
         * 执行代理方法
         * @param proxy  代理对象
         * @param method 代理方法
         * @param args   方法的参数
         * @return
         * @throws InvocationTargetException
         * @throws IllegalAccessException
         */
        @Override
        public Object invoke(Object proxy, Method method, Object[] args)
                throws InvocationTargetException, IllegalAccessException {
            System.out.println("动态代理之前的业务处理.");
            Object result = method.invoke(target, args); // 执行调用方法(此方法执行前后,可以进行相关业务处理)
            return result;
        }
    }
​
    public static void main(String[] args) {
        // 执行 JDK Proxy
        JDKProxy jdkProxy = new JDKProxy();
        Car carInstance = (Car) jdkProxy.getInstance(new Taxi());
        carInstance.running();
  }

以上程序的执行结果是:

1
2
erlang复制代码动态代理之前的业务处理.
The taxi is running.

可以看出 JDK Proxy 实现动态代理的核心是实现 Invocation 接口,我们查看 Invocation 的源码,会发现里面其实只有一个 invoke() 方法,源码如下:

1
2
3
4
java复制代码public interface InvocationHandler {
  public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable;
}

这是因为在动态代理中有一个重要的角色也就是代理器,它用于统一管理被代理的对象,显然 InvocationHandler 就是这个代理器,而 invoke() 方法则是触发代理的执行方法,我们通过实现 Invocation 接口来拥有动态代理的能力。

CGLib 的实现

在使用 CGLib 之前,我们要先在项目中引入 CGLib 框架,在 pom.xml 中添加如下配置:

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

CGLib 实现代码如下:

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
java复制代码import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
​
import java.lang.reflect.Method;
​
public class CGLibExample {
​
    static class Car {
        public void running() {
            System.out.println("The car is running.");
        }
    }
​
    /**
     * CGLib 代理类
     */
    static class CGLibProxy implements MethodInterceptor {
        private Object target; // 代理对象
​
        public Object getInstance(Object target) {
            this.target = target;
            Enhancer enhancer = new Enhancer();
            // 设置父类为实例类
            enhancer.setSuperclass(this.target.getClass());
            // 回调方法
            enhancer.setCallback(this);
            // 创建代理对象
            return enhancer.create();
        }
​
        @Override
        public Object intercept(Object o, Method method,
                                Object[] objects, MethodProxy methodProxy) throws Throwable {
            System.out.println("方法调用前业务处理.");
            Object result = methodProxy.invokeSuper(o, objects); // 执行方法调用
            return result;
        }
    }
​
    // 执行 CGLib 的方法调用
    public static void main(String[] args) {
        // 创建 CGLib 代理类
        CGLibProxy proxy = new CGLibProxy();
        // 初始化代理对象
        Car car = (Car) proxy.getInstance(new Car());
        // 执行方法
        car.running();
  }

以上程序的执行结果是:

1
2
erlang复制代码方法调用前业务处理.
The car is running.

可以看出 CGLib 和 JDK Proxy 的实现代码比较类似,都是通过实现代理器的接口,再调用某一个方法完成动态代理的,唯一不同的是,CGLib 在初始化被代理类时,是通过 Enhancer 对象把代理对象设置为被代理类的子类来实现动态代理的。因此被代理类不能被关键字 final 修饰,如果被 final 修饰,再使用 Enhancer 设置父类时会报错,动态代理的构建会失败。

  1. Lombok 原理分析

在开始讲 Lombok 的原理之前,我们先来简单地介绍一下 Lombok,它属于 Java 的一个热门工具类,使用它可以有效的解决代码工程中那些繁琐又重复的代码,如 Setter、Getter、toString、equals 和 hashCode 等等,向这种方法都可以使用 Lombok 注解来完成。

例如,我们使用比较多的 Setter 和 Getter 方法,在没有使用 Lombok 之前,代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Person {
    private Integer id;
    private String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

在使用 Lombok 之后,代码是这样的:

1
2
3
4
5
java复制代码@Data
public class Person {
    private Integer id;
    private String name;
}

可以看出 Lombok 让代码简单和优雅了很多。

小贴士:如果在项目中使用了 Lombok 的 Getter 和 Setter 注解,那么想要在编码阶段成功调用对象的 set 或 get 方法,我们需要在 IDE 中安装 Lombok 插件才行,比如 Idea 的插件如下图所示:

image.png

Lombok 的实现和反射没有任何关系,前面说了反射是程序在运行期的一种自省(introspect)能力,而 Lombok 的实现是在编译期就完成了,为什么这么说呢?

回到刚才 Setter/Getter 的方法,打开 Person 的编译类就会发现,使用了 Lombok 的 @Data 注解后的源码如下:

image.png
可以看出 Lombok 是在编译期就为我们生成了对应的字节码。

其实 Lombok 是基于 Java 1.6 实现的 JSR 269: Pluggable Annotation Processing API 来实现的,也就是通过编译期自定义注解处理器来实现的,它的执行步骤如下:

image.png
从流程图中可以看出,在编译期阶段,当 Java 源码被抽象成语法树(AST)之后,Lombok 会根据自己的注解处理器动态修改 AST,增加新的代码(节点),在这一切执行之后就生成了最终的字节码(.class)文件,这就是 Lombok 的执行原理。

3.动态代理知识点扩充

当面试官问动态代理的时候,经常会问到它和静态代理的区别?静态代理其实就是事先写好代理类,可以手工编写也可以使用工具生成,但它的缺点是每个业务类都要对应一个代理类,特别不灵活也不方便,于是就有了动态代理。

动态代理的常见使用场景有 RPC 框架的封装、AOP(面向切面编程)的实现、JDBC 的连接等。

Spring 框架中同时使用了两种动态代理 JDK Proxy 和 CGLib,当 Bean 实现了接口时,Spring 就会使用 JDK Proxy,在没有实现接口时就会使用 CGLib,我们也可以在配置中指定强制使用 CGLib,只需要在 Spring 配置中添加 <aop:aspectj-autoproxy proxy-target-class="true"/>即可。

小结

上面介绍了JDK Proxy 和 CGLib 的区别,JDK Proxy 是 Java 语言内置的动态代理,必须要通过实现接口的方式来代理相关的类,而 CGLib 是第三方提供的基于 ASM 的高效动态代理类,它通过实现被代理类的子类来实现动态代理的功能,因此被代理的类不能使用 final 修饰。

除了 JDK Proxy 和 CGLib 之外,还介绍了 Java 中常用的工具类 Lombok 的实现原理,它其实和反射是没有任何关系的;最后讲了动态代理的使用场景以及 Spring 中动态代理的实现方式。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java基础学习10之链表 链表 链表基本的结构 链表实现结

发表于 2021-11-11

链表

「这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战」。

关于作者

  • 作者介绍

🍓 博客主页:作者主页

🍓 简介:JAVA领域优质创作者🥇、一名在校大三学生🎓、在校期间参加各种省赛、国赛,斩获一系列荣誉🏆。

🍓 关注我:关注我学习资料、文档下载统统都有,每日定时更新文章,励志做一名JAVA资深程序猿👨‍💻。


链表是一种基本的数据结构,但好似对于数据结构的部分,强调一下几点:

  1. 在整个Java开发领域之中,没有一本书去真正讲解数据结构的书,只能去看C语言的数据结构:
  2. 在所有开发之中,都会存在数据结构的身影,可以这样去解释:数据结构的精通与否,完全决定于以后。
  3. 数据结构的核心:引用数据类型操作。

链表实际上可以理解为遗传数据,或者按照专业性的说法,可以理解为动态的对象数组,对象数组的最大优点:是表示“多”的概念,例如:多个雇员。但是传统的对象数组有一个最大的问题在于,里面保存的数据长度是固定的。思考:如果现在想要扩大一个对象数组的范围?

建立一个新的对象数组,而后将原本的内容拷贝到新的数组之中,再改变原数组的引用方式。

1
2
3
4
5
java复制代码public class TestLinkDemo{
public static void main(String args[]){
Object ob[] = new Object [3];
}
}

但是再实际的开发之中,要面临的一个问题是:数组是一个定长的线性结构,也就是说虽然以上代码可以满足于存放多个内容,但是一旦我们呢的内容不足或者是内容过多,可能会导致资源的浪费。要想解决此类问题最好的做法就是不定义一个固定长度的数组 ,有多少数据就保存多少数据。

链表基本的结构

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
java复制代码class Node{//因为只有Node类才可以在保存数据的同时设置数据
private Object data;//真正要保存的数据
private Node next;//定义下一个节点
public Node(Object data){
this.data = data;
}
public void setData(Object data){
this.data = data;
}
public Object getData(){
return this.data;
}
public void setNext(Node next){
this.next = next;
}
public Node getNext(){
return this.next;
}
}
public class TestLinkDemo{
public static void main(String args[]){
//1.封装几个节点
Node root = new Node("火车头");
Node n1 = new Node("车厢1");
Node n2 = new Node("车厢2");
Node n3 = new Node("车厢3");
//2.设置节点关系
root.setNext(n1);
n1.setNext(n2);
n2.setNext(n3);
//3.输出链表
print(root);
}
public static void print(Node node){
if(node != null){//表示当前是有节点的
System.out.println(node.getData());
print(node.getNext());//继续向下取出
}

}
}

在整个链表的实现过程中,Node类的作用:保存数据和保存下一个节点,但是我们发现客户端需要自己来进行节点的创建操作以及关系的配置。所谓的链表就是需要有一个单独的类,假设叫Link,通过Link类来实现Node的数据保存和关系处理。

链表实现结构说明

通过之前的分析,可以发现链表的最大作用类就是Node,但是以上程序都是由用户自己去匹配节点关系的,但是这些节点的匹配工作不应该由用户完成,应该由一个程序专门去负责。

那么专门负责几点操作的类,就成为链表类——Link,负责处理几点关系,而用户不用关心节点的问题,只需关心Link的处理操作即可。

image-20210809111013328

真实开发——标准过程

image-20210809111231614

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码class Link{//负责对链表的操作
//将Node定义内部类,表示Node类只能为Link类提供服务
private class Node{//负责数据与节点的关系匹配
private Object data;//真正要保存的数据
private Node next;//定义下一个节点
public Node(Object data){
this.data = data;
}
public void setData(Object data){
this.data = data;
}
public Object getData(){
return this.data;
}
}
//以下为Link类
}
public class TestLinkDemo{
public static void main(String args[]){

}
}

增加链表数据—public void add(数据)

通过上面程序的分析,可以发下,对于链表的实现,Node类是整个操作的关键,但是首先来研究一下之前程序的问题:Node是一个单独的类是可以被用户直接使用的,但是这个类由用户直接去使用,没有任何意义,即:这个类有用,但不能让用户去使用,让Link类去使用。

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
java复制代码class Link{//负责对链表的操作
//将Node定义内部类,表示Node类只能为Link类提供服务
private class Node{//负责数据与节点的关系匹配
private Object data;//真正要保存的数据
private Node next;//定义下一个节点
public Node(Object data){
this.data = data;
}
public void setData(Object data){
this.data = data;
}
public Object getData(){
return this.data;
}
public void setNext(Node next){
this.next = next;
}
public Node getNext(){
return this.next;
}
//第一次调用:this = Link.root
//第二次调用:this = Link.root.next
//第三次调用:this = Link.root.next.next
public void addNode(Node newNode){//处理节点关系
if(this.next == null){ //当前节点下一个为空
this.next = newNode;
}else{//当前节点的下一个不为空
this.next.addNode(newNode);
}
}
public void nodePrint(){
System.out.println(this.getData());
if (this.getNext()==null)
{
return;
}else{
this.getNext().nodePrint();
}
}
}
//以下为Link类------------------------------------------------
private Node root; //属于根节点,没有根节点就无法数据的保存
//增加数据
public void add(Object data){
if(data == null){//人为追加规定,不允许存放null值
return ;//结束方法调用
}
//如果要想进行数据的保存,那么必须将数据封装在Node类里面
//如果没有封装,则无法确认好节点的先后顺序
Node newNode = new Node(data);
if(this.root == null){
this.root = newNode;//第一个节点设置为根节点
}else{//根节点存在了
this.root.addNode(newNode);
}
}
//输出数据
public void print(){
if (this.root == null){
return;
}
System.out.println(this.root.getData());
if (this.root.getNext()==null){
return;
}
else{
this.root.getNext().nodePrint();
}
}
}
public class TestLinkDemo1{
public static void main(String args[]){
Link link = new Link();
link.add("Hello");
link.add("World");
link.print();
}
}

增加多个数据—public void addAll(数据数组)

1
2
3
4
5
java复制代码public void addAll(String date[]){
for(int x = 0;x<date.length;x++){
this.add(date[x]);
}
}
2.5 统计数据个数—public int size()

在Link类中定义

1
java复制代码private int count;//统计个数

在增加数据的最后一行添加count++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void add(Object data){
if(data == null){//人为追加规定,不允许存放null值
return ;//结束方法调用
}
//如果要想进行数据的保存,那么必须将数据封装在Node类里面
//如果没有封装,则无法确认好节点的先后顺序
Node newNode = new Node(data);
if(this.root == null){
this.root = newNode;//第一个节点设置为根节点
}else{//根节点存在了
this.root.addNode(newNode);
}
count++;
}

链表数据转换为对象数组—public Object[] toArray()

对于链表的这种数据结构,最为关键的是两个操作:删除和取得全部数据。

在Link类中定义一个操作数组的脚标:

1
java复制代码private int foot = 0;

要把数据保存的数组,Link类和Node类都需要使用,那么可以在Link类中定义返回数组,必须以属性的形式出现,只有这样,Node类才可以访问这个数组并进行操作。

1
java复制代码private Object [] retData ; //返回类型

在Link类中增加toArray()方法:

1
2
3
4
5
6
7
8
9
10
java复制代码//链表数据转换为对象数组
public Object[] toArray(){
if(this.count == 0){
return null;
}
this.retData = new Object[this.count];
this.root.toArrayNode();
this.foot = 0;//下表清零操作
return this.retData;
}

在Node中增加toArrayNode()方法:

1
2
3
4
5
6
java复制代码public void toArrayNode(){
Link.this.retData[Link.this.foot++] = this.data;
if(this.next != null){
this.next.toArrayNode();
}
}

不过按照以上的方式进行开发,每一次调用toArray()方法,都要重复的进行数据的的遍历,如果在数据没有修改的情况下,这种做法是一种低效的做法,最好的方法是增加一个修改标记,如果发现数据增加了或删除的话,表示要重新遍历数据。

image-20210813171359533

链表查询数据—public boolean contains(查找对象)

现在如果想查询某个数据是否存在,那么基本的操作原理:逐个盘查,盘查的具体对象应该交给Node去完成,前提是:有数据存在。

在Link类之中,增加查询操作:

1
2
3
4
5
6
java复制代码//查找链表的指定数据是否存在
public boolean contains(Object search){
if(search == null && this.root == null)
return false;
return this.root.containsNode(search);
}

在Node类中,完成具体查询,查询流程为:

​ 判断当前节点的内容是否满足于查询内容,如果满足返回ture;

​ 如果当前节点内容不满足,则向后继续查询,如果没有后续节点了,则返回false。

1
2
3
4
5
6
7
8
9
10
java复制代码public boolean containsNode(Object search){
if(search.equals(this.data))
return true;
else{
if(this.next != null){//判断下一个节点是否为空
return this.next.containsNode(search);
}
return false;
}
}

根据索引取得数据—public Object get(int index)

在一个链表之中会有多个节点保存数据,现在要求可以取得指定节点的数据。但是在进行这一操作的过程之中,有一个小问题:如果要取得数据的索引超过了数据的保存个数,那么是无法取得的。

​ 在Link类之中增加一个get(int index)方法:

1
2
3
4
5
6
7
8
java复制代码//根据索引取得数据
public Object get(int index){
if(index >= this.count){
return null;
}
this.foot = 0;
return this.root.getNode(index);
}

在Node类之中增加一个getNdoe(int index)方法:

1
2
3
4
5
6
7
8
9
java复制代码//第一次this == Link.root
//第二次this == Link.root.next
public Object getNode(int index){
if(Link.this.foot++ == index){
return this.data;
}else{
return this.next.getNode(index);
}
}

修改指定索引数据—public void set(int index,Object newData)

如果修改数据只需要进行数据的替换。

在Link类之中增加一个set(int index,Object newData)方法:

1
2
3
4
5
6
7
8
java复制代码//修改指定索引数据
public void set(int index,Object newData){
if(index >= this.count){
return ;
}
this.foot = 0;
this.root.setNode(index,newData);
}

在Node类之中增加一个getNode(int index)方法:

1
2
3
4
5
6
7
8
9
java复制代码public void setNode(int index, Object newData){
if(Link.this.foot ++ == index){//索引相同
this.data = newData;
}else{
if(this.next != null){
this.next.setNode(index,newData);
}
}
}

删除数据—public void remove(数据)

对于链表之中的内容,之前完成的是增加操作和查询操作,但是从链表之中也会存在删除数据的操作,可是删除数据的操作要分为两种情况讨论:

​ 情况一:删除的数据不是根节点,待删节点的上一个next指向待删节点的next。

​ 所有的处理操作应该交给Node进行处理。

​ 情况二:删除的数据是根节点,下一个节点保存为跟节点。

​ 如果删除的是根节点,意味着Link中的根节点的保存需要发生变化,该操作主要在Link中处理。

image-20210813174659850

在Link中增加一个删除remove(Object data)方法

1
2
3
4
5
6
7
8
9
10
11
java复制代码//删除数据
public void remove(Object data){
if(this.contains(data)){//如果数据存在则进行数据处理
if(this.root.data.equals(data)){//首先需要判断要删除的数据是否为根节点数据
this.root = this.root.next;//根节点变为下一个节点
}else{//不是根节点
this.root.next.removeNode(this.root,data);
}
this.count --;
}
}

在Node类之中增加一个removeNode(Node previous, Object data)方法:

1
2
3
4
5
6
7
8
9
java复制代码//第一次:this = Link.root.next、previous= Link.root;
//第二次:this = Link.root.next.next、previous= Link.root.next;
public void removeNode(Node previous, Object data){
if(this.data.equals(data)){//当前节点为要删除的节点
previous.next = this.next;
}else{
this.next.removeNode(this,data);
}
}

Link链表类模板

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
java复制代码class Link{//负责链表的操作
//将Node定义内部类,表示Node类只能为Link类提供服务
private class Node{//负责数据与节点的关系匹配
private Object data;//真正要保存的数据
private Node next;//定义下一个节点
public Node(Object data){
this.data = data;
}
public void setData(Object data){
this.data = data;
}
public Object getData(){
return this.data;
}
public void setNext(Node next){
this.next = next;
}
public Node getNext(){
return this.next;
}
//第一次调用:this = Link.root
//第二次调用:this = Link.root.next
//第三次调用:this = Link.root.next.next
public void addNode(Node newNode){//处理节点关系
if(this.next == null){ //当前节点下一个为空
this.next = newNode;
}else{//当前节点的下一个不为空
this.next.addNode(newNode);
}
}
public void nodePrint(){
System.out.println(this.getData());
if (this.getNext()==null)
{
return;
}else{
this.getNext().nodePrint();
}
}
public void toArrayNode(){
Link.this.retData[Link.this.foot++] = this.data;
if(this.next != null){
this.next.toArrayNode();
}
}
public boolean containsNode(Object search){
if(search.equals(this.data))
return true;
else{
if(this.next != null){//判断下一个节点是否为空
return this.next.containsNode(search);
}
return false;
}
}
//第一次this == Link.root
//第二次this == Link.root.next
public Object getNode(int index){
if(Link.this.foot++ == index){
return this.data;
}else{
return this.next.getNode(index);
}
}

public void setNode(int index, Object newData){
if(Link.this.foot ++ == index){//索引相同
this.data = newData;
}else{
if(this.next != null){
this.next.setNode(index,newData);
}
}
}
//第一次:this = Link.root.next、previous= Link.root;
//第二次:this = Link.root.next.next、previous= Link.root.next;
public void removeNode(Node previous, Object data){
if(this.data.equals(data)){//当前节点为要删除的节点
previous.next = this.next;
}else{
this.next.removeNode(this,data);
}
}
}
//以下为Link类------------------------------------------------
private Object [] retData ; //返回类型
private int foot = 0;//操作下标
private int count;//统计个数
private Node root; //属于根节点,没有根节点就无法数据的保存
//增加数据
public void add(Object data){
if(data == null){//人为追加规定,不允许存放null值
return ;//结束方法调用
}
//如果要想进行数据的保存,那么必须将数据封装在Node类里面
//如果没有封装,则无法确认好节点的先后顺序
Node newNode = new Node(data);
if(this.root == null){
this.root = newNode;//第一个节点设置为根节点
}else{//根节点存在了
this.root.addNode(newNode);
}
count++;
}
//判断链表是否为空
public boolean isEmpty(){
this.count=0;
return false;
}

//增加多个数据
public void addAll(String date[]){
for(int x = 0;x<date.length;x++){
this.add(date[x]);
}
}
public int size(){
return this.count;
}

//输出数据
public void print(){
if (this.root == null){
return;
}
System.out.println(this.root.getData());
if (this.root.getNext()==null){
return;
}
else{
this.root.getNext().nodePrint();
}
}
//链表数据转换为对象数组
public Object[] toArray(){
if(this.count == 0){
return null;
}
this.retData = new Object[this.count];
this.root.toArrayNode();
this.foot = 0;//下表清零操作
return this.retData;
}
//查找链表的指定数据是否存在
public boolean contains(Object search){
if(search == null && this.root == null)
return false;
return this.root.containsNode(search);
}
//根据索引取得数据
public Object get(int index){
if(index >= this.count){
return null;
}
this.foot = 0;
return this.root.getNode(index);
}

//修改指定索引数据
public void set(int index,Object newData){
if(index >= this.count){
return ;
}
this.foot = 0;
this.root.setNode(index,newData);
}
//删除数据
public void remove(Object data){
if(this.contains(data)){//如果数据存在则进行数据处理
if(this.root.data.equals(data)){//首先需要判断要删除的数据是否为根节点数据
this.root = this.root.next;//根节点变为下一个节点
}else{//不是根节点
this.root.next.removeNode(this.root,data);
}
this.count --;
}
}
}

综合案例

建立宠物商店,包括销售宠物上架、下架、关键字查询,要求程序的关系即可,对于宠物的信息只要有三项:名字、年龄、颜色。

对应的关系:一个宠物商店有多种宠物,如果按照表设计应该属于一对多关系映射,但是现在问题,一方是宠物商店,多方是宠物,但是宠物又分为猫、狗、猪、驴、鱼等。

image-20210813223132675

1、建立宠物标准

1
2
3
4
5
java复制代码interface Pet{//定义宠物
public String getName();
public String getColor();
public int getAge();
}

2、对于宠物商店,只关注于宠物的标准,而不关心具体是那种宠物

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码class PetShop{
private Link pets = new Link();//开辟一个链表,保存宠物信息
public void add(Pet pet){//上架宠物
this.pets.add(pet);
}
public void delete(Pet pet){//下架宠物
this.pets.delete(pet);
}
public Link getPets(){ //得到全部宠物
return this.pets;
}
public Link search(String keyword){//关键字查找
Link result = new Link();
Object [] data = this.pets.toArray();
for(int i = 0; i < data.length ; i++){
Pet pet = (Pet) data[i];
if(pet.getName().contains(keyword) || pet.getColor().contains(keyword)){
result.add(pet); //满足查询结果
}
}
return result;
}
}

3、定义宠物狗

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
java复制代码class Dog implements Pet{
private String name;
private String color;
private int age;
public String getName(){
return this.name;
}
public String getColor(){
return this.color;
}
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(this == obj){
return false;
}
if(!(obj instanceof Dog)){
return false;
}
Dog pet = (Dog) obj;
return this.name.equals(pet.name) && this.color.equals(pet.color) && this.age.equals(pet.age);
}
public int getAge(){
return this.age;
}
public Dog(String name, String color, int age){
this.name = name ;
this.color = color;
this.age = age;
}
public String toString(){
return "【狗】名字 = " + this.name +
"颜色 = " + this.color +
"年龄 = " +this.age;
}
}

定义宠物猫

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
java复制代码class Cat implements Pet{
private String name;
private String color;
private int age;
public String getName(){
return this.name;
}
public String getColor(){
return this.color;
}
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(this == obj){
return false;
}
if(!(obj instanceof Cat)){
return false;
}
Cat pet = (Cat) obj;
return this.name.equals(pet.name) && this.color.equals(pet.color) && this.age.equals(pet.age);
}
public int getAge(){
return this.age;
}
public Cat(String name, String color, int age){
this.name = name ;
this.color = color;
this.age = age;
}
public String toString(){
return "【猫】名字 = " + this.name +
"颜色 = " + this.color +
"年龄 = " +this.age;
}
}

5、测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Pets{
public static void main(String args[]){
PetShop ps = new PetShop();
ps.add(new Dog("小黑","黑色",1));
ps.add(new Dog("金毛","金色",2));
ps.add(new Dog("拉布拉多","白色",3));
ps.add(new Dog("萨摩耶","白色",2));
ps.add(new Cat("加菲猫","黄色",3));
ps.add(new Dog("波斯猫","金色",4));
ps.delete(new Dog("萨摩耶","白色",2));
Link all = ps.search("白");
Object [] data = all.toArray();
for(int i = 0 ; i < data.length ; i++){
System.out.println(data[i]);
}

}
}

6、完整代码

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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
java复制代码class Link{//负责链表的操作
//将Node定义内部类,表示Node类只能为Link类提供服务
private class Node{//负责数据与节点的关系匹配
private Object data;//真正要保存的数据
private Node next;//定义下一个节点
public Node(Object data){
this.data = data;
}
public void setData(Object data){
this.data = data;
}
public Object getData(){
return this.data;
}
public void setNext(Node next){
this.next = next;
}
public Node getNext(){
return this.next;
}
//第一次调用:this = Link.root
//第二次调用:this = Link.root.next
//第三次调用:this = Link.root.next.next
public void addNode(Node newNode){//处理节点关系
if(this.next == null){ //当前节点下一个为空
this.next = newNode;
}else{//当前节点的下一个不为空
this.next.addNode(newNode);
}
}
public void nodePrint(){
System.out.println(this.getData());
if (this.getNext()==null)
{
return;
}else{
this.getNext().nodePrint();
}
}
public void toArrayNode(){
Link.this.retData[Link.this.foot++] = this.data;
if(this.next != null){
this.next.toArrayNode();
}
}
public boolean containsNode(Object search){
if(search.equals(this.data))
return true;
else{
if(this.next != null){//判断下一个节点是否为空
return this.next.containsNode(search);
}
return false;
}
}
//第一次this == Link.root
//第二次this == Link.root.next
public Object getNode(int index){
if(Link.this.foot++ == index){
return this.data;
}else{
return this.next.getNode(index);
}
}

public void setNode(int index, Object newData){
if(Link.this.foot ++ == index){//索引相同
this.data = newData;
}else{
if(this.next != null){
this.next.setNode(index,newData);
}
}
}
//第一次:this = Link.root.next、previous= Link.root;
//第二次:this = Link.root.next.next、previous= Link.root.next;
public void removeNode(Node previous, Object data){
if(this.data.equals(data)){//当前节点为要删除的节点
previous.next = this.next;
}else{
this.next.removeNode(this,data);
}
}
}
//以下为Link类------------------------------------------------
private Object [] retData ; //返回类型
private int foot = 0;//操作下标
private int count;//统计个数
private Node root; //属于根节点,没有根节点就无法数据的保存
//增加数据
public void add(Object data){
if(data == null){//人为追加规定,不允许存放null值
return ;//结束方法调用
}
//如果要想进行数据的保存,那么必须将数据封装在Node类里面
//如果没有封装,则无法确认好节点的先后顺序
Node newNode = new Node(data);
if(this.root == null){
this.root = newNode;//第一个节点设置为根节点
}else{//根节点存在了
this.root.addNode(newNode);
}
count++;
}
//判断链表是否为空
public boolean isEmpty(){
this.count=0;
return false;
}

//增加多个数据
public void addAll(String date[]){
for(int x = 0;x<date.length;x++){
this.add(date[x]);
}
}
public int size(){
return this.count;
}

//输出数据
public void print(){
if (this.root == null){
return;
}
System.out.println(this.root.getData());
if (this.root.getNext()==null){
return;
}
else{
this.root.getNext().nodePrint();
}
}
//链表数据转换为对象数组
public Object[] toArray(){
if(this.count == 0){
return null;
}
this.retData = new Object[this.count];
this.root.toArrayNode();
this.foot = 0;//下表清零操作
return this.retData;
}
//查找链表的指定数据是否存在
public boolean contains(Object search){
if(search == null && this.root == null)
return false;
return this.root.containsNode(search);
}
//根据索引取得数据
public Object get(int index){
if(index >= this.count){
return null;
}
this.foot = 0;
return this.root.getNode(index);
}

//修改指定索引数据
public void set(int index,Object newData){
if(index >= this.count){
return ;
}
this.foot = 0;
this.root.setNode(index,newData);
}
//删除数据
public void remove(Object data){
if(this.contains(data)){//如果数据存在则进行数据处理
if(this.root.data.equals(data)){//首先需要判断要删除的数据是否为根节点数据
this.root = this.root.next;//根节点变为下一个节点
}else{//不是根节点
this.root.next.removeNode(this.root,data);
}
this.count --;
}
}
}
interface Pet{//定义宠物
public String getName();
public String getColor();
public int getAge();
}
class PetShop{
private Link pets = new Link();//开辟一个链表,保存宠物信息
public void add(Pet pet){//上架宠物
this.pets.add(pet);
}
public void delete(Pet pet){//下架宠物
this.pets.remove(pet);
}
public Link getPets(){ //得到全部宠物
return this.pets;
}
public Link search(String keyword){//关键字查找
Link result = new Link();
Object [] data = this.pets.toArray();
for(int i = 0; i < data.length ; i++){
Pet pet = (Pet) data[i];
if(pet.getName().contains(keyword) || pet.getColor().contains(keyword)){
result.add(pet); //满足查询结果
}
}
return result;
}
}
class Dog implements Pet{
private String name;
private String color;
private int age;
public String getName(){
return this.name;
}
public String getColor(){
return this.color;
}
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(this == obj){
return false;
}
if(!(obj instanceof Dog)){
return false;
}
Dog pet = (Dog) obj;
return this.name.equals(pet.name) && this.color.equals(pet.color) && this.age == pet.age;
}
public int getAge(){
return this.age;
}
public Dog(String name, String color, int age){
this.name = name ;
this.color = color;
this.age = age;
}
public String toString(){
return "【狗】名字 = " + this.name +
",颜色 = " + this.color +
",年龄 = " +this.age;
}
}

class Cat implements Pet{
private String name;
private String color;
private int age;
public String getName(){
return this.name;
}
public String getColor(){
return this.color;
}
public boolean equals(Object obj){
if(obj == null){
return false;
}
if(this == obj){
return false;
}
if(!(obj instanceof Cat)){
return false;
}
Cat pet = (Cat) obj;
return this.name.equals(pet.name) && this.color.equals(pet.color) && this.age == pet.age;
}
public int getAge(){
return this.age;
}
public Cat(String name, String color, int age){
this.name = name ;
this.color = color;
this.age = age;
}
public String toString(){
return "【猫】名字 = " + this.name +
",颜色 = " + this.color +
",年龄 = " +this.age;
}
}
public class Pets{
public static void main(String args[]){
PetShop ps = new PetShop();
ps.add(new Dog("小黑","黑色",1));
ps.add(new Dog("金毛","金色",2));
ps.add(new Dog("拉布拉多","白色",3));
ps.add(new Dog("萨摩耶","白色",2));
ps.add(new Cat("加菲猫","黄色",3));
ps.add(new Dog("波斯猫","金色",4));
ps.delete(new Dog("萨摩耶","白色",2));
Link all = ps.search("白");
Object [] data = all.toArray();
for(int i = 0 ; i < data.length ; i++){
System.out.println(data[i]);
}

}
}

image-20210813230334734

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Security 跨域与CORS

发表于 2021-11-11

「这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战」。

前言

一、认识跨域

跨域是一种浏览器同源安全策略,即浏览器单方面限制脚本的跨域访问。

怎样会造成跨域?

当前页面URL和请求的URL首部不同则会造成跨域。通俗点讲就是两个URL地址,端口之前部分,只要有一点不同即发生跨域。

  • 在http://a.baidu.com下访问https://a.baidu.com资源会形成协议跨域。
  • 在a.baidu.com下访问b.baidu.com资源会形成主机跨域。
  • 在a.baidu.com:80下访问a.baidu.com:8080资源会形成端口跨域。

二、解决跨域的常见方式

1. JSONP

由于浏览器允许一些带有src属性的标签跨域,常见的有iframe、script、img等,所以JSONP利用script标签可以实习跨域。

实现思路

  • 前端通过script标签请求,并在callback中指定返回的包装实体名称为jsonp(可以自定义)
1
javascript复制代码<script src="http://aaa.com/getusers?callback=jsonp"></script>
  • 后端将返回结果包装成所需数据格式
1
2
3
4
5
6
7
8
json复制代码jsonp({
"error":200,
"message":"请求成功",
"data":[{
"username":"张三",
"age":20
}]
})

总结:JSONP实现起来很简单,但是只支持GET请求跨域,存在较大的局限性

2.CORS

CORS(Cross-Origin Resource Sharing) 的规范中有一组新增的HTTP首部字段,允许服务器声明其提供的资源允许哪些站点跨域使用。

注意:CORS不支持IE8以下版本的浏览器。

大多数浏览器中即便是跨域请求,请求依然是会正常发送到服务端,服务端接收并处理,只是返回到浏览器的信息被浏览器拦截屏蔽了。这样会对服务器造成不必要的资源浪费。

在CORS的规范中则避免了这个问题:

  • 浏览器在请求时会先发一个请求方法为OPTIONS的预检请求,用于确认是否允许跨域,只有服务端允许,才会发出实际请求。
  • 预检请求允许服务端通知浏览器跨域携带身份凭证(如cookie)

常见首部字段

CORS新增的HTTP首部字段由服务器控制,下面介绍几个常用首部字段:

  • Access-Control-Allow-Origin
    • 设置允许哪些站点跨域请求,使用URL首部匹配原则。
    • 设置为 * 表示允许所有网站请求
    注意:
    • 当需要浏览器请求携带凭证的时候,不允许设置为*****
      • 设置了具体站点信息,Vary需要携带Origin属性,因为服务器对不同的域会返回不同的内容:
1
2
java复制代码Access-Control-Allow-Origin: http://aaa.com
Vary: Accept-Encoding,Origin
  • Access-Control-Allow-Methods
    • 仅在预检请求的响应中指定有效
    • 表明服务器允许请求的HTTP方法
    • 多个用逗号隔开
  • Access-Control-Allow-Headers
    • 仅在预检请求的响应中指定有效
    • 表明服务器允许携带的首部字段
    • 多个用逗号隔开
  • Access-Control-Max-Age
    • 指明本次预检请求的有效期
    • 有效期内无需再次发起请求
  • Access-Control-Allow-Credentials
    • 为true时,通知浏览器接下来的正式请求带上用户凭证信息(cookie等),服务器也可以使用Set-Cookie向用户浏览器写入新的cookie。
    • 此时Access-Control-Allow-Origin不能设置为 *

总结:在使用CORS时,通常有以下两种访问控制场景

简单请求

  • 不携带自定义请求头信息的GET请求、HEAD请求
  • Content-Type为application/x-www-form-urlencoded、multipart/form-data或 text/plain的POST请求

请求时,会在请求头中自动添加一个Origin属性,值为当前页面URL首部。服务端接收到请求,返回信息。如果返回信息中存在跨域访问控制属性,浏览器会根据这些属性值判断是否被允许,如果允许,则跨域成功。 所以只需要后端在返回的响应头中添加 Access-Control-Allow-Origin 字段并填入允许跨域访问的站点即可。

预检请求(非简单请求)

预检请求不同于简单请求,它会发送一个 OPTIONS 请求到目标站点,以查明该请求是否安全,防止请求对目标站点的数据造成破坏。

三、Spring Security启用CORS支持

Spring Security对CORS提供了非常好的支持,只需在配置器中启用CORS支持,并编写一 个CORS配置源即可。

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
java复制代码@Override
protected void configure(HttpSecurity http) throws Exception {
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);
http.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/api/**").hasRole("USER")
.antMatchers("/app/api/**", "/captcha.jpg").permitAll()
.anyRequest()
.authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
// 指定处理登录请求的路径,修改请求的路径,默认为/login
.loginProcessingUrl("/mylogin").permitAll()
.csrf().disable()
.cors();
}

@Bean
CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration corsConfiguration = new CorsConfiguration();
//允许从百度站点跨域
corsConfiguration.setAllowedOrigins(Arrays.asList("https://www.baidu.com"));
//允许GET和POST方法
corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST"));
//允许携带凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有URL生效
source.registerCorsConfiguration("/**",corsConfiguration);
return source;
}

注意:
CorsConfigurationSource为这个包下的
import org.springframework.web.cors.CorsConfigurationSource;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ(4) 消息确认机制详解

发表于 2021-11-11

前言

在前几篇文章中,主要认识了下 RabbitMQ 的组成和基本使用,并通过 SpringBoot 工程整合了 RabbitMQ,做了一个完整的 Demo。一般说,引入了新的中间件,数据的风险性就又要多一层考虑,那么 Rabbitmq 的消息它是怎么知道它有没有被消费者消费的呢?生产者又怎么确保自己发送成功了呢,我们在这篇文章中将对这些问题进行演示学习。

一、为什么要进行消息确认?

​ 在mq 中,消费者和生产者并不直接进行通信,生产者只负责把消息发送到队列,消费者只负责从队列获取消息(不管是push还是pull)。

  • 消费者从队列中获取到消息之后,这条消息就不存在队列中了,但是如果此时消费者所在的信道因为网络中断没有消费到,那这条消息就被永远的丢失了,所以,我们希望等待消费者成功消费掉这个消息之后再删除这条消息。
  • 而在发送消息的时候也是这样的,生产者发消息给交换机,也不能保证消息准确发送过去了,消息就像石沉大海一样,所以这样需要一个消息确认。

这个机制就是 消息确认机制。

二、消息确认流程

image-20211101093707237

在流程图中,我们可与看到消息确认是分为生产者确认和消费者确认的。

这两个机制都是受到 TCP 协议的启发,它们对数据安全非常重要。

补充:

  • 在RabbitMQ 中有两种事务机制来确保消息的安全送达,分别是事务机制和确认机制。事务机制需要每个消息或每组消息发布提交的通道设置为事务性的,非常耗费性能,降低了消息吞吐量。因此,实际中通常采用确认机制即可。

三、生产者确认

由生产者发送到 consumer 的链路为 producer -> broker -> exchange -> queue -> consumer 。

在编码时我们可以用两个选项用来控制消息投递的可靠性:

  • 消息从 producer 到 RabbitMQ broker cluster 成功,则会返回一个 confirmCallback;
  • 消息从 exchange 到 queue 投递失败,则会返回一个 returnCallback

我们可以利用这两个 callback 接口来控制消息的一致性和处理一部分的异常情况。

3.1 代码准备

3.1.1 配置文件

1、在配置文件中需要添加:

1
2
3
yaml复制代码spring:
rabbitmq
publisher-confirm-type:

它有三个值:

  • NONE:禁用发布确认模式,是默认值
  • CORRELATED:发布消息成功到交换器后触发回调方法
  • SIMPLE:值经测试有两种效果,其一效果和CORRELATED值一样会触发回调方法,其二在发布消息成功后使用rabbitTemplate调用waitForConfirms或waitForConfirmsOrDie方法等待broker节点返回发送结果,根据返回结果来判定下一步的逻辑,要注意的点是waitForConfirmsOrDie方法如果返回false则会关闭channel,则接下来无法发送消息到broker;
3.1.2 配置类

思考:如何在项目启动的时候从数据库中加载这些配置并进行绑定,后续我们来研究下。

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
typescript复制代码@Configuration
public class ConfirmConfig {
// 交换机
public static final String confirm_exchange_name = "confirm_exchange";
// 队列
public static final String confirm_queue_name="confirm_queue";
// routingkey
public static final String confirm_routing_key = "key1";

// 声明交换机
@Bean("confirmExchange")
public DirectExchange confirmExchange(){
return new DirectExchange(confirm_exchange_name);
}
// 声明队列
@Bean("confirmQueue")
public Queue confirmQueue() {
return QueueBuilder.durable(confirm_queue_name).build();
}
// 绑定队列到交换机
@Bean
public Binding queueBingExchange(){
return BindingBuilder.bind(confirmQueue()).to(confirmExchange()).with(confirm_routing_key);
}
}
3.1.3 回调接口
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
typescript复制代码@Slf4j
@Component
public class MyCallBack implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnCallback {

@Autowired
private RabbitTemplate rabbitTemplate;

/**
* 向rabbitTemplate 注入回调失败的类
* 后置处理器:其他注解都执行结束才执行。
*/
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnCallback(this);
}
/**
* 交换机确认回调方法
* 发消息 交换机接收到了 回调
* @param correlationData :保存回调消息的 ID 及相关信息,交换机收到消息 ack=true代表成功;ack=false 代表失败
* @param ack :true 代表交换机收到了
* @param cause : 原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack){
log.info("交换机已经收到 ID 为:{} 的消息",correlationData.getId());
}else{
log.info("交换机未收到 ID为 {} 的消息,原因是 {}",correlationData.getId(),cause);
}
}

/**
* 当消息传送到队列过程中不可抵达的时候 将消息返回给生产者
* @param message
* @param replyCode
* @param replyText
* @param exchange
* @param routingKey
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
log.error("消息 {} ,被交换机 {} 退回原因 {}",message,exchange,replyText);
}
}

3.2 交换机确认–生产消费测试

使用交换机回调,就配置 publisher-confirm-type: 为CORRELATED

1
2
3
4
5
less复制代码  @GetMapping("/sendMsg/{message}")
public void sendConfirmMsg(@PathVariable String message){
rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name+'1', ConfirmConfig.confirm_routing_key,message,new CorrelationData("1"));
log.info("发送消息内容:{}",message);
}

当生产者获取不到消息的时候进入回调函数执行 false 的代码。

image-20211111145809537

实现接口 ConfirmCallback ,重写其confirm()方法,方法内有三个参数correlationData、ack、cause。

  • correlationData:对象内部只有一个 id 属性,用来表示当前消息的唯一性。
  • ack:消息投递到broker 的状态,true表示成功。
  • cause:表示投递失败的原因。

3.1.5 队列确认–回退接口

交换机接收到消息后可以判断当前的路径发送没有问题,但是不能保证消息能够发送到路由队列的。而发送者是不知道这个消息有没有送达队列的,因此,我们需要在队列中进行消息确认。这就是回退消息。

实现接口ReturnCallback,重写 returnedMessage() 方法,方法有五个参数message(消息体)、replyCode(响应code)、replyText(响应内容)、exchange(交换机)、routingKey(队列)。

添加注解:

1
sql复制代码publisher-returns: true

执行代码:

1
2
3
arduino复制代码rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name, ConfirmConfig.confirm_routing_key,message+"1",new CorrelationData("1"));

rabbitTemplate.convertAndSend(ConfirmConfig.confirm_exchange_name, ConfirmConfig.confirm_routing_key+"2",message+"2",new CorrelationData("2"));

回退执行

1
css复制代码消息 (Body:'hello22' MessageProperties [headers={spring_returned_message_correlation=2}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0]) ,被交换机 confirm_exchange 退回原因 NO_ROUTE

阶段小结:

生产者的消息确认机制有两种:

  • 一个是从生产者发送到交换机的确认回调;
  • 一个是从交换机发送到队列的确认回调;

生产者发送到交换机,需要配置一个参数 publisher-confirm-type ,它默认是 none,没有开启,可以把它改为 correlated ,即消息成功发送后会触发一个回调;然后我们根据 ack 的一个状态进行判断,如果为 true ,则代表发送成功。

还有一个交换机到队列的回调,将 publisher-returns 改为 true 即可,触发 returnedMessage 。

四、消费者确认

首先介绍消息消费的前提,rabbitmq 消费消息有两种模式,一个是推送 push ,一个是自己拉取pull。

  • 推模式:消息中间件主动将消息推送给消费者
  • 拉模式:消费者主动从消息中间件拉取消息。

但实际使用中,拉取消息是会降低系统吞吐量的,以及消费者很难实时获取消息,因此,一般使用的是push 模式。

在 mq 推消息给消费者不是等消费者消费完一个再推一个,而是根据prefetch_count 参数来决定可以推多个消息到消费者的缓存里面。

在消费者确认中,为了保证数据不会丢失,RabbitMQ 支持消息确定ACK。ACK 机制是消费者从 RabbitMQ 收到消息并处理完成后,返回给RabbitMQ,RabbitMQ 收到反馈后才将此消息从队列中删除。

4.1 自动确认

自动确认是指消费者在消费消息的时候,当消费者收到消息后,消息就会被 RabbitMQ 从队列中删除掉。这种模式认为 “发送即成功”。这是不安全的,因为消费者可能在业务中并没有成功消费完就中断了。

下面我们通过 debug 来测试下这个逻辑。可以看到在debug 状态下,消费者还未消费,该队列中就没有任何数据了。

image-20211101103518109

4.2 手动确认 autoAck:false

手动确认又分为肯定确认和否定确认。

4.2.1 肯定确认 BasicAck
1
2
bash复制代码// false 表示只确认 b.DelivertTag 这条消息,true 表示确认 小于等于 b.DelivertTag 的所有消息(批量确认)
channel.basicAck(b.getEnvelope().getDeliveryTag(),false);

image-20211101104601852

当消费者消费完数据后,队列中一共还剩18条,有一条消息待确认。

image-20211101104718522

4.2.2 否定确认: BasicNack、BasicReject

否定确认的场景不多,但有时候某个消费者因为某种原因无法立即处理某条消息时,就需要否定确认了.

否定确认时,需要指定是丢弃掉这条消息,还是让这条消息重新排队,过一会再来,又或者是让这条消息重新排队,并尽快让另一个消费者接收并处理它.

1
php复制代码 丢弃:requeue: false:channel.BasicNack(deliveryTag: e.DeliveryTag, multiple: false, requeue: false);
1
php复制代码重新排队( requeue: true): channel.BasicNack(deliveryTag: e.DeliveryTag, multiple: false, requeue: true);

一般来说,如果出现异常,就使用channel.BasicNack 把消费失败的消息重新放入到队列中去。

4.3 springboot 版本确认

Springboot 的确认模式有三种,配置如下:

1
ini复制代码spring.rabbitmq.listener.simple.acknowledge-mode=manual
  • NONE : 不确认 :
    • 1、默认所有消息消费成功,会不断的向消费者推送消息
    • 2、因为 rabbitmq 认为所有消息都被消费成功。所以队列中存在丢失消息风险。
  • AUTO:自动确认
    • 1、根据消息处理逻辑是否抛出异常自动发送 ack(正常)和nack(异常)给服务端,如果消费者本身逻辑没有处理好这条数据就存在丢失消息的风险。
    • 2、使用自动确认模式时,需要考虑的另一件事情就是消费者过载。
  • MANUAL:手动确认
    • 1、手动确认在业务失败后进行一些操作,消费者调用 ack、nack、reject 几种方法进行确认,如果消息未被 ACK 则发送到下一个消费者或重回队列。
    • 2、ack 用于肯定确认;nack 用于 否定确认 ;reject 用于否定确认(一次只能拒绝单条消息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
less复制代码@Component
@Slf4j
public class MsgConfirmController {
@RabbitListener(queues = ConfirmConfig.confirm_queue_name)
public void consumerConfirm(Message message, Channel channel) throws IOException {
if(message.getBody().equals("2")){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//basicAck:表示成功确认,使用此回执方法后,消息会被rabbitmq broker 删除。
log.info("接收的消息为:{}",message);
}else{
channel.basicReject(message.getMessageProperties().getDeliveryTag(),true);
log.info("未消费数据");
}
}
}

发现它可以将消息返回给队列,然后又消费这个数据,不断消费,造成了死循环,消息无限投递。

image-20211111162536324

这时候可以改成 false,然后配置下死信队列,将该消息发送到死信队列中。

总结

本文主要对消息的确认进行了 springboot 微服务版本的测试,通过两个服务之间的互相调用来验证 rabbitmq 的消息确认可行性。在后面的文章中,我们将对rabbitmq 更多细节进行深入研究。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Shell tee 妙用 一、问题发生: 二、思考以及解决:

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」

一、问题发生:

近期在写脚本实现ORACLE一键安装单机11G/12C/18C/19C并建库脚本(shell),期间在测试11G、12C时遇到了一个问题,执行安装命令时:

1
bash复制代码runInstaller -silent -force -ignoreSysPrereqs -responseFile ${SOFTWAREDIR}/db.rsp -ignorePrereq

安装进程不会等待安装完成再继续执行下一步操作,而是进入后台进程进行安装,直接执行下一步的命令,导致安装失败。

二、思考以及解决:

为此,我想出几个解决方向:

1.通过expec交互命令去监控,太麻烦,未能付诸实践。

2.通过read -p增加一个提示,人为判断安装成功后,回车继续下一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码if [ "${DB_VERSION}" = "11.2.0.4" ] || [ "${DB_VERSION}" = "12.2.0.1" ]; then
echo "Oracle Software Install Starting......"
echo
echo
sleep 20
echo
echo
echo "When Successfully Setup Software Apper. Then Press Enter continue."
echo
echo
read -p "Please Don't Press Enter. Now Waiting..........."
echo
fi

缺点:这个方法实现简单,但是有违我脚本全自动的初衷。

3.通过写日志,然后while do去grep循环获取日志,每隔5秒搜索一次,当获取到Successfully Setup Software时break退出,继续执行下一步。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码su - oracle -c "${SOFTWAREDIR}/database/runInstaller -silent -force -ignoreSysPrereqs -responseFile ${SOFTWAREDIR}/db.rsp -ignorePrereq" >>${SOFTWAREDIR}/setupDatabase.out
echo
echo
sleep 20
echo "Oracle Software Install Starting......"
echo
echo
while true; do
echo -n "."
sleep 5s
grep "Successfully Setup Software" ${SOFTWAREDIR}/setupDatabase.out >>/dev/null
if [ $? -eq 0 ]; then
echo
echo
c1 "Successfully Setup Software." blue
break
fi
done

缺点:这个方法实现了自动化,并且可以成功执行。但是,安装日志无法输出到控制台,都写入到日志中了,如果安装失败也没法知道,会一直无限循环,需要去手动查看安装日志setupDatabase.out判断是否成功,不直观。

4.经过以上的思路,接下来我只需要实现,在将日志输入到日志的同时,不影响控制台输出即可,所以我就想到了 tee 命令,修改3的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码su - oracle -c "${SOFTWAREDIR}/database/runInstaller -silent -force -ignoreSysPrereqs -responseFile ${SOFTWAREDIR}/db.rsp -ignorePrereq" | tee ${SOFTWAREDIR}/setupDatabase.out

while true; do
echo -n "."
sleep 5s
grep "Successfully Setup Software" ${SOFTWAREDIR}/setupDatabase.out >>/dev/null
if [ $? -eq 0 ]; then
echo
echo
c1 "Successfully Setup Software." blue
break
fi
done

结果测试时,惊喜的发现while判断是在安装命令执行成功后才执行的,于是继续修改脚本为:

1
2
3
4
bash复制代码if [[ "${DB_VERSION}" = "12.2.0.1" ]] || [[ "${DB_VERSION}" = "11.2.0.4" ]]; then
su - oracle -c "${SOFTWAREDIR}/database/runInstaller -silent -force -ignoreSysPrereqs -responseFile ${SOFTWAREDIR}/db.rsp -ignorePrereq" |tee ${SOFTWAREDIR}/setupDatabase.out
rm -rf ${SOFTWAREDIR}/setupDatabase.out
fi

继续测试,命令成功执行,完成安装,达到用户无感知,安装结果如下:

撒花✿✿ヽ(°▽°)ノ✿,庆祝!!!

由此,可以推测出:tee 命令时可以阻止程序后台运行。

三、关于tee命令可参考:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

手把手教你实现超大数据量带进度条导入导出功能(一导入篇)

发表于 2021-11-11

「这是我参与11月更文挑战的第4天,活动详情查看:2021最后一次更文挑战」

概述

数据入导出功能是程序员开发中比较常见的功能,操作导入导出的人为业务人员,大批量数据的导入导出往往需要长时间的等待,为了让操作人员对导入导出进度直观可见,为导入导出功能添加进度条就是的该功能的使用体验有值得提升。

效果演示

20211111_160744.gif

开发流程

  1. 在“上传” 按钮下方编写静态进度条html代码,默认为隐藏状态
1
2
3
css复制代码<div id="progress" style="height:20px;width:100%;background: #efefef;border:1px solid #eee;border-radius:10px;display:none;">
<div class="bar" style="background: green;width:10%;height: 100%;border-radius:10px;line-height:20px;">0%</div>
</div>

效果如下:

微信截图_20211111145713.png

说明:具体使用时通过动态修改内部div的宽度即可实现进度条的变化。

  1. 在文件上传Form表单内添加taskId隐藏框,完整html代码如下:
1
2
3
4
5
6
7
8
css复制代码<button style="width: 100px;" onclick="upload()" id="uploadBtn">上传</button>&nbsp;&nbsp;&nbsp;&nbsp;(仅限CSV格式)<br><br>
<div id="progress" style="height:20px;width:100%;background: #efefef;border:1px solid #eee;border-radius:10px;">
<div class="bar" style="background: green;width:10%;height: 100%;border-radius:10px;line-height:20px;">0%</div>
</div>
<form id="fileForm" enctype="multipart/form-data">
<input type="file" accept="text/csv" style="display: none;" id="fileInput" onchange="onFileChange()" name="upLoadFile">
<input type="hidden" id="taskId" name="taskId">
</form>

说明: 添加taskId用来标识本次上传的任务唯一性
3. 编写文件上传js代码

文件上传开始初始化进度条展示,进度为零,然后开启一个定时任务定时更新上传进度,进度值通过后台接口获取。代码如下:

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
javascript复制代码function upload(){
$('#fileInput').click()
}
function onFileChange(){
var bar =$('#progress').show().find('.bar')
bar.text('0%')
bar.css({width:'0%'});
var taskId = ''+ new Date().getTime()
$('#taskId').val(taskId)
timer = setInterval(function(){
$.ajax({
type:'post',
url:'getProgress',
data:{taskId:taskId},
dataType: "json",
}).success(function(data){
if(data.result){
bar.text(data.value + '%')
bar.css({width:data.value + '%'})
}
}).error(function(e){
})
},2000);
$.ajax({
type:'post',
url:'upload',
data:new FormData($('#fileForm')[0]),
cache: false,
processData: false,
contentType: false,
}).success(function(data){
clearInterval(timer)
$('#progress').hide().find('.bar').css({width:'0%'})
// do some things
}).error(function(e){
clearInterval(timer)
$('#progress').hide().find('.bar').css({width:'0%'})
// do some things
})
},
  1. 编写Java处理上传业务代码
    主要逻辑为,获取文件总条数,然后循环读取记录分批处理,同时更新任务进度(已经处理记录数占总条数的百分比)入内存(集群环境可以存入中间件如redis),key为taskId,value为百分比值。
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
ini复制代码public void execute() throws Exception{
JSONObject jsonObject = new JSONObject();
jsonObject.put("result",true);
jsonObject.put("msg","成功");
UploadFile upLoadFile = getFile("upLoadFile","/",Integer.MAX_VALUE);
final File file = upLoadFile.getFile();
final int lineNumber;
// 获取文件总条数
try (final FileReader in1 = new FileReader(file); final LineNumberReader lineNumberReader = new LineNumberReader(in1)){
lineNumberReader.skip(file.length());
lineNumber = lineNumberReader.getLineNumber();
}
final String username = getUsername();
final String taskId = getPara("taskId");
int sum = 0;
final Pattern compile = Pattern.compile("1\\d{10}");
try(final FileReader in = new FileReader(file); final BufferedReader reader = new BufferedReader(in);){
String str;
List<String> mobileList =new ArrayList<>();
while ((str = reader.readLine()) != null){
String mobile = str.trim().split(",")[0];
if(!compile.matcher(mobile).matches()){
jsonObject.put("result",false);
jsonObject.put("msg","手机号格式不正确("+mobile+")");
continue;
}
mobileList.add(mobile);
if(mobileList.size() == 100){
int num = remoteUploadUser(mobileList,username);
mobileList.clear();
}
sum ++;
taskProgressMap.put(taskId,sum * 100/lineNumber);
}
if(!mobileList.isEmpty()){
int num = remoteUploadUser(mobileList,username);
mobileList.clear();
}
}catch (Exception e){
jsonObject.put("result",false);
jsonObject.put("msg","系统错误:" + e.getMessage());
}
jsonObject.put("count",count);
renderJson(jsonObject.toJSONString());
}

public void getProgress(){
JSONObject jsonObject = new JSONObject();
jsonObject.put("result",true);
jsonObject.put("msg","成功");
final String taskId = getPara("taskId");
final Integer integer = taskProgressMap.get(taskId);
if(integer == null){
jsonObject.put("result",false);
jsonObject.put("msg","失败");
}
jsonObject.put("value",integer);
renderJson(jsonObject.toJSONString());
}

总结

  1. 超大数据文件处理需要分批处理。
  2. 前端组件需要先编写静态页面,然后将其动态化

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Nexus简介及小白使用IDEA打包上传到Nexus3私服详

发表于 2021-11-11

「这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战」

一、Nexus是什么

开始在使用Maven时,总是会听到nexus这个词,一会儿maven,一会儿nexus,为什么它总是和maven一起被提到呢?

Maven作为一个优秀的构建工具、依赖管理工具、项目信息管理工具,在进行依赖管理的时候,通过pom.xml里面的

1
2
3
4
5
bash复制代码<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>

来精准定位找到对应的Java类库。在这个过程当中我们需要从仓库去找到对应的jar包引入到我们的项目当中,由此我们解决了合作开发中依赖增多、版本不一致、版本冲突、依赖臃肿等问题。
在这里插入图片描述
Maven有本地仓库和远程仓库两种,当Maven根据坐标寻找构件时,它首先会查看本地仓库,如果本地仓库存在此构件,则直接使用;如果本地仓库不存在此构件,或者需要查看是否有更新的构件版本,Maven会去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。

在这里插入图片描述
说到此,相信大家也明白了,Nexus是一种远程仓库,也是私服的一种。

SNAPSHOT
快照版本,在 maven 中 SNAPSHOT 版本代表正式发布(release)的版本之前的开发版本,在 pom 中用 x.y-SNAPSHOT 表示。

RELEASE
发布版本,稳定版本,在 maven 中 RELEASE 代表着稳定的版本,unchange,不可改变的,在 maven 中 SNAPSHOT 与 RELEASE 版本在策略上是完全不同的方式,SNAPSHOT 会根据你的配置不同,频繁的从远程仓库更新到本地仓库;而 RELEASE 则只会在第一次下载到本地仓库,以后则会先直接从本地仓库中寻找。

二、使用Nexus3搭建maven私服

在网上找到几个参考:
Windows中使用Nexus3搭建maven私服
maven 私服 nexus3.x 搭建 与使用
Maven私服Nexus3.x环境构建操作记录

三、IDEA打包上传到Nexus3私服

1.配置 .m2 下的 settings.xml

首先,这个文件在系统盘当前设备登录用户的.m2文件下,加入认证机制
在这里插入图片描述
没有就去网上copy一个配置好的,自己配置容易出错

->settings.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
bash复制代码<servers>
<server>
<id>nexus</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>maven-snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>maven-releases</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>rdc-releases</id>
<username>xxxxxxxxx</username>
<password>xxxxx</password>
</server>
<server>
<id>rdc-snapshots</id>
<username>xxxxxxxxx</username>
<password>xxxx</password>
</server>
</servers>

2.配置 IDEA 项目 下的 pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码 <distributionManagement>
<repository>
<id>nexus</id>
<name>nexus</name>
<url>http://xxxx:port/repository/maven-snapshots/</url>
</repository>

<snapshotRepository>
<id>maven-snapshots</id>
<name>maven-snapshots</name>
<url>http://xxxx:port/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>

这里标签中的id对应第一条server中的id ,url取得值可以直接在这里写,也可以如下图中settings文件中的值,name可以自定义
->settings.xml<-

1
2
3
4
bash复制代码<profile>
<altReleaseDeploymentRepository> http://xxxx:port/repository/maven-snapshots/ </altReleaseDeploymentRepository>
<altSnapshotDeploymentRepository> http://xxxx:port/repository/maven-snapshots/</altSnapshotDeploymentRepository>
</profile>

同样,上面pom.xml改成如下格式

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码<distributionManagement>
<repository>
<id>nexus</id>
<name>nexus</name>
<url>${altReleaseDeploymentRepository}</url>
</repository>

<snapshotRepository>
<id>maven-snapshots</id>
<name>maven-snapshots</name>
<url>${altSnapshotDeploymentRepository}</url>
</snapshotRepository>
</distributionManagement>

3.配置上传地址,地址去私服中copy

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
bash复制代码<profile> 
<!--profile 的 id-->
<id>dev</id>
<repositories>
<repository>
<!--仓库 id,repositories 可以配置多个仓库,保证 id 不重复-->
<id>nexus</id>
<!--仓库地址,即 nexus 仓库组的地址-->
<url>http://localhost:8081/nexus/content/groups/public/</url>
<!--是否下载 releases 构件-->
<releases>
<enabled>true</enabled>
</releases>
<!--是否下载 snapshots 构件-->
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<!-- 插件仓库,maven 的运行依赖插件,也需要从私服下载插件 -->
<pluginRepository>
<!-- 插件仓库的 id 不允许重复,如果重复后边配置会覆盖前边 -->
<id>central</id>
<name>Nexus Plugin Repository</name>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>

4.其他配置(可忽略)

比如配置阿里的镜像,映射阿里中央仓库(下载jar包快一点)

1
2
3
4
5
6
7
8
bash复制代码<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>

配置本地仓库(无需联网使用jar包)

1
bash复制代码<localRepository>E:\maven_repository</localRepository>

5.settings.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
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
bash复制代码<?xml version="1.0" encoding="UTF-8"?>

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<!-- 本地仓库地址 -->
<localRepository>D:\mvn_repo\repository</localRepository>


<!-- 以下配置为上传jar包配置 -->
<pluginGroups>

</pluginGroups>

<proxies>

</proxies>

<servers>
<server>
<!-- id,对应项目里面pom.xml里面distributionManagement配置的id -->
<id>maven-releases</id>
<!-- 登录nexus的用户名 -->
<username>admin</username>
<!-- 登录nexus的密码 -->
<password>admin123</password>
</server>
<server>
<!-- id,对应项目里面pom.xml里面distributionManagement配置的id -->
<id>maven-snapshots</id>
<!-- 登录nexus的用户名 -->
<username>admin</username>
<!-- 登录nexus的密码 -->
<password>admin123</password>
</server>
<!-- 配置拦截器mirror登录的用户名密码。他会拦截所有的请求到mirror指定的地址下载jar包 如果只需要去私服下载jar包则只需配置此项 -->
<server>
<!-- id,对应mirror中id -->
<id>nexus</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>


<!-- 以下配置为下载jar包配置 通用 -->

<mirrors>
<!-- 强制让jar包下载走私服 -->
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://192.168.65.129:8081/repository/maven-public/</url>
</mirror>
</mirrors>

<profiles>
<profile>
<!-- 对应activeProfiles-activeProfile的内容 -->
<id>nexus</id>
<!-- 仓库地址 -->
<repositories>
<repository>
<!-- 私服id,覆盖maven-model模块下的父id,让maven不走中央仓库下载,走私服下载 -->
<id>central</id>
<!-- 名字 -->
<name>Nexus</name>
<!-- 私服地址,写central后,会去mirror里面找 -->
<url>http://central</url>
<!-- 支持releases版本 -->
<releases>
<enabled>true</enabled>
</releases>
<!-- 支持snapshots版本 -->
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<!-- 插件地址 -->
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Nexus Plugin Repository</name>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
<!-- 配置全局的url地址 供上传jar包时动态获取 -->
<properties>
<ReleaseRepository>http://192.168.65.129:8081/repository/maven-releases/</ReleaseRepository>
<SnapshotRepository>http://192.168.65.129:8081/repository/maven-snapshots/</SnapshotRepository>
</properties>
</profile>
</profiles>

<!-- 选择使用的profile -->
<activeProfiles>
<activeProfile>nexus</activeProfile>
<!-- <activeProfile>rdc</activeProfile>-->
</activeProfiles>


</settings>

6.IDEA打包上传

使用idea打包,打包时会根据pom文件中version判断是快照版本还是发布版本,version中0.0.1-SNAPSHOT 带SNAPSHOT为快照版本,删掉SNAPSHOT为发布版本,上传到私服时根据version自动判断上传到哪个仓库

注意,打包时 项目的pom文件一定要把maven-plugin删除掉,不然打包完成后会生成BOOT-INF文件夹,会造成上传到私服后,配置依赖后能下载到jar包,但是使用时报包找不到错误

1
2
3
4
bash复制代码	<version>1.1-SNAPSHOT</version>
<build>

</build>

然后clean后deploy
在这里插入图片描述
看到BUILD SUCCESS的提示则为成功,可以在自己私服上查看

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何快速找到电脑/服务器上的 node_modules和快速

发表于 2021-11-11

问题:如何快速找到电脑/服务器上的 node_modules和快速删除?(推荐npkill)

初衷:

Node.js 项目或者前端项目使用 npm 安装依赖模块时,总是在项目根目录上新建 node_modules ,并且在里面安装一系列的包。一般来说需要占用不少的空间,随着项目越来越多,依赖模块越来越多,空间会越来越大,在某些时候,这会带来一些问题。假如你有一台阿里云的服务器,云盘很小,并且不能够或者不好加云盘,在磁盘吃紧的时候,这时候如果能快速了解到所有node_modules占用的大小,并且删除,是一个很好的提高效率的方案。

npkill

npkill 是一个 Node.js 包 可以帮助我们快速列出系统中的每一个 node_modules 文件夹,还有它们所占用的空间大小。之后你可以选择要删除具体的 node_modules 文件夹。

用法

npm 全局安装

1
bash复制代码$ npm -g i npkill

pnpm 全局安装:

1
bash复制代码$ pnpm -g i npkill(更推荐)

npkill demo gif

图片来自 npkill 的 github README , 操作过程如上图所示:
npkill 在查找的时候右上方会显示为 searching 表示正在查找中,查找完成则会显示为 search completed。

按照提示,点击空格键来删除,一次一个。更多功能请查看官方地址文档:

提示:

对于使用 npm 安装的依赖,这个是不错的解决方案,但是如果是使用 pnpm 安装就完全不用考虑 node_modules 占用空间的问题,因为 pnpm 并不会在每一个 node_modules 都安装一遍模块,而是通过链接的形式,最终链接到具体的目录地址,对于 pnpm 的安转依赖原理,推荐查看一位大佬的文章(# 关于现代包管理器的深度思考——为什么现在我更推荐 pnpm 而不是 npm/yarn?)

由于 npm 的一些历史问题,和现在的样子,希望更多的时候使用 pnpm 而不是 npm.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java中关于保留小数点后几位(可以四舍五入的)的方法

发表于 2021-11-11

在实际开发中,我们相对一些除法后的结果进行简单的输出,或者定制型的显示效果,下面几种方法供参考(全文以保留两位小数为例):

一、格式化保留小数方法

1. 格式化printf输出

对于学过c语言的同学来说,这种方法见怪不怪了!
主要使用printf来格式化输出:

1
2
3
java复制代码        double ll =len/(1024*1024.0);
System.out.println("文件大小为"+ll+"MB");//36.364097595214844
System.out.printf("%.2f",ll);//33.36

2.DecimalFormat类

这种方法对于初学者来说不常用,因为这种方法牵扯到了DecimalFormat类的使用,所以不够友好

1
2
java复制代码        DecimalFormat d = new DecimalFormat("#.00");
System.out.println(d.format(ll));//36.36

3.通过运算输出

通过计算,让小数点后移几位,保留整型,然后再除以后移的位数,所得结果是刚好想要保留的位数

1
java复制代码  System.out.println((int)(ll*100)/100.0);//36.36

以上不能保证四舍五入,如需四舍五入精确值,继续往下:

二、四舍五入保留2位小数为例

(1).使用BigDecimal类

下面代码中: 
public BigDecimal setScale(int newScale, int roundingMode) // newScale 为小数点后保留的位数, roundingMode 为变量进行取舍的方式;
  BigDecimal.ROUND_HALF_UP 属性含义为为四舍五入

1
2
3
4
java复制代码double f = 3.1415;
BigDecimal bd = new BigDecimal(f);
double f1 = bd.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
输出结果f1为 3.14;

(2).DecimalFormat类使用

#.00 表示两位小数 #.0000四位小数 以此类推…

1
2
3
java复制代码String format = new DecimalFormat("#.0000").format(3.1415926);
System.out.println(format);
输出结果为 3.1416

(3).String.Format方法

 %.2f 中 %. 表示 小数点前任意位数 2 表示两位小数 格式后的结果为f 表示浮点型。

1
2
3
4
java复制代码double num = 3.1415926;
String result = String.format("%.4f", num);
System.out.println(result);
输出结果为:3.1416

(4).

最后乘积的0.01d表示小数点后保留的位数(四舍五入),0.0001 为小数点后保留4位,以此类推…

1
2
3
java复制代码double num = Math.round(3.141592 * 100) * 0.01d;
System.out.println(num);
输出结果为:3.14

如需转载,请注链接!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…370371372…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%