文章目录
- 模块分析
- 模块
- 分析
- 描述五张表的关系
- 重要知识讲解
- 抽取成一个BaseServlet
- SpringIOC思想(底层)
- 实现代码
- IOC概述
- SPI机制(为学习框架做思想和技术铺垫)
- SPI引入
- 1. 标准/规范
- 2. 具体的实现
- 3. 调用
- SPI介绍
- SPI练习JDBC4.0免注册驱动原理
- Servlet实现方式三 ServletContainerInitializer
模块分析
模块
项目分为三个模块
- 用户模块:完成增删改查
- 角色模块:增删改查
- 权限模块:增删改查
分析
用户和角色的关系:
一个用户有多个角色,张三可以是SVIP,也可以是绿钻
一个角色可以对应多个用户,SVIP可以是张三,也可以是李四
结果:用户和角色属于多对多的关系,根据表设计原则,多对多关系创建中间表,在中间表起码要有另外俩张主表用户和角色的主键作为外键进行关联
角色和权限的关系:
一个角色有多个权限,SVIP可以点赞20次,可以使qq名变红
一个权限可以对应多个角色,使qq名变红可以是SVIP,也可以是VIP
结果:角色和权限属于多对多的关系,根据表设计原则,多对多关系创建中间表,在中间表起码有另外俩张主表权限和角色的主键作为外键进行关联
注:用户 角色 权限具有经典的五张表
描述五张表的关系
重要知识讲解
抽取成一个BaseServlet
问题:对用户进行增删改查,那么单对用户就要写4个Servlet,这样创建的类太多比较麻烦
解决措施:我们想的是对于用户只创建一个Servlet,路径的话变成/user/*,这样关于用户的所有操作都会放到这个Servlet类中,在里面定义方法来分别操作用户,方法名和路径名保持相同,代码看着简单明了,很简洁
问题:但是这样做我们就只能使用在用户模块,不能使用其他模块,造成代码冗余,还得必须使用if判断到底执行哪个方法
解决措施:前面已经学过可以获取前端的请求数据,那么就可以获取到请求路径,然后用反射来执行方法,然后将其抽取到一个类中,让其他模块创建的Servlet类继承这个类,其他模块的Servlet类中只写操作方法即可
关键:this关键字
代码如下:
public class BaseServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//获取请求路径
String uri = request.getRequestURI();
int i = uri.lastIndexOf("/");
String methodName = uri.substring(i + 1);
//判断方法名
//名字固定,不利于整体书写
// if ("findAll".equals(methodName)){
// findAll(request,response);
// } else if ("update".equals(methodName)) {
// update(request,response);
// } else if ("add".equals(methodName)) {
// add(request,response);
// }else if ("delete".equals(methodName)){
// delete(request,response);
// }
//用反射技术,使用固定代码执行所有方法
//方法名和路径相同,利用反射获取方法名
Class aClass = this.getClass();
try {
Method method = aClass.getMethod(methodName,HttpServletRequest.class,HttpServletResponse.class);
method.invoke(this,request,response);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@WebServlet("/user/*")
public class UserServlet extends BaseServlet {
private void delete(HttpServletRequest request, HttpServletResponse response) {
System.out.println("删除用户");
}
public void add(HttpServletRequest request, HttpServletResponse response) {
System.out.println("增加用户");
}
public void update(HttpServletRequest request, HttpServletResponse response) {
System.out.println("更新用户");
}
public void findAll(HttpServletRequest request, HttpServletResponse response) {
}
}
SpringIOC思想(底层)
实现代码
问题:我们以前创建对象都是通过new创建对象,例如:创建业务层对象:
UserServiceImple userServiceImpl = new UserServiceImpl();
这种方式创建对象的弊端是:严重耦合。如果业务层实现类UserServiceImpl被删除了,web会报错。或者在实际开发中向对业务层进行扩展,一般是定义一个新的业务层类UserServiceImpl2,然后将之前的UserServiceImpl替换,就造成web层无法运行,那么太过于耦合
解决措施:降低耦合。就是不在web层中使用new方式创建对象
使用:反射+面向接口编程+读取配置文件+工程设计模式等方式来取代new创建对象
- 定义接口
在业务层下定义接口,然后定义一个impl包,将UserServiceImpl类放到impl包下,这样就可以将上面创建业务层对象改成:
UserService userService = new UserServiceImpl();
这样的话就是实现左边解耦了,如果扩展实现类只需要修改等号右边
- 书写properties文件
在resources下定义一个beans.properties文件,然后按照key=value格式书写,key:userService,value:写UserServiceImpl的全路径名
- 使用反射+读取配置文件创建对象
使用ResourceBundle抽象类去读取properties配置文件,利用反射创建对象
- 定义一个工具类,使用工厂设计模式来创建对象
定义一个Map集合,key存properties文件内容=左边的值,value存用反射创建的对象
代码如下:
工厂类:
package com.itheima.case2.utils;
import java.util.HashMap;
import java.util.ResourceBundle;
/*
TODO:当前工厂类的作用就是创建对象的
回顾:
一个类对象:
1)单例 单个对象
2)多例 多个对象
我们这里实现产生的对象是单例 userService=com.itheima.case2.service.impl.UserServiceImpl
1.创建一个Map集合: new HashMap<String,Object>();
2.Map集合的key:例如配置文件等号左边的标识userService roleService
3.Map集合的value:就是创建的对象UserServiceImpl 类的对象 RoleServiceImpl类的对象
4.实现步骤:
1)创建map集合存储创建的对象
2)定义静态方法创建具体类的对象
3)在静态方法中判断map集合的key是否有值,如果没有值,说明第一创建对象
4)直接使用反射+读取配置文件方式创建对象
5)将创建的对象作为map集合的value和key存入到map中
6)返回给调用者创建的对象
*/
public class BeansFactory {
// 1)创建map集合存储创建的对象
/*
key value
userService UserServiceImpl0x001
roleService RoleServiceImpl0x002
*/
private static HashMap<String,Object> map = new HashMap<>();
// 2)定义静态方法创建具体类的对象
//多线程安全问题:t1 t2
public static synchronized <T> T getInstance(String key) throws Exception{//String key=userService roleService
// 3)在静态方法中判断map集合的key是否有值,如果没有值,说明第一创建对象
Object obj = map.get(key);
// 4)直接使用反射+读取配置文件方式创建对象
if(obj == null){
//说明当前map集合中没有传递的key对应的值
//4.1使用反射+读取配置文件创建对象
ResourceBundle bundle = ResourceBundle.getBundle("beans");//参数beans表示要关联的配置文件的名字,不能书写后缀名
//4.2根据配置文件的key获取值 t1
//userService=com.itheima.case2.service.impl.UserServiceImpl
String classNameStr = bundle.getString(key);//"com.itheima.case2.service.impl.UserServiceImpl"
//4.3使用反射创建对象
Class<?> clazz = Class.forName(classNameStr);
obj = clazz.newInstance();//调用UserServiceImpl类的无参构造方法
// 5)将创建的对象作为map集合的value和key存入到map中
map.put(key,obj);//t1线程创建的对象0x001 t2线程创建的对象0x002
}
// 6)返回给调用者创建的对象
return (T)obj;
}
}
配置文件:
userService=com.itheima.case2.service.impl.UserServiceImpl
web层UserServlet
UserService userService = BeanFactory.getInstance("userService");
IOC概述
Inversion of Control:控制反转
以前我们要获取对象,我们自己new主动获取,现在有了工厂模式,我们需要获取对象,是工厂创建,我们被动接收工厂创建的对象,这就是控制反转,说白了就是ioc采用工厂模式创建对象达到解耦合
其实SpringIOC底层是Map集合,我们经常会说SpringIOC容器即Map集合。
SPI机制(为学习框架做思想和技术铺垫)
SPI引入
1. 标准/规范
- 工程 spi_interface
- 只有一个接口car
2. 具体的实现
- 工程 honda_car 和 tesla_car
- 工程依赖了spi_interface
pom.xml- 有一个实现类,实现了标准
HondaCar implements Car
TeslaCar implements Car- 还有一个配置文件
1). 在类路径classpath下
resources/META-INF/services
2). 文件名: 接口的全限定名
com.itheima.Car
3). 文件内容: 实现类的全限定名
com.itheima.impl.HondaCar3. 调用
- 工程 spi_test
- 工程依赖了 honda_car 和 tesla_car
- 测试类 SpiTest
测试类代码
# ServiceLoader<Car> cars = ServiceLoader.load(Car.class);加载接口实现
1. 加载当前工程依赖的所有工程 classpath:META-INF/services目录下
跟当前接口参数同名的文件
(classpath:META-INF.services/com.itheima.Car文件)
2. 当前案例依赖了两个工程,那么这两个工程的配置文件都会被读取到
honda_car===META-INF.services/com.itheima.Car文件中的com.itheima.impl.HondaCar
tesla_car===META-INF.services/com.itheima.Car文件中的com.itheima.impl.TeslaCar
注意:配置文件名必须是实现的接口全路径,配置文件中书写实现类的全路径
3. 通过反射创建接口文件中配置的实例
Class clazz= Class.forName("com.itheima.impl.TeslaCar");
Car car = clazz.newInstance();
ServiceLoader类介绍
- ServiceLoader功能和ClassLoader功能类似,能装载类文件,但是使用时是有区别的
- ServiceLoader装载的是一系列有某种共同特征的实现类(类实现同一个接口,在实现类的工程中的resources目录下存在META-INF/services目录下接口同名的文件),即这些类实现接口或者抽象类。而ClassLoader是可以加载任何类的类加载器;
- ServiceLoader加载时需要特殊的配置:
- 在类路径:classpath:META-INF/services/接口全路径文件
- 在文件中配置实现类全路径com.itheima.impl.HondaCar
- ServiceLoader还实现了Iterable接口,可以进行迭代
- 原理:在ServiceLoader.load的时候,根据传入的接口Class对象,遍历META-INF/services目录下的以该接口命名的文件中的所有类,将创建实现类的对象返回。
SPI介绍
全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件
Java的SPI机制就是将一些类信息写在约定的文件中,然后由特定的类加载器ServiceLoader加载解析文件获取资源
Java SPI 基于"接口编程+策略模式+配置文件(约定)"组合实现的动态加载机制
运用场景:
场景 | 说明 |
---|---|
数据库驱动 | 数据库驱动加载接口实现类的加载 JDBC加载不同类型数据库的驱动 |
日志门面SLF4J接口实现类加载 | SLF4J加载不同提供商的日志实现类 |
Spring | Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等 |
Dubbo | Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口 |
SpringBoot | SpringBoot基于SPI思想实现自动装配 |
SPI练习JDBC4.0免注册驱动原理
public class JdbcDemo {
public static void main(String[] args) throws Exception {
//1.加载驱动
// Class.forName("com.mysql.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql:///dbvue", "root", "1234");
System.out.println(connection);
}
}
//DriverManager类源码:
public class DriverManager {
//静态代码块
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
.....
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//spi
/*
1.java.sql.Driver 是一个接口
2.java.sql.Driver接口实现类,com.mysql.jdbc.Driver
使用SerciveLoader类加载器调用方法获取java.sql.Driver接口的实现类对象放到LoadedDrivers
中
*/
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(java.sql.Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
......
}
}
Servlet实现方式三 ServletContainerInitializer
前俩种方式是注解和xml
第三种就是spi方式,后面学习的框架底层就是采用这种方式
ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架.
运行原理
- ServletContainerInitializer接口的实现类通过java SPI声明自己是ServletContainerInitializer 的提供者.
- web容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.
- 在onStartup中通过编码方式将组件servlet加载到ServletContext
小结
ServletContainerInitializer 在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filter等,servlet规范中通过ServletContainerInitializer实现此功能。每个框架要使用ServletContainerInitializer就必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作