一、场景还原
简单的后台服务方案:创建一个 Bean
----> 来个 BeanDao
再来个 ----> BeanService
里面通过 new BeanDao()
来调用持久层方法,最后来个 ----> BeanServlet
通过 new BeanService()
来调用业务层。
相信上面的代码各位大佬都曾写过。那么这些伪代码里都存在哪些问题呢?
- 耦合严重,所有的对象引用都直指具体的实现类。
- 编码繁琐,开发者需编写大量代码去实例化对象并为其属性赋值以完成
Bean
的初始化。 - 修改困难,当面对需求修改或某类型的所有方法统一增加日志、鉴权等功能时,会让开发者原地爆炸。
二、解决方案
问题找出来了,那么我们应该怎么去自己动手解决这些问题呢?(那位说用 Spring Framework
的同学,麻烦你请出去一下~!手动狗头)
- 创建自己的对象管理容器,让它去帮我们完成对象的创建与赋值,即:
IoC
。 - 面向接口编程,降低对象间的耦合性。
- 动态代理增强,抽取日志、鉴权等非业务逻辑功能到动态代理方法中,即:
AOP
。 - 自定义注解,通过注解标记需要对象管理容器进行处理的类、属性或方法。
三、代码实现
这部分,博主只拿出重点部分出来进行解释说明,具体代码可在文末连接下载。
-
首先,解耦合第一步需要进行的是面向接口开发的处理,想
Spring MVC
中DAO
和Service
层一样,这里不做过多赘述。 -
其次,既然要实现
IoC
就一定会有自己的Bean
容器(单例),用来维护项目中所有的类实例化对象。
BeanContainer
import java.util.HashMap;
import java.util.Map;
/**
* @author Supreme_Sir
* @version 1.0
* @className BeanContainer
* @description 自定义单例的 Bean 容器
* @date 2020/10/13 14:05
**/
public class BeanContainer {
/**
* Bean 容器:装载已完成(可包含实例化不完全的 Bean)实例化的对象
*/
private Map<String, Object> container = new HashMap<>();
/**
* 对象 id 和全限定类名的映射关系记录表
*/
private Map<String, String> idClassMap = new HashMap<>();
private static final BeanContainer beanContainer = new BeanContainer();
private BeanContainer() {
}
/**
* 获取单例模式下的 BeanContainer 对象
* @return BeanContainer
*/
public static BeanContainer getBeanContainer() {
return beanContainer;
}
/**
* 向容器中添加 Bean
* @param id 对象别名
* @param clazz 全限定类名
* @param instance 对象实例
*/
public void put(String id, String clazz, Object instance) {
container.put(clazz, instance);
idClassMap.put(id, clazz);
}
/**
* 向容器中添加 Bean
* @param clazz 全限定类名
* @param instance 对象实例
*/
public void put(String clazz, Object instance) {
container.put(clazz, instance);
}
/**
* 获取 Bean,可通过对象别名和全限定类名获取
* @param key
* @return
*/
public Object get(String key) {
Object result = container.get(key);
if (result == null) {
result = container.get(idClassMap.get(key));
}
return result;
}
}
- 容器有了,那么我们就要创建把类装进方法。该功能的实现主要有一下几个步骤:
- 获取绝对路径
- 递归扫描这个路径下的所有
.class
文件。注意:不是.java
文件,因为真正的代码执行用的不是源码。 - 加载具有公共空参构造函数的类,但要排除控制层的对象,因为控制层对象的实例化是由
Web
容器管理的。同时,在此处还需登记需要自动装配的类和需要代理增强的类。 - 为需要自动装配属性的对象赋值。
- 为需要增强的对象生成代理对象。
BeanScanner
import com.idol.annotation.Autowired;
import com.idol.annotation.Service;
import com.idol.annotation.Transaction;
import com.idol.controller.MyHttpServlet;
import com.idol.proxy.BeanProxyFactory;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
/**
* @author Supreme_Sir
* @version 1.0
* @className BeanScanner
* @description 用以扫描并实例化指定包下的 Bean
* @date 2020/10/13 15:26
**/
public class BeanScanner {
private static final String EXT = "class";
/**
* 记录需要进行属性填充的类
*/
private static final Map<String, List<Field>> FIT_FIELD_MAP = new HashMap<>(16);
/**
* 记录需要进行 AOP 增强的类
*/
private static final Map<String, List<Method>> ENHANCER_METHOD_MAP = new HashMap<>(16);
/**
* 根据包名获取包的URL
* @param packageName com.idol
* @return
*/
private static String getPackagePath(String packageName) throws UnsupportedEncodingException {
String pkgDirName = packageName.replace(".", File.separator);
URL url = Thread.currentThread().getContextClassLoader().getResource(pkgDirName);
return url == null ? null : URLDecoder.decode(url.getFile(), "UTF-8");
}
/**
* 遍历指定目录下所有扩展名为class的文件
*/
private static List<File> getAllClassFile(String strPath, List<File> fileList) {
File dir = new File(strPath);
// 该文件目录下文件全部放入数组
File[] files = dir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
String fileName = files[i].getName();
// 判断是文件还是文件夹
if (files[i].isDirectory()) {
// 获取文件绝对路径
getAllClassFile(files[i].getAbsolutePath(), fileList);
// 判断文件名是否以 .class 结尾
} else if (fileName.endsWith(EXT)) {
fileList.add(files[i]);
} else {
continue;
}
}
}
return fileList;
}
/**
* 加载类
*
* @param file
* @param packagePath
* @param packageName
* @return
* @throws ClassNotFoundException
*/
private static void initObjects(File file, String packagePath, String packageName) throws
ClassNotFoundException, IllegalAccessException, InstantiationException {
// 拿到全限定类名
String absPath = file.getAbsolutePath().substring(0, file.getAbsolutePath().length() - EXT.length() - 1);
String className = absPath.substring(packagePath.length() - 1).replace(File.separatorChar, '.');
className = className.startsWith(".") ? packageName + className : packageName + "." + className;
// 将指定类型的类实例化后放入自定义的 Bean 容器中
Class<?> clazz = Class.forName(className);
if (canBeInit(clazz)) {
// 登记带有 @Autowired 注解的类属性
registAutowaredField(clazz);
// 登记带有 @Transaction 注解的类方法
registTranscationMethod(clazz);
Class<?> loadClass = Thread.currentThread().getContextClassLoader().loadClass(className);
String[] split = className.split("\\.");
BeanContainer.getBeanContainer().put(split[split.length - 1], className, loadClass.newInstance());
}
}
/**
* 为 @Autowared 注入装配对象
*
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
private static void fitField() throws NoSuchFieldException, IllegalAccessException {
for (Map.Entry entry : FIT_FIELD_MAP.entrySet()) {
Object bean = BeanContainer.getBeanContainer().get((String) entry.getKey());
List<Field> fields = (List<Field>) entry.getValue();
for (Field field : fields) {
Field declaredField = bean.getClass().getDeclaredField(field.getName());
declaredField.setAccessible(true);
declaredField.set(bean, BeanContainer.getBeanContainer().get(
declaredField.getType().toString().split(" ")[1]));
}
}
FIT_FIELD_MAP.clear();
}
/**
* 登记需要自动装配的属性
*
* @param clazz
*/
private static void registAutowaredField(Class<?> clazz) {
String name = clazz.getName();
Field[] fields = clazz.getDeclaredFields();
List<Field> fieldList = new ArrayList<>();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
fieldList.add(field);
}
}
FIT_FIELD_MAP.put(name, fieldList);
}
/**
* 登记需要进行增强的方法
*
* @param clazz
*/
private static void registTranscationMethod(Class<?> clazz) {
if (clazz.isAnnotationPresent(Service.class)) {
String name = clazz.getName();
Method[] methods = clazz.getDeclaredMethods();
List<Method> methodList = new ArrayList<>();
for (Method method : methods) {
if (method.isAnnotationPresent(Transaction.class)) {
methodList.add(method);
}
}
ENHANCER_METHOD_MAP.put(name, methodList);
}
}
/**
* 判断当前类是否符合实例化条件
*
* @param clazz
* @return
*/
private static boolean canBeInit(Class<?> clazz) {
boolean result = false;
if (!clazz.isInterface()) {
// 不实例化控制层对象
if (clazz.getSuperclass() != MyHttpServlet.class) {
Constructor<?>[] constructors = clazz.getConstructors();
// 判断是否有空参的公共构造函数
if (constructors.length > 0) {
for (Constructor<?> constructor : constructors) {
if (constructor.getParameters().length < 1) {
return true;
}
}
}
}
}
return result;
}
/**
* 创建代理对象,开启数据库事务
*/
private static void createProxy() {
BeanProxyFactory proxyFactory = (BeanProxyFactory) BeanContainer.getBeanContainer().get("BeanProxyFactory");
for (Map.Entry entry : ENHANCER_METHOD_MAP.entrySet()) {
String beanContainerKey = (String) entry.getKey();
Object object = BeanContainer.getBeanContainer().get(beanContainerKey);
Class<?>[] interfaces = object.getClass().getInterfaces();
if (interfaces.length > 0) {
beanContainerKey = interfaces[0].toString().split(" ")[1];
}
object = proxyFactory.createTransactionProxy(object);
BeanContainer.getBeanContainer().put(beanContainerKey, object);
}
ENHANCER_METHOD_MAP.clear();
}
/**
* 扫描指定的包
*
* @param packageName
* @throws UnsupportedEncodingException
* @throws IllegalAccessException
* @throws InstantiationException
* @throws ClassNotFoundException
* @throws NoSuchFieldException
*/
public static void scanPackage(String packageName) throws UnsupportedEncodingException,
IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchFieldException {
String packagePath = getPackagePath(packageName);
// 获取所有 .class 文件
List<File> classFile = getAllClassFile(packagePath, new ArrayList<File>());
for (File file : classFile) {
// 初始化对象
initObjects(file, packagePath, packageName);
}
// 为对象注入属性
fitField();
// 创建数据库事务代理对象
createProxy();
}
}
- 准备工作做完了,那什么时候对类进行初始化呢?由于本工程是
Web
项目,且服务器使用的Tomcat
。 那么,就从Tomcat
监听入手。
WebStartListener
import com.idol.factory.BeanScanner;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
/**
* @author Supreme_Sir
* @version 1.0
* @className StartListener
* @description Tomcat 启动监听,以在 Tomcat 启动阶段完成类的自动扫描、加载和装配
* 注意:本类必须在所有子包的外面
* @date 2020/10/13 14:26
**/
@WebListener
public class WebStartListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// 获取当前类所在包
String packageName = WebStartListener.class.getPackage().getName();
try {
BeanScanner.scanPackage(packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
System.out.println("Tomcat 关闭了~~~");
}
}
-
到这里,就剩下最后一个需要解决的问题了,那就是:控制层的
Bean
是由Web
容器管理的,而我们又要做怎么才能为其实现服务层属性的自动注入呢?我这里给到的处理方式是:插一杠子~!对,就是要在
Tomcat
实例化控制层对象时插一杠子。即:所有的Servlet
都需要继承HttpServlet
,那么我们就先自定义一个继承自HttpServlet
的类,然后重写父类中的init
方法,在init
方法执行时,完成属性的自动赋值。最后,让所有控制层的类继承自定义的HttpServlet
即可。
MyHttpServlet
import com.idol.annotation.Autowired;
import com.idol.factory.BeanContainer;
import javax.servlet.http.HttpServlet;
import java.lang.reflect.Field;
/**
* @author Supreme_Sir
* @version 1.0
* @className MyHttpServlet
* @description 自定义 MyHttpServlet 对象,以在 Servlet 执行前完成控制层具体的对象的属性装配
* @date 2020/10/14 0:18
**/
public class MyHttpServlet extends HttpServlet {
@Override
public void init() {
Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Autowired.class)) {
field.setAccessible(true);
try {
field.set(this, BeanContainer.getBeanContainer().get(
field.getType().toString().split(" ")[1]));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
源码
---------- 遇良人先成家,遇贵人先立业,未遇良人先自立,未遇贵人先自修。父母帮衬先攒钱,父母不帮顾眼前。 ----------