IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Spring之手写SpringMVC5个注解(之IOC,DI优化)了解三级缓存 -> 正文阅读

[Java知识库]Spring之手写SpringMVC5个注解(之IOC,DI优化)了解三级缓存

前言

又鸽了辣么久,失策,失策,那么今天的这个东西呢,其实是对前面写的那个东西(手写那个mvc5个注解的那篇文章的优化)。当然这个内容也是来自咱们《Spring5核心原理与30个类手写实战 》这本书。强推,有时间背面试题不如看这本书,咱们把全流程走一下。
我们先大体说一说流程,然后在讨论一些细节。

流程回顾

咱们先回顾一下咱们原来写的流程。
在这里插入图片描述

原来我们是把所有的方法都放在了这个一个类里面,HUDispatcherServlet 这个上帝类里面。
这样显然是不行的,所以咱们得优化一下下。不过这里咱们目前是只优化这个IOC,DI 的内容。后面咱们优化
AOP,MVC的内容。

项目结构

那么接下来咱们得重新修改一下咱们的项目了。

配置文件

首先这个配置文件和上一次的一样,不会发生改变。
在这里插入图片描述
在这里插入图片描述

项目结构

在这里插入图片描述
光看这个咱们可能有点迷糊,那么咱们来说说这个流程。
这个IOC和DI的启动流程。

IOC/DI流程

在这里插入图片描述
这里我解释一下

ApplicationContext

这个呢,是咱们整个IOC的入口,其实在Spring里面有一个BeanFactory这个是顶层接口,然后那个ApplicationContext是那个接口的实现。这里没有搞那么复杂,不过流程大体流程是这样子的。

这个ApplicationContext提供了一个非常重要的方法,就是getBean()方法。这个方法就可以帮我们把那个容器里面的对象拿出来。

BeanDefinitionReader

这个是一个解析器嘛,负责解析各种配置文件,比如xml,yaml,properites之类的。
解析之后,要做的是把这个配置文件对应的内容给解析出来,然后把东西封装到BeanDefinition里面

BeanDefinition

这个玩意呢,是封装的我们的类的(被扫描出来)的信息。比如
我们在配置文件里面写的玩意是
在这里插入图片描述
我们要扫描这个包下面的所有类,那么这个扫描首先是reader读取了要扫描包的信息,然后reader去扫描
之后把扫描结果,类名,全包名封装起来。下面是它的代码,可以看到。

package com.huterox.spring.framework.beans.config;

public class HUBeanDefinition {
    private String factoryBeanName;
    private String beanClassName;

    public void setFactoryBeanName(String factoryBeanName) {
        this.factoryBeanName = factoryBeanName;
    }

    public void setBeanClassName(String beanClassName) {
        this.beanClassName = beanClassName;
    }

    public String getFactoryBeanName() {
        return factoryBeanName;
    }

    public String getBeanClassName() {
        return beanClassName;
    }
}

BeanWrapper

这玩意呢,其实是实例化后的对象,同样我们也是把这玩意封装起来了。
在这里插入图片描述
然后我们可以再看到这个代码

package com.huterox.spring.framework.beans;

public class HUBeanWrapper {
    private Object wrapperInstance;
    private Class<?> wrapperClass;
    public HUBeanWrapper(Object instance) {
        this.wrapperInstance = instance;
        this.wrapperClass = this.wrapperInstance.getClass();
    }

    public Object getWrapperInstance() {
        return wrapperInstance;
    }

    public Class<?> getWrapperClass() {
        return wrapperClass;
    }
}

getBean()方法

最后就是我们的这个方法,这个方法呢,是我们的核心
在这里插入图片描述
我们的依赖注入就是在这里完成的,同时也是在这里面会产生这个ABA问题嘛
下面这个就是别人博客里面的什么三级缓存
在这里插入图片描述
我们接下来会好好说说这个。
这个一级缓存到底是啥,二级是啥,三级是啥
看到这里咱们也用了
在这里插入图片描述

IOC启动流程(HUApplicationContext)

启动/调用

接下来咱们好好聊聊这个IOC
首先是咱们的这个MVC部分,咱们是做了一个拆分,把IOC独立了
所以我们可以看到这里,这里的话就被启动了,调用了
在这里插入图片描述
这个MVC部分呢,调用了咱们的容器,然后我们的容器开始启动工作。

流程

OK到了咱们这个流程了
一来进入咱们的构造方法
在这里插入图片描述

获取Definition

先获取到咱们的Definition。这里有两步嘛。
主要是reader读取配置文件,然后扫描包封装信息嘛。
这里我把这个reader的代码给出来

package com.huterox.spring.framework.beans.suports;

import com.huterox.spring.framework.beans.config.HUBeanDefinition;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Properties;

public class HUBeanDefinitionReader {

    private Properties contextConfig = new Properties();
    //需要被注册到容器里面的类
    private List<String> registryBeanClass = new ArrayList<>();
    public HUBeanDefinitionReader(String[] configLocations) {
        //1.加载配置文件并解析,当前只支持properties先这样处理
        doLoadConfig(configLocations[0]);
        //2.读取配置文件,并且封装为BeanDefinition对象
        doScanner(contextConfig.getProperty("scanPackage"));
        doLoadBeanDefinitions();
    }

    //负责读取配置文件
    public List<HUBeanDefinition> doLoadBeanDefinitions() {
        //这里注意此时我们存入的是类的包名com.xx.xx.xx
        List<HUBeanDefinition> result = new ArrayList<>();
        try {

            for (String className:registryBeanClass){
                Class<?> beanClass = Class.forName(className);
                if(beanClass.isInterface()) continue;
                //把类名放在里面 beanClass.getName()是获取全包名
                result.add(doCreateBeanDefition(toLowerFirstCase(beanClass.getSimpleName()), beanClass.getName()));
                //把这玩意的接口也给它
                for(Class<?> InfClass:beanClass.getInterfaces()){
                    result.add(doCreateBeanDefition(InfClass.getSimpleName(), beanClass.getName()));
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }

        return result;
    }

    private HUBeanDefinition doCreateBeanDefition(String factoryBeanName, String beanClassName) {
        HUBeanDefinition beanDefinition = new HUBeanDefinition();
        beanDefinition.setFactoryBeanName(factoryBeanName);
        beanDefinition.setBeanClassName(beanClassName);
        return beanDefinition;
    }


    private void doLoadConfig(String contextproperties) {
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextproperties);
        try {
            contextConfig.load(is);
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if(is!=null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    private void doScanner(String scanPackage){
        // 扫描我们想要控制的那个包下面的所有类
        //存进去的是 com.huterox.test.xx(类名)
        URL url = this.getClass().getClassLoader().getResource("/" + scanPackage.replaceAll("\\.", "/"));

        assert url != null;
        File classDir = new File(url.getFile());
        for(File file: Objects.requireNonNull(classDir.listFiles())){
            if(file.isDirectory()){
                this.doScanner(scanPackage+"."+file.getName());
            }else {
                if (!file.getName().endsWith(".class")) continue;
                String clazzName = (scanPackage + "." + file.getName().replace(".class", ""));
                registryBeanClass.add(clazzName);
            }
        }
    }

    private String toLowerFirstCase(String simpleName){
        //字母转换,符合javabean标准
        char[] chars = simpleName.toCharArray();
        if(67<=(int)chars[0]&&(int)chars[0]<=92)
            chars[0] +=32;
        return String.valueOf(chars);
    }

}

BeanDefinition的代码上面给了,咱们就不重复了(所以好好看看本文,代码是在每一步给你的,不是最后面全部贴出来滴!不过方向,绝对不会让读者手打代码,毕竟我也要回过来看呀)

然后这里注意的是,此时我们把这个BeanDefinition给放在了我们的Map里面
在这里插入图片描述

这个存储的是所有的类(test包下面)的信息,对比刚刚被人博客里面的三级缓存的图

在这里插入图片描述
你发现这个一级缓存的初始容量很大,那咱们的definition你会发现这个其实存的东西是最多的。
再来对比一下给出的spring里面的那个关于三级缓存的解释:

1. 一级缓存-singletonObjects是用来存放就绪状态的Bean。保存在该缓存中的Bean所实现Aware子接口的方法已经回调完毕,自定义初始化方法已经执行完毕,也经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理;

2. 二级缓存-earlySingletonObjects是用来存放早期曝光的Bean,一般只有处于循环引用状态的Bean才会被保存在该缓存中。保存在该缓存中的Bean所实现Aware子接口的方法还未回调,自定义初始化方法未执行,也未经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理。如果启用了Spring AOP,并且处于切点表达式处理范围之内,那么会被增强,即创建其代理对象。 这里额外提一点,普通Bean被增强(JDK动态代理或CGLIB)的时机是在AbstractAutoProxyCreator实现的BeanPostProcessor的postProcessorAfterInitialization方法中,而处于循环引用状态的Bean被增强的时机是在AbstractAutoProxyCreator实现的SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法中。

3. 三级缓存-singletonFactories是用来存放创建用于获取Bean的工厂类-ObjectFactory实例。在IoC容器中,所有刚被创建出来的Bean,默认都会保存到该缓存中。

不过在咱们这里由于还没涉及到那么多复杂的东西,所以我们这边只是简单处理,这个一级缓存就是我们首次扫描到的Bean

创建容器

这里的话咱们是一个方法嘛
doCreateBean();
这个doCreateBean()其实调用的是getBean()
在这里插入图片描述

getBean()方法(重点)

这个getBean方法就是做了好几步骤
在这里插入图片描述

instanceBean

这个方法是负责实例化咱们的对象
在这里插入图片描述
你会发现此时我们又是把那个第一次没有DI注入的对象给存进去了 这里

private Map<String, Object> factoryBeanObjectCache = new HashMap<>();

对比刚刚这句话

2. 二级缓存-earlySingletonObjects是用来存放早期曝光的Bean,一般只有处于循环引用状态的Bean才会被保存在该缓存中。保存在该缓存中的Bean所实现Aware子接口的方法还未回调,自定义初始化方法未执行,也未经过BeanPostProcessor实现类的postProcessorBeforeInitialization、postProcessorAfterInitialization方法处理。如果启用了Spring AOP,并且处于切点表达式处理范围之内,那么会被增强,即创建其代理对象。 这里额外提一点,普通Bean被增强(JDK动态代理或CGLIB)的时机是在AbstractAutoProxyCreator实现的BeanPostProcessor的postProcessorAfterInitialization方法中,而处于循环引用状态的Bean被增强的时机是在AbstractAutoProxyCreator实现的SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法中。

当然咱们现在没有AOP哈,不过这个算不算是我们早期暴露出来的Bean

populateBean

最后就是完成我们的这个注入
这个的话和我们以前的那一套一样

在这里插入图片描述
然后加入我们最后一级缓存

HUApplicationContext完整代码

package com.huterox.spring.framework.context;

import com.huterox.spring.framework.annotation.HUAutowired;
import com.huterox.spring.framework.annotation.HUController;
import com.huterox.spring.framework.annotation.HUService;
import com.huterox.spring.framework.beans.HUBeanWrapper;
import com.huterox.spring.framework.beans.suports.HUBeanDefinitionReader;
import com.huterox.spring.framework.beans.config.HUBeanDefinition;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HUApplicationContext {

    private String[] configLocations;
    private HUBeanDefinitionReader reader;

    private Map<String, HUBeanDefinition> beanDefinitionMap = new HashMap<>();
    private Map<String, HUBeanWrapper> factoryBeanInstanceCache = new HashMap<>();
    private Map<String, Object> factoryBeanObjectCache = new HashMap<>();

    public HUApplicationContext(String... configLocations) {
        this.configLocations = configLocations;
        //加载配置文件读取器
        this.reader = new HUBeanDefinitionReader(this.configLocations);
        List<HUBeanDefinition> beanDefinitions = this.reader.doLoadBeanDefinitions();
        //2.缓存beanDefinitions对象
        try {
            doRegistryBeanDefintition(beanDefinitions);
        } catch (Exception e) {
            e.printStackTrace();
        }

        //3.创建IOC容器,把类都放在IOC容器里面
        doCreateBean();
    }

    private void doCreateBean() {
        for (Map.Entry<String, HUBeanDefinition> beanDefinitionEntry : this.beanDefinitionMap.entrySet()) {
            String beanName = beanDefinitionEntry.getKey();
            //非延时加载
            getBean(beanName);
        }
    }

    private void doRegistryBeanDefintition(List<HUBeanDefinition> beanDefinitions) throws Exception {
        //把Definition对象放在咱们的map里面,beanDefinition是保存的类名,全包名
        for (HUBeanDefinition beanDefinition : beanDefinitions) {
            //双键存储便于双向查找
            if (this.beanDefinitionMap.containsKey(beanDefinition.getFactoryBeanName())) {
                throw new Exception("The " + beanDefinition.getFactoryBeanName() + " is exists!");
            }
            this.beanDefinitionMap.put(beanDefinition.getFactoryBeanName(), beanDefinition);
            this.beanDefinitionMap.put(beanDefinition.getBeanClassName(), beanDefinition);
        }
    }

    //创建Bean的实例,完成依赖注入
    public Object getBean(String beanName) {
        //得到对象的全包名,当然这个信息封装在beanDefinition里面
        HUBeanDefinition beanDefinition = this.beanDefinitionMap.get(beanName);
        //实例化
        Object instance = instanceBean(beanName, beanDefinition);
        if (instance == null) return null;
        //实例封装为beanWrapper当中
        HUBeanWrapper beanWrapper = new HUBeanWrapper(instance);
        //将这个wrapper对象放在容器里面IOC里面
        this.factoryBeanInstanceCache.put(beanName, beanWrapper);
        //完成依赖注入
        populateBean(beanName, beanDefinition, beanWrapper);
        return this.factoryBeanInstanceCache.get(beanName).getWrapperInstance();
    }

    private void populateBean(String beanName, HUBeanDefinition beanDefinition, HUBeanWrapper beanWrapper) {
        //开始做依赖注入给值
        Object instance = beanWrapper.getWrapperInstance();
        Class<?> clazz = beanWrapper.getWrapperClass();
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            if (!(field.isAnnotationPresent(HUAutowired.class))) continue;
            HUAutowired autowired = field.getAnnotation(HUAutowired.class);
            String autowiredBeanName = autowired.value().trim();
            if ("".equals(autowiredBeanName)) {
                autowiredBeanName = field.getType().getName();
            }
            field.setAccessible(true);

            try {
                if (!this.factoryBeanInstanceCache.containsKey(autowiredBeanName)) continue;
                //这个就是为什么要按照标准来写首字母要小写
                field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance());
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }

    }

    private Object instanceBean(String beanName, HUBeanDefinition beanDefinition) {
        String className = beanDefinition.getBeanClassName();
        Object instance = null;
        try {
            Class<?> clazz = Class.forName(className);
            if (!(clazz.isAnnotationPresent(HUService.class) || clazz.isAnnotationPresent(HUController.class))) {
                return null;
            }
            instance = clazz.newInstance();
            //接下来的这部分是AOP的入口,先忽略


            //三级缓存,为了解决这种循环依赖的问题,所以在后面进行DI的时候,这里
            //先进行一个缓存。因为完整的生命周期是对象要完成DI注入后,如果没有先进行缓存
            //那么在同一时刻进行DI注入的时候,出现A依赖B,B依赖A,并且缓存池里面没有B,要创建B
            //然后B又要A,A因为没有B又创建不了的情况,先初步缓存这样A要B的时候,创建B然后要依赖A
            //此时A在创建的时候有个缓存,这样B可以被创建,然后A最后完成注入,同样B也会完成。这部分逻辑后面再说
            this.factoryBeanObjectCache.put(beanName, instance);


        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return instance;
    }

    public Object getBean(Class className) {
        return getBean(className.getName());
    }

    public int getBeanDefinitionCount() {
        return this.beanDefinitionMap.size();
    }

    public String[] getBeanDefinitionNames() {
        return this.beanDefinitionMap.keySet().toArray(new String[this.beanDefinitionMap.size()]);
    }
}

MVC调用

之后就是给咱的MVC干活了,这个时候原来MVC部分的代码都不要了。

package com.huterox.spring.framework.webmvc.servlet;

import com.huterox.spring.framework.annotation.*;
import com.huterox.spring.framework.context.HUApplicationContext;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

public class HUDispatcherServlet extends HttpServlet {




    private Map<String,Method> handlerMapping = new HashMap<>();

    private HUApplicationContext context = null;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);//保证post/get同时都能处理
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            System.out.println("hello this request will be processed by mymvcframeword ");
            this.doDispatch(req,resp);//相应的处理
        } catch (Exception e) {
            e.printStackTrace();
            resp.getWriter().write("服务器异常500:"+Arrays.toString(e.getStackTrace()));
        }
    }


    @Override
    public void init(ServletConfig config) throws ServletException {

        context = new HUApplicationContext(config.getInitParameter("Myproperties"));


        //5.初始化HandlerMapping(配置器)
        doInitHandlerMapping();
        //6.完成~~给请求分发器,去去执行对应方法


    }

    private void doInitHandlerMapping() {

        if(this.context.getBeanDefinitionCount()==0) return;
        String [] beanNames = this.context.getBeanDefinitionNames();
        //由于我们加入IOC是在那个调用了getBean之后我们才有滴,所以我一开始判断
        //是按照beanDefinition来玩的,有些那个bean是没有Controller之类的注解的,所以IOC不会加入容器,所以没有
        //这个对象,所以注意判断null
        for (String beanName:beanNames) {
            Object instance = this.context.getBean(beanName);
            if(instance==null){
                continue;
            }
            Class<?> clazz = instance.getClass();
            if(!clazz.isAnnotationPresent(HUController.class)) continue;
            String baseURl="";
            if(clazz.isAnnotationPresent(HURequestMapping.class)){
                HURequestMapping requestMapping = clazz.getAnnotation(HURequestMapping.class);
                baseURl = requestMapping.value();
            }
            for (Method method : clazz.getMethods()) {
                if(!method.isAnnotationPresent(HURequestMapping.class))continue;
                HURequestMapping requestMapping = method.getAnnotation(HURequestMapping.class);
                String url =("/" + baseURl +"/"+ requestMapping.value()).replaceAll("/+","/");

                handlerMapping.put(url,method);
                //把url和对应的method放在一起
            }
        }
    }
    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception {
        String url = req.getRequestURI();
        String contextPath = req.getContextPath();
        //tomcat里面配的一开始的那个不要
        url = url.replaceAll(contextPath,"").replaceAll("/+","/");
        System.out.println(url);
        if(!this.handlerMapping.containsKey(url)){
            resp.getWriter().write("404 Not Found!");
            return;
        }

        //获取请求参数
        Map<String,String[]> paramsMap = req.getParameterMap();

        Method method = this.handlerMapping.get(url);
//        String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
//        Object o = IOC.get(beanName);
//        //这部分是一些那啥参数
//        method.invoke(o,new Object[]{req,resp,params.get("name")[0]});

        Class<?>[] parameterTypes = method.getParameterTypes();
        Object[] paramValues = new Object[parameterTypes.length];
//        动态给值

        for (int i = 0; i < parameterTypes.length; i++) {
            Class<?> parameterType = parameterTypes[i];
            if(parameterType==HttpServletRequest.class){
                paramValues[i] = req;
                continue;
            }else if(parameterType==HttpServletResponse.class){
                paramValues[i] =resp;
                continue;
            }else{
                //目前只是针对String类型的
                Annotation[][] pa = method.getParameterAnnotations();
                //pa[0]表示第一个参数 pa[0][0]第一个参数的第一个注解
                for(int j=0;j<pa.length;j++){
                    for(Annotation a:pa[i]){
                        if(a instanceof HURequestParam){
                            String paramName = ((HURequestParam) a).value();
                            if(!"".equals(paramName)){
                                String strings =Arrays.toString(paramsMap.get(paramName))
                                        .replaceAll("\\[|\\]","")
                                        .replaceAll("\\s","");//过滤
                                if(parameterType==String.class){

                                    paramValues[i] = strings;

                                }else if(parameterType==Integer.TYPE ||parameterType==Integer.class ){

                                    paramValues[i] = Integer.valueOf(strings);
                                }else if(parameterType==Double.TYPE ||parameterType==Double.class ){

                                    paramValues[i] = Double.valueOf(strings);
                                }else if(parameterType==Float.TYPE ||parameterType==Float.class ){
                                    paramValues[i] = Float.valueOf(strings);
                                }

                            }

                        }
                    }
                }

            }
        }

        method.invoke(this.context.getBean(method.getDeclaringClass()),paramValues);

    }


}

这样就完成了工作

测试

在这里插入图片描述
一切正常

其他(复现此项目)

接下来最后说说,这个没给出的东西,代码哪里获取。首先这个项目就是在原来的这个
Spring之手写SpringMVC5个注解里面,不同的地方都给出了了,其他压根没动。比如项目配置

<!DOCTYPE web-app PUBLIC
 "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
 "http://java.sun.com/dtd/web-app_2_3.dtd" >

<web-app>

  <display-name>Archetype Created Web Application</display-name>


  <servlet>
    <servlet-name>gpmvc</servlet-name>
    <servlet-class>com.huterox.spring.framework.webmvc.servlet.HUDispatcherServlet</servlet-class>
    <init-param>
      <param-name>Myproperties</param-name>
      <param-value>application.properties</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
<!--    tomcat启动的时候后创建执行init-->
  </servlet>
  <servlet-mapping>
    <servlet-name>gpmvc</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

在这里插入图片描述
根本没变。

总结

这个东西还是很有意思的,后面咱们在优化~

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-03-11 21:59:56  更:2022-03-11 22:00:55 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 10:36:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码