Home Tags Posts tagged with "面试"

面试

在我们的框架使用中大量运用到反射机制,我们以后的开发也必然会使用到反射,那么反射的性能分析就很有必要了,这篇文章将通过代码实例的方式为您对比反射和new之间调用方法及创建对象的性能,以及小小的优化(代码地址:Desktop\个人学习代码\Reflection)

 

package com.yang.reflection;


import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

/**
 * 测试New调用对象方法和反射调用对象方法的性能
 * test01:New
 * test02:Reflection
 * test03:关闭安全检查的反射
 * 注:如不关闭安全检查则无法修改私有属性
 */
public class ReflectionVsNew_Method {

    public static void test01(){
        long startTime = System.currentTimeMillis();
        User user = new User();
        for (int i = 0; i < 1000000000; i++) {
            user.getName();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("New方法所消耗的时间为:"+(endTime-startTime)+"ms");
    }

    /**
     * @throws ClassNotFoundException
     * @throws NoSuchMethodException
     *
     * getDeclaredMethod(name,parameterTypes)
     * @param name 方法名
     * @param parameterTypes 方法所需要传入的参数
     *
     * method.invoke(object,args)
     * @param object 执行方法的对象
     * @param args 方法所需要传入的参数
     */
    public static void test02() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        long startTime = System.currentTimeMillis();
        User user = new User();
        Class c1= Class.forName("com.yang.reflection.User");
        Method getName = c1.getDeclaredMethod("getName", null);
        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user,null);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("反射方法所消耗的时间为:"+(endTime-startTime)+"ms");
    }

    //反射方式调用,关闭检测
    public static void test03() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        long startTime = System.currentTimeMillis();
        User user = new User();
        Class c1= Class.forName("com.yang.reflection.User");
        Method getName = c1.getDeclaredMethod("getName", null);
        getName.setAccessible(true);
        for (int i = 0; i < 1000000000; i++) {
            getName.invoke(user,null);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("关闭安全检查后,反射方法所消耗的时间为:"+(endTime-startTime)+"ms");
    }

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        test01();
        System.out.println("===========================");
        test02();
        System.out.println("===========================");
        test03();
    }

}

总结:
反射比new大概慢了400倍
关闭安全检查后,大概慢了200倍

反射调用方法四部曲
    1.传入需要调用方法的所属对象
    2.获取该对象对应的Class对象
    3.通过Class对象得到需要调用的方法
    4.执行方法名.invoke()

0 36

运行环境和另一篇博客(Zset)的一样,读者需要可自行查找:http://yangbili.co/redis%e6%88%90%e7%bb%a9%e6%8e%92%e8%a1%8c%e6%a6%9c%e5%ae%9e%e7%8e%b0%ef%bc%88set%ef%bc%89/

废话不多说,直接上代码:

 



import com.alibaba.fastjson.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.List;
import java.util.Set;


/**
 * 2021.4.2
 * 学习了Redis的基本数据类型,决定在Springboot环境下进行使用
 *
 * 目的:实现对其他人的查询,添加,删除及共同好友的查看
 * 采用Set数据结构,
 */

@SpringBootTest
public class YangSetTest {


    //注入rediTemplate
    @Autowired
    private RedisTemplate<String,String> redisTemplate;


    //创建用户A
    private final String ANAME = "咩咩咩";

    //创建用户B
    private final String BNAME = "大黑狗";

    /*1.批量添加关注列表,模拟从数据库中获得*/
    @Test
    void batchAdd() {
        Long start = System.currentTimeMillis();
        for(int i = 0; i < 10; i++){
            redisTemplate.opsForSet().add(ANAME,i+"号明星");
        }
        System.out.println("添加成功");
        System.out.println("批量新增关注所消耗的时间:"+(System.currentTimeMillis()-start));
    }

    /*2.关注某人*/
    @Test
    void add(){
        redisTemplate.opsForSet().add(BNAME,"电气鼠");
        System.out.println("关注成功");
    }

    /*3.获取当前的关注总数(基数)*/
    @Test
    void getCardNum(){
        Long size = redisTemplate.opsForSet().size(ANAME);
        System.out.println(ANAME+"的关注总数为:"+size);
    }

    /*4.取消单个关注*/
    @Test
    void remmove(){
        redisTemplate.opsForSet().remove(ANAME,"3号关注");
        System.out.println("取消成功");
    }

    /*5.查找单个关注*/
    @Test
    void selete(){
        Boolean member = redisTemplate.opsForSet().isMember(ANAME, BNAME);
        if(member == true){
            System.out.println("名为"+BNAME+"的人是"+ANAME+"的关注!");
        }
        else {
            System.out.println("对方不是+"+ANAME+"的关注!");
        }
    }

    /** 6.事务,A添加B,B同意
     *  那么A的列表和B的列表都要变化
     *  如果只有一个有变化,那么添加失败
     */
    @Test
    void addMulti() {
        //开启乐观锁(模拟多线程下)
        //这里假设B是明星,多个人关注B
        redisTemplate.watch(BNAME);
        //开启事务
        redisTemplate.multi();
        try {
            //A关注B
            redisTemplate.opsForSet().add(ANAME, BNAME);
            //B的粉丝列表也要变化;
            redisTemplate.opsForSet().add(BNAME, ANAME);
            System.out.println("关注成功!");
        } catch (Exception e) {
            System.out.println("出现错误!关注失败,事务取消");
            //取消事务,事务队列中的所有命令都不会被执行
            redisTemplate.discard();
            //解锁
            redisTemplate.unwatch();
        }
    }

    /*7.看破红尘,取消所有关注*/
//    @Test
//    void removerAll(){
//        重复调用remove()方法
//        redisTemplate.opsForSet().remove(ANAME,"","");
//    }

    /*8.一览天下,获取用户关注列表*/
    @Test
    void lookMem(){
        //调用psForSet().members
        Set<String> members = redisTemplate.opsForSet().members(BNAME);
        //使用JSON.toJSONString(Object)将对象转换为JSON字符串
        System.out.println(JSON.toJSONString(members));
    }

    /*9.宠幸后宫,翻牌子随机获取一个关注对象*/
    @Test
    void randomLookMem(){
        //调用psForSet().members
        String s = redisTemplate.opsForSet().randomMember(ANAME);
        System.out.println(s);
    }

    /*10.多人运动,随机选出关注列表中的元素,获取多个关注对象*/
    @Test
    void randomLookMems(){
        //个数为:3
        List<String> strings = redisTemplate.opsForSet().randomMembers(ANAME,3);
        System.out.println(strings);
    }

    /*11.推荐关注,将用户A关注列表中的对象推送给B*/
    @Test
    void move() {
        //调用opsForSet().move()方法
        redisTemplate.opsForSet().move(ANAME,"8号明星",BNAME);
        System.out.println("推荐关注成功!");
    }

    /**
     * 12.并,差,交及的使用
     *
     * 交集,可做共同关注
     * 实验步骤:
     * 1.先往用户A,B中添加共同关注对象"电气鼠"
     * 2.查看二者的共同关注(也可以获得多个,略)
     */
    @Test
    void comment(){
        Set<String> intersect = redisTemplate.opsForSet().intersect(ANAME, BNAME);
        System.out.println(ANAME+"和"+BNAME+"的共同关注对象为:"+intersect);
        System.out.println(ANAME+"和"+BNAME+"都是LSP...");
    }

    /*并集,统计两个用户的关注对象之合*/
    @Test
    void union(){
        Set<String> union = redisTemplate.opsForSet().union(ANAME, BNAME);
        System.out.println(ANAME+"和"+BNAME+"的关注合列表为:"+union);
    }

    /*差集,可以判断用户之间的关注差异*/
    @Test
    void differ(){
        Set<String> difference = redisTemplate.opsForSet().difference(ANAME, BNAME);
        System.out.println(ANAME+"和"+BNAME+"的关注列表差集为:"+difference);
        System.out.println("根据此功能也可以推送让B用户关注其他");
    }

}

0 38

1.什么是双亲委派机制?

双亲委派机制其实是说,当一个类加载器收到了类加载的请求时,它并不会立即去加载指定的类,而是要把这个请求委托给自己的父类去加载(因为类加载器都很懒),只有当父加载器加载不了时,该加载器才自己进行类加载

 

2.什么时候父加载器加载不了?(因为加载器各司其职)

这就涉及到JAVA中四种类型的类加载器了,它们分别是:

  • Bootstrap ClassLoader(根加载器,主要是rt下)
  • Extention ClassLoader(扩展加载器,主要是ext下)
  • Application ClassLoader(应用加载器,主要是ClassPath下,自己写的代码)
  • User ClassLoader(自定义加载器,可加载指定路径的Class文件)

那么,一个用户自定义的类,是不可能被Extention ClassLoader和Bootstrap ClassLoader加载器加载的

 

 

3.为什么需要双亲委派?

  • 避免类的重复加载,当父加载器已经加载了该类,子加载器就不会再重新加载该类;
  • 保证安全性,防止核心JAVA API被修改(比如自己写个有害的Integer类);

4.”父子加载器”之间的关系是继承吗?

  • 注意:类加载器中的父子关系一般不会以继承实现,而是以组合来复用父类加载器的代码;
public abstract class ClassLoader {

        // The parent class loader for delegation

        private final ClassLoader parent;

    }

5.双亲委派是怎么实现的?

双亲委派模型对于保证Java程序的稳定运作很重要,但它的实现并不复杂。

实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法之中

protected Class<?> loadClass(String name, boolean resolve)

            throws ClassNotFoundException

        {

            synchronized (getClassLoadingLock(name)) {

                // First, check if the class has already been loaded

                Class<?> c = findLoadedClass(name);

                if (c == null) {

                    long t0 = System.nanoTime();

                    try {

                        if (parent != null) {

                            c = parent.loadClass(name, false);

                        } else {

                            c = findBootstrapClassOrNull(name);

                        }

                    } catch (ClassNotFoundException e) {

                        // ClassNotFoundException thrown if class not found

                        // from the non-null parent class loader

                    }



                    if (c == null) {

                        // If still not found, then invoke findClass in order

                        // to find the class.

                        long t1 = System.nanoTime();

                        c = findClass(name);



                        // this is the defining class loader; record the stats

                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);

                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);

                        sun.misc.PerfCounter.getFindClasses().increment();

                    }

                }

                if (resolve) {

                    resolveClass(c);

                }

                return c;

            }

        }


代码不难理解,主要就是以下几个步骤:

1、先检查类是否已经被加载过

2、若没有加载则调用父加载器的loadClass()方法进行加载

3、若父加载器为空则默认使用启动类加载器作为父加载器。(为空则是rt包下的类)

4、如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

 

6.如何主动破坏双亲委派机制?

知道了双亲委派模型的实现,那么想要破坏双亲委派机制就很简单了。

因为他的双亲委派过程都是在loadClass方法中实现的,那么想要破坏这种机制,那么就自定义一个类加载器,重写其中的loadClass方法,使其不进行双亲委派即可。

 

7.loadClass()、findClass()、defineClass()区别

lassLoader中和类加载有关的方法有很多,前面提到了loadClass,除此之外,还有findClass和defineClass等,那么这几个方法有什么区别呢?

  • loadClass() 就是主要进行类加载的方法,默认的双亲委派机制就实现在这个方法中。
  • findClass() 根据名称或位置加载.class字节码
  • definclass() 把字节码转化为Class

这里面需要展开讲一下loadClass和findClass,我们前面说过,当我们想要自定义一个类加载器的时候,并且像破坏双亲委派原则时,我们会重写loadClass方法。

那么,如果我们想定义一个类加载器,但是不想破坏双亲委派模型的时候呢

这时候,就可以继承ClassLoader,并且重写findClass方法。findClass()方法是JDK1.2之后的ClassLoader新添加的一个方法。

/**

     * @since  1.2

     */

    protected Class<?> findClass(String name) throws ClassNotFoundException {

        throw new ClassNotFoundException(name);

    }

这个方法只抛出了一个异常,没有默认实现。

JDK1.2之后已不再提倡用户直接覆盖loadClass()方法,而是建议把自己的类加载逻辑实现到findClass()方法中。

因为在loadClass()方法的逻辑里,如果父类加载器加载失败,则会调用自己的findClass()方法来完成加载。

所以,如果你想定义一个自己的类加载器,并且要遵守双亲委派模型,那么可以继承ClassLoader,并且在findClass中实现你自己的加载逻辑即可。

 

8.双亲委派被破坏的例子

双亲委派机制的破坏不是什么稀奇的事情,很多框架、容器等都会破坏这种机制来实现某些功能。

第一种被破坏的情况是在双亲委派出现之前。

由于双亲委派模型是在JDK1.2之后才被引入的,而在这之前已经有用户自定义类加载器在用了。所以,这些是没有遵守双亲委派原则的。

第二种,是JNDI、JDBC等需要加载SPI接口实现类的情况。

第三种是为了实现热插拔热部署工具。为了让代码动态生效而无需重启,实现方式时把模块连同类加载器一起换掉就实现了代码的热替换。

第四种时tomcat等web容器的出现。

第五种时OSGI、Jigsaw等模块化技术的应用。

 

 

9.为什么JNDI,JDBC等需要破坏双亲委派?

我们日常开发中,大多数时候会通过API的方式调用Java提供的那些基础类,这些基础类时被Bootstrap加载的。

但是,调用方式除了API之外,还有一种SPI的方式。

如典型的JDBC服务,我们通常通过以下方式创建数据库连接:

Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "1234");

在以上代码执行之前,DriverManager会先被类加载器加载,因为java.sql.DriverManager类是位于rt.jar下面的 ,所以他会被根加载器加载。

类加载时,会执行该类的静态方法。其中有一段关键的代码是:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这段代码,会尝试加载classpath下面的所有实现了Driver接口的实现类。

那么,问题就来了。

DriverManager是被根加载器加载的,那么在加载时遇到以上代码,会尝试加载所有Driver的实现类,但是这些实现类基本都是第三方提供的,根据双亲委派原则,第三方的类不能被根加载器加载。

那么,怎么解决这个问题呢?

于是,就在JDBC中通过引入ThreadContextClassLoader(线程上下文加载器,默认情况下是AppClassLoader)的方式破坏了双亲委派原则。

我们深入到ServiceLoader.load方法就可以看到:

public static <S> ServiceLoader<S> load(Class<S> service) {

        ClassLoader cl = Thread.currentThread().getContextClassLoader();

        return ServiceLoader.load(service, cl);

    }

第一行,获取当前线程的线程上下⽂类加载器 AppClassLoader,⽤于加载 classpath 中的具体实现类。

 

10.为什么Tomcat要破坏双亲委派

我们知道,Tomcat是web容器,那么一个web容器可能需要部署多个应用程序。

不同的应用程序可能会依赖同一个第三方类库的不同版本,但是不同版本的类库中某一个类的全路径名可能是一样的。

如多个应用都要依赖hollis.jar,但是A应用需要依赖1.0.0版本,但是B应用需要依赖1.0.1版本。这两个版本中都有一个类是com.hollis.Test.class。

如果采用默认的双亲委派类加载机制,那么是无法加载多个相同的类。

所以,Tomcat破坏双亲委派原则,提供隔离的机制,为每个web容器单独提供一个WebAppClassLoader加载器。

Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。

 

11.模块化技术与类加载机制

近几年模块化技术已经很成熟了,在JDK 9中已经应用了模块化的技术。

其实早在JDK 9之前,OSGI这种框架已经是模块化的了,而OSGI之所以能够实现模块热插拔和模块内部可见性的精准控制都归结于其特殊的类加载机制,加载器之间的关系不再是双亲委派模型的树状结构,而是发展成复杂的网状结构。

在JDK中,双亲委派也不是绝对的了。

在JDK9之前,JVM的基础类以前都是在rt.jar这个包里,这个包也是JRE运行的基石。

这不仅是违反了单一职责原则,同样程序在编译的时候会将很多无用的类也一并打包,造成臃肿。

在JDK9中,整个JDK都基于模块化进行构建,以前的rt.jar, tool.jar被拆分成数十个模块,编译的时候只编译实际用到的模块,同时各个类加载器各司其职,只加载自己负责的模块。

Class<?> c = findLoadedClass(cn);

    if (c == null) {

        // 找到当前类属于哪个模块

        LoadedModule loadedModule = findLoadedModule(cn);

        if (loadedModule != null) {

            //获取当前模块的类加载器

            BuiltinClassLoader loader = loadedModule.loader();

            //进行类加载

            c = findClassInModuleOrNull(loadedModule, cn);

         } else {

              // 找不到模块信息才会进行双亲委派

                if (parent != null) {

                  c = parent.loadClassOrNull(cn);

                }

          }

    }

总结

我已熟悉类加载机制

请说说你理解的类加载器

通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”

说说有哪几种类加载器,他们的职责分别是什么,他们之前存在什么样的约定。

类加载的结构如下:preview

  • BootstrapClassLoader:启动类类加载器,它用来加载<JAVA_HOME>/jre/lib路径,Xbootclasspath参数指定的路径以<JAVA_HOME>/jre/classes中的类。BootStrapClassLoader是由c++实现的。
  • ExtClassLoader:拓展类类加载器,它用来加载<JAVA_HOME>/jre/lib/ext路径以及java.ext.dirs系统变量指定的类路径下的类。
  • AppClassLoader:应用程序类类加载器,它主要加载应用程序ClassPath下的类(包含jar包中的类)。它是java应用程序默认的类加载器。
  • 用户自定义类加载器:用户根据自定义需求,自由的定制加载的逻辑,继承AppClassLoader,仅仅覆盖findClass()即将继续遵守双亲委派模型。
  • *ThreadContextClassLoader:线程上下文加载器,它不是一个新的类型,更像一个类加载器的角色,ThreadContextClassLoader可以是上述类加载器的任意一种,但往往是AppClassLoader,作用我们后面再说。

在虚拟机启动的时候会初始化BootstrapClassLoader,然后在Launcher类中去加载ExtClassLoader、AppClassLoader,并将AppClassLoader的parent设置为ExtClassLoader,并设置线程上下文类加载器。

Launcher是JRE中用于启动程序入口main()的类,让我们看下Launcher的代码

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            //加载扩展类类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            //加载应用程序类加载器,并设置parent为extClassLoader
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //设置默认的线程上下文类加载器为AppClassLoader
        Thread.currentThread().setContextClassLoader(this.loader);
        //此处删除无关代码。。。
        }




上面画的几种类加载器是遵循双亲委派模型的,其实就是,当一个类加载器去加载类时先尝试让父类加载器去加载,如果父类加载器加载不了再尝试自身加载。这也是我们在自定义ClassLoader时java官方建议遵守的约定。





ExtClassLoader为什么没有设置parent?

让我们看看下面代码的输出结果

 public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = Test.class.getClassLoader();
        System.out.println(classLoader);
        System.out.println(classLoader.getParent());
        System.out.println(classLoader.getParent().getParent());
    }

看看结果是啥

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@5a61f5df
null

因为BootstrapClassLoader是由c++实现的,所以并不存在一个Java的类,因此会打印出null,所以在ClassLoader中,null就代表了BootStrapClassLoader(有些片面)

那么双亲委派的好处是什么呢?

双亲委派模型能保证基础类仅加载一次不会让jvm中存在重名的类。比如String.class,每次加载都委托给父加载器,最终都是BootstrapClassLoader,都保证java核心类都是BootstrapClassLoader加载的,保证了java的安全与稳定性。

 

 

那自己怎么去实现一个ClassLoader呢?请举个实际的例子。

自己实现ClassLoader时只需要继承ClassLoader类,然后覆盖findClass(String name)方法即可完成一个带有双亲委派模型的类加载器。

我们看下ClassLoader#loadClass的代码

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 查看是否已经加载过该类,加载过的类会有缓存,是使用native方法实现的
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //父类不为空则先让父类加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                    //父类是null就是BootstrapClassLoader,使用启动类类加载器加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 父类类加载器不能加载该类
                }

                //如果父类未加载该类
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //让当前类加载器加载
                    c = findClass(name);
                }
            }
            return c;
        }
    }

经典的模板方法模式,子类只需要实现findClass,关心从哪里加载即可。

还有一点,parent需要自己设置哦,可以放在构造函数做这个事情。

 

 

为什么不继承AppClassLoader呢?

因为它和ExtClassLoader都是Launcher的静态类,都是包访问路径权限的。

 

 

为什么需要o.getClass().getMethod(“printVersion”).invoke(o);这样通过反射获取method调用,不能先强转成Test,然后test.printVersion()吗?

我:因为如果你这么写

Test test = (Test)o;
o.printVersion();

Test.class会隐性的被加载当前类的ClassLoader加载,当前Main方法默认的ClassLoader为AppClassLoader,而不是我们自定义的MyClassLoader。

那会发生什么呢?

会抛出ClassCastException,因为一个类,就算包路径完全一致,但是加载他们的ClassLoader不一样,那么这两个类也会被认为是两个不同的类。

 

 

转载自:请叫我程序猿大人 -知乎

1、Spring是什么?

Spring是一个轻量级的IoC和AOP容器框架。是为Java应用程序提供基础性服务的一套框架,目的是用于简化企业应用程序的开发,它使得开发者只需要关心业务需求。主要包括以下七个模块:

  • Spring Context:提供框架式的Bean访问方式,以及企业级功能(JNDI、定时任务等);
  • Spring Core:核心类库,所有功能都依赖于该类库,提供IOC和DI服务;
  • Spring AOP:AOP服务;
  • Spring Web:提供了基本的面向Web的综合特性,提供对常见框架如Struts2的支持,Spring能够管理这些框架,将Spring的资源注入给框架,也能在这些框架的前后插入拦截器;
  • Spring MVC:提供面向Web应用的Model-View-Controller,即MVC实现。
  • Spring DAO:对JDBC的抽象封装,简化了数据访问异常的处理,并能统一管理JDBC事务;
  • Spring ORM:对现有的ORM框架的支持;

 

2、Spring 的优点?

(1)spring属于低侵入式设计,代码的污染极低;

(2)spring的DI机制将对象之间的依赖关系交由框架处理,减低组件的耦合性;

(3)Spring提供了AOP技术,支持将一些通用任务,如安全、事务、日志、权限等进行集中式管理,从而提供更好的复用。

(4)spring对于主流的应用框架提供了集成支持。

 

3、Spring的IoC理解:

(1)IOC就是控制反转,指创建对象的控制权转移给Spring框架进行管理,并由Spring根据配置文件去创建实例和管理各个实例之间的依赖关系,对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部依赖。

java程序中的每个业务逻辑至少需要两个或以上的对象来协作完成,通常,每个对象在使用他的合作对象时,自己均要使用像new object() 这样的语法来完成合作对象的申请工作。你会发现:对象间的耦合度高了。而IOC的思想是:Spring容器来实现这些相互依赖对象的创建、协调工作。对象只需要关系业务逻辑本身就可以了。从这方面来说,对象如何得到他的协作对象的责任被反转了(IOC、DI)。

(2)最直观的表达就是,所有的类都会在spring容器中登记,告诉spring你是个什么东西,你需要什么东西,然后spring会在系统运行到适当的时候,把你要的东西主动给你,同时也把你交给其他需要你的东西。所有的类的创建、销毁都由 spring来控制,也就是说控制对象生存周期的不再是引用它的对象,而是spring。对于某个具体的对象而言,以前是它控制其他对象,现在是所有对象都被spring控制,所以这叫控制反转。

(3)IoC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。比如对象A需要操作数据库,以前我们总是要在A中自己编写代码来获得一个Connection对象,有了 spring我们就只需要告诉spring,A中需要一个Connection,至于这个Connection怎么构造,何时构造,A不需要知道。在系统运行时,spring会在适当的时候制造一个Connection,然后像打针一样,注射到A当中,这样就完成了对各个对象之间关系的控制。A需要依赖 Connection才能正常运行,而这个Connection是由spring注入到A中的,依赖注入的名字就这么来的。那么DI是如何实现的呢? Java 1.3之后一个重要特征是反射(reflection),它允许程序在运行的时候动态的生成对象、执行对象的方法、改变对象的属性,spring就是通过反射来实现注入的。关于反射的相关资料请查阅java doc。
原文链接:https://blog.csdn.net/it_man/article/details/4402245

(3)Spring的IOC有四种创建对象的方式

  • 无参构造
  • 下标赋值
  • 类型
  • 参数名

(4)Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。

 

4、Spring的AOP理解:

OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,会导致大量代码的重复,而不利于各个模块的重用。

AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,提高系统的可维护性。可用于权限认证、日志、事务处理。

AOP实现的关键在于 代理模式,AOP代理主要分为静态代理和动态代理。静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。

(1)AspectJ是静态代理,也称为编译时增强,AOP框架会在编译阶段生成AOP代理类,并将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。

(2)Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理:

① JDK动态代理只提供接口的代理,不支持类的代理,要求被代理类实现接口。JDK动态代理的核心是InvocationHandler接口和Proxy类,在获取代理对象时,使用Proxy类来动态创建目标类的代理类(即最终真正的代理类,这个类继承自Proxy并实现了我们定义的接口),当代理对象调用真实对象的方法时, InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;

InvocationHandler 的 invoke(Object  proxy,Method  method,Object[] args):proxy是最终生成的代理对象;  method 是被代理目标实例的某个具体方法;  args 是被代理目标实例某个方法的具体入参, 在方法反射调用时使用。

② 如果被代理类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

(3)静态代理与动态代理区别在于生成AOP代理对象的时机不同,相对来说AspectJ的静态代理方式具有更好的性能,但是AspectJ需要特定的编译器进行处理,而Spring AOP则无需特定的编译器处理。

IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。

 

5、Spring AOP里面的几个名词的概念:

(1)连接点(Join point):指程序运行过程中所执行的方法。在Spring AOP中,一个连接点总代表一个方法的执行。

(2)切面(Aspect):被抽取出来的公共模块,可以用来会横切多个对象。Aspect切面可以看成 Pointcut切点 和 Advice通知 的结合,一个切面可以由多个切点和通知组成。

在Spring AOP中,切面可以在类上使用 @AspectJ 注解来实现。

(3)切点(Pointcut):切点用于定义 要对哪些Join point进行拦截。

切点分为execution方式和annotation方式。execution方式可以用路径表达式指定对哪些方法拦截,比如指定拦截add*、search*。annotation方式可以指定被哪些注解修饰的代码进行拦截。

(4)通知(Advice):指要在连接点(Join Point)上执行的动作,即增强的逻辑,比如权限校验和、日志记录等。通知有各种类型,包括Around、Before、After、After returning、After throwing。

(5)目标对象(Target):包含连接点的对象,也称作被通知(Advice)的对象。 由于Spring AOP是通过动态代理实现的,所以这个对象永远是一个代理对象。

(6)织入(Weaving):通过动态代理,在目标对象(Target)的方法(即连接点Join point)中执行增强逻辑(Advice)的过程。

(7)引入(Introduction):添加额外的方法或者字段到被通知的类。Spring允许引入新的接口(以及对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现 IsModified 接口,以便简化缓存机制。

几个概念的关系图可以参考下图:

网上有张非常形象的图,描述了各个概念所处的场景和作用,贴在这里供大家理解:

 

6、Spring通知(Advice)有哪些类型?

(1)前置通知(Before Advice):在连接点(Join point)之前执行的通知。

(2)后置通知(After Advice):当连接点退出的时候执行的通知(不论是正常返回还是异常退出)。

(3)环绕通知(Around Advice):包围一个连接点的通知,这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也可以选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

(4)返回后通知(AfterReturning Advice):在连接点正常完成后执行的通知(如果连接点抛出异常,则不执行)

(5)抛出异常后通知(AfterThrowing advice):在方法抛出异常退出时执行的通知

 

同一个Aspect,不同advice的执行顺序:

(1)没有异常情况下的执行顺序:

  • around before advice
  • before advice
  • target method 执行
  • around after advice
  • after advice
  • afterReturning

(2)有异常情况下的执行顺序:

  • around before advice
  • before advice
  • target method 执行
  • around after advice
  • after advice
  • afterThrowing
  • java.lang.RuntimeException: 异常发生

 

7、Spring容器的启动流程:

详细内容可以阅读这篇文章:https://blog.csdn.net/a745233700/article/details/113761271

(1)初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中:

① 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
② 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成  BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
③ 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象
(2)将配置类的BeanDefinition注册到容器中:

(3)调用refresh()方法刷新容器:

① prepareRefresh()刷新前的预处理:
② obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory:
③ prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件:
④ postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置
⑤ invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器:
⑥ registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
⑦ initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
⑧ initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到:
⑨ onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
⑩ registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件:
⑪  finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象;
⑫ finishRefresh():发布BeanFactory容器刷新完成事件:

8、BeanFactory和ApplicationContext有什么区别?

BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

(1)BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:

继承MessageSource,因此支持国际化。
资源文件访问,如URL和文件(ResourceLoader)。
载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
提供在监听器中注册bean的事件。
(2)①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。

②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。

③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

(3)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

(4)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。

 

9、Spring Bean的生命周期?

简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation –> 属性赋值 Populate  –> 初始化 Initialization  –> 销毁 Destruction

但具体来说,Spring Bean的生命周期包含下图的流程:

 

(1)实例化Bean:

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
②如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
③如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

如果对bean详细加载流程的感兴趣的读者,可以阅读这篇文章:https://blog.csdn.net/a745233700/article/details/113840727

 

10、 Spring中bean的作用域:

(1)singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。

(2)prototype:为每一个bean请求创建一个实例。

(3)request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。

(4)session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。

(5)global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。

 

11、Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。

(1)对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。

(2)对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。

有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。

无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。

也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

 

12、Spring基于xml注入bean的几种方式:

set()方法注入;
构造器注入:①通过index设置参数的位置;②通过type设置参数类型;
静态工厂注入;
实例工厂;
详细内容请参考这篇文章:Spring中bean的注入方式

 

13、Spring如何解决循环依赖问题:

详细内容强烈建议参考这篇文章:Spring如何解决循环依赖问题

循环依赖问题在Spring中主要有三种情况:

(1)通过构造方法进行依赖注入时产生的循环依赖问题。
(2)通过setter方法进行依赖注入且是在多例(原型)模式下产生的循环依赖问题。
(3)通过setter方法进行依赖注入且是在单例模式下产生的循环依赖问题。
在Spring中,只有第(3)种方式的循环依赖问题被解决了,其他两种方式在遇到循环依赖问题时都会产生异常。这是因为:

第一种构造方法注入的情况下,在new对象的时候就会堵塞住了,其实也就是”先有鸡还是先有蛋“的历史难题。
第二种setter方法(多例)的情况下,每一次getBean()时,都会产生一个新的Bean,如此反复下去就会有无穷无尽的Bean产生了,最终就会导致OOM问题的出现。
Spring在单例模式下的setter方法依赖注入引起的循环依赖问题,主要是通过二级缓存和三级缓存来解决的,其中三级缓存是主要功臣。解决的核心原理就是:在对象实例化之后,依赖注入之前,Spring提前暴露的Bean实例的引用在第三级缓存中进行存储。

 

14、Spring的自动装配:

在spring中,使用autowire来配置自动装载模式,对象无需自己查找或创建与其关联的其他对象,由容器负责把需要相互协作的对象引用赋予各个对象。

(1)在Spring框架xml配置中共有5种自动装配:

no:默认的方式是不进行自动装配的,通过手工设置ref属性来进行装配bean。
byName:通过bean的名称进行自动装配,如果一个bean的 property 与另一bean 的name 相同,就进行自动装配。
byType:通过参数的数据类型进行自动装配。
constructor:利用构造函数进行装配,并且构造函数的参数通过byType进行装配。
autodetect:自动探测,如果有构造方法,通过 construct的方式自动装配,否则使用 byType的方式自动装配。
(2)基于注解的自动装配方式:

使用@Autowired、@Resource注解来自动装配指定的bean。在使用@Autowired注解之前需要在Spring配置文件进行配置,<context:annotation-config />。在启动spring IoC时,容器自动装载了一个AutowiredAnnotationBeanPostProcessor后置处理器,当容器扫描到@Autowied、@Resource或@Inject时,就会在IoC容器自动查找需要的bean,并装配给该对象的属性。在使用@Autowired时,首先在容器中查询对应类型的bean:

如果查询结果刚好为一个,就将该bean装配给@Autowired指定的数据;

如果查询的结果不止一个,那么@Autowired会根据名称来查找;

如果上述查找的结果为空,那么会抛出异常。解决方法时,使用required=false。

@Autowired可用于:构造函数、成员变量、Setter方法

注:@Autowired和@Resource之间的区别:

(1) @Autowired默认是按照类型装配注入的,默认情况下它要求依赖对象必须存在(可以设置它required属性为false)。

(2) @Resource默认是按照名称来装配注入的,只有当找不到与名称匹配的bean才会按照类型来装配注入。

 

15、Spring事务的实现方式和实现原理:

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。Spring只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过binlog或者undo log实现的。Spring会在事务开始时,根据当前环境中设置的隔离级别,调整数据库隔离级别,由此保持一致。

(1)Spring事务的种类:

spring支持编程式事务管理和声明式事务管理两种方式:

①编程式事务管理使用TransactionTemplate。

②声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。唯一不足地方是,最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。

(2)spring的事务传播机制:

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘

④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

⑦ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。

(3)Spring中的隔离级别:

① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。

② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。

③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。

④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。

⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。

 

16、Spring 框架中都用到了哪些设计模式?

Spring设计模式的详细使用案例可以阅读这篇文章:https://blog.csdn.net/a745233700/article/details/112598471

(1)工厂模式:Spring使用工厂模式,通过BeanFactory和ApplicationContext来创建对象

(2)单例模式:Bean默认为单例模式

(3)策略模式:例如Resource的实现类,针对不同的资源文件,实现了不同方式的资源获取策略

(4)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术

(5)模板方法:可以将相同部分的代码放在父类中,而将不同的代码放入不同的子类中,用来解决代码重复的问题。比如RestTemplate, JmsTemplate, JpaTemplate

(6)适配器模式:Spring AOP的增强或通知(Advice)使用到了适配器模式,Spring MVC中也是用到了适配器模式适配Controller

(7)观察者模式:Spring事件驱动模型就是观察者模式的一个经典应用。

(8)桥接模式:可以根据客户的需求能够动态切换不同的数据源。比如我们的项目需要连接多个数据库,客户在每次访问中根据需要会去访问不同的数据库

 

17、Spring框架中有哪些不同类型的事件?

Spring 提供了以下5种标准的事件:

(1)上下文更新事件(ContextRefreshedEvent):在调用ConfigurableApplicationContext 接口中的refresh()方法时被触发。

(2)上下文开始事件(ContextStartedEvent):当容器调用ConfigurableApplicationContext的Start()方法开始/重新开始容器时触发该事件。

(3)上下文停止事件(ContextStoppedEvent):当容器调用ConfigurableApplicationContext的Stop()方法停止容器时触发该事件。

(4)上下文关闭事件(ContextClosedEvent):当ApplicationContext被关闭时触发该事件。容器被关闭时,其管理的所有单例Bean都被销毁。

(5)请求处理事件(RequestHandledEvent):在Web应用中,当一个http请求(request)结束触发该事件。

如果一个bean实现了ApplicationListener接口,当一个ApplicationEvent 被发布以后,bean会自动被通知。

https://blog.csdn.net/a745233700/article/details/80959716

 

1.#{}和${}的区别是什么?

  • #{}是预编译处理,${}是字符串替换。
  • Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值;
  • Mybatis在处理${}时,就是把${}替换成变量的值。
  • 使用#{}可以有效的防止SQL注入,提高系统安全性。

2、当实体类中的属性名和表中的字段名不一样 ,怎么办 ?

第1种: 通过在查询的sql语句中定义字段名的别名,让字段名的别名和实体类的属性名一致 
    <select id=”selectorder” parametertype=”int” resultetype=”me.gacl.domain.order”> 
       select order_id id, order_no orderno ,order_price price form orders where order_id=#{id}; 
    </select> 
第2种: 通过<resultMap>来映射字段名和实体类属性名的一一对应的关系 
    <select id="getOrder" parameterType="int" resultMap="orderresultmap">
        select * from orders where order_id=#{id}
    </select>
   <resultMap type=”me.gacl.domain.order” id=”orderresultmap”> 
        <!–用id属性来映射主键字段–> 
        <id property=”id” column=”order_id”> 
        <!–用result属性来映射非主键字段,property为实体类属性名,column为数据表中的属性–> 
        <result property = “ordernocolumn =”order_no”/> 
        <result property=”price” column=”order_price” /> 
    </reslutMap>

3、 模糊查询like语句该怎么写?

第1种:在Java代码中添加sql通配符。
    string wildcardname = “%smi%”; 
    list<name> names = mapper.selectlike(wildcardname);

    <select id=”selectlike”> 
     select * from foo where bar like #{value} 
    </select>
第2种:在sql语句中拼接通配符,会引起sql注入
    string wildcardname = “smi”; 
    list<name> names = mapper.selectlike(wildcardname);

    <select id=”selectlike”> 
     select * from foo where bar like "%"#{value}"%"
    </select>

4、通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法,参数不同时,方法能重载吗?

Dao接口,就是人们常说的Mapper接口,接口的全限名,就是映射文件中的namespace的值,接口的方法名,就是映射文件中MappedStatement的id值,接口方法内的参数,就是传递给sql的参数。Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MappedStatement,举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面id = findStudentById的MappedStatement。在Mybatis中,每一个<select>、<insert>、<update>、<delete>标签,都会被解析为一个MappedStatement对象。

Dao接口里的方法,是不能重载的,因为是全限名+方法名的保存和寻找策略。

Dao接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Dao接口生成代理proxy对象,代理对象proxy会拦截接口方法,转而执行MappedStatement所代表的sql,然后将sql执行结果返回。

 

5、Mybatis的Xml映射文件中,不同的Xml映射文件,id是否可以重复?

不同的Xml映射文件,如果配置了namespace,那么id可以重复;如果没有配置namespace,那么id不能重复;

原因就是namespace+id是作为Map<String, MapperStatement>的key使用的,如果没有namespace,就剩下id,那么,id重复会导致数据互相覆盖。有了namespace,自然id就可以重复,namespace不同,namespace+id自然也就不同。

备注:在旧版本的Mybatis中,namespace是可选的,不过新版本的namespace已经是必须的了。

6、Mybatis是如何进行分页的?分页插件的原理是什么?

Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。

分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。

 

7、简述Mybatis的插件运行原理,以及如何编写一个插件。

答:Mybatis仅可以编写针对ParameterHandler、ResultSetHandler、StatementHandler、Executor这4种接口的插件,Mybatis使用JDK的动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这4种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler的invoke()方法,当然,只会拦截那些你指定需要拦截的方法。

编写插件:实现Mybatis的Interceptor接口并复写intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,最后在配置文件中配置你编写的插件。

 

8.Mybatis 分页