架构探险——第三章(搭建轻量级Java Web框架)
目录
实现IOC
请求转发器
总结
解决的问题
- servlet的数量会随业务功能的扩展而不断增加,我们有必要减少servlet的数量,交给controller处理,它负责调用service的相关方法,并将返回值放入request或response中。
- service目前是通过new的方式来创建的,这样导致一个应用中会创建多个对象,这样是不科学的。我们可以通过一种“依赖注入”的思想,让框架来为我们创建所需要的对象。
掌握的技能
- 如何快速搭建开发框架
- 如何加载并读取配置文件
- 如何实现一个简单的IOC容器
- 如何加载指定的类
- 如何初始化框架
实现IOC
框架在实现IOC(Inversion of Control,控制反转)的时候,通过框架自身来示例化Bean。
Bean从哪里来,怎么获取Bean,怎么实现Bean的管理?
第一步:加载Bean
第二部:实例化Bean
第三部:根据注解,将Bean注入
开发一个类加载器
如果对类的加载机制不熟的话,可以先看看《深入理解Java虚拟机》的第七章——虚拟机类加载机制。
在Java语言里面,类型的加载和连接过程都是在运行期间完成的,这样会在类加载时稍微增加一些性能开销,但是却能为Java应用程序提供高度的灵活性,Java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态链接这个特点实现的。例如,如果编写一个使用接口的应用程序,可以等到运行时再指定其实际的实现。这种组装应用程序的方式广泛应用于Java程序之中。
目标:获取指定包名下的所有类。
分析:由className通过类加载器可以加载到内存中,将包下面的class加载并放入到Set<Class<?>>。包名可能是一个file或者一个jar包。
具体实现:
/**
* 类操作工具类
*/
public final class ClassUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ClassUtil.class);
/**
* 获取类加载器
*/
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* 加载类
*/
public static Class<?> loadClass(String className, boolean isInitialized) {
Class<?> cls;
try {
cls = Class.forName(className, isInitialized, getClassLoader());
} catch (ClassNotFoundException e) {
LOGGER.error("load class failure", e);
throw new RuntimeException(e);
}
return cls;
}
/**
* 加载类(默认将初始化类)
*/
public static Class<?> loadClass(String className) {
return loadClass(className, true);
}
/**
* 获取指定包名下的所有类
*/
public static Set<Class<?>> getClassSet(String packageName) {
Set<Class<?>> classSet = new HashSet<Class<?>>();
try {
Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String protocol = url.getProtocol();
if (protocol.equals("file")) {
String packagePath = url.getPath().replaceAll("%20", " ");
addClass(classSet, packagePath, packageName);
} else if (protocol.equals("jar")) {
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
if (jarURLConnection != null) {
JarFile jarFile = jarURLConnection.getJarFile();
if (jarFile != null) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class")) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
doAddClass(classSet, className);
}
}
}
}
}
}
}
} catch (Exception e) {
LOGGER.error("get class set failure", e);
throw new RuntimeException(e);
}
return classSet;
}
private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
File[] files = new File(packagePath).listFiles(new FileFilter() {
public boolean accept(File file) {
return (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory();
}
});
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String className = fileName.substring(0, fileName.lastIndexOf("."));
if (StringUtil.isNotEmpty(packageName)) {
className = packageName + "." + className;
}
doAddClass(classSet, className);
} else {
String subPackagePath = fileName;
if (StringUtil.isNotEmpty(packagePath)) {
subPackagePath = packagePath + "/" + subPackagePath;
}
String subPackageName = fileName;
if (StringUtil.isNotEmpty(packageName)) {
subPackageName = packageName + "." + subPackageName;
}
addClass(classSet, subPackagePath, subPackageName);
}
}
}
private static void doAddClass(Set<Class<?>> classSet, String className) {
Class<?> cls = loadClass(className, false);
classSet.add(cls);
}
}
以下是两篇我对类加载器的总结:
关于类加载器
类加载器与Web容器
自定义注解
目标:在控制器类上使用Controller注解,在控制器类的方法上使用Action注解,在服务类上使用Service注解,在控制器类中可使用Inject注解将服务类依赖注入进来。
分析:JDK 5中引入了源代码中的注解(annotation)这一机制。注解使得Java源代码中不但可以包含功能性的实现代码,还可以添加元数据。注解的功能类似于代码中的注释,所不同的是注解不是提供代码功能的说明,而是实现程序功能的重要组成部分。Java注解已经在很多框架中得到了广泛的使用,用来简化程序中的配置。
Action方法注解代码如下:
/**
* Action 方法注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
/**
* 请求类型与路径
*/
String value();
}
- @Target – 这个注解可以让你来指定你的注解应该被用在那个java元素上. 可能的目标类型是 ANNOTATION_TYPE, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER 和 TYPE. 在我们的 @ReconField 注解中他被指定到了 FIELD 级别.
- @Retention – 它可以让你指定注解在何时生效. 可能的值有 CLASS, RUNTIME 和 SOURCE. 因为我们将会在运行时 RUNTIME 处理这个注解, 所以那就是我们需要设置的值.
- @interface用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。
根据注解获取相应的类
之前我们已经可以通过ClassUtil类加载指定包下所有的类,这样我们可以通过循环所有加载的类,根据class的isAnnotationPresent()来判断是否是对应注解下的类,并将获取的java.lang.class实例放进classSet。
比如获取应用包名下所有的Service类:
public static Set<Class<?>> getServiceClassSet() {
Set<Class<?>> classSet = new HashSet<Class<?>>();
for (Class<?> cls : CLASS_SET) {
if (cls.isAnnotationPresent(Service.class)) {
classSet.add(cls);
}
}
return classSet;
}
实现Bean容器
1.通过反射实例化对象。
调用class的newInstance()方法来实例化实例。
/**
* 反射工具类
*/
public final class ReflectionUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ReflectionUtil.class);
/**
* 创建实例
*/
public static Object newInstance(Class<?> cls) {
Object instance;
try {
instance = cls.newInstance();
} catch (Exception e) {
LOGGER.error("new instance failure", e);
throw new RuntimeException(e);
}
return instance;
}
/**
* 创建实例(根据类名)
*/
public static Object newInstance(String className) {
Class<?> cls = ClassUtil.loadClass(className);
return newInstance(cls);
}
/**
* 调用方法
*/
public static Object invokeMethod(Object obj, Method method, Object... args) {
Object result;
try {
method.setAccessible(true);
result = method.invoke(obj, args);
} catch (Exception e) {
LOGGER.error("invoke method failure", e);
throw new RuntimeException(e);
}
return result;
}
/**
* 设置成员变量的值
*/
public static void setField(Object obj, Field field, Object value) {
try {
field.setAccessible(true);
field.set(obj, value);
} catch (Exception e) {
LOGGER.error("set field failure", e);
throw new RuntimeException(e);
}
}
}
2.将实例化的对象放进Map中,通过kay去获取对应的value(Bean对象)。
实现方法:
/**
* Bean 助手类
*/
public final class BeanHelper {
private static final Map<Class<?>, Object> BEAN_MAP = new HashMap<Class<?>, Object>();
static {
Set<Class<?>> beanClassSet = ClassHelper.getBeanClassSet();
for (Class<?> beanClass : beanClassSet) {
Object obj = ReflectionUtil.newInstance(beanClass);
BEAN_MAP.put(beanClass, obj);
}
}
/**
* 获取 Bean 映射
*/
public static Map<Class<?>, Object> getBeanMap() {
return BEAN_MAP;
}
/**
* 获取 Bean 实例
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(Class<T> cls) {
if (!BEAN_MAP.containsKey(cls)) {
throw new RuntimeException("can not get bean by class: " + cls);
}
return (T) BEAN_MAP.get(cls);
}
/**
* 设置 Bean 实例
*/
public static void setBean(Class<?> cls, Object obj) {
BEAN_MAP.put(cls, obj);
}
}
实现依赖注入功能
至此,我们基本已经完成了所有的工具类,现在将开发IocHelper来实现依赖注入。
只需要在IocHelper的静态代码块中实现相关逻辑,就能完成IOC容器的初始化工作。
当IocHelper这个类被加载的时候,就会加载它的静态代码块。后面统一在一个地方加载这个IocHelper。
代码如下:
/**
* 依赖注入助手类
*/
public final class IocHelper {
static {
//获取所有的Bean类与Bean实例之间的映射关系
Map<Class<?>, Object> beanMap = BeanHelper.getBeanMap();
if (CollectionUtil.isNotEmpty(beanMap)) {
//遍历Bean Map
for (Map.Entry<Class<?>, Object> beanEntry : beanMap.entrySet()) {
//从BeanMap中获取Bean类与Bean实例
Class<?> beanClass = beanEntry.getKey();
Object beanInstance = beanEntry.getValue();
//获取Bean类定义的所有成员变量(简称Bean Field)
Field[] beanFields = beanClass.getDeclaredFields();
if (ArrayUtil.isNotEmpty(beanFields)) {
//遍历Bean Field
for (Field beanField : beanFields) {
//判断当前Bean Field是否带有Inject注解
if (beanField.isAnnotationPresent(Inject.class)) {
//在Bean Map中获取Bean Field对应的实例
Class<?> beanFieldClass = beanField.getType();
Object beanFieldInstance = beanMap.get(beanFieldClass);
if (beanFieldInstance != null) {
//通过反射初始化BeanField的值
ReflectionUtil.setField(beanInstance, beanField, beanFieldInstance);
}
}
}
}
}
}
}
}
此时,在这个Ioc框架中所管理的对象都是单例的。IOC从BeanHelper中获取BeanMaP,而BeanMap中的对象都是事先创建好,放入这个Bean容器的。
加载controller
思路:通过ClassHelper,我们可以获取所有定义了Controller注解的类,可以通过反射获取该类中所有带有Action注解的方法(简称Action方法),获取action注解中的请求表达式,进而获取请求方法与请求路径,封装一个请求对象(request)与处理对象(Handler),最后将Request于Handler建立一个隐射关系,放入一个action Map中,并提供一个可根据请求方法与请求路径获取处理对象的方法。
ControllerHelper代码如下:
/**
* 控制器助手类
*/
public final class ControllerHelper {
/**
* 用于存放请求与处理器的映射关系(简称Action Map)
*/
private static final Map<Request, Handler> ACTION_MAP = new HashMap<Request, Handler>();
static {
//获取所有的Controller类
Set<Class<?>> controllerClassSet = ClassHelper.getControllerClassSet();
if (CollectionUtil.isNotEmpty(controllerClassSet)) {
//遍历这些Controller类
for (Class<?> controllerClass : controllerClassSet) {
//获取Controller类中定义的方法
Method[] methods = controllerClass.getDeclaredMethods();
if (ArrayUtil.isNotEmpty(methods)) {
//遍历这些Controller类中定义的方法
for (Method method : methods) {
//判断当前方法中是否带有action注解
if (method.isAnnotationPresent(Action.class)) {
//从action注解中获取Url映射规则
Action action = method.getAnnotation(Action.class);
String mapping = action.value();
//验证Url映射规则
if (mapping.matches("\\w+:/\\w*")) {
String[] array = mapping.split(":");
if (ArrayUtil.isNotEmpty(array) && array.length == 2) {
//获取请求方法与请求路径
String requestMethod = array[0];
String requestPath = array[1];
Request request = new Request(requestMethod, requestPath);
Handler handler = new Handler(controllerClass, method);
//初始化Action Map
ACTION_MAP.put(request, handler);
}
}
}
}
}
}
}
}
/**
* 获取 Handler
*/
public static Handler getHandler(String requestMethod, String requestPath) {
Request request = new Request(requestMethod, requestPath);
return ACTION_MAP.get(request);
}
}
初始化框架
我们创建了ClassHelper、BeanHelper、IOCHelper、ControllerHelper,这四个Helper类需要通过一个入口来加载他们,实际上就是加载他们的静态代码块。
/**
* 加载相应的 Helper 类
*/
public final class HelperLoader {
public static void init() {
Class<?>[] classList = {
ClassHelper.class,
BeanHelper.class,
AopHelper.class,
IocHelper.class,
ControllerHelper.class
};
for (Class<?> cls : classList) {
ClassUtil.loadClass(cls.getName());
}
}
}
现在就可以直接调用HelperLoader的init方法来加载这些Helper了,这里只是为了加载更集中。
请求转发器
以上过程都是在为这一步做准备。我们现在需要写一个Servlet,让他处理所有的请求。
思路:从HttpServletRequest对象中获取请求方法与请求路径,通过ControllerHelper的getHandler方法来获取handler对象。当拿到Handler对象后,我们可以方便的获取Controller的类,通过BeanHelper的getBean方法获取Controller的实例对象。随后可以从HttpServletRequest对象中的所有请求参数,并将其初始化到一个param的对象中。
在Param中会有一系列的get方法,可通过参数名获取指定类型的参数值,也可以获取所有参数的map结构。
还可从Handler对象中获取Action的方法返回值,该返回值可能有两种情况:
(1)若返回值是View类型的视图对象,则返回一个jsp页面。
(2)若返回值是Data类型的数据对象,则返回一个JSON数据。
一下便是MVC框中最核心的DispatcherServlet类,代码如下:
/**
* 请求转发器
*/
@WebServlet(urlPatterns = "/*", loadOnStartup = 0)
public class DispatcherServlet extends HttpServlet {
@Override
public void init(ServletConfig servletConfig) throws ServletException {
//初始化相关的Helper类
HelperLoader.init();
//获取ServletContext对象(用于注册Servlet)
ServletContext servletContext = servletConfig.getServletContext();
//注册Servlet
registerServlet(servletContext);
UploadHelper.init(servletContext);
}
private void registerServlet(ServletContext servletContext) {
//注册处理JSP的servlet
ServletRegistration jspServlet = servletContext.getServletRegistration("jsp");
jspServlet.addMapping("/index.jsp");
jspServlet.addMapping(ConfigHelper.getAppJspPath() + "*");
//注册处理静态资源的默认的servlet
ServletRegistration defaultServlet = servletContext.getServletRegistration("default");
defaultServlet.addMapping("/favicon.ico");
defaultServlet.addMapping(ConfigHelper.getAppAssetPath() + "*");
}
@Override
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
ServletHelper.init(request, response);
try {
//获取请求方法与请求路径
String requestMethod = request.getMethod().toLowerCase();
String requestPath = request.getPathInfo();
//获取Action处理器
Handler handler = ControllerHelper.getHandler(requestMethod, requestPath);
if (handler != null) {
//获取Controller类机器Bean实例
Class<?> controllerClass = handler.getControllerClass();
Object controllerBean = BeanHelper.getBean(controllerClass);
//创建请求参数对象
Param param;
if (UploadHelper.isMultipart(request)) {
param = UploadHelper.createParam(request);
} else {
param = RequestHelper.createParam(request);
}
//调用Action方法
Object result;
Method actionMethod = handler.getActionMethod();
if (param.isEmpty()) {
result = ReflectionUtil.invokeMethod(controllerBean, actionMethod);
} else {
result = ReflectionUtil.invokeMethod(controllerBean, actionMethod, param);
}
//处理Action返回值
if (result instanceof View) {
handleViewResult((View) result, request, response);
} else if (result instanceof Data) {
handleDataResult((Data) result, response);
}
}
} finally {
ServletHelper.destroy();
}
}
private void handleViewResult(View view, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String path = view.getPath();
if (StringUtil.isNotEmpty(path)) {
if (path.startsWith("/")) {
response.sendRedirect(request.getContextPath() + path);
} else {
Map<String, Object> model = view.getModel();
for (Map.Entry<String, Object> entry : model.entrySet()) {
request.setAttribute(entry.getKey(), entry.getValue());
}
request.getRequestDispatcher(ConfigHelper.getAppJspPath() + path).forward(request, response);
}
}
}
private void handleDataResult(Data data, HttpServletResponse response) throws IOException {
Object model = data.getModel();
if (model != null) {
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
String json = JsonUtil.toJson(model);
writer.write(json);
writer.flush();
writer.close();
}
}
}
通过这个DispatcherServlet来处理所有的请求,根据请求信息从ControllerHelper中获取对应的Action方法,然后通过反射技术调用Action方法,同时需要具体的传入方法参数,最后拿到返回值并判断返回值的类型,进行相应的处理。
总结
在这一章,搭建了一个简单的MVC框架,定义了一系列的注解;通过一系列的Helper类来初始化MVC框架;通过DispatcherServlet来处理所有的请求;根据请求方法和请求路径来来调用具体的Action方法,判断Action方法的返回值,若为View类型,则调转到JSP页面,若为Data类型,则返回json数据。
在这一章中,学习了jdk中的类加载器、注解,对Java的认识又更深一步,期间感谢在黄老师创建的群里耐心回答我的问题的朋友。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?