面试题总结
java 集合
Collection
存放单一元素
List- 有序可重复
 ArrayList:Object数组,不同步,线程不安全- 内部基于动态数组实现,比 
Array使用起来更灵活 - 可以添加 
null值,但是不建议,会难以维护判空异常 
- 内部基于动态数组实现,比 
 Vector:古早的以Object数组为基础的LinkedList:双向链表(1.6 之前是循环链表,1.7 取消了循环),不同步,线程不安全- 因为是由链表构成的,所以不支持随机访问,不能实现 
RandomAccess接口 
- 因为是由链表构成的,所以不支持随机访问,不能实现 
 
Set- 无序不可重复
 HashSet:基于HashMap实现- 检查重复:对象加入 
HashSet时,会计算hashCode值来判断对象的加入位置,同时也会与其他加入的对象的hashCode的值比较,如果没有相符的,则会认为没有重复;否则,会调用equals()方法来检查hashCode相等的对象是否真的相同 
- 检查重复:对象加入 
 LinkedHashSet:HashSet的子类,通过LinkedHashLMap实现TreeMap:红黑树实现
Queue- 按照特定顺序排序,有序可重复
 
Comparable:出自java.lang包,有一个compareTo(Object obj)方法Comparator:出自java.util包,有一个compare(Object obj1, Object obj2)方法
Map
以键值对形式存储
- 
HashMap:1.8 之前以数组和链表组成,链表是为了解决哈希冲突存在;之后改成了数组/链表+红黑树(当链表长度超过阈值 8 时,先判断数组是否超过 64,超过,将链表转换为红黑树,没超过,先把数组扩容),线程不安全- 
因为线程安全问题,效率高于
HashTable - 
可以存储
Null key和Null value,但是null键只有一个,null值可以有多个 - 
默认初始化大小为 16,每次扩容容量变为原来 2 倍,如果给了初始值,则每次扩容容量为 2 的幂次方大小
 - 
- 第一个线程:在头节点插入前被调度挂起
 - 第二个线程:因为使用的是头插法,原来的头节点插入新链表后,第二个节点头插法插入同一个数组下标下的链表,线程结束
 - 第一个线程:把新数组下标中存储第二个线程的更新后的那个头节点,此时尾节点的后继指向了头节点
 
 - 
1.8 后,多个线程更新同一个桶的数据会产生数据覆盖的风险
 
 - 
 - 
LinkedHashMap:继承自HashMap,底层是基于拉链式散列结构即数组和链表或红黑树组成,另外多加了双向链表,可以头插和尾插数据,线程不安全 - 
HashTable:数组+链表,链表主要为了解决哈希冲突,线程安全(因为内部方法都是用synchronized修饰的)- 不允许存储 
Null key和Null value 
 - 不允许存储 
 - 
TreeMap:红黑树,线程不安全- 实现了 
NavigableMap接口:有了对集合内元素搜索的能力 - 实现了 
SortedMap接口:有了对集合中元素根据键排序的能力,默认是按Key的升序排序 
 - 实现了 
 
遍历方式
Concurrenthashmap 是怎么实现线程安全的(具体读写)
- 1.7   之前主要依赖于分段锁 - 
Segment- 由多个 
Segment组成(默认为 16),每个Segment维护一个HashEntery数组(数组+链表,类似与 1.7 之前的HashMap) - 每个 
Segment继承ReetrantLock,用于实现分段锁,每个锁只锁容器其中一部分数据,多线程访问不会存在锁竞争,提高并发访问率- 因为继承了 
ReetrantLock,所以Segment是一种可重入锁,扮演锁的角色 
 - 因为继承了 
 - 读操作
- 如 
get(key)不会加锁,直接通过volatile变量保证可见性 
 - 如 
 - 写操作
put、remove需要锁住单个Segment,并不会影响其他Segment- 计算 
key的hash值,找到Segment,然后使用Segment的ReentrantLock.lock()进行加锁操作 - 插入或删除数据后释放锁
 
 
 - 由多个 
 - 1.8 后彻底移除了 
Segment,改用数组+链表+红黑树的数据结构,并使用CAS + Synchronized实现线程安全- 由 
Node<K, V>[] table作为主数据存储 - 当表长度超过一定阈值(8)时,转换为红黑树
 - 锁粒度更细, 
synchronized只锁定当前链表或红黑二叉树的首节点,只要不产生hash冲突就不会产生并发 - 读操作
get(key)操作不会加锁:- 计算 
key的hash值,找到bucket(数组槽) - 直接读取 
table[index],如果是链表或红黑树,则遍历查找 - 由于 
Node的value使用volatile修饰,所以可以保 证可见性,无需加锁 
- 计算 
 
 - 写操作:
put(key, value):- 计算 
key的hash值,找到数组的index位置 CAS方法创建新Node:如果该bucket为空,则用CAS插入新节点,避免加锁Synchronized锁定链表或红黑树- 如果 
CAS失败,说明该bucket已有数据,则用synchronized直接锁定该桶的头节点,进行插入 - 如果是链表,遍历后插入
 - 如果是红黑树,按照红黑树规则插入
 
- 如果 
 
- 计算 
 
 
 - 由 
 
红黑树的优势
红黑树是一种自平衡二叉搜索树( BFS )
- 优势
- 保持平衡,最坏情况下时间复杂度 
O(logn)- 普通的二叉搜索树( 
BFS)在极端情况下会退化成链表(即高度接近n),导致O(n)的查询复杂度 - 红黑树通过旋转和变色操作保持平衡,保证最坏情况下的增删改查操作都维持在 
O(logn),不会出现严重的退化 
 - 普通的二叉搜索树( 
 - 插入、删除效率高
- 插入、删除操作的调整成本较低:自平衡调整(变色 + 最多 2 次旋转)开销较小
 
 - 适用于高并发
- 在 
ConcurrentHashMap中,当桶中的链表长度超过 8 时,转换成红黑树,提高查询性能 - 插入、删除的调整操作相对较少,在并发环境下更稳定,不会频繁进行全书重平衡
 
 - 在 
 - 空间占用较低
- 红黑树不需要存储额外的平衡因子
 - 只需要额外存储 1 位颜色信息(红/黑)
 
 
 - 保持平衡,最坏情况下时间复杂度 
 
jvm 虚拟机
数据库的隔离级别
定义了多个事务并发执行时的数据可见性,主要用于控制脏读、不可重复读、幻读等问题
- 读未提交:最低的隔离级别,事务可以读取其他未提交事务的数据,并发性高,但数据一致性最差
- 脏读:事务 A 读取了事务 B 未提交的数据,如果 B 回滚, A 读到的数据就是无效的
 - 不可重复读:事务 A 多次读取同一行数据,期间事务 B 修改并提交,导致 A 读取的值不一致
 - 幻读:事务 A 读取了某个范围的数据,事务 B 插入了新数据,导致 A 重新读取时看到了幻影数据
 
 - 读已提交:只能读取已经提交的数据,避免了脏读,比读未提交安全,仍支持较高的并发性
- 不可重复读、幻读
 
 - 可重复读:事务执行期间,读取的所有数据保持一致,防止不可重复读。
- 幻读
 Mysql InnoDB通过MVCC(多版本并发控制)避免了幻读
 - 可串行化:最高级别,每个事务必须依次执行,完全避免并发问题
- 可能导致死锁
 - 需要表级锁或行级锁,影响系统吞吐量
 
 
数据库索引优化措施
volatile
主要用于保证变量的可见性和防止指令重排,但不保证原子性,每次使用都要到主内存中读取
- 
当一个变量被
volatile修饰后,所有线程都能看见它的最新值:JVM 通过内存屏障和禁止CPU缓存优化,确保变量的修改立即对所有线程可见 - 
volatile会禁止CPU和编译器对代码进行指令重排,保证代码按预期执行顺序运行:通过插入特定的内存屏障来禁止指令重排,例如:双重校验锁实现对象单例(线程安全)- 
为变量
uniqueInstance分配内存空间 - 
初始化
uniqueInstance - 
将
uniqueInstace指向分配的内存地址 
但是由于
JVM具有指令重排的特性,执行顺序可能变成 1-> 3 -> 2。指令在单线程的情况下不会出现问题,但是多线程环境下会导致一个线程获得还没有初始化的实例 - 
 - 
不能保证原子性:因为当多个线程同时读取被
volatile修饰的变量时,可能同时读取到同一个值,而不是依次读取- 可以利用 
synchronized、Lock、AtomicInteger来实现正确操作 
 - 可以利用 
 
synchronized
用于保证代码块或方法在多线程环境下的同步执行,以确保原子性、可见性、有序性
- 保证原子性:确保多个线程同时访问共享资源时,操作不会被中断
 - 保证可见性:线程对变量的修改对其他线程立即可见(因为同步代码块执行时,会刷新 
CPU缓存) - 保证有序性:防止指令重排,确保代码按预期执行
 
用法
- 
修饰实例方法:锁当前对象
- 
synchronized void method() { // 业务代码 } 
 - 
 - 
修饰静态方法:锁当前类,会作用于类的所有对象实例,进入同步代码前要获取当前
class的锁- 
synchronized static void method() { // 业务代码 }静态
synchronized方法和非静态synchronized方法之间的调用不互斥:因为访问静态方法占用的锁是当前类的锁,二访问非静态方法占用的锁是当前实例对象锁 
 - 
 - 
修饰代码块:锁指定对象/类,对括号里指定的对象/类加锁
synchronized(Object)表示进入同步代码库前要获得给指定对象的锁synchronized(类.class)表示进入同步代码前要获得给指定Class的锁
 - 
构造方法不能 用
synchronized修饰,但是可以在方法内部去修饰代码块 
底层原理
synchronized底层是依靠JVM进入/退出对象的Monitor(监视器)实现的- 获取对象的 
Monitor:- 若 
Monitor空闲,则线程占用Monitor,执行代码 - 若 
Monitor被占用,线程进入阻塞状态,直到获取Monitor 
 - 若 
 - 释放 
Monitor:- 代码执行完毕,或抛出异常,线程释放 
Monitor,其他线程才能进入 
 - 代码执行完毕,或抛出异常,线程释放 
 
- 获取对象的 
 
ReentrantLock
是 Java 并发包( java.util.concurrent.locks )提供的一种可重入且独占式的锁,相较于 synchronized ,它提供了更灵活的锁管理:
- 
可重入锁:也叫递归锁,同一线程可多次获取同一把锁
- 在 
A线程中开启锁后调用B线程(也开启锁),不会阻塞。因为同一线程能多次lock(),但要匹配unlock()的次数 
 - 在 
 - 
支持公平锁/非公平锁
- 公平锁:锁被释放后,先申请的线程先得到锁。性能较差,上下文切换更频繁
 - 非公平锁:锁被释放后,后申请的线程可能会先获取锁,是随机或者按照其他优先级排序。性能更好,但可能导致某些线程饿死
 
 - 
支持可中断锁:
lockInterruptibly()允许线程在等待锁时被中断,抛出异常(Thread.interrupt()) - 
支持超时获取锁:提供了
tryLock(time, unit)的方法,尝试在指定时间内获取锁,避免死锁,超时后返回false,可以执行其他逻辑,而不会永远阻塞 - 
支持多个条件变量:
Condition接口- 在一个 
Lock对象中可以创建多个Condition实例(即对象监视器),线程对象可以注册在指定的Condition中,从而可以有选择的进行线程通知,在调度线程上更加灵活。 - 在使用 
notify()/notifyAll()方法进行通知时,被通知的线程是由JVM选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知” 
 - 在一个 
 
synchronized vs ReentrantLock
| 对比 | synchronized | ReentrantLock | 
|---|---|---|
| 可重入 | ✅ 支持 | ✅ 支持 | 
| 公平锁 | ❌ 默认非公平 | ✅ 可公平/非公平 | 
| 阻塞中断 | ❌ 不支持 | ✅ lockInterruptibly() 支持中断 | 
| 超时获取 | ❌ 不支持 | ✅ tryLock(time, unit) 可超时 | 
| 性能 | 🚀 低竞争时优化 | 🚀 更灵活 | 
自动装配(Autowiring)
是 Spring 依赖注入(DI)的一种方式,它可以让 Spring 自动根据类型、名称等,找到合适的 Bean 并注入,而无需手动配置
| 自动装配方式 | 说明 | 
|---|---|
no(默认) | 不使用自动装配,需手动 @Bean 或 XML 配置 | 
byName | 根据 属性名 匹配同名 Bean | 
byType | 根据 类型 匹配 Bean | 
constructor | 根据 构造方法参数 自动装配 | 
autodetect(过时) | 先尝试 constructor,如果失败,则 byType | 
自动配置主要由@EnableAutoConfiguration实现,添加了@EnableAutoConfiguration注解,会导入AutoConfigurationImportSelector类,里面的selectImports方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有含有META-INF/spring.factories的jar包,将对应key为@EnableAutoConfiguration注解全名对应的value类全部装配到IOC容器中。
设计模式应用及好处
解决特定问题的最佳实践,能够提高代码的可维护性、复用性和扩展性
设计模式分为三大类:
| 类型 | 作用 | 常见模式 | 
|---|---|---|
| 创建型模式 | 解决对象创建问题,隐藏对象创建逻辑 | 单例、工厂、抽象工厂、建造者、原型 | 
| 结构型模式 | 解决类与类之间的关系,提高复用性 | 代理、装饰、适配器、桥接、组合、外观、享元 | 
| 行为型模式 | 解决对象间通信,提高灵活性 | 观察者、策略、命令、责任链、模板方法、状态、迭代器、备忘录、解释器 | 
创建型模式
- 单例模式:确保一个类只有一个实例,并提供一个全局访问点
 - 工厂模式:在不暴露创建对象的逻辑的前提下,使用工厂方法来创建对象
 - 抽象工厂模式:提供一个接口,用于创建相关或依赖对象的实例,而不需要指定实际实现类
 - 建造者模式:将复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示
 - 原型模式:通过克隆来创建对象,避免了通过 
new关键字显示调用构造函数的开销 
构造型模式
- 适配器模式:将一个类的接口转换成客户希望的另一个接口,使得原本由于接口不兼容而无法一起工作的类可以一起工作
 - 桥接模式:将抽象部分与它的实现部分分离,以便它们可以独立 的变化
 - 组合模式:将对象组合成树形结构以表示部分-整体的层次结构,使得客户端使用单个对象或组合对象具有一致性
 - 装饰器模式:动态地给一个对象添加一些额外的职责,就增加功能而言,装饰器模式比生成子类方式更为灵活
 - 外观模式:为子系统中的一组接口提供一个一致的界面,使得子系统更容易使用
 - 享元模式:运用共享技术来有效地支持大量细粒度对象的复用
 - 代理模式:为其他对象提供一种代理以控制对这个对象的访问
 
行为型模式
- 责任链模式:为解除请求的发送者和接收者之间的耦合,而将请求的处理对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止
 - 命令模式:将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作
 - 解释器模式:给定一个语言,定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子
 - 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素,而又不需要暴露该对象的内部表示
 - 中介者模式:用一个中介对象封装一系列的对象交互,使得这些对象不需要显示地互相引用,从而降低耦合度
 - 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态
 - 观察者模式:定义对象间的一种一对多的依赖关系,当前对象的状态发生改变时,所有依赖于它的对象都得到通知并自动更新
 - 状态模式:允许一个对象在其内部状态发生改变时改变其行为,对象看起来似乎改变了它的类
 - 策略模式:定义一系列的算法,将每个算法封装起来 ,并使它们之间可以互换
 - 模板方法模式:定义一个操作中的算法骨架,将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤
 - 过滤器设计模式:允许在不改变原始对象的情况下,动态地添加或删除对象的行为
 
设计模式六大原则
- 单一职责原则:一个类应该只有一个引起它变化的原因。一个类应该只有一项职责,这样可以保证类的内聚性,并且降低类之间的耦合
 - 开闭原则:一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。当需要添加新功能时,应尽量通过扩展已有代码来实现,而不是修改已有代码。
 - 里氏替换原则:子类应该能够替换父类并且不影响程序的正确性。这意味着在使用继承时,子类不能修改父类已有的行为,而只能扩展父类的功能
 - 接口隔离原则:客户端不应该依赖于它不需要的接口。一个类应该只提供它需要的接口,而不应该强迫客户端依赖于它不需要的接口
 - 依赖倒置原则:高层模块不应该依赖于底层模块,都应该依赖于抽象。抽象不应该依赖于具体实现,而具体实现应该依赖于抽象
 - 迪米特法则:一个对象应该对其他对象保持最少的了解。一个对象只应该与它直接作用的对象发生交互,而不应该与其他任何对象发生直接交互。可以降低类之间的耦合性,提高系统的灵活性和可维护性
 
组合使用
- 工厂模式 + 单例模式:使用工厂模式创建对象,通过单例模式来保证该工厂只有一个实例,从而减少创建对象时的开销
 - 模板方法模式 + 策略模式:使用模板方法模式来定义算法的骨架,同时使用策略模式来定义算法的不同实现方式,以实现更高的灵活性
 - 策略模式 + 工厂模式:使用工厂模式创建不同的策略对象,然后使用策略模式来选择不同的策略,以实现不同的功能
 - 适配器模式 + 装饰器模式:适配器模式用于一个接口转换成另一个接口,而装饰器模式则用于动态地给对象添加一些额外的职责。
 - 观察者模式 + 命令模式:观察者模式用于观察对象的状态变化,并及时通知观察者。而命令模式则用于将一个请求封装成一个对象,可以在运行时动态地切换命令的接收者。当我们需要观察对象的状态变化,并在状态变化时执行一些命令时,可以使用
 
好处
✅ 提高代码复用性,减少重复代码 ✅ 降低代码耦合度,使模块独立 ✅ 增强扩展性,新增功能时修改更少 ✅ 符合 SOLID 设计原则,提高软件质量
如何处理异常
@RestController 和@Controller 的区别
@RestController
- 适用于 
RESTful API - 继承自 
@Controller,并自动包含@ResponseBody - 方法返回值默认是 
JSON或XML(基于Jackson或Gson进行序列化) - 不需要手动加 
@ResponseBody 
@Controller
- 
适用于返回视图(如
Thymeleaf,JSP) - 
需要手动使用
@ResponseBody才能返回JSON - 
默认返回的是一个视图(
HTML页面),而不是JSON如果在方法上加上
@RequestBody则该API不会返回视图,而是返回JSON数据 
java 里的 json
常用的 JSON 解释库包括
- 
Jackson(Spring默认推荐)// 对象 转 json
String json = objectMapper.writeValueAsString(user);
// json 转 对象
User userObj = objectMapper.readValue(json, User.class);
// json 转 map
Map<String, Object> map = objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {}); - 
Gson(Google提供)// 对象 转 json
String json = gson.toJson(new User("Bob", 30));
// json 转 对象
User user = gson.fromJson(json, User.class); - 
FastJSON(阿里巴巴提供,性能较优)// 对象 转 json
String json = JSON.toJSONString(new User("Charlie", 28));
// json 转 对象
User user = JSON.parseObject(json, User.class); - 
org.json(官方JSON解析库)import org.json.JSONObject;
JSONObject jsonObject = new JSONObject("{\"name\":\"David\",\"age\":40}");
System.out.println(jsonObject.getString("name")); // David 
| JSON 库 | 适用场景 | 优势 | 劣势 | 
|---|---|---|---|
| Jackson | Spring Boot 默认 | 功能强大,支持 POJO 直接转换 | 依赖较多 | 
| Gson | 轻量级 JSON 解析 | API 简单易用 | 速度稍慢 | 
| FastJSON | 高性能 JSON 解析 | 解析速度快 | 安全性争议 | 
| org.json | 轻量级官方库 | 纯粹 JSON 处理 | 功能较少 | 
正向、反向代理
正向代理(服务端看不见客户端)
客户端通过代理服务器访问目标服务器。客户端先向代理服务器发送请求,代理服务器在代表客户端去访问目标服务器,并将结果返回给客户端。
流程:
- 客户端请求访问目标服务器,但无法直接访问
 - 请求先发送到正向代理服务器
 - 代理服务器转发请求到目标服务器,获取响应数据
 - 代理服务器将数据返回给客户端
 
反向代理( 客户端看不见服务端)
客户端不能直接访问目标服务器,而是通过代理服务器访问,代理服务器再去请求目标服务器,并将结果返回给客户端
用于:
- 负载均衡
 - 隐藏真实服务器
 - 安全防护(防 DDoS )
 - 缓存加速
 
流程:
- 客户端向代理服务器(反向代理)发送请求
 - 反向代理服务器将请求转发给后端真实服务器
 - 真实服务器返回响应数据给反向代理服务器
 - 反向代理服务器将数据返回给客户端
 
== 和 equals 的区别
对于字符串变量来说,使用 "" 和 "equals" 比较字符串时,其比较方法不同。"" 比较两个变量本身的值,即两个对象在内存中的首地址,"equals" 比较字符串包含内容是否相同
对于非字符串变量来说,如果没有对 equals() 进行重写的话,"==" 和 "equals" 方法的作用是相同的,都是用来比较队形在堆内存中的首地址,即用来比较两个引用变量是否指向同一个对象
==:比较的是两个字符串内存地址的数值是否相等,属于数值比较equals:比较的是两个字符串的内容,属于内容比较
java final 关键字作用
- 
修饰类:当
final修饰类时,表示这个类不能被继承,是类继承体系中的最终形态。如,String类就是用final修饰,保证了String类的不可变性和安全性 - 
修饰方法:用
final修饰的方法不能在子类中被重写。 - 
修饰变量:当
final修饰基本数据类型的变量时,该变量一旦被赋值就不能再更改。对于引用数据类型,final修饰意味着这个引用变量不能再指向其他对象,但是对象本身的内容可以改变。 
Spring IOC 的实现原理
IOC:控制反转,是一种设计思想。在传统的 Java SE 程序设计中,我们直接在对象内部通过 new 的方式来创建对象,是程序主动创建依赖对象;而在 Spring 程序中,IOC 是有专门的容器去控制对象。
所谓控制就是对象的创建、初始化、销毁:
- 
创建对象:原来是
new一个,现在是Spring容器创建 - 
初始化对象:原来是对象自己通过构造器或
setter方法给依赖的对象赋值,现在由Spring容器自动注入 - 
销毁对象:原来是直接给对象复制
null或做一些销毁操作,现在是Spring容器管理生命周期负责销毁对象 
所谓反转:是反转控制权,我们由对象的控制者变成了 IOC 的被动控制者
深拷贝和浅拷贝的区别
- 
浅拷贝:只复制对象本身和其内部的值类型字段,但不会复制对象内部的引用类型字段。
 - 
深拷贝:在复制对象的同时,将对象的所有引用类型字段的内容也复制一份,而不是共享引用。
 
mybatis 中 # 和 $ 符号的区别
- 
Mybatis在处理#{}时,会创建预编译的SQL语句,将SQL中的#{}替换为?号,在执行SQL时会为预编译SQL中的占位符(?)赋值,调用PreparedStatement的set方法来赋值,预编译的SQL语句执行效率高,并且可以防止SQL注入,提供更高的安全性,适合传递参数 - 
Mybatis在处理${}时,只是创建普通的SQL语句,然后在执行SQL语句时Mybatis将参数直接拼入到SQL里,不能防止SQL注入,因为参数直接拼接到SQL语句中,如果参数未经过验证、过滤、可能会导致安全问题 
maven 中 install 和 package 区别
- 
package:此命令会对项目进行编译、测试,然后把项目打包成特定格式的文件,像JAR、WAR等 - 
install:该命令不但会完成package的操作,还会把打包好的文件安装到本地的Maven仓库里,方便其他项目引用 
maven 怎么管理不同的版本号
- 
直接指定版本号
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>...</version>
</dependency>
</dependencies> - 
使用属性管理版本号
<properties>
<junit.version>4.13.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies> 
