bingfeng

bingfeng 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑

日拱一卒,功不唐捐

个人动态

bingfeng 关注了用户 · 2月1日

沉默王二 @itwanger

《Web全栈开发进阶之路》作者
公众号:沉默王二
微信:qing_geee
专注于有趣的Java技术,有益的程序人生

关注 8277

bingfeng 收藏了文章 · 1月25日

开发中的你的Git提交规范吗?

1. 前言

目前大部分公司都在使用Git作为版本控制,每个程序员每天都要进行代码的提交。很多开发者也包括我自己,有时候赶时间或者图省事,就这么提交:

git commit -m "修改bug,优化代码"

过了一段,突然去查找一个具体的提交你会发现不是特别好找。因此我们需要规范我们的代码提交来避免这种情况。同时良好的提交规范也有助于我们生成清晰的ChangeLog,更利于同事之间的协作。

如果你想成为知名开源项目的贡献者更要规范自己的代码提交。

2. Git提交规范

目前业内做的比较好的,比较具有参考价值的就是知名前端框架AngularJS的提交规范。我们先来看一个例子:

Git提交信息

对应的格式:

<type>[optional scope]: <description>
# 空行
[optional body]
# 空行
[optional footer]
更严格的项目可能提交要求使用英文描述,特别是国际化的开源项目。

根据上面这个例子我们来了解一下这个业界比较认可的Git提交规范。

type

refactor 表示本次提交的是重构代码,也就是它是一个提交的类型type,除了refactor还有:

  • feat 新功能,顾名思义就是新需求的实现。
  • fix 修复,就是对bug的修复。
  • docs 文档,主要用来描述文档的变更。
  • style 主要是代码风格相关的提交,比如格式化等。
  • refactor 重构代码,对已有功能的重构,但是区别于bugfix。
  • test 测试相关的提交,不太常用。
  • chore 构建过程或辅助工具的变动,不太常用,比如之前用Maven,后面换成了Gradle。

每次提交声明提交的type是必须的,它让本次提交的作用一目了然。

scope(可选)

用来表明本次提交影响的范围,方便快速定位。你可以写明影响的是哪个模块(通常是模块名称)或者是哪个层(数据层、服务层、还是视图层)。

subject

就是上面的修改版权信息,是对本次提交的简短描述概括。就像胖哥写文章要起一个标题一样,不要过长。

body(可选)

就是比较详细描述本次提交涉及的条目,罗列代码功能,这里胖哥习惯用markdown的列表语法,也就是用中划线换行隔开条目。当然body不是必选的,如果subject能够描述清楚的话。

foot(可选)

描述与本次提交相关联的break changeissue

break change

指明本次提交是否产生了破坏性修改,类似版本升级、接口参数减少、接口删除、迁移等。如果产生了上述的影响强烈建议在提交信息中写明break change,有利于出问题时快速定位,回滚,复盘。

issue

如果发现项目有bug、或者有优化的建议、甚至新增一个任务,就可以利用issue给项目提交一个任务。

Git issue

issue不是一些Git平台的专属功能,JIRA等平台也有类似功能,它们的作用大同小异,都可以很好地反应项目的成长状况和参与度。那么在Git提交时,我们可以在foot区域关联本次提交涉及的issue

# 涉及
issues  #F12YC,#F45JW
# 关闭
Closes #F12YC
这里没有固定格式,不过尽量去参考一些知名项目去做。

3. 工具安利

说了这么多,相信你已经对Git提交的规范有所了解了。这里推荐一些有用的工具来帮助你将这些规范落实到位。在Intellij IDEA的插件市场有很多Git Commit Message模板插件,可以可视化的实现这些规范。

Git提交信息插件

你可以去插件市场搜索获取相关的插件。好了今天的分享就到这里,多多关注:码农小胖哥,学习更多有用的编程实用技巧。

关注公众号:Felordcn 获取更多资讯

个人博客:https://felord.cn

查看原文

bingfeng 发布了文章 · 1月18日

移除List中的元素,你的姿势对了吗?

之前遇到对List进行遍历删除的时候,出现来一个ConcurrentModificationException 异常,可能好多人都知道list遍历不能直接进行删除操作,但是你可能只是跟我一样知道结果,但是不知道为什么不能删除,或者说这个报错是如何产生的,那么我们今天就来研究一下。

一、异常代码

我们先看下这段代码,你有没有写过类似的代码

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("开始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加结束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      list.remove(next);
    }
  }
  System.out.println("执行结束 size:" + list.size());
}

毫无疑问,执行这段代码之后,必然报错,我们看下报错信息。

我们可以通过错误信息可以看到,具体的错误是在checkForComodification 这个方法产生的。

二、ArrayList源码分析

首先我们看下ArrayListiterator这个方法,通过源码可以发现,其实这个返回的是ArrayList内部类的一个实例对象。

public Iterator<E> iterator() {
  return new Itr();
}

我们看下Itr类的全部实现。

private class Itr implements Iterator<E> {
  int cursor;       // index of next element to return
  int lastRet = -1; // index of last element returned; -1 if no such
  int expectedModCount = modCount;

  Itr() {}

  public boolean hasNext() {
    return cursor != size;
  }

  @SuppressWarnings("unchecked")
  public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
      throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
      throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
  }

  public void remove() {
    if (lastRet < 0)
      throw new IllegalStateException();
    checkForComodification();

    try {
      ArrayList.this.remove(lastRet);
      cursor = lastRet;
      lastRet = -1;
      expectedModCount = modCount;
    } catch (IndexOutOfBoundsException ex) {
      throw new ConcurrentModificationException();
    }
  }

  @Override
  @SuppressWarnings("unchecked")
  public void forEachRemaining(Consumer<? super E> consumer) {
    Objects.requireNonNull(consumer);
    final int size = ArrayList.this.size;
    int i = cursor;
    if (i >= size) {
      return;
    }
    final Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length) {
      throw new ConcurrentModificationException();
    }
    while (i != size && modCount == expectedModCount) {
      consumer.accept((E) elementData[i++]);
    }
    // update once at end of iteration to reduce heap write traffic
    cursor = i;
    lastRet = i - 1;
    checkForComodification();
  }

  final void checkForComodification() {
    if (modCount != expectedModCount)
      throw new ConcurrentModificationException();
  }
}

参数说明:

cursor : 下一次访问的索引;

lastRet :上一次访问的索引;

expectedModCount :对ArrayList修改次数的期望值,初始值为modCount

modCount : 它是AbstractList的一个成员变量,表示ArrayList的修改次数,通过addremove方法可以看出;

几个常用方法:

hasNext():

public boolean hasNext() {
    return cursor != size;
}

如果下一个访问元素的下标不等于size,那么就表示还有元素可以访问,如果下一个访问的元素下标等于size,那么表示后面已经没有可供访问的元素。因为最后一个元素的下标是size()-1,所以当访问下标等于size的时候必定没有元素可供访问。

next()

public E next() {
  checkForComodification();
  int i = cursor;
  if (i >= size)
    throw new NoSuchElementException();
  Object[] elementData = ArrayList.this.elementData;
  if (i >= elementData.length)
    throw new ConcurrentModificationException();
  cursor = i + 1;
  return (E) elementData[lastRet = i];
}

注意下,这里面有两个非常重要的地方,cursor初始值是0,获取到元素之后,cursor 加1,那么它就是下次索要访问的下标,最后一行,将i赋值给了lastRet这个其实就是上次访问的下标。

此时,cursor变为了1,lastRet变为了0。

最后我们看下ArrayListremove()方法做了什么?

public boolean remove(Object o) {
  if (o == null) {
    for (int index = 0; index < size; index++)
      if (elementData[index] == null) {
        fastRemove(index);
        return true;
      }
  } else {
    for (int index = 0; index < size; index++)
      if (o.equals(elementData[index])) {
        fastRemove(index);
        return true;
      }
  }
  return false;
}
private void fastRemove(int index) {
  modCount++;
  int numMoved = size - index - 1;
  if (numMoved > 0)
    System.arraycopy(elementData, index+1, elementData, index,
                     numMoved);
  elementData[--size] = null; // clear to let GC do its work
}

重点:

我们先记住这里,modCount初始值是0,删除一个元素之后,modCount自增1,接下来就是删除元素,最后一行将引用置为null是为了方便垃圾回收器进行回收。

三、问题定位

到这里,其实一个完整的判断、获取、删除已经走完了,此时我们回忆下各个变量的值:

cursor : 1(获取了一次元素,默认值0自增了1);

lastRet :0(上一个访问元素的下标值);

expectedModCount :0(初始默认值);

modCount : 1(进行了一次remove操作,变成了1);

不知道你还记不记得,next()方法中有两次检查,如果已经忘记的话,建议你往上翻一翻,我们来看下这个判断:

final void checkForComodification() {
  if (modCount != expectedModCount)
    throw new ConcurrentModificationException();
}

modCount不等于expectedModCount的时候抛出异常,那么现在我们可以通过上面各变量的值发现,两个变量的值到底是多少,并且知道它们是怎么演变过来的。那么现在我们是不是清楚了ConcurrentModificationException异常产生的愿意呢!

就是因为,list.remove()导致modCountexpectedModCount的值不一致从而引发的问题。

四、解决问题

我们现在知道引发这个问题,是因为两个变量的值不一致所导致的,那么有没有什么办法可以解决这个问题呢!答案肯定是有的,通过源码可以发现,Iterator里面也提供了remove方法。

public void remove() {
  if (lastRet < 0)
    throw new IllegalStateException();
  checkForComodification();

  try {
    ArrayList.this.remove(lastRet);
    cursor = lastRet;
    lastRet = -1;
    expectedModCount = modCount;
  } catch (IndexOutOfBoundsException ex) {
    throw new ConcurrentModificationException();
  }
}

你看它做了什么,它将modCount的值赋值给了expectedModCount,那么在调用next()进行检查判断的时候势必不会出现问题。

那么以后如果需要remove的话,千万不要使用list.remove()了,而是使用iterator.remove(),这样其实就不会出现异常了。

public static void main(String[] args) {

  List<Integer> list = new ArrayList<>();

  System.out.println("开始添加元素 size:" + list.size());

  for (int i = 0; i < 100; i++) {
    list.add(i + 1);
  }

  System.out.println("元素添加结束 size:" + list.size());

  Iterator<Integer> iterator = list.iterator();

  while (iterator.hasNext()) {
    Integer next = iterator.next();
    if (next % 5 == 0) {
      iterator.remove();
    }
  }
  System.out.println("执行结束 size:" + list.size());
}

建议:

另外告诉大家,我们在进行测试的时候,如果找不到某个类的实现类,因为有时候一个类有超级多的实现类,但是你不知道它到底调用的是哪个,那么你就通过debug的方式进行查找,是很便捷的方法。

五、总结

其实这个问题很常见,也是很简单,但是我们做技术的就是把握细节,通过追溯它的具体实现,发现它的问题所在,这样你不仅仅知道这样有问题,而且你还知道这个问题具体是如何产生的,那么今后不论对于你平时的工作还是面试都是莫大的帮助。

本期分享就到这里,谢谢各位看到此处,

记得点个赞呦!

查看原文

赞 4 收藏 3 评论 3

bingfeng 关注了用户 · 1月13日

程序猿DD @coderdd

翟永超,《Spring Cloud微服务实战》作者,Spring Cloud中文社区创始人,SpringForAll社区发起人之一。

关注我的公众号:程序猿DD

每日技术干货推送,还有一月多次的赠书、赠票等活动免费领取!

关注 1669

bingfeng 发布了文章 · 2020-12-15

JavaIO流(一)-字节输入流与字符输入流

IO流详解

一、输入流

字节输入流

FileInputSteam

1、构造方法:

public FileInputStream(File file) {}
public FileInputStream(FileDescriptor fdObj){}
public FileInputStream(String name){}

2、read方法:

// 每次读取一个字节
public int read(){}

// 读取b.length个字节到byte数组中
public int read(byte b[]){}

// 从输入流中读取len个字节到字节数组中
public int read(byte b[], int off, int len){}

3、文件读取:

1、read()
每次读取一个字节数据,返回字节数,如果到达文件末尾,返回-1。

文本

abc

public static void method_01(String filePath) throws IOException {
  FileInputStream inputStream = null;
  try {
    inputStream = new FileInputStream(filePath);
        // 每次读取一个字节
    for (int i = 0; i < 4; i++) {
      int read = inputStream.read();
      System.out.println(read);
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != inputStream) {
      // 关闭IO流
      inputStream.close();
    }
  }
}

执行结果:

97
98
99
-1

从执行结果可以看出,前三次读取到了数据,返回了对应的ASCII码,当读取到文件末尾的时候,则返回-1。


2、read(byte[] b)
读入缓冲区的字节总数,如果到达文件末尾,则返回-1。

文本:

abcdefg

声明一个大于真实数据的byte数组读取数据。

public static void method_02(String filePath) throws IOException {

  FileInputStream inputStream = null;
  try {
    inputStream = new FileInputStream(filePath);

    // 声明的长度大于真实数据长度
    byte[] bytes = new byte[20];

    int length = inputStream.read(bytes);

    System.out.println("字节数组长度:" + bytes.length + "  读取到的数据字节长度:" + length);

    for (byte b : bytes) {
      System.out.print(b + " | ");
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != inputStream) {
      inputStream.close();
    }
  }
}

执行结果:

字节数组长度:20  读取到的数据字节长度:7
97 | 98 | 99 | 100 | 101 | 102 | 103 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 

可以看出,当我们byte数组的长度大于字节数组的真实长度之后,那么后面的空间全部使用0做补位,这也恰恰反映了另外一个问题,我们在使用byte[]数组读取文件的时候,千万不要说我设置足够大的长度,就可以高枕无忧提高读取效率,如果遇到小文件,那么也是很容易造成效率低下的。


3、read(byte b[], int off, int len)
读入缓冲区的字节总数,如果读取到文件末尾,则返回-1;

文本:

abcdefg

声明一个固定大小的byte数组,循环读取数据

我们的文本有七个字节,声明了一个2个长度的数组,应该循环四次,第五次读取的时候返回-1。

public static void method_03(String filePath) throws IOException {

  FileInputStream inputStream = null;
  try {
    inputStream = new FileInputStream(filePath);

    // 声明2个长度
    byte[] bytes = new byte[2];

    int i = 0;
    while (i < 5) {
      int length = inputStream.read(bytes, 0, bytes.length);
      System.out.println("第" + (i + 1) + "次读取,length: " + length);
      System.out.println("开始输出:");
      for (int j = 0; j < length; j++) {
        System.out.print(bytes[j] + " | ");
      }
      System.out.println();
      i++;
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != inputStream) {
      inputStream.close();
    }
  }
}

执行结果:

第1次读取,length: 2
开始输出:
97 | 98 | 
第2次读取,length: 2
开始输出:
99 | 100 | 
第3次读取,length: 2
开始输出:
101 | 102 | 
第4次读取,length: 1
开始输出:
103 | 
第5次读取,length: -1
开始输出:

注意:

可能有的朋友会遇到我之前遇到的问题,他的文本里面写了汉字或者标点符号,会出现乱码,我们给文本最后再追加一个中文,并且把每次读取到的byte数组转换成String进行输出,看会出现什么情况。

public static void method_03(String filePath) throws IOException {
  FileInputStream inputStream = null;
  try {
    inputStream = new FileInputStream(filePath);

    byte[] bytes = new byte[2];

    int i = 0;
    while (i < 5) {
      inputStream.read(bytes, 0, bytes.length);
      
      // 将byte[]转换成string
      String s = new String(bytes, StandardCharsets.UTF_8);
      System.out.println(s);
      i++;
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != inputStream) {
      inputStream.close();
    }
  }
}

结果:

ab
cd
ef
g�
��

刚开始脑子抽了,感觉这是什么问题,怎么会乱码呢,如果稍微上点心的都会发现,中文占3个byte字节,你用2个存储那铁定乱码呀。那么你肯定想过那我把数组声明大一点不就好了,如果你这么想过,那你可能还不知道社会的险恶。

那么到底怎么办呢?真的就没办法了吗?接下来我们用一个例子来学习如何解决这种问题。

一个🌰:
将文本中的内容读取出来,输出到控制台。

既然我们知道,上面的乱码是因为英文和中文占用的字节数不同引起的,那我们要是知道了整个文件占用的字节长度,那么不就一次性可以读取出来了。恰好FileInputStream提供了这样一个方法(available),让我们可以获取到整个文件所占用的字节数。

public static void printConsole(String filePath) throws IOException {

  FileInputStream inputStream = null;
  try {
    inputStream = new FileInputStream(filePath);

       // 获取到整个文本占用的整个字节数
    int available = inputStream.available();

    // 声明数组
    byte[] bytes = new byte[available];

    // 读取数据
    int readLength = inputStream.read(bytes, 0, available);
    String s = new String(bytes, StandardCharsets.UTF_8);

    System.out.println("读取到的长度:" + readLength + "  available:" + available);
    System.out.println("读取到的内容: " + s);
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != inputStream) {
      inputStream.close();
    }
  }
}

结果:

读取到的长度:30  available:30
读取到的内容: abcdef一个程序员的成长

这样的话,我们就可以读取到文本中完整的内容了。只有了解了这些,那么才会了解我们的写文件下载的时候为什么要判断 读取到的字节数 !=-1 这样的操作,不然真的很难记住。


字符输入流

FileReader

1、构造方法:

public FileReader(String fileName){};

public FileReader(File file){};

public FileReader(FileDescriptor fd){};

2、read方法:

public int read(){};

public int read(char cbuf[], int offset, int length){};

public int read(char cbuf[]){};

3、文件读取:

FileReader的read方法读取出来的是一个独立的字符(char),所以面对英文和中文的混合,我们不会因为占用字节的不同从而导致出现乱码的情况。

文本:

abcdef一个程序员的成长

读取内容的代码:

public static void method_01(String filePath) throws IOException {

  FileReader fr = null;
  try {
    fr = new FileReader(filePath);

    int c;
    // 每次读取一个字符,等于-1即表示文件读取到末尾
    while ((c = fr.read()) != -1) {
      System.out.println((char) c);
    }
  } catch (FileNotFoundException e) {
    e.printStackTrace();
  } finally {
    if (null != fr) {
      fr.close();
    }
  }

结果:

a
b
c
d
e
f
一
个
程
序
员
的
成
长

根据结果可以看出,read()读取出来的每次就是一个单独的字符,那么另外两个read方法跟字节流读取都是一样的。

更多内容请关注微信公众号:

查看原文

赞 10 收藏 5 评论 0

bingfeng 发布了文章 · 2020-11-25

为了一个HTTP请求问题,差点和iOS干起来

本次斗殴事件起因全部归iOS,为啥这么说,http请求都不会发,瞎写的什么玩意(ps:他应该不会看到...)。

在处理本次冲突中,意外发现了另外一个存在已久的bug,我们先说说这个玩意,再说我们之间的恩怨。因为这是息息相关的。

SpringBoot中的过滤器

过滤器这东西应该很常见了,但是你的过滤器真的起到拦截的作用了,这里就算你起到拦截的作用了,但是你的过滤器能拦截到指定的路径吗?先看一下我原始写法。

谨慎参考:

@WebFilter(filterName = "baseFilter", urlPatterns = "/*")
public class BaseFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

首先这里说下,如果你这是特别单纯的加个@WebFilter就以为ok了,那我告诉你,脸会被打的很疼的。

因为这个注解是servlet的,所以你一定要记得在启动类上加@ServletComponentScan此注解,这样在应用启动的时候,过滤器才会被扫描到。

我们写了一个Controller的接口访问了下,可以看到拦截器确实拦截到了我们的请求。

你以为的只是你以为

我们项目有时候大了,不知道引入了什么东西,有时候会导致这个过滤器呢就无法被注入,看到那行报错呢可能脑子还没反应过来,但是CV大法已经打开了度娘,找到了问题原因,度娘说你加个@Commponent注解好了。然后也确实好了,然后接下来他都如何操作。

@Component
@WebFilter(filterName = "baseFilter", urlPatterns = "/user/*")
public class BaseFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        HttpServletRequest request = (HttpServletRequest) req;

        String url = request.getRequestURL().toString();

        System.out.println(url);
        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

然而,不巧的是加了@Component注解虽然解决了问题,但是呢urlPatterns拦截的指定路径却没有生效。

我这里是一个pub开头的请求,拦截器拦截的user开头的,然后如下:

他居然将所有的请求给我拦截了下来,不是我想象的那样,那我们该如何解决这种问题呢?往下看同学。

SpringBoot如何注入过滤器

这里我就不列举众多的注入方式了,以免混淆大家,我就直接告诉你们怎么正确注入就ok了,本人已经亲测,而且管理起来很是方便。

过滤器写法

过滤器除了实现Filter之外,不要加任何的东西,就是这么简单。

public class BaseFilter implements Filter {


    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) {

        HttpServletRequest request = (HttpServletRequest) req;

        String url = request.getRequestURL().toString();

        System.out.println(url);
        System.out.println("baseFilter 拦截了 /*");
        
        filterChain.doFilter(req, resp);
    }
}

过滤器注入

我们这里直接通过配置类的方式将过滤器注入,这样呢,我们这里也一目了然,看到我们所有的过滤器,以及过滤器规则。

下面的这些参数都是基本配置,基本都是必填,name你就写过滤器的类名,首字母小写就好了,order就是过滤器的执行顺序,数字越小,越先执行。

这样我们一个完整的过滤器就配置好了。当你再访问/pub接口时,是不会被BaseFilter拦截到的。

这里也推荐大家以后尽量这样去配置。

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<BaseFilter> baseFilter() {
        FilterRegistrationBean<BaseFilter> filterBean = new FilterRegistrationBean<>();
        filterBean.setFilter(new BaseFilter());
        filterBean.setName("baseFilter");
        filterBean.addUrlPatterns("/user/*");
        filterBean.setOrder(1);
        return filterBean;
    }
}

我与iOS的一战

我们先看报的错,再来聊聊这次的锅我是怎么甩的

RequestRejectedException: The request was rejected because the URL was not normalized.

看到没因为网址不标准,导致请求被拒绝。

非说我接口有问题,本来想奋起反抗,看到对方比我身材威猛,想想还是抓到实质性证据在甩他吧。

既然说请求网址不正确,我猜测就是请求路径中是不是有什么猫腻,那我们就抓包呗。

最后在我们各种手段之下拿到了真凭实据。诸位法官请看:

他的请求路径:http://127.0.0.1:8080//user/list

他的请求路径中出现了双斜杠,这样肯定报错啊。这里需要说明下,报错是因为引入了Security安全框架。

既然已经确定问题,那我必须奋起反抗,找他甩锅,当他看到这个时候,对吧自己也无话可说,只能默默的把锅背上。

就这样我这次又顺利的甩锅成功。

解决与反思

虽然锅甩出去了,但是问题还是要解决的。

其实按正常逻辑来说,不管我们引入了什么东西,只要请求路径正确,及时路径中出现再多的斜杠,我们也应该做好处理,不能影响用户的访问。所以我们就通过过滤器就行一个处理。

public class UriFormatFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain filterChain)
            throws ServletException, IOException {

        // 路径隔离符号
        String separateSymbol = "/";

        String uri = req.getRequestURI();
        StringBuilder newUrl = new StringBuilder();

        String[] split = uri.split(separateSymbol);

        for (String s : split) {
            if (StringUtils.isNotBlank(s)) {
                newUrl.append(separateSymbol).append(s);
            }
        }

        req = new HttpServletRequestWrapper(req) {
            @Override
            public String getRequestURI() {
                return newUrl.toString();
            }
        };
        filterChain.doFilter(req, res);
    }
}

最后将过滤器注入

这里order为啥写-100,如果你写1,你以为它会是第一个执行,其实不然,在执行他之前,可能框架的一些过滤器会先执行,所以为了保险起见,我们就设置为-100,确保请求进来之后先走它。

@Bean
public FilterRegistrationBean<UriFormatFilter> uriFormatFilter() {
    FilterRegistrationBean<UriFormatFilter> filterBean = new FilterRegistrationBean<>();
    filterBean.setFilter(new UriFormatFilter());
    filterBean.setName("uriFormatFilter");
    filterBean.addUrlPatterns("/*");
    filterBean.setOrder(-100);
    return filterBean;
}

注意

如果你在过滤器中注意一些Mapper、Service之类的话,可能会出现问题,调用的时候被注入的对象可能是个null,这就涉及到类的加载顺序,我就不在这里bibi了,真有人遇到了再说。反正我已经解决了[Doge]。

参考文章:

https://blog.csdn.net/chenmen...
https://blog.csdn.net//qq_300...

更多精彩内容请关注微信公众号:一个程序员的成长

查看原文

赞 2 收藏 0 评论 1

bingfeng 发布了文章 · 2020-11-17

我的服务器接连被黑客攻击,我好难

最近在几台测试服务器上跑一些业务数据,但是过了几天服务器突然变的奇慢无比,敲个命令就像卡壳一样,有时候甚至都连接不上,最开始我以为是网络问题,就强行kill掉进程,重新跑一下进程,最后实在受不了,就上阿里云后台说重启下服务器吧,结果看到CPU的占用率已经到达了100%。

这是CPU是恢复正常之后截图

看到这样我以为是因为我跑了大量的数据导致CPU飙升的,然后我就kill到了进程,并且重启了服务器,启动之后CPU正常,我以为就是我跑数据导致的,此后我就没用这台服务器跑数据了,我就单纯的以为这就算处理好了,没想到等我过几天部署测试包的时候发现,又是奇慢无比,看了下CPU占用率又是99.9%,事实证明我还是太年轻了。

终于忍无可忍,就深究下吧,先用linux命令(top)查看下,到底是什么占用了这么多CPU资源,结果如下图:

看到的瞬间第一感觉就是,这是啥玩意,这是谁部署的。问了下平时身旁的背锅侠,好像也不是他弄的,看来这次这锅是甩不了了,那就只能...

What?中病毒了?

根据过往的经验,这玩意不应该是点了网页上的小姐姐才会发生的事情吗?我这为什么也就中毒了。

这东西是啥

既然已经中毒了,那就来看看这是什么东西吧。

挖矿病毒,大家身在同一个工地都应该或多或少都听过挖矿吧,要是挖到个币,就不用苦逼写文章了,话说回来,要想挖币需要很强的计算资源,那么也就需要众多的服务器来支撑,这里面有些逼呢又不想投入太多,只能通过一些恶毒的手段,将脚本植入的我们的服务器,比如我们需要安装一个Redis,那么像我英文不太好的人,可能第一时间不是去官网,而是找度娘,如果你正好找的资源里面被人植入了这种东西,那么很不凑巧,你的服务器可能要帮别人搞点东西了。

如何处理这种病毒

既然中了这种病毒,导致我们的服务器很卡,那么肯定要将它杀死,可能没怎么接触过Linux的同学,已经考虑重装镜像了。

其实大可不必。

首先呢我们找到此进程将其kill掉。

接下来删除kdevtmpfsi文件,一般在tmp目录下

还有一个文件(kinsing)我们也要将其杀死删掉

这里需要注意,我试了几台服务器kinsing文件可能存在不同的位置,但是我们可以通过上面的方式看到文件路径,将其找到删除就好。

这个时候我们通过top查看CPU的使用率,可以发现已经正常了


就在我以为万事大吉的时候,现实又给了我沉痛的一击,没过几分钟CPU使用率又到了99.96%,我要崩溃了。

跟度娘经过深入交流之后,终于知道了问题所在。

查看服务器的定时任务,crontab -l,大概会看到如下的任务,没有就不用管了,你可以将此ip查一下,一般都是国外的ip。

我们将这些定时任务删除即可,这个链接就是在我们kill到进程、删除文件之后进行下载,然后通过脚本再跑起来。

这也就是为什么我明明杀死了病毒,没过多久又出现了的原因。

到这里我们已经完全处理到此病毒了,如果你用的是阿里云ECS,当遇到这种东西的时候,其实会短信通知你,只不过当时太年轻没怎么在意,另外服务器端口默认是22,自己最好改个端口,不然很容易被恶人攻击。

现在服务器敲起来贼爽,再也不卡顿了。


更多精彩内容请关注微信公众号:一个程序员的成长

查看原文

赞 7 收藏 2 评论 13

bingfeng 赞了文章 · 2020-11-05

岁月如何才能静好

静中有争,稳中有急

欲争先静,宁静致运
急事需稳,稳中求胜

胜中有生,败中有文

欲胜先生,活着就是胜利
败亦求文,挫折不足为训

我今天47,找了第三份工作,coding依旧。

第一次写程序在88年,忘不了 a=a+1 带来的惊奇。

1995年,大学时,通宵玩三国,有一次良心发现,用两个通宵用 Matlab 3 改写了学校 IBM MainFrame实现了机器里的有限元的程序,同样的程序,但在PC 386上跑。传言那个IBM竞有夸张的 2G 内存,三个计算机教室共用同一台机器,我那可怜的 386 只有 4M内存,还是扩容后的。

工作后第一年抄了一个公式编辑器,把数学公式编辑+计算求值+存到Metafile里,领导给涨了工资,内心有悔。

换了个工作,东拼西凑,抄了个ORM+CodeGenerator, 靠这个混了一段时间,一混混到了2019年。

2019年,觉得老罗挺可怜,买了他两个手机和一些周边,可惜还是没挡住他去卖烟...

不由让我想起蓝淀厂,火器营,清水河边...

就快新年了,去年立的Flag倒下一片,也不都怨中美贸易战,自己懒的事我就不提了...

新年再立一愿,搞个自己的网站。做出来再说,做不出来就当我没说!

本文参与了 SegmentFault思否征文「2019 总结」,欢迎正在阅读的你也加入。
查看原文

赞 5 收藏 0 评论 0

bingfeng 回答了问题 · 2020-10-30

为什么说二级索引不唯一,不唯一是指什么不唯一呢?

这就涉及到聚簇索引和非聚簇索引了,聚簇索引主键id和数据是存储在一行的,但是非聚簇索引,也就是我们自己创建的索引,它的索引和数据是分开的,主键id是不可能重复的,所以对于聚簇索引来说那就是唯一的,但是对于二级普通索引(聚簇索引)来说,创建的索引是非常有可能出现重复值得,所以二级索引是不唯一的,但是如果你的索引是唯一索引,那么这个索引就是唯一的。

个人理解。

关注 5 回答 4

bingfeng 赞了文章 · 2020-09-28

Mybatis批量更新三种方式

Mybatis实现批量更新操作
方式一:

<update id="updateBatch"  parameterType="java.util.List">  
    <foreach collection="list" item="item" index="index" open="" close="" separator=";">
        update tableName
        <set>
            name=${item.name},
            name2=${item.name2}
        </set>
        where id = ${item.id}
    </foreach>      
</update>

但Mybatis映射文件中的sql语句默认是不支持以" ; " 结尾的,也就是不支持多条sql语句的执行。所以需要在连接mysql的url上加 &allowMultiQueries=true 这个才可以执行。
方式二:

<update id="updateBatch" parameterType="java.util.List">
        update tableName
        <trim prefix="set" suffixOverrides=",">
            <trim prefix="c_name =case" suffix="end,">
                <foreach collection="list" item="cus">
                    <if test="cus.name!=null">
                        when id=#{cus.id} then #{cus.name}
                    </if>
                </foreach>
            </trim>
            <trim prefix="c_age =case" suffix="end,">
                <foreach collection="list" item="cus">
                    <if test="cus.age!=null">
                        when id=#{cus.id} then #{cus.age}
                    </if>
                </foreach>
            </trim>
        </trim>
        <where>
            <foreach collection="list" separator="or" item="cus">
                id = #{cus.id}
            </foreach>
        </where>
</update>

这种方式貌似效率不高,但是可以实现,而且不用改动mysql连接
效率参考文章:https://blog.csdn.net/xu19166...
方式三:
临时改表sqlSessionFactory的属性,实现批量提交的java,但无法返回受影响数量。

public int updateBatch(List<Object> list){
        if(list ==null || list.size() <= 0){
            return -1;
        }
        SqlSessionFactory sqlSessionFactory = SpringContextUtil.getBean("sqlSessionFactory");
        SqlSession sqlSession = null;
        try {
            sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
            Mapper mapper = sqlSession.getMapper(Mapper.class);
            int batchCount = 1000;//提交数量,到达这个数量就提交
            for (int index = 0; index < list.size(); index++) {
                Object obj = list.get(index);
                mapper.updateInfo(obj);
                if(index != 0 && index%batchCount == 0){
                    sqlSession.commit();
                }                    
            }
            sqlSession.commit();
            return 0;
        }catch (Exception e){
            sqlSession.rollback();
            return -2;
        }finally {
            if(sqlSession != null){
                sqlSession.close();
            }
        }
        
}

其中 SpringContextUtil 是自己定义的工具类 用来获取spring加载的bean对象,其中getBean() 获得的是想要得到的sqlSessionFactory。Mapper 是自己的更具业务需求的Mapper接口类,Object是对象。
总结
方式一 需要修改mysql的连接url,让全局支持多sql执行,不太安全
方式二 当数据量大的时候 ,效率明显降低
方式三 需要自己控制,自己处理,一些隐藏的问题无法发现。

附件:SpringContextUtil.java

@Component
public class SpringContextUtil implements ApplicationContextAware{

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    public static Object getBean(Class T){
        try {
            return applicationContext.getBean(T);
        }catch (BeansException e){
            return null;
        }
    }

    public static Object getBean(String name){
        try {
            return applicationContext.getBean(name);
        }catch (BeansException e){
            return null;
        }
    }
}
查看原文

赞 6 收藏 4 评论 0

认证与成就

  • 获得 59 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-01-02
个人主页被 3.5k 人浏览