Java Web 项目学习(三) 过滤敏感词 前缀树 反射 类加载

 

 

 

 

指针2 一直不回头,指针3小范围的回头。

 

 

 敏感词可以定义在数据库中,也可以定义在一个文件中。在resource下建立sensitive-works.txt 将敏感词按行书写(一个敏感词占一行)

写一个工具类SensitiveFilter ,在Util包下,完成上述三个工作。

@Component
public class SensitiveFilter {
    /**
     *定义前缀树结构
     */
    private class TrieNode{
        //关键字结束标识
        private boolean isKeyWordEnd =false;
        //子节点 (key 是下级字符,value 是下级子节点)
        private Map <Character,TrieNode> subNodes = new HashMap<>();

        public boolean isKeyWordEnd() {
            return isKeyWordEnd;
        }

        public void setKeyWordEnd(boolean keyWordEnd) {
            isKeyWordEnd = keyWordEnd;
        }

        //添加子节点
        public void addSubNode(Character c, TrieNode node){
            subNodes.put(c,node);
        }
        //获取子节点
        public TrieNode getSubNode(Character c){
            return subNodes.get(c);
        }
    }

    /**
     * 根据敏感词,初始化前缀树
     */
    //定义logger
    private static final Logger  logger=  LoggerFactory.getLogger(SensitiveFilter.class);
    //定义常量 替换符
    private static final String REPLACEMENT ="*";

    // 根节点
    private TrieNode rootNode = new TrieNode();

    //在程序启动的时候,有且只有调用一次,读取敏感词。
    @PostConstruct  //这个注解的意思:这是一个初始化方法,当容器实例化调用bean之后,@PostConstruct修饰的方法会被自动调用
    private void init(){
        //获取类加载器,类加载器是从类路径(.class路径下)下去加载资源。
        // 程序一编译,所有的都会编译到.class中去。包括resource
        try(
                InputStream is =this.getClass().getClassLoader().getResourceAsStream("sensitive-words.txt");
                //字节流转化为字符流, 字符流转化为缓冲流
                BufferedReader reader = new BufferedReader(new InputStreamReader (is));
        ){
            String keyword;
            while ((keyword =reader.readLine()) != null){
                //添加到前缀树中。
                this.addKeyWord(keyword);
            }
        }catch (IOException e){
            logger.error("加载敏感词文件失败"+e.getMessage());
        }

    }

    //添加敏感词到前缀树中去
    private void addKeyWord(String keyword) {
        TrieNode tempNode = this.rootNode;
        for (int i = 0 ;i < keyword.length() ;i++){
            Character c = keyword.charAt(i);
            TrieNode subNode = tempNode.getSubNode(c);
            if (subNode==null){
                //初始化子节点
                subNode = new TrieNode();
                tempNode.addSubNode(c,subNode);
            }
            //指向子节点,进行循环
            tempNode =subNode;
            if (i==keyword.length()-1){
                tempNode.setKeyWordEnd(true);
            }
        }
    }


    //判断是否是符号
    private boolean isSymbol(Character c){
        //CharUtils.isAsciiAlphanumeric()判断是否是普通的字符
        //0x2E80 ~ox9FFF 是东亚的文字范围,东亚的文字范围不人为是特殊符号。
        //因此c不是普通字符,并且在 东亚文字范围之外,认为是特殊字符
        return !CharUtils.isAsciiAlphanumeric(c) && (c<0x2E80 || c>0x9FFF);
    }
    /**
     * 过滤敏感词
     * @param text   待过滤文本
     * @return  过滤后文本
     */
    public String filter (String text){
        if (StringUtils.isBlank(text)){
            return null;
        }
        //指针1
        TrieNode tempNode = this.rootNode;
        //指针2
        int begin =0;
        //指针3
        int cur = 0;
        //结果
        StringBuilder sb = new StringBuilder();

        while (cur < text.length()){
            char c = text.charAt(cur);
            //跳过符号    ※开※票※
            if(isSymbol(c)){
                //如果指针1在根节点,此符号计入结果,指针2向下走
                if(tempNode == rootNode){
                    sb.append(c);
                    begin++;
                }
                cur++;
                continue; //不加不行,到这(不继续往下)返回继续下个循环了。
            }
            //查看当前字符是否是敏感词
            tempNode = tempNode.getSubNode(c);
            if (tempNode == null){ //不是敏感词
                begin = cur;
                sb.append(text.charAt(begin));//向结果添加
                cur = ++begin; //指针2。3向下走
                tempNode =rootNode; //指针1 归位
            }else if(tempNode.isKeyWordEnd){//发现敏感词,替换begin-cur 的字符串
                sb.append(REPLACEMENT);
                begin = ++cur;
                tempNode =rootNode; //指针1 归位
            }else {//继续检查下一个字符
                cur++;
            }
        }

        //将最后一批字符记录结果。 (因为不是敏感词,因此指针2到end后,指针1不到end,这部分没有记录)
        sb.append(text.substring(begin));
        return  sb.toString();
    }
}

 

知识:

@PostConstruct

不是Spring提供的。是Java自己的注解。

@PostConstruct该注解被用来修饰一个非静态的void()方法。被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。  

PostConstruct一般写在在构造函数之后执行,init()方法之前。

通常我们会是在Spring框架中使用到@PostConstruct注解 该注解的方法在整个Bean初始化中的执行顺序:Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)

应用:在静态方法中调用依赖注入的Bean中的方法。

 

类加载器

Class 对象

class对象是没办法用new关键字得到的,因为它是jvm生成用来保存对应类的信息的。当我们定义好一个类文件并编译成.class字节码后,编译器同时为我们创建了一个Class对象并将它保存.class文件中。同时在jvm内部有一个类加载机制,即在需要的时候(懒加载)将.class文件和对应的Class对象加载到内存中。例如,Person.java文件编译成Person.class的同时也会产生一个对应的Class对象。

类的加载过程

加载:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。就是将class文件读入内存,并为之创建一个class对象。(任何类被使用时,系统都会建立一个class对象)

链接:验证字节码的安全性和完整性,准备阶段正式为静态域分配存储空间,注意此时只是分配静态成员变量的存储空间,不包含实例成员变量,如果必要的话,解析这个类创建的对其他类的所有引用。

1)验证:是否有正确的内部结构,并和其他类协调一致;

2)准备:负责为类的静态成员分配内存,并设置默认初始化值(将静态成员变量从类里面单独拿出来,把它放到内存方法区当中的数据共享区当中,再为它标记一个可属,告诉属于哪个类的);

3)解析:将类的二进制数据中的符号引用替换为直接引用;

初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量。类的初始化时机

1) 创建类的实例 new

2) 类的静态变量,或者为静态变量赋值

3) 类的静态方法

4) 使用反射方式来强制创建某个类或接口对应的java.lang.Class对象

5) 初始化某个类的子类 (父类比子类先进内存,所以可以看出Object类是最先进内存的)

6) 直接使用java.exe命令来运行某个主类

 

类加载器的组成

1)Bootstrap ClassLoader 启动类加载器(null):也被称为引导加载器,负责java核心类的加载(比如System,String等)。该类加载器主要职责是将JAVA_HOME路径下的\lib目录中能被虚拟机识别的类库(比如rt.jar)加载到虚拟机内存中。Java程序无法直接引用该类加载器

2)Extension ClassLoader 扩展类加载器:负载JRE的扩展目录中的jar包加载

3)Application ClassLoader 负责在JVM启动时加载来自java命令的class文件,已经classpath环境变量所指定的jar包和类路径。

 

import java.util.*;
class Initable {
  //编译期静态常量
  static final int staticFinal = 47;
  //非编期静态常量
  static final int staticFinal2 =
    ClassInitialization.rand.nextInt(1000);
  static {
    System.out.println("Initializing Initable");
  }
}

class Initable2 {
  //静态成员变量
  static int staticNonFinal = 147;
  static {
    System.out.println("Initializing Initable2");
  }
}

class Initable3 {
  //静态成员变量
  static int staticNonFinal = 74;
  static {
    System.out.println("Initializing Initable3");
  }
}

public class ClassInitialization {
  public static Random rand = new Random(47);
  public static void main(String[] args) throws Exception {
    //字面常量获取方式获取Class对象
    Class initable = Initable.class;
    System.out.println("After creating Initable ref");
    //不触发类初始化
    System.out.println(Initable.staticFinal);
    //会触发类初始化
    System.out.println(Initable.staticFinal2);
    //会触发类初始化
    System.out.println(Initable2.staticNonFinal);
    //forName方法获取Class对象
    Class initable3 = Class.forName("Initable3");
    System.out.println("After creating Initable3 ref");
    System.out.println(Initable3.staticNonFinal);
  }
}

调用Initable.staticFinal变量时没有触发初始化,这是因为staticFinal属于编译期静态常量,在编译阶段通过常量传播优化的方式将Initable类的常量staticFinal存储到了一个称为NotInitialization类的常量池中,在以后对Initable类常量staticFinal的引用实际都转化为对NotInitialization类对自身常量池的引用,所以在编译期后,对编译期常量的引用都将在NotInitialization类的常量池获取,这也就是引用编译期静态常量不会触发Initable类初始化的重要原因。

调用了Initable.staticFinal2变量后就触发了Initable类的初始化,注意staticFinal2虽然被static和final修饰,但其值在编译期并不能确定,因此staticFinal2并不是编译期常量,使用该变量必须先初始化Initable类。

Initable2和Initable3类中都是静态成员变量并非编译期常量,引用都会触发初始化

双亲委托模式

某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次追溯。如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。 双亲委托机制的最大的好处就是可以确保是安全的。这种机制保证了不会出现用户自己能定义java.lang.Object类的情况出现。类加载器除了用于加载类,也是安全的最基本屏障。

 

要点:

  • 获取Class对象引用的方式3种,通过继承自Object类的getClass方法,Class类的静态方法forName以及字面常量的方式”.class”。
  • 其中实例类的getClass方法和Class类的静态方法forName都将会触发类的初始化阶段,而字面常量获取Class对象的方式则不会触发初始化。
  • 初始化是类加载的最后一个阶段,也就是说完成这个阶段后类也就加载到内存中(Class对象在加载阶段已被创建),此时可以对类进行各种必要的操作了(如new对象,调用静态成员等),注意在这个阶段,才真正开始执行类中定义的Java程序代码或者字节码。

必须对类进行初始化的5种场景:

  • 使用new关键字实例化对象时、读取或者设置一个类的静态字段(不包含编译期常量)以及调用静态方法的时候,必须触发类加载的初始化过程(类加载过程最终阶段)。
  • 使用反射包(java.lang.reflect)的方法对类进行反射调用时,如果类还没有被初始化,则需先进行初始化,这点对反射很重要。
  • 初始化一个类的时候,如果其父类还没进行初始化则需先触发其父类的初始化。
  • 当Java虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类),虚拟机会先初始化这个主类
  • 当使用JDK 1.7 的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应类没有初始化时,必须触发其初始化(这点看不懂就算了,这是1.7的新增的动态语言支持,其关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,这是一个比较大点的话题,这里暂且打住)

 

 

反射

JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象。

  1. Object类中的getObject()方法
  2. Class类中的方法(将类名作为字符串传递给Class类中的静态方法forName即可)。
  3.  类名.class 获取到字节码文件对象(任意数据类型都具备一个class静态属性,看上去要比第一种方式简单)。
// 1.对象的引用.getClass();    // 已经知道该类的对象通过getClass方法
        Class<?> cls1 = person.getClass();
        // 3.类名.class--->实际在告诉我们任何一个类都有一个隐含的静态成员变量class
        Class<?> cls2 = Person.class;
        // 2.Class.forName(URL);
        Class<?> cls3 = Class.forName("com.newer.cn.Person");
        // 通过加载器加载类
        Class<?> cls4 = TestPerson.class.getClassLoader().loadClass("com.newer.cn.Person");
 
        
        //不管c1  or c2都代表了Foo类的类类型,一个类只可能是Class类的一个实例对象
        System.out.println(c1 == c2);
        结果:true

 

this.getClass().getClassLoader().getResourceAsStream(path)

 class是指当前类的class对象,getClassLoader()是获取当前的类加载器(3中当中的一种)

getResourceAsStream(path)是用来获取资源的,而类加载器默认是从classPath下获取资源的,class文件,

所以这段代码总的意思是通过类加载器在classPath目录下获取资源.并且是以流的形式

 

报错:

  1. 出现在用Maven使用clean中。
     No valid Maven installation found. Either set the home directory in the configuration dialog or set the M2_HOME environment variable on your system. 
    原因是配置错了Maven。因为我解压了两个地方的maven 结果我的settings file和 Maven 的地址并不匹配。 

     修改后运行成功。

     

     

  2. Maven编译时报错
    INFO] --- maven-resources-plugin:3.2.0:resources (default-resources) @ community ---
    [INFO] Using 'UTF-8' encoding to copy filtered resources.
    [INFO] Using 'UTF-8' encoding to copy filtered properties files.
    [INFO] Copying 1 resource
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD FAILURE
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  1.176 s
    [INFO] Finished at: 2021-06-08T20:15:30+08:00
    [INFO] ------------------------------------------------------------------------
    [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources) on project community: Input length = 1 -> [Help 1]
    [ERROR] 
    [ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
    [ERROR] Re-run Maven using the -X switch to enable full debug logging.
    [ERROR] 
    [ERROR] For more information about the errors and possible solutions, please read the following articles:
    [ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

    方法一:配置编码方式

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    方法二:修改 maven-plugins 版本 (注意这里不是在依赖中修改。而是在后面的build) 

    <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
    
                <!--在这里修改版本-->
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-resources-plugin</artifactId>
                    <version>2.4.3</version>
                </plugin>
                <!---->
    
            </plugins>
        </build>

    参考:https://blog.csdn.net/qq_35526165/article/details/112203197

 

posted @ 2021-06-09 14:07  白清欢  阅读(236)  评论(0编辑  收藏  举报