Java最常见的面试题--2

设计模式

83.有哪些熟悉的设计模式?

  • 单例模式

    某个类的实例对象只有一个,你没有办法去new,因为构造器是被private修饰的,一般通过getInstance()的方法来获取它们的实例。getInstance()的返回值是一个对象的引用,并不是一个新的实例

public class Singleton(单例模式) {  
    private static Singleton instance;  
    private Singleton (){}  //构造函数
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
} 
//synchronized 是同步锁
  • 观察者模式

举个栗子:假设有三个人,小美(女,22),小王和小李。小美很漂亮,小王和小李是两个程序猿,时刻关注着小美的一举一动。有一天,小美说了一句:“谁来陪我打游戏啊。”这句话被小王和小李听到了,结果乐坏了,蹭蹭蹭,没一会儿,小王就冲到小美家门口了,在这里,小美是被观察者,小王和小李是观察者,被观察者发出一条信息,然后观察者们进行相应的处理

看代码

定义一个接口,它有一个函数
public interface Person {//(相当于一个进群规则)
   //小王和小李通过这个接口可以接收到小美发过来的消息
   void getMessage(String s);
}

这个接口相当于一个进群规则,只有Person才能进群,小美发送通知的时候就会在这个群里发消息,在这个群里面的人都会收到消息。

public class LaoWang implements Person {

   private String name = "小王";

   public LaoWang() {
   }

   @Override
   public void getMessage(String s) { 
       System.out.println(name + "接到了小美打过来的电话,电话内容是:" + s);
   }

}

public class LaoLi implements Person {

   private String name = "小李";

   public LaoLi() {
   }

   @Override
   public void getMessage(String s) {
       System.out.println(name + "接到了小美打过来的电话,电话内容是:->" + s);
   }
}

再看看小美的代码:

public class XiaoMei {
//这个list相当于一个群,注意这个群有一个规则,就是具有getMessage的才能进来
   List list = new ArrayList();
    public XiaoMei(){
    }
//把某个人(这个人必须满足进群规则,就是继承Person)拉进群里
    public void addPerson(Person person){
        list.add(person);
    }

    //遍历list,把自己的通知发送给所有群里的人
    public void notifyPerson() {
        for(Person person:list){
            person.getMessage("你们过来吧,谁先过来谁就能陪我一起玩儿游戏!");
        }
    }
}

我们写一个测试类来看一下结果对不对

public class Test {
   public static void main(String[] args) {

       XiaoMei xiao_mei = new XiaoMei();
       LaoWang lao_wang = new LaoWang();
       LaoLi lao_li = new LaoLi();

       //小王和小李被小美拉进群
       xiao_mei.addPerson(lao_wang);
       xiao_mei.addPerson(lao_li);

       //小美在群里发通知
       xiao_mei.notifyPerson();
   }
}
  • 装饰者模式 对已有的业务逻辑进一步的封装,使其增加额外的功能

举个栗子,我想吃三明治,首先我需要一根大大的香肠,然后我喜欢吃奶油,在香肠上面加一点奶油,然后需要再放一点蔬菜,最后再用两片面包把它包裹起来,形成一个三明治。

首先,我们需要写一个Food类,让其他所有食物都来继承这个类

public class Food {

   private String food_name;

   public Food() {
   }

   public Food(String food_name) {
       this.food_name = food_name;
   }

   public String make() {
       return food_name;
   };
}

然后我们写几个子类继承它
构造方法传入一个Food类型的参数,然后在make方法中加入一些自己的逻辑

//面包类
public class Bread extends Food {

   private Food basic_food;

   public Bread(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+面包";
   }
}

//奶油类
public class Cream extends Food {

   private Food basic_food;

   public Cream(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+奶油";
   }
}

//蔬菜类
public class Vegetable extends Food {

   private Food basic_food;

   public Vegetable(Food basic_food) {
       this.basic_food = basic_food;
   }

   public String make() {
       return basic_food.make()+"+蔬菜";
   }

}

Test类

public class Test {
   public static void main(String[] args) {
       Food food = new Bread(new Vegetable(new Cream(new Food("香肠"))));
       System.out.println(food.make());
   }
}

看到没有,一层一层封装,我们从里往外看:最里面我new了一个香肠,在香肠的外面我包裹了一层奶油,在奶油的外面我又加了一层蔬菜,最外面我放的是面包


运行结果
  • 适配器模式 将两种完全不同的事物联系到一起,就像现实生活中的变压器

假设一个手机充电器需要的电压是20V,但是正常的电压是220V,这时候就需要一个变压器,将220V的电压转换成20V的电压,这样,变压器就将20V的电压和手机联系起来了。

public class Test {
    public static void main(String[] args) {
        Phone phone = new Phone();//定义一个手机
        VoltageAdapter adapter = new VoltageAdapter();//定义一个适配器
        phone.setAdapter(adapter);//手机装上适配器
        phone.charge();//手机充电
    }
}

// 手机类
class Phone {

    public static final int V = 220;// 正常电压220v,是一个常量

    private VoltageAdapter adapter;

    // 充电
    public void charge() {
        adapter.changeVoltage();
    }

    public void setAdapter(VoltageAdapter adapter) {
        this.adapter = adapter;
    }
}

// 变压器
class VoltageAdapter {
    // 改变电压的功能
    public void changeVoltage() {
        System.out.println("正在充电...");
        System.out.println("原始电压:" + Phone.V + "V");
        System.out.println("经过变压器转换之后的电压:" + (Phone.V - 200) + "V");
    }
}
运行结果
  • 工厂模式

简单工厂模式:1.一个抽象的接口,2.多个抽象接口的实现类,3.一个工厂类,用来实例化抽象的接口

一个抽象的接口

// 抽象产品类
abstract class Car {
    public void run();//能跑
    public void stop();//能停
}

多个抽象接口的实现类

// 具体实现类
class Benz implements Car {
    public void run() {
        System.out.println("Benz开始启动了。。。。。");
    }

    public void stop() {
        System.out.println("Benz停车了。。。。。");
    }
}

class Ford implements Car {
    public void run() {
        System.out.println("Ford开始启动了。。。");
    }

    public void stop() {
        System.out.println("Ford停车了。。。。");
    }
}

一个工厂类

// 工厂类
class Factory {
    public static Car getCarInstance(String type) {
        Car c = null;
        if ("Benz".equals(type)) {
            c = new Benz();
        }
        if ("Ford".equals(type)) {
            c = new Ford();
        }
        return c;
    }
}

测试类

public class Test {

   public static void main(String[] args) {
       Car c = Factory.getCarInstance("Benz");
       if (c != null) {
           c.run();
           c.stop();
       } else {
           System.out.println("造不了这种汽车。。。");
       }

   }

}
  • 代理模式

    举个栗子:打官司,律师就代理你进行辩论;开庭前(编译期间)你花钱请了一个有名的律师,他就是静态代练,你没钱,开庭的时候(运行期间)。法院随机给你找一个律师,这就是动态代理。

84.简单工厂和抽象工厂有什么区别?

抽象工厂模式:
多个抽象产品类,每个抽象产品类可以派生出多个具体产品类
一个抽象工厂类,可以派生出多个具体工厂类
每个具体工厂类可以创建多个具体产品类的实例

Spring / Spring MVC

85.为什么要使用 spring?
简介:Spring是一个轻量级的控制反转(IOC)和面向切面(AOP)的容器框架
目的:解决企业应用开发的复杂性
86.解释一下什么是 aop?

AOP(Aspect-Oriented Programming,面向方面编程),可以说是OOP(Object-Oriented Programing,面向对象编程)的补充和完善

目的:当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。
操作:将那些影响了多个类的公共行为封装到一个可重用模块,并将其名为“Aspect”,即方面。简单来说:将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。比如权限认证、日志、事务处理
87.解释一下什么是 ioc?
翻译:控制反转
解释:借助于“第三方”实现具有依赖关系的对象之间的解耦


全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用
为何叫控制反转
去掉IOC

软件系统在没有引入IOC容器之前,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,
自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。

软件系统在引入IOC容器之后,这种情形就完全改变了,,由于IOC容器的加入,对象A与对象B之间失去了直接联系,
所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。
对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,
这就是“控制反转”这个名称的由来。

88.spring 常用的注入方式有哪些?
a.构造方法注入
b.setter注入
c.基于注解的注入
声明一个简单的bean

public class Roles {
    private int id;
    private String roleName;
    public Roles() {
    }
    public Roles(int id,String roleName) {
        this.id=id;
        this.roleName = roleName;
    }
}

构造方法注入


        
        
    

setter注入 我们能发现构造器 set注入是


        
        
    

测试main方法

public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        Roles r=(Roles)context.getBean("roles");
        System.out.println(r.toString());
    }

基于注解的注入

Autowired是自动注入,自动从spring的上下文找到合适的bean来注入
Resource用来指定名称注入
Qualifier和Autowired配合使用,指定bean的名称,如
    @Autowired
    @Qualifier("userDAO")
    private UserDAO userDAO;

89.spring 中的 bean 是线程安全的吗?
容器本身并没有提供Bean的线程安全策略
90.spring 支持几种 bean 的作用域?
singleton:单例模式,在整个Spring IoC容器中,使用singleton定义的Bean将只有一个实例

prototype:原型模式,每次通过容器的getBean方法获取prototype定义的Bean时,都将产生一个新的Bean实例

request:对于每次HTTP请求,使用request定义的Bean都将产生一个新实例,即每次HTTP请求将会产生不同的Bean实例。只有在Web应用中使用Spring时,该作用域才有效

session:对于每次HTTP Session,使用session定义的Bean都将产生一个新实例。同样只有在Web应用中使用Spring时,该作用域才有效

globalsession:每个全局的HTTP Session,使用session定义的Bean都将产生一个新实例。典型情况下,仅在使用portlet context的时候有效。同样只有在Web应用中使用Spring时,该作用域才有效
91.spring 自动装配 bean 有哪些方式?
1.在XML中进行显式配置
2.在Java中进行显式配置
3.隐式的bean发现机制和自动装配
92.spring 事务实现方式有哪些?
含义:事务是逻辑上的一组操作,要么都执行,要么都不执行
事物的特性(ACID):

事务的特性

原子性: 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性: 执行事务前后,数据保持一致;
隔离性: 并发访问数据库时,一个用户的事物不被其他事物所干扰,各并发事务之间数据库是独立的;
持久性: 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响
来源https://juejin.im/post/5b00c52ef265da0b95276091

1.编程式事务管理对基于 POJO (通指没有使用Entity Beans的普通java对象)的应用来说是唯一选择。我们需要在代码中调用beginTransaction()、commit()、rollback()等事务管理相关的方法,这就是编程式事务管理。
2.基于 TransactionProxyFactoryBean 的声明式事务管理
3.基于 @Transactional 的声明式事务管理
4.基于 Aspectj AOP 配置事务
93.说一下 spring 的事务隔离?
事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:

  • 脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

  • 丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。
    例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。

  • 不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。

  • 幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。

不可重复度和幻读区别:
不可重复读的重点是修改,幻读的重点在于新增或者删除。
例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为 1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导 致A再读自己的工资时工资变为 2000;这就是不可重复读。
例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
94.说一下 spring mvc 运行流程?
Spring运行流程描述:

  1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获;
  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI)。然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;
  3. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter;(附注:如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(...)方法)
  4. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
    HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
    数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
    数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
    数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
  5. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;
  6. 根据返回的ModelAndView,选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet ;
  7. ViewResolver 结合Model和View,来渲染视图;
  8. 将渲染结果返回给客户端。

95.spring mvc 有哪些组件?
Spring MVC的核心组件:
DispatcherServlet:中央控制器,把请求给转发到具体的控制类
Controller:具体处理请求的控制器
HandlerMapping:映射处理器,负责映射中央处理器转发给controller时的映射策略
ModelAndView:服务层返回的数据和视图层的封装类
ViewResolver:视图解析器,解析具体的视图
Interceptors :拦截器,负责拦截我们定义的请求然后做处理工作
96. @RequestMapping 的作用是什么?
RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。
RequestMapping注解有六个属性,下面我们把她分成三类进行说明。

  • value, method:
    value:指定请求的实际地址
    method:指定请求的method类型, GET、POST、PUT、DELETE等;

  • consumes,produces
    consumes:指定处理请求的提交内容类型(Content-Type),例如
    application/json, text/html;
    produces:指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回;

  • params,headers
    params: 指定request中必须包含某些参数值是,才让该方法处理。
    headers:指定request中必须包含某些指定的header值,才能让该方法处理请求。

Spring Boot / Spring Cloud

97.什么是 spring boot?
简化了Spring众多框架中所需的大量且繁琐的配置文件,所以 SpringBoot是一个服务于框架的框架,服务范围是简化配置文件。

  • why?
    Spring Boot使编码变简单
    Spring Boot使配置变简单
    Spring Boot使部署变简单
    Spring Boot使监控变简单
    Spring的不足

98. spring boot 核心配置文件是什么?
Spring Boot提供了两种常用的配置文件:
properties文件
相对于properties文件而言,yml文件更年轻,也有很多的坑。可谓成也萧何败萧何,yml通过空格来确定层级关系,使配置文件结构跟清晰,但也会因为微不足道的空格而破坏了层级关系。
99.spring boot 有哪些方式可以实现热部署?
①. 使用spring loaded**
在项目配置文件中添加如下代码:


        
            
                
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                    
                    
                        org.springframework
                        springloaded
                        1.2.6.RELEASE
                    
                
            
        
    

添加完毕后需要使用mvn指令运行:
首先找到IDEA中的Edit configurations ,然后进行如下操作:(点击左上角的"+",然后选择maven将出现右侧面板,在红色划线部位输入如图所示指令,你可以为该指令命名(此处命名为MvnSpringBootRun))



点击保存将会在IDEA项目运行部位出现,点击绿色箭头运行即可


image

②. 使用spring-boot-devtools
在项目的pom文件中添加依赖:
 
 
     org.springframework.boot
     spring-boot-devtools
 

然后:使用 shift+ctrl+alt+"/" (IDEA中的快捷键) 选择"Registry" 然后勾选 compiler.automake.allow.when.app.running
100.jpa 和 hibernate 有什么区别?

JPA和Hibernate之间的关系,可以简单的理解为JPA是标准接口,Hibernate是实现。那么Hibernate是如何实现与JPA的这种关系的呢。Hibernate主要是通过三个组件来实现的,及hibernate-annotation、hibernate-entitymanager和hibernate-core。

  • hibernate-annotation是Hibernate支持annotation方式配置的基础,它包括了标准的JPA annotation以及Hibernate自身特殊功能的annotation。
  • hibernate-core是Hibernate的核心实现,提供了Hibernate所有的核心功能。
  • hibernate-entitymanager实现了标准的JPA,可以把它看成hibernate-core和JPA之间的适配器,它并不直接提供ORM的功能,而是对hibernate-core进行封装,使得Hibernate符合JPA的规范。

101.什么是 spring cloud
从字面理解,Spring Cloud 就是致力于分布式系统、云服务的框架。
Spring Cloud 为开发人员提供了快速构建分布式系统中一些常见模式的工具,例如:
配置管理
服务注册与发现
断路器
智能路由
服务间调用
负载均衡
微代理
控制总线
一次性令牌
全局锁
领导选举
分布式会话
集群状态
分布式消息
……

使用 Spring Cloud 开发人员可以开箱即用的实现这些模式的服务和应用程序。这些服务可以任何环境下运行,包括分布式环境,也包括开发人员自己的笔记本电脑以及各种托管平台。
一个认识spring cloud的项目(来源网络...)
https://pan.baidu.com/s/1i7lFCPn
102. spring cloud 断路器的作用是什么?
在Spring Cloud中使用了Hystrix 来实现断路器的功能,断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决,如果问题似乎已经得到纠正,应用程序可以尝试调用操作。

断路器增加了稳定性和灵活性,以一个系统,提供稳定性,而系统从故障中恢复,并尽量减少此故障的对性能的影响。它可以帮助快速地拒绝一个操作,即很可能失败的操作,而不是等待操作超时(或者不返回)的请求,以保持系统的响应时间。如果断路器提高每次改变状态的时间的事件,该信息可以被用来监测由断路器保护系统的部件的健康状况,或以提醒管理员当断路器跳闸,以在打开状态。
103.spring cloud 的核心组件有哪些?
①. 服务发现——Netflix Eureka
一个RESTful服务,用来定位运行在AWS地区(Region)中的中间层服务。由两个组件组成:Eureka服务器和Eureka客户端。Eureka服务器用作服务注册服务器。Eureka客户端是一个java客户端,用来简化与服务器的交互、作为轮询负载均衡器,并提供服务的故障切换支持。Netflix在其生产环境中使用的是另外的客户端,它提供基于流量、资源利用率以及出错状态的加权负载均衡。
②. 客服端负载均衡——Netflix Ribbon
Ribbon,主要提供客户侧的软件负载均衡算法。Ribbon客户端组件提供一系列完善的配置选项,比如连接超时、重试、重试算法等。Ribbon内置可插拔、可定制的负载均衡组件。
③. 断路器——Netflix Hystrix
断路器可以防止一个应用程序多次试图执行一个操作,即很可能失败,允许它继续而不等待故障恢复或者浪费 CPU 周期,而它确定该故障是持久的。断路器模式也使应用程序能够检测故障是否已经解决。如果问题似乎已经得到纠正,应用程序可以尝试调用操作。
④. 服务网关——Netflix Zuul
类似nginx,反向代理的功能,不过netflix自己增加了一些配合其他组件的特性。
⑤. 分布式配置——Spring Cloud Config
这个还是静态的,得配合Spring Cloud Bus实现动态的配置更新。

Hibernate

104.为什么要使用 hibernate?
对JDBC访问数据库的代码做了封装,大大简化了数据访问层繁琐的重复性代码。

Hibernate是一个基于JDBC的主流持久化框架,是一个优秀的ORM(对象关系映射)实现。他很大程度的简化DAO层的编码工作

hibernate使用Java反射机制,而不是字节码增强程序来实现透明性。

hibernate的性能非常好,因为它是个轻量级框架。映射的灵活性很出色。它支持各种关系数据库,从一对一到多对多的各种复杂关系。
105.什么是 ORM 框架?
对象-关系映射(Object-Relational Mapping,简称ORM)面向对象的开发方法是当今企业级应用开发环境中的主流开发方法,关系数据库是企业级应用环境中永久存放数据的主流数据存储系统。对象和关系数据是业务实体的两种表现形式,业务实体在内存中表现为对象,在数据库中表现为关系数据。内存中的对象之间存在关联和继承关系,而在数据库中,关系数据无法直接表达多对多关联和继承关系。因此,对象-关系映射(ORM)系统一般以中间件的形式存在,主要实现程序对象到关系数据库数据的映射。
106.hibernate 中如何在控制台查看打印的 sql 语句?

spring.jpa.properties.hibernate.show_sql=true          //控制台是否打印
spring.jpa.properties.hibernate.format_sql=true        //格式化sql语句
spring.jpa.properties.hibernate.use_sql_comments=true  //指出是什么操作生成了该语句

107.hibernate 有几种查询方式?
1.hql查询

HQL是面向对象查询操作的

hql查询,sql查询,条件查询

Query query = session.createQuery("from Customer where name = ?");
query.setParameter(0, "苍老师");
Query.list();

2.sql查询

SQL是结构化查询语言 是面向数据库表结构的

SQLQuery query = session.createSQLQuery("select * from customer");
List list = query.list();

3.条件查询

查询所有    session.creartCriteria(实体对象.class)
=======================================================
案例是:
/ 获得session对象
 Session session = HibernateUtils.openSession();
 // 通过session获得查询对象 criteria
 Criteria criteria = session.createCriteria(Customer.class); // 括号里面的参数就是查询条件了,表示我们查询的就是这个实体对象.
 List list = criteria.list(); // 这表示接收返回值.
 // 遍历打印结果
 for (Customer customer : list) {
 System.out.println(customer);
 }
 }

查询单个字段 
======================================================
案例是:
Session session = HibernateUtils.openSession();
 // 通过session获得查询对象 criteria
 Criteria criteria = session.createCriteria(Customer.class); // 括号里面的参数就是查询条件了,表示我们查询的就是这个实体对象.
 // 赋值我们需要查询的实体的变量名(也就是需要查询的字段名)
 criteria.setProjection(Projections.property("cust_name"));// Projection 投影的意思. 
 // 查询后返回的就是Object
 List list = criteria.list(); // 这表示接收返回值.
 // 遍历打印结果
 for (Object object : list) {
 System.out.println(object);

查询多个字段
案例是 
=======================================================
 通过session获得查询对象 criteria
 Criteria criteria = session.createCriteria(Customer.class); // 括号里面的参数就是查询条件了,表示我们查询的就是这个实体对象.
 // 赋值我们需要查询的实体的变量名(也就是需要查询的字段名)
 ProjectionList list2 = Projections.projectionList();
 list2.add(Projections.property("cust_name"));
 
 list2.add(Projections.property("cust_id"));
 // 然后将这个集合与对象建立关系.
 criteria.setProjection(list2);
 // 查询后返回的就是Object
 List list = criteria.list(); // 这表示接收返回值.
 // 遍历打印结果
 for (Object[] object : list) {
 System.out.println(Arrays.toString(object));
 }
 }
 
 

108.在 hibernate 中使用 Integer 和 int 做映射有什么区别?
在Hibernate中,如果将OID定义为Integer类型,那么Hibernate就可以根据其值是否为null而判断一个对象是否是临时的,如果将OID定义为了int类型,还需要在hbm映射文件中设置其unsaved-value属性为0。
109.hibernate 是如何工作的?
hibernate工作原理:

  • 通过Configuration config = new Configuration().configure();//读取并解析hibernate.cfg.xml配置文件
  • hibernate.cfg.xml中的读取并解析映射信息
  • 通过SessionFactory sf = config.buildSessionFactory();//创建SessionFactory
  • Session session = sf.openSession();//打开Sesssion
  • Transaction tx = session.beginTransaction();//创建并启动事务Transation
  • persistent operate操作数据,持久化操作
  • tx.commit();//提交事务
  • 关闭Session
  • 关闭SesstionFactory

110.说一下 hibernate 的缓存机制?

  • 什么是缓存机制?

当我们频繁访问数据库时,尤其像Hibernate持久层框架,会导致数据库访问性能降低,因此我们期望有一种机制能提供一个"缓存空间",我们将需要的数据复制到这个"缓存空间",当数据查询时,我们先在这个"缓存空间"里找,如果没有,我们再去数据库查找,这样就减少了与数据库的访问,从而提高了数据库访问性能,这就是缓存机制。

  • 缓存分类
    1:一级缓存:Hibernate默认的缓存机制,它属于Session级别的缓存机制,也就是说Session关闭,缓存数据消失。
    2:二级缓存:属于SessionFactory级别的缓存,二级缓存是全局性的,应用中的所有Session都共享这个二级缓存。
    二级缓存默认是关闭的,一旦开启,当我们需要查询数据时,会先在一级缓存查询,没有,去二级缓存,还没有,好,咱们再去数据库,因此缓存机制大大提高了数据库的访问性能。

  • 什么样的数据适合存放到第二级缓存中?
    很少被修改的数据 帖子的最后回复时间
    经常被查询的数据 电商的地点
    不是很重要的数据,允许出现偶尔并发的数据
    不会被并发访问的数据
    常量数据

扩展:hibernate的二级缓存默认是不支持分布式缓存的。使用 memcahe,redis等中央缓存来代替二级缓存

可以练习一下Hibernate
https://www.jianshu.com/p/93b5f2c7cb83

111.hibernate 对象有哪些状态?
hibernate里对象有三种状态:

  • Transient(瞬时):对象刚new出来,还没设id,设了其他值。
  • Persistent(持久):调用了save()、saveOrUpdate(),就变成Persistent,有id。
  • Detached(脱管):当session close()完之后,变成Detached。

112.在 hibernate 中 getCurrentSession 和 openSession 的区别是什么?

  • openSession 从字面上可以看得出来,是打开一个新的session对象,而且每次使用都是打开一个新的session,假如连续使用多次,则获得的session不是同一个对象,并且使用完需要调用close方法关闭session。
  • getCurrentSession ,从字面上可以看得出来,是获取当前上下文一个session对象,当第一次使用此方法时,会自动产生一个session对象,并且连续使用多次时,得到的session都是同一个对象,这就是与openSession的区别之一.实际用这个多一点。

113.hibernate 实体类必须要有无参构造函数吗?为什么?
必须,因为hibernate框架会调用这个默认构造方法来构造实例对象,即Class类的newInstance方法,这个方法就是通过调用默认构造方法来创建实例对象的。
-。-
注意就是,如果你没有提供任何构造方法,虚拟机会自动提供默认构造方法(无参构造器),但是如果你提供了其他有参数的构造方法的话,虚拟机就不再为你提供默认构造方法,这时必须手动把无参构造器写在代码里,否则new Xxxx()是会报错的,所以默认的构造方法不是必须的,只在有多个构造方法时才是必须的,这里“必须”指的是“必须手动写出来”。

Mybatis

114.mybatis 中 #{}和 ${}的区别是什么
#将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号。如:order by #user_id#,如果传入的值是111,那么解析成sql时的值为order by "111", 如果传入的值是id,则解析成的sql为order by "id".

  1. $将传入的数据直接显示生成在sql中。如:order by $user_id$,如果传入的值是111,那么解析成sql时的值为order by user_id, 如果传入的值是id,则解析成的sql为order by id.

3.#方式能够很大程度防止sql注入。
  
4.$方式无法防止Sql注入。

5.$方式一般用于传入数据库对象,例如传入表名.
  
6.一般能用#的就别用$.

7.#$在预编译处理中是不一样的。#类似jdbc中的PreparedStatement,对于传入的参数,在预处理阶段会使用?代替,比如:
select * from student where id = ?;
待真正查询的时候即在数据库管理系统中(DBMS)才会代入参数。
而${}则是简单的替换
select * from student where id = 2;
115.mybatis 有几种分页方式?

  • 数组分页
查询出全部数据,然后在list中截取需要的部分。
在dao层,创建StudentMapper接口
List queryStudentsByArray();

xml配置文件StudentMapper.xml文件


service接口
List queryStudentsByArray(int currPage, int pageSize);

实现接口serviceImpl
@Override
    public List queryStudentsByArray(int currPage, int pageSize) {
        //查询全部数据
        List students = studentMapper.queryStudentsByArray();
        //从第几条数据开始
        int firstIndex = (currPage - 1) * pageSize;
        //到第几条数据结束
        int lastIndex = currPage * pageSize;
        return students.subList(firstIndex, lastIndex); //直接在list中截取
    }
  • sql分页
在dao层,创建StudentMapper接口
List queryStudentsBySql(Map data);

xml配置文件StudentMapper.xml文件


service接口
List queryStudentsBySql(int currPage, int pageSize);

实现接口serviceImpl
@Override
public List queryStudentsBySql(int currPage, int pageSize) {
        Map data = new HashedMap();
        data.put("currIndex", (currPage-1)*pageSize);
        data.put("pageSize", pageSize);
        return studentMapper.queryStudentsBySql(data);
    }
  • 拦截器分页(数据量很大)
    大致说明

利用JDBC对数据库进行操作就必须要有一个对应的Statement对象,Mybatis在执行Sql语句前也会产生一个包含Sql语句的Statement对象,而且对应的Sql语句是在Statement之前产生的,所以我们就可以在它成Statement之前对用来生成Statement的Sql语句下手。在Mybatis中Statement语句是通过RoutingStatementHandler对象的prepare方法生成的。所以利用拦截器实现Mybatis分页的一个思路就是拦截StatementHandler接口的prepare方法,然后在拦截器方法中把Sql语句改成对应的分页查询Sql语句,之后再调用StatementHandler对象的prepare方法,即调用invocation.proceed()。更改Sql语句这个看起来很简单,而事实上来说的话就没那么直观,因为包括sql等其他属性在内的多个属性都没有对应的方法可以直接取到,它们对外部都是封闭的,是对象的私有属性,所以这里就需要引入反射机制来获取或者更改对象的私有属性的值了。对于分页而言,在拦截器里面我们常常还需要做的一个操作就是统计满足当前条件的记录一共有多少,这是通过获取到了原始的Sql语句后,把它改为对应的统计语句再利用Mybatis封装好的参数和设置参数的功能把Sql语句中的参数进行替换,之后再执行查询记录数的Sql语句进行总记录数的统计

定义对分页操作封装的一个实体类Page

import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
   
/** 
 * 对分页的基本数据进行一个简单的封装 
 */  
public class Page {  
   
    private int pageNo = 1;//页码,默认是第一页  
    private int pageSize = 15;//每页显示的记录数,默认是15  
    private int totalRecord;//总记录数  
    private int totalPage;//总页数  
    private List results;//对应的当前页记录  
    private Map params = new HashMap();//其他的参数我们把它分装成一个Map对象  
   
    public int getPageNo() {  
       return pageNo;  
    }  
   
    public void setPageNo(int pageNo) {  
       this.pageNo = pageNo;  
    }  
   
    public int getPageSize() {  
       return pageSize;  
    }  
   
    public void setPageSize(int pageSize) {  
       this.pageSize = pageSize;  
    }  
   
    public int getTotalRecord() {  
       return totalRecord;  
    }  
   
    public void setTotalRecord(int totalRecord) {  
       this.totalRecord = totalRecord;  
       //在设置总页数的时候计算出对应的总页数,在下面的三目运算中加法拥有更高的优先级,所以最后可以不加括号。  
       int totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : totalRecord/pageSize + 1;  
       this.setTotalPage(totalPage);  
    }  
   
    public int getTotalPage() {  
       return totalPage;  
    }  
   
    public void setTotalPage(int totalPage) {  
       this.totalPage = totalPage;  
    }  
   
    public List getResults() {  
       return results;  
    }  
   
    public void setResults(List results) {  
       this.results = results;  
    }  
     
    public Map getParams() {  
       return params;  
    }  
     
    public void setParams(Map params) {  
       this.params = params;  
    }  
   
    @Override  
    public String toString() {  
       StringBuilder builder = new StringBuilder();  
       builder.append("Page [pageNo=").append(pageNo).append(", pageSize=")  
              .append(pageSize).append(", results=").append(results).append(  
                     ", totalPage=").append(totalPage).append(  
                     ", totalRecord=").append(totalRecord).append("]");  
       return builder.toString();  
    }  
   
}  

定义自己的分页拦截器PageInterceptor.java

/**
 * @Intercepts 说明是一个拦截器
 * @Signature 拦截器的签名
 * type 拦截的类型 四大对象之一( Executor,ResultSetHandler,ParameterHandler,StatementHandler)
 * method 拦截的方法
 * args 参数,高版本需要加个Integer.class参数,不然会报错
 */
@Intercepts( {  
       @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class}) })  
public class PageInterceptor implements Interceptor {  
   
    private String databaseType;//数据库类型,不同的数据库有不同的分页方法  
     
    /** 
     * 拦截后要执行的方法 
     */  
    public Object intercept(Invocation invocation) throws Throwable {  
       //对于StatementHandler其实只有两个实现类,一个是RoutingStatementHandler,另一个是抽象类BaseStatementHandler,  
       //BaseStatementHandler有三个子类,分别是SimpleStatementHandler,PreparedStatementHandler和CallableStatementHandler,  
       //SimpleStatementHandler是用于处理Statement的,PreparedStatementHandler是处理PreparedStatement的,而CallableStatementHandler是  
       //处理CallableStatement的。Mybatis在进行Sql语句处理的时候都是建立的RoutingStatementHandler,而在RoutingStatementHandler里面拥有一个  
       //StatementHandler类型的delegate属性,RoutingStatementHandler会依据Statement的不同建立对应的BaseStatementHandler,即SimpleStatementHandler、  
       //PreparedStatementHandler或CallableStatementHandler,在RoutingStatementHandler里面所有StatementHandler接口方法的实现都是调用的delegate对应的方法。  
       //我们在PageInterceptor类上已经用@Signature标记了该Interceptor只拦截StatementHandler接口的prepare方法,又因为Mybatis只有在建立RoutingStatementHandler的时候  
       //是通过Interceptor的plugin方法进行包裹的,所以我们这里拦截到的目标对象肯定是RoutingStatementHandler对象。  
       RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();  
       //通过反射获取到当前RoutingStatementHandler对象的delegate属性  
       StatementHandler delegate = (StatementHandler)ReflectUtil.getFieldValue(handler, "delegate");  
       //获取到当前StatementHandler的 boundSql,这里不管是调用handler.getBoundSql()还是直接调用delegate.getBoundSql()结果是一样的,因为之前已经说过了  
       //RoutingStatementHandler实现的所有StatementHandler接口方法里面都是调用的delegate对应的方法。  
       BoundSql boundSql = delegate.getBoundSql();  
       //拿到当前绑定Sql的参数对象,就是我们在调用对应的Mapper映射语句时所传入的参数对象  
       Object obj = boundSql.getParameterObject();  
       //这里我们简单的通过传入的是Page对象就认定它是需要进行分页操作的。  
       if (obj instanceof Page) {  
           Page page = (Page) obj;  
           //通过反射获取delegate父类BaseStatementHandler的mappedStatement属性  
           MappedStatement mappedStatement = (MappedStatement)ReflectUtil.getFieldValue(delegate, "mappedStatement");  
           //拦截到的prepare方法参数是一个Connection对象  
           Connection connection = (Connection)invocation.getArgs()[0];  
           //获取当前要执行的Sql语句,也就是我们直接在Mapper映射语句中写的Sql语句  
           String sql = boundSql.getSql();  
           //给当前的page参数对象设置总记录数  
           this.setTotalRecord(page,  
                  mappedStatement, connection);  
           //获取分页Sql语句  
           String pageSql = this.getPageSql(page, sql);  
           //利用反射设置当前BoundSql对应的sql属性为我们建立好的分页Sql语句  
           ReflectUtil.setFieldValue(boundSql, "sql", pageSql);  
       }  
       return invocation.proceed();  
    }  
   
   
    /** 
     * 拦截器对应的封装原始对象的方法 
     */  
    public Object plugin(Object target) {  
       return Plugin.wrap(target, this);  
    }  
   
    /** 
     * 设置注册拦截器时设定的属性 
     */  
    public void setProperties(Properties properties) {  
       this.databaseType = properties.getProperty("databaseType");  
    }  
     
    /** 
     * 根据page对象获取对应的分页查询Sql语句,这里只做了两种数据库类型,Mysql和Oracle 
     * 其它的数据库都 没有进行分页 
     * 
     * @param page 分页对象 
     * @param sql 原sql语句 
     * @return 
     */  
    private String getPageSql(Page page, String sql) {  
       StringBuffer sqlBuffer = new StringBuffer(sql);  
       if ("mysql".equalsIgnoreCase(databaseType)) {  
           return getMysqlPageSql(page, sqlBuffer);  
       } else if ("oracle".equalsIgnoreCase(databaseType)) {  
           return getOraclePageSql(page, sqlBuffer);  
       }  
       return sqlBuffer.toString();  
    }  
     
    /** 
     * 获取Mysql数据库的分页查询语句 
     * @param page 分页对象 
     * @param sqlBuffer 包含原sql语句的StringBuffer对象 
     * @return Mysql数据库分页语句 
     */  
    private String getMysqlPageSql(Page page, StringBuffer sqlBuffer) {  
       //计算第一条记录的位置,Mysql中记录的位置是从0开始的。  
       int offset = (page.getPageNo() - 1) * page.getPageSize();  
       sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize());  
       return sqlBuffer.toString();  
    }  
     
    /** 
     * 获取Oracle数据库的分页查询语句 
     * @param page 分页对象 
     * @param sqlBuffer 包含原sql语句的StringBuffer对象 
     * @return Oracle数据库的分页查询语句 
     */  
    private String getOraclePageSql(Page page, StringBuffer sqlBuffer) {  
       //计算第一条记录的位置,Oracle分页是通过rownum进行的,而rownum是从1开始的  
       int offset = (page.getPageNo() - 1) * page.getPageSize() + 1;  
       sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize());  
       sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset);  
       //上面的Sql语句拼接之后大概是这个样子:  
       //select * from (select u.*, rownum r from (select * from t_user) u where rownum < 31) where r >= 16  
       return sqlBuffer.toString();  
    }  
     
    /** 
     * 给当前的参数对象page设置总记录数 
     * 
     * @param page Mapper映射语句对应的参数对象 
     * @param mappedStatement Mapper映射语句 
     * @param connection 当前的数据库连接 
     */  
    private void setTotalRecord(Page page,  
           MappedStatement mappedStatement, Connection connection) {  
       //获取对应的BoundSql,这个BoundSql其实跟我们利用StatementHandler获取到的BoundSql是同一个对象。  
       //delegate里面的boundSql也是通过mappedStatement.getBoundSql(paramObj)方法获取到的。  
       BoundSql boundSql = mappedStatement.getBoundSql(page);  
       //获取到我们自己写在Mapper映射语句中对应的Sql语句  
       String sql = boundSql.getSql();  
       //通过查询Sql语句获取到对应的计算总记录数的sql语句  
       String countSql = this.getCountSql(sql);  
       //通过BoundSql获取对应的参数映射  
       List parameterMappings = boundSql.getParameterMappings();  
       //利用Configuration、查询记录数的Sql语句countSql、参数映射关系parameterMappings和参数对象page建立查询记录数对应的BoundSql对象。  
       BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);  
       //通过mappedStatement、参数对象page和BoundSql对象countBoundSql建立一个用于设定参数的ParameterHandler对象  
       ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);  
       //通过connection建立一个countSql对应的PreparedStatement对象。  
       PreparedStatement pstmt = null;  
       ResultSet rs = null;  
       try {  
           pstmt = connection.prepareStatement(countSql);  
           //通过parameterHandler给PreparedStatement对象设置参数  
           parameterHandler.setParameters(pstmt);  
           //之后就是执行获取总记录数的Sql语句和获取结果了。  
           rs = pstmt.executeQuery();  
           if (rs.next()) {  
              int totalRecord = rs.getInt(1);  
              //给当前的参数page对象设置总记录数  
              page.setTotalRecord(totalRecord);  
           }  
       } catch (SQLException e) {  
           e.printStackTrace();  
       } finally {  
           try {  
              if (rs != null)  
                  rs.close();  
               if (pstmt != null)  
                  pstmt.close();  
           } catch (SQLException e) {  
              e.printStackTrace();  
           }  
       }  
    }  
     
    /** 
     * 根据原Sql语句获取对应的查询总记录数的Sql语句 
     * @param sql 
     * @return 
     */  
    private String getCountSql(String sql) {  
         
       return "select count(1) from (" + sql + ")";  
    }  
     
    /** 
     * 利用反射进行操作的一个工具类 
     * 
     */  
    private static class ReflectUtil {  
       /** 
        * 利用反射获取指定对象的指定属性 
        * @param obj 目标对象 
        * @param fieldName 目标属性 
        * @return 目标属性的值 
        */  
       public static Object getFieldValue(Object obj, String fieldName) {  
           Object result = null;  
           Field field = ReflectUtil.getField(obj, fieldName);  
           if (field != null) {  
              field.setAccessible(true);  
              try {  
                  result = field.get(obj);  
              } catch (IllegalArgumentException e) {  
                  // TODO Auto-generated catch block  
                  e.printStackTrace();  
              } catch (IllegalAccessException e) {  
                  // TODO Auto-generated catch block  
                  e.printStackTrace();  
              }  
           }  
           return result;  
       }  
        
       /** 
        * 利用反射获取指定对象里面的指定属性 
        * @param obj 目标对象 
        * @param fieldName 目标属性 
        * @return 目标字段 
        */  
       private static Field getField(Object obj, String fieldName) {  
           Field field = null;  
          for (Class clazz=obj.getClass(); clazz != Object.class; clazz=clazz.getSuperclass()) {  
              try {  
                  field = clazz.getDeclaredField(fieldName);  
                  break;  
              } catch (NoSuchFieldException e) {  
                  //这里不用做处理,子类没有该字段可能对应的父类有,都没有就返回null。  
              }  
           }  
           return field;  
       }  
   
       /** 
        * 利用反射设置指定对象的指定属性为指定的值 
        * @param obj 目标对象 
        * @param fieldName 目标属性 
         * @param fieldValue 目标值 
        */  
       public static void setFieldValue(Object obj, String fieldName,  
              String fieldValue) {  
           Field field = ReflectUtil.getField(obj, fieldName);  
           if (field != null) {  
              try {  
                  field.setAccessible(true);  
                  field.set(obj, fieldValue);  
              } catch (IllegalArgumentException e) {  
                  // TODO Auto-generated catch block  
                  e.printStackTrace();  
              } catch (IllegalAccessException e) {  
                  // TODO Auto-generated catch block  
                  e.printStackTrace();  
              }  
           }  
        }  
    }  
   
}  

接着我们在Mybatis的配置文件里面注册该拦截器

追加
  
         
             //数据库类型
         
 
  
  
  
      
      
         
      
      
         
             
         
      
      
         
             
             
                
                
                 
                
             
         
      
      
         
      

这样我们的拦截器就已经定义并且配置好了,接下来我们就来测试一下。假设在我们的UserMapper.xml中有如下这样一个Mapper映射信息

  

测试代码

SqlSession sqlSession = sqlSessionFactory.openSession();  
try {  
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);  
    Page page = new Page();  
    page.setPageNo(2);  
    List users = userMapper.findPage(page);  
    page.setResults(users);  
    System.out.println(page);  
} finally {  
    sqlSession.close();  
}  
  • RowBounds分页
数据量小
dao层加入RowBounds参数
public List queryUsersByPage(String userName, RowBounds rowBounds);

service层
@Override
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.SUPPORTS)
public List queryRolesByPage(String roleName, int start, int limit) {
        return roleDao.queryRolesByPage(roleName, new RowBounds(start, limit));
    }

116.mybatis 逻辑分页和物理分页的区别是什么?

  • 物理分页速度上并不一定快于逻辑分页,逻辑分页速度上也并不一定快于物理分页。
  • 物理分页总是优于逻辑分页:没有必要将属于数据库端的压力加到应用端来,就算速度上存在优势,然而其它性能上的优点足以弥补这个缺点。

117.mybatis 是否支持延迟加载?延迟加载的原理是什么?
什么是延迟加载?

举个例子:如果查询订单并且关联查询用户信息。如果先查询订单信息即可满足要求,当我们需要查询用户信息时再查询用户信息。把对用户信息的按需去查询就是延迟加载。 所以延迟加载即先从单表查询、需要时再从关联表去关联查询,大大提高数据库性能,因为查询单表要比关联查询多张表速度要快
这个出自https://blog.csdn.net/eson_15/article/details/51668523

关联查询:SELECT orders.*, user.username FROM orders, USER WHERE orders.user_id = user.id 
延迟加载相当于: 
SELECT orders.*, 
(SELECT username FROM USER WHERE orders.user_id = user.id)username FROM orders

  所以这就比较直观了,也就是说,我把关联查询分两次来做,而不是一次性查出所有的。
第一步只查询单表orders,必然会查出orders中的一个user_id字段,
然后我再根据这个user_id查user表,也是单表查询。下面来总结一下如何使用这个延迟加载

Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false
它的原理是,使用CGLIB创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用a.getB().getName(),拦截器invoke()方法发现a.getB()是null值,那么就会单独发送事先保存好的查询关联B对象的sql,把B查询上来,然后调用a.setB(b),于是a的对象b属性就有值了,接着完成a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是Mybatis,几乎所有的包括Hibernate,支持延迟加载的原理都是一样的。
118.说一下 mybatis 的一级缓存和二级缓存?
一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。

二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap 存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现Serializable序列化接口(可用来保存对象的状态),可在它的映射文件中配置

对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。这么做的目的是避免脏读
119.mybatis 和 hibernate 的区别有哪些?
(1)Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。
(2)Mybatis直接编写原生态sql,可以严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,因为这类软件需求变化频繁,一但需求变化要求迅速输出成果。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件,则需要自定义多套sql映射文件,工作量大。
(3)Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件,如果用hibernate开发可以节省很多代码,提高效率。
120.mybatis 有哪些执行器(Executor)?
Mybatis有三种基本的执行器(Executor):

  • SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。
  • ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map内,供下一次使用。简言之,就是重复使用Statement对象。
  • BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理。与JDBC批处理相同。

121.mybatis 如何编写一个自定义插件?
转自https://blog.csdn.net/qq_30051265/article/details/80266434

Mybatis自定义插件针对Mybatis四大对象(Executor、StatementHandler 、ParameterHandler 、ResultSetHandler )进行拦截,具体拦截方式为:
Executor:拦截执行器的方法(log记录)
StatementHandler :拦截Sql语法构建的处理
ParameterHandler :拦截参数的处理
ResultSetHandler :拦截结果集的处理

Mybatis自定义插件必须实现Interceptor接口:

public interface Interceptor {
    Object intercept(Invocation invocation) throws Throwable;
    Object plugin(Object target);
    void setProperties(Properties properties);
}
/*
intercept方法:拦截器具体处理逻辑方法 
plugin方法:根据签名signatureMap生成动态代理对象 
setProperties方法:设置Properties属性
*/

自定义插件demo:

// ExamplePlugin.java
@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
  Object target = invocation.getTarget(); //被代理对象
  Method method = invocation.getMethod(); //代理方法
  Object[] args = invocation.getArgs(); //方法参数
  // do something ...... 方法拦截前执行代码块
  Object result = invocation.proceed();
  // do something .......方法拦截后执行代码块
  return result;
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}
/*一个@Intercepts可以配置多个@Signature,@Signature中的参数定义如下: 
type:表示拦截的类,这里是Executor的实现类;
method:表示拦截的方法,这里是拦截Executor的update方法;
args:表示方法参数。
*/

Kafka

122.kafka 是什么?

出自https://mp.weixin.qq.com/s/zsYDoCMLZ3yfLWJ2j2ccTw
Kafka可以让合适的数据以合适的形式出现在合适的地方。Kafka的做法是提供消息队列,让生产者单往队列的末尾添加数据,让多个消费者从队列里面依次读取数据然后自行处理。之前连接的复杂度是O(N^2),而现在降低到O(N)
Kafka是一种高吞吐量的分布式发布订阅消息系统,

先上传说中的懵逼图

懵逼图

分析相关概念

Producer:Producer即生产者,消息的产生者,是消息的入口。

Broker:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,
我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个不重复的编号,
如图中的broker-0、broker-1等……

Topic:消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。
在每个broker上都可以创建多个topic。

Partition:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。
同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹!

Replication:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。
在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,
follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。

Message:每一条发送的消息主体。

Consumer:消费者,即消息的消费方,是消息的出口。

Consumer Group:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。
同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!

Zookeeper:kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性。
kafka的工作流程

写数据
producer就是生产者,是数据的入口,Producer在写入数据的时候永远的找leader,不会直接将数据写入follower!

写数据流程,

需要注意的一点是,消息写入leader后,follower是主动的去leader进行同步的!producer采用push模式将数据发布到broker,每条消息追加到分区中,顺序写入磁盘,所以保证同一分区内的数据是有序的!写入示意图如下:

partition即是不同的分区
  • kafka为什么要做分区?
    1、 方便扩展。因为一个topic可以有多个partition,所以我们可以通过扩展机器去轻松的应对日益增长的数据量。
    2、 提高并发。以partition为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。

  • 如果某个topic有多个partition,producer又怎么知道该将数据发往哪个partition呢?kafka中有几个原则:
    1、 partition在写入的时候可以指定需要写入的partition,如果有指定,则写入对应的partition。
    2、 如果没有指定partition,但是设置了数据的key,则会根据key的值hash出一个partition。
    3、 如果既没指定partition,又没有设置key,则会轮询选出一个partition。

  • 保证消息不丢失是一个消息队列中间件的基本保证:
    kafka通过ACK应答机制!在生产者向队列写入数据的时候可以设置参数来确定是否确认kafka接收到数据,这个参数可设置的值为0、1、all。
    0代表producer往集群发送数据不需要等到集群的返回,不确保消息发送成功。安全性最低但是效率最高。
    1代表producer往集群发送数据只要leader应答就可以发送下一条,只确保leader发送成功。
    all代表producer往集群发送数据需要所有的follower都完成从leader的同步才会发送下一条,确保leader发送成功和所有的副本都完成备份。安全性最高,但是效率最低。
    最后要注意的是,如果往不存在的topic写数据,kafka会自动创建topic,分区和副本的数量根据默认配置都是1。

保存数据

  • 先了解Partition 结构
    前面说过了每个topic都可以分为一个或多个partition,如果你觉得topic比较抽象,那partition就是比较具体的东西了!Partition在服务器上的表现形式就是一个一个的文件夹,每个partition的文件夹下面会有多组segment文件,每组segment文件又包含.index文件、.log文件、.timeindex文件(早期版本中没有)三个文件, log文件就实际是存储message的地方,而index和timeindex文件为索引文件,用于检索消息,看图
    partition结构

    如上图,这个partition有三组segment文件,每个log文件的大小是一样的,但是存储的message数量是不一定相等的(每条的message大小不一致)。文件的命名是以该segment最小offset来命名的,如000.index存储offset为0~368795的消息,kafka就是利用分段+索引的方式来解决查找效率的问题。
  • Message结构
    上面说到log文件就实际是存储message的地方,我们在producer往kafka写入的也是一条一条的message,那存储在log中的message是什么样子的呢?消息主要包含消息体、消息大小、offset、压缩类型……等等!我们重点需要知道的是下面三个:
    1、 offset:offset是一个占8byte的有序id号,它可以唯一确定每条消息在parition内的位置!
    2、 消息大小:消息大小占用4byte,用于描述消息的大小。
    3、 消息体:消息体存放的是实际的消息数据(被压缩过),占用的空间根据具体的消息而不一样。

消费数据
消息存储在log文件后,消费者就可以进行消费了。我们知道消息队列通信的两种模式是点对点模式和发布订阅模式。Kafka采用的是点对点的模式,消费者主动的去kafka集群拉取消息,与producer相同的是,消费者在拉取消息的时候也是找leader去拉取。

多个消费者可以组成一个消费者组(consumer group),每个消费者组都有一个组id!同一个消费组者的消费者可以消费同一topic下不同分区的数据,但是不会组内多个不同的消费者消费同一分区的数据!!!是不是有点绕。我们看下图:

消费组消费数据

图示是消费者组内的消费者小于partition数量的情况,所以会出现某个消费者消费多个partition数据的情况。
如果是消费者组的消费者多于partition的数量,那会不会出现多个消费者消费同一个partition的数据呢?上面已经提到过不会出现这种情况!多出来的消费者不消费任何partition的数据。

存储策略
无论消息是否被消费,kafka都会保存所有的消息。那对于旧数据有什么删除策略呢?
1、 基于时间,默认配置是168小时(7天)。
2、 基于大小,默认配置是1073741824。
需要注意的是,kafka读取特定消息的时间复杂度是O(1),所以这里删除过期的文件并不会提高kafka的性能!

查找数据:利用segment+offset配合查找
假如现在需要查找一个offset为368801的message是什么样的过程?

查找数据

1、 先找到offset的368801message所在的segment文件(利用二分法查找),这里找到的就是在第二个segment文件。

2、 打开找到的segment中的.index文件(也就是368796.index文件,该文件起始偏移量为368796+1,我们要查找的offset为368801的message在该index内的偏移量为368796+5=368801,所以这里要查找的相对offset为5)。由于该文件采用的是稀疏索引的方式存储着相对offset及对应message物理偏移量的关系,所以直接找相对offset为5的索引找不到,这里同样利用二分法查找相对offset小于或者等于指定的相对offset的索引条目中最大的那个相对offset,所以找到的是相对offset为4的这个索引。

3、 根据找到的相对offset为4的索引确定message存储的物理偏移位置为256。打开数据文件,从位置为256的那个地方开始顺序扫描直到找到offset为368801的那条Message。

这套机制是建立在offset为有序的基础上,利用segment+有序offset+稀疏索引+二分查找+顺序查找等多种手段来高效的查找数据!至此,消费者就能拿到需要处理的数据进行处理了。那每个消费者又是怎么记录自己消费的位置呢?在早期的版本中,消费者将消费到的offset维护zookeeper中,consumer每间隔一段时间上报一次,这里容易导致重复消费,且性能不好!在新的版本中消费者消费到的offset已经直接维护在kafk集群的__consumer_offsets这个topic中!

123.使用 kafka 集群需要注意什么?
集群的数量不是越多越好,最好不要超过 7 个,因为节点越多,消息复制需要的时间就越长,整个群组的吞吐量就越低。
集群数量最好是单数,因为超过一半故障集群就不能用了,设置为单数容错率更高。

消息队列之 RabbitMQ

124.rabbitmq介绍
官网https://www.rabbitmq.com/getstarted.html

RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息。

  • 什么是AMQP?
    AMQP,Advanced Message Queuing Protocol,翻译成中文即高级消息队列协议。它是一个提供统一消息服务的应用层二进制协议。基于此协议的客户端与消息中间件可传递消息,并不受不同平台、开发语言和操作系统的影响,即可跨平台传输。
  • AMQP模型


    AMQP模型

    主要组件和逻辑概念

1.Publisher(生产者):消息生产者,AMQP定义了消息的格式,所以生产者需要按照AMQP消息格式生产数据。

2.Broker(服务器):Broker是一个物理上的服务器(或虚拟机),它是部署了消息中间件并接收处理客户端请求的实体。

3.Exchange(交换机):它是一个逻辑上的概念,负责接收消息,是整个消息中间件的入口。

4.Queue(队列):它负责保存消息,并将消息转发给消费者。

5.Binding(绑定):它是Exchange与Queue之间的虚拟连接,实现了根据不同的Routing Key(路由规则)将消息路由到对应的Queue上。

6.Message(消息):消息,本质上就是服务器与客户端传输的数据,由元信息和消息体组成。

7.Virtual Host(虚拟主机):它是一个逻辑上的概念,一个Broker上可以有多个Virtual Host,它起到一个命名空间的作用,可以让服务于不同业务的Exchange和Queue隔离开,是实现权限控制的最小单位。

8.Connection(连接):客户端与服务器之间的网络连接。

9.Channel(信道):一次客户端与服务器之间的通信,相当于JMS中的Session的概念。

  • AMQP的主要处理流程
    生产者是将消息发送到Exchange,Exchange根据路由规则Routing Key将消息路由到不同的Queue上,如果Queue上有消费者监听,则消费者可以获得消息。生产者在生产消息的时候是不知道消费者的状态的,消费者在消费消息时也是不知道消息是从哪个生产者来的,即生产者与消费者之间的完全解耦的。

125.rabbitmq 的使用场景有哪些?
你可以根据自己想需要进行应用,具体代码可以去官网
六种应用场景

  • 1.传递hello world

  • 2.在工人之间分配任务(竞争消费者模式)

  • 3.一次向多个消费者发送消息

  • 4.有选择地接收消息


    比如,只把error message写到log file而将所有log message显示在控制台。

  • 5.据模式(主题)接收消息

  • 6.RPC

在应用场景2中描述了如何使用work queue将耗时的task分配到不同的worker中。但是,如果我们task是想在远程的计算机上运行一个函数并等待返回结果呢。这根场景2中的描述是一个完全不同的事件。这一模式被称为远程过程调用

126.rabbitmq 有哪些重要的角色?
RabbitMQ 中重要的角色有:生产者、消费者和代理:
生产者:消息的创建者,负责创建和推送数据到消息服务器;
消费者:消息的接收方,用于处理数据和确认消息;
代理:就是 RabbitMQ 本身,用于扮演“快递”的角色,本身不生产消息,只是扮演“快递”的角色
127.rabbitmq 有哪些重要的组件?

  • ConnectionFactory(连接管理器):应用程序与Rabbit之间建立连接的管理器,程序代码中使用。
  • Channel(信道):消息推送使用的通道。
  • Exchange(交换器):用于接受、分配消息。
  • Queue(队列):用于存储生产者的消息。
  • RoutingKey(路由键):用于把生成者的数据分配到交换器上。
  • BindingKey(绑定键):用于把交换器的消息绑定到队列上。

128.rabbitmq 中 vhost 的作用是什么?
vhost是rabbitmq分配权限的最小细粒度。比如我们可以为一个用户分配一个可以访问哪个或者哪一些vhost的权限。
但是不能为用户分配一个可以访问哪一些exchange,或者queue的权限,因为rabbitmq的权限细粒度没有细化到交换器和队列,他的最小细粒度是vhost;一个broker可以开设多个vhost,用于不同用户的权限分离

129. rabbitmq 的消息是怎么发送的?
首先客户端必须连接到 RabbitMQ 服务器才能发布和消费消息,客户端和 rabbit server 之间会创建一个 tcp 连接,一旦 tcp 打开并通过了认证(认证就是你发送给 rabbit 服务器的用户名和密码),你的客户端和 RabbitMQ 就创建了一条 amqp 信道(channel),信道是创建在“真实” tcp 上的虚拟连接,amqp 命令都是通过信道发送出去的,每个信道都会有一个唯一的 id,不论是发布消息,订阅队列都是通过这个信道完成的。
130.rabbitmq 怎么保证消息的稳定性?
1.提供了事务的功能。(要么成功,要么失败,保持数据一致性,同步阻塞卡出等待你是成功还是失败;太耗性能会造成吞吐量的下降。)
2.通过将 channel 设置为 confirm(确认)模式。

生产者将信道设置成confirm模式,一旦信道进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,broker就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出,broker回传给生产者的确认消息中delivery-tag域包含了确认消息的序列号,此外broker也可以设置basic.ack的multiple域,表示到这个序列号之前的所有消息都已经得到了处理.
confirm模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果RabbitMQ因为自身内部错误导致消息丢失,就会发送一条nack消息,生产者应用程序同样可以在回调方法中处理该nack消息;
消费者(Consumer)的Confirm模式:消费者在声明队列时,可以指定noAck参数,当noAck=false时,RabbitMQ会等待消费者显式发回ack信号后才从内存(和磁盘,如果是持久化消息的话)中移去消息。采用消息确认机制后,只要令noAck=false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直持有消息直到消费者显式调用basic.Ack为止。

basic.ack: 用于肯定确认,multiple参数用于多个消息确认。
basic.recover:是路由不成功的消息可以使用recovery重新发送到队列中。
basic.reject:是接收端告诉服务器这个消息我拒绝接收,不处理,可以设置是否放回到队列中还是丢掉,而且只能一次拒绝一个消息,官网中有明确说明不能批量拒绝消息,为解决批量拒绝消息才有了basicNack。
basic.nack:可以一次拒绝N条消息,客户端可以设置basicNack方法的multiple参数为true,服务器会拒绝指定了delivery_tag的所有未确认的消息(tag是一个64位的long值,最大值是9223372036854775807)。
131. rabbitmq 怎么避免消息丢失?
1.消息持久化
2.ACK确认机制
3.设置集群镜像模式
4.消息补偿机制

  • 1.消息持久化
RabbitMQ 的消息默认存放在内存上面,如果不特别声明设置,消息不会持久化保存到硬盘上面的,
如果节点重启或者意外crash掉,消息就会丢失。
所以就要对消息进行持久化处理。如何持久化,下面具体说明下:

要想做到消息持久化,必须满足以下三个条件,缺一不可。

1)Exchange 设置持久化(durable字段设置为true)
2)Queue 设置持久化( durable 设置为 true)
3)Message持久化发送:发送消息设置发送模式deliveryMode=2,代表持久化消息
  • 3.设置集群镜像模式
    缺点: 系统的吞吐量会有所下降

  • 4.消息补偿机制

消息补偿机制需要建立在消息要写入DB日志,发送日志,接受日志,两者的状态必须记录。
然后根据DB日志记录check 消息发送消费是否成功,不成功,进行消息补偿措施,重新发送消息处理。

132.要保证消息持久化成功的条件有哪些?
声明队列/交换器必须设置持久化 durable 设置为 true.
消息推送投递模式必须设置持久化,deliveryMode 设置为 2(持久)。
消息已经到达持久化交换器。
消息已经到达持久化队列
133.rabbitmq 持久化有什么缺点?
持久化的缺地就是降低了服务器的吞吐量,因为使用的是磁盘而非内存存储,从而降低了吞吐量。可尽量使用 ssd 硬盘来缓解吞吐量的问题。
134.rabbitmq 有几种广播类型?
三种广播模式:

  • fanout: 所有bind到此exchange的queue都可以接收消息(纯广播,绑定到RabbitMQ的接受者都能收到消息);
  • direct: 通过routingKey和exchange决定的那个唯一的queue可以接收消息;
  • topic:所有符合routingKey(此时可以是一个表达式)的routingKey所bind的queue可以接收消息

135.rabbitmq 怎么实现延迟消息队列?
1.通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能;
2.用 RabbitMQ-delayed-message-exchange 插件实现延迟功能。
136.rabbitmq 集群有什么用?
集群主要有以下两个用途:
1.高可用:某个服务器出现问题,整个 RabbitMQ 还可以继续使用;
2.高容量:集群可以承载更多的消息量。
137.rabbitmq 节点的类型有哪些?
磁盘节点:消息会存储到磁盘。
内存节点:消息都存储在内存中,重启服务器消息丢失,性能高于磁盘类型。
138.rabbitmq 集群搭建需要注意哪些问题?
1.各节点之间使用“--link”连接,此属性不能忽略。
2.各节点使用的 erlang cookie 值必须相同,此值相当于“秘钥”的功能,用于各节点的认证。
3.整个集群中必须包含一个磁盘节点。
139.rabbitmq 每个节点是其他节点的完整拷贝吗?为什么?
不是,原因有以下两个:
1.存储空间的考虑:如果每个节点都拥有所有队列的完全拷贝,这样新增节点不但没有新增存储空间,反而增加了更多的冗余数据;
2.性能的考虑:如果每条消息都需要完整拷贝到每一个集群节点,那新增节点并没有提升处理消息的能力,最多是保持和单节点相同的性能甚至是更糟。
140.rabbitmq 集群中唯一一个磁盘节点崩溃了会发生什么情况?
如果唯一磁盘的磁盘节点崩溃了,不能进行以下操作:

不能创建队列
不能创建交换器
不能创建绑定
不能添加用户
不能更改权限
不能添加和删除集群节点

唯一磁盘节点崩溃了,集群是可以保持运行的,但你不能更改任何东西。
141.rabbitmq 对集群节点停止顺序有要求吗?
RabbitMQ 对集群的停止的顺序是有要求的,应该先关闭内存节点,最后再关闭磁盘节点。如果顺序恰好相反的话,可能会造成消息的丢失。

声明

文章引用参考来自:https://mp.weixin.qq.com/s/q_SduX1NfyDtA-l-3tiXEA
文章只是作为自己的学习笔记,借鉴了网上的许多案例

你可能感兴趣的:(Java最常见的面试题--2)