起源:cnblogs.com/lonely-wolf/p/14127957.html
随着 Spring
的崛起以及其性能的欠缺,当初可能绝大部分我的项目的开发都是应用 Spring(全家桶)
来进行开发,Spring
也的确和其名字一样,是开发者的春天,Spring
解放了程序员的双手,而等到 SpringBoot
进去之后配置文件大大减少,更是进一步解放了程序员的双手。
然而也正是因为 Spring
家族产品的弱小,使得咱们习惯了面向 Spring
开发,那么如果有一天没有了 Spring
,是不是感觉心里一空,可能一下子连最根本的接口都不会写了,尤其是没有接触过 Servlet
编程的敌人。因为退出没有了 Spring
等框架,那么咱们就须要利用最原生的 Servlet
来本人实现接口门路的映射,对象也须要本人进行治理。
Spring 能帮咱们做什么
Spring
是为解决企业级利用开发的复杂性而设计的一款框架,Spring
的设计理念就是:简化开发。
在 Spring
框架中,所有对象都是 bean
,所以其通过面向 bean
编程(BOP),联合其核心思想依赖注入(DI)和面向切面((AOP)编程,Spring
实现了其平凡的简化开发的设计理念。
Spring 最新教程:https://www.javastack.cn/spring/
管制反转(IOC)
IOC
全称为:Inversion of Control。管制反转的基本概念是:不必创建对象,然而须要形容创建对象的形式。
简略的说咱们原本在代码中创立一个对象是通过 new
关键字,而应用了 Spring
之后,咱们不在须要本人去 new
一个对象了,而是间接通过容器外面去取进去,再将其主动注入到咱们须要的对象之中,即:依赖注入。
也就说创建对象的控制权不在咱们程序员手上了,全副交由 Spring
进行治理,程序要只须要注入就能够了,所以才称之为管制反转
依赖注入(DI)
依赖注入(Dependency Injection,DI)就是 Spring
为了实现管制反转的一种实现形式,所有有时候咱们也将管制反转间接称之为依赖注入。
面向切面编程(AOP)
AOP
全称为:Aspect Oriented Programming。AOP
是一种编程思维,其外围结构是方面(切面),行将那些影响多个类的公共行为封装到可重用的模块中,而使本来的模块内只需关注本身的个性化行为。
AOP
编程的罕用场景有:Authentication(权限认证)、Auto Caching(主动缓存解决)、Error Handling(对立错误处理)、Debugging(调试信息输入)、Logging(日志记录)、Transactions(事务处理)等。
利用 Spring 来实现 Hello World
最原生的 Spring
须要较多的配置文件,而 SpringBoot
省略了许多配置,相比拟于原始的 Spring
又简化了不少,在这里咱们就以 SpringBoot
为例来实现一个简略的接口开发。
Spring Boot 根底就不介绍了,举荐下这个实战教程:
https://github.com/javastacks…
1、新建一个 maven
我的项目,pom
文件中引入依赖(省略了少部分属性):
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2、新建一个 HelloController
类:
package com.lonely.wolf.note.springboot.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {@GetMapping("/demo")
public String helloWorld(String name){return "Hello:" + name;}
}
3、最初新建一个 SpringBoot
启动类:
package com.lonely.wolf.note.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "com.lonely.wolf.note.springboot")
class MySpringBootApplication {public static void main(String[] args) {SpringApplication.run(MySpringBootApplication.class, args);
}
}
4、当初就能够输出测试门路:http://localhost:8080/hello/demo?name= 双子孤狼
进行测试,失常输入:Hello:双子孤狼
。
咱们能够看到,利用 SpringBoot
来实现一个简略的利用开发非常简单,能够不须要任何配置实现一个简略的利用,这是因为 SpringBoot
外部曾经做好了约定(约定优于配置思维),包含容器 Tomcat
都被默认集成,所以咱们不须要任何配置文件就能够实现一个简略的 demo
利用。
如果没有了 Spring
通过下面的例子咱们能够发现,利用 Spring
来实现一个 Hello World
非常简单,然而如果没有了 Spring
,咱们又该如何实现这样的一个 Hello World
接口呢?
基于 Servlet 开发
在还没有框架之前,编程式基于原始的 Servlet
进行开发,上面咱们就基于原生的 Servlet
来实现一个简略的接口调用。
1、pom
文件引入依赖,须要留神的是,package
属性要设置成 war
包,为了节俭篇幅,这里没有列出 pom
残缺的信息:
<packaging>war</packaging>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.72</version>
</dependency>
</dependencies>
2、在 src/main
上面新建文件夹 webapp/WEB-INF
,而后在 WEB-INF
上面新建一个 web.xml
文件:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:javaee="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4">
<display-name>Lonely Wolf Web Application</display-name>
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.lonely.wolf.mini.spring.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/hello/*</url-pattern>
</servlet-mapping>
</web-app>
这外面定义了 selvlet
和 servlet-mapping
两个标签,这两个标签必须一一对应,下面的标签定义了 servlet
的地位,而上面的 servlet-mapping
文件定义了门路的映射,这两个标签通过 servlet-name
标签对应。
3、新建一个 HelloServlet
类继承 HttpServlet
:
package com.lonely.wolf.mini.spring.servlet;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 原始 Servlet 接口编写,个别须要实现 GET 和 POST 办法,其余办法能够视具体情况选择性继承
*/
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doPost(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("text/html;charset=utf-8");
response.getWriter().write("Hello:" + request.getParameter("name"));
}
}
4、执行 maven
打包命令,确认胜利打包成 war
包:
5、RUN-->Edit Configurations
,而后点击左上角的 +
号,新建一个 Tomcat Server
,如果是第一次配置,默认没有 Tomcat Server
选项,须要点击底部的 xx more items...
:
6、点击左边的 Deployment
,而后依照下图顺次点击,最初在弹框内找到下面打包好的 war
包文件:
7、选中之后,须要留神的是,上面 Application Context
默认会带上 war
包名,为了不便,咱们须要把它删掉,即不必上下文门路,只保留一个根门路 /
(当然上下文也能够保留,然而每次申请都要带上这一部分),再抉择 Apply
,点击 OK
,即可实现部署:
8、最初咱们在浏览器输出申请门路http://localhost:8080/hello?name= 双子孤狼
,即可失去返回:Hello:双子孤狼
。
下面咱们就实现了一个简略的 基于Servlet
的接口开发,能够看到,配置十分麻烦,每减少一个 Servlet
都须要减少对应的配置,所以才会有许多框架的呈现来帮咱们简化开发,比方原来很风行的 Struts2
框架,当然当初除了一些比拟老的我的项目,个别咱们都很少应用,而更多的是抉择 Spring
框架来进行开发。
模拟 Spring
Spring
的源码体系十分宏大,大部分人对其源码都敬而远之。的确,Spring
毕竟通过了这么多年的迭代,功能丰富,我的项目宏大,不是一下子就能看懂的。尽管 Spring
难以了解,然而其最外围的思维依然是咱们下面介绍的几点,接下来就基于 Spring
最外围的局部来模仿,本人入手实现一个超级迷你版本的 Spring
(此版本并不蕴含 AOP
性能)。
1、pom
依赖和下面放弃不变,而后 web.xml
作如下扭转,这里会拦挡所有的接口 /*
,而后多配置了一个参数,这个参数其实也是为了更形象的模仿 Spring
:
<servlet>
<servlet-name>myDispatcherServlet</servlet-name>
<servlet-class>com.lonely.wolf.mini.spring.v1.MyDispatcherServlet</servlet-class>
<init-param>
<param-name>defaultConfig</param-name>
<param-value>application.properties</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>myDispatcherServlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
2、在 respurces
上面新建一个配置文件 application.properties
,用来定义扫描的根本门路:
basePackages=com.lonely.wolf.mini.spring
3、创立一些相干的注解类:
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfAutowired {String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfController {String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfGetMapping {String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfRequestParam {String value() default "";
}
package com.lonely.wolf.mini.spring.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WolfService {String value() default "";
}
4、这个时候最外围的逻辑就是 MyDispatcherServlet
类了:
package com.lonely.wolf.mini.spring.v1;
import com.lonely.wolf.mini.spring.annotation.*;
import com.lonely.wolf.mini.spring.v1.config.MyConfig;
import org.apache.commons.lang3.StringUtils;
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 MyDispatcherServlet extends HttpServlet {private MyConfig myConfig = new MyConfig();
private List<String> classNameList = new ArrayList<String>();
private Map<String,Object> iocContainerMap = new HashMap<>();
private Map<String,HandlerMapping> handlerMappingMap = new HashMap<>();
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {this.doPost(request,response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {this.doDispatch(request, response);
} catch (Exception e) {e.printStackTrace();
}
}
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{String requestUrl = this.formatUrl(request.getRequestURI());
HandlerMapping handlerMapping = handlerMappingMap.get(requestUrl);
if (null == handlerMapping){response.getWriter().write("404 Not Found");
return;
}
// 获取办法中的参数类型
Class<?>[] paramTypeArr = handlerMapping.getMethod().getParameterTypes();
Object[] paramArr = new Object[paramTypeArr.length];
for (int i=0;i<paramTypeArr.length;i++){Class<?> clazz = paramTypeArr[i];
// 参数只思考三种类型,其余不思考
if (clazz == HttpServletRequest.class){paramArr[i] = request;
}else if (clazz == HttpServletResponse.class){paramArr[i] = response;
} else if (clazz == String.class){Map<Integer,String> methodParam = handlerMapping.getMethodParams();
paramArr[i] = request.getParameter(methodParam.get(i));
}else{System.out.println("暂不反对的参数类型");
}
}
// 反射调用 controller 办法
handlerMapping.getMethod().invoke(handlerMapping.getTarget(), paramArr);
}
private String formatUrl(String requestUrl) {requestUrl = requestUrl.replaceAll("/+","/");
if (requestUrl.lastIndexOf("/") == requestUrl.length() -1){requestUrl = requestUrl.substring(0,requestUrl.length() -1);
}
return requestUrl;
}
@Override
public void init(ServletConfig config) throws ServletException {
//1. 加载配置文件
try {doLoadConfig(config.getInitParameter("defaultConfig"));
} catch (Exception e) {System.out.println("加载配置文件失败");
return;
}
//2. 依据获取到的扫描门路进行扫描
doScanPacakge(myConfig.getBasePackages());
//3. 将扫描到的类进行初始化,并存放到 IOC 容器
doInitializedClass();
//4. 依赖注入
doDependencyInjection();
System.out.println("DispatchServlet Init End...");
}
private void doDependencyInjection() {if (iocContainerMap.size() == 0){return;}
// 循环 IOC 容器中的类
Iterator<Map.Entry<String,Object>> iterator = iocContainerMap.entrySet().iterator();
while (iterator.hasNext()){Map.Entry<String,Object> entry = iterator.next();
Class<?> clazz = entry.getValue().getClass();
Field[] fields = clazz.getDeclaredFields();
// 属性注入
for (Field field : fields){
// 如果属性有 WolfAutowired 注解则注入值(临时不思考其余注解)if (field.isAnnotationPresent(WolfAutowired.class)){String value = toLowerFirstLetterCase(field.getType().getSimpleName());// 默认 bean 的 value 为类名首字母小写
if (field.getType().isAnnotationPresent(WolfService.class)){WolfService wolfService = field.getType().getAnnotation(WolfService.class);
value = wolfService.value();}
field.setAccessible(true);
try {Object target = iocContainerMap.get(beanName);
if (null == target){System.out.println(clazz.getName() + "required bean:" + beanName + ",but we not found it");
}
field.set(entry.getValue(),iocContainerMap.get(beanName));// 初始化对象,前面注入
} catch (IllegalAccessException e) {e.printStackTrace();
}
}
}
// 初始化 HanderMapping
String requestUrl = "";
// 获取 Controller 类上的申请门路
if (clazz.isAnnotationPresent(WolfController.class)){requestUrl = clazz.getAnnotation(WolfController.class).value();}
// 循环类中的办法,获取办法上的门路
Method[] methods = clazz.getMethods();
for (Method method : methods){
// 假如只有 WolfGetMapping 这一种注解
if(!method.isAnnotationPresent(WolfGetMapping.class)){continue;}
WolfGetMapping wolfGetMapping = method.getDeclaredAnnotation(WolfGetMapping.class);
requestUrl = requestUrl + "/" + wolfGetMapping.value();// 拼成实现的申请门路
// 不思考正则匹配门路 /xx/* 的状况,只思考齐全匹配的状况
if (handlerMappingMap.containsKey(requestUrl)){System.out.println("反复门路");
continue;
}
Annotation[][] annotationArr = method.getParameterAnnotations();// 获取办法中参数的注解
Map<Integer,String> methodParam = new HashMap<>();// 存储参数的程序和参数名
retryParam:
for (int i=0;i<annotationArr.length;i++){for (Annotation annotation : annotationArr[i]){if (annotation instanceof WolfRequestParam){WolfRequestParam wolfRequestParam = (WolfRequestParam) annotation;
methodParam.put(i,wolfRequestParam.value());// 存储参数的地位和注解中定义的参数名
continue retryParam;
}
}
}
requestUrl = this.formatUrl(requestUrl);// 次要是避免门路多了 / 导致门路匹配不上
HandlerMapping handlerMapping = new HandlerMapping();
handlerMapping.setRequestUrl(requestUrl);// 申请门路
handlerMapping.setMethod(method);// 申请办法
handlerMapping.setTarget(entry.getValue());// 申请办法所在 controller 对象
handlerMapping.setMethodParams(methodParam);// 申请办法的参数信息
handlerMappingMap.put(requestUrl,handlerMapping);// 存入 hashmap
}
}
}
/**
* 初始化类,并放入容器 iocContainerMap 内
*/
private void doInitializedClass() {if (classNameList.isEmpty()){return;}
for (String className : classNameList){if (StringUtils.isEmpty(className)){continue;}
Class clazz;
try {clazz = Class.forName(className);// 反射获取对象
if (clazz.isAnnotationPresent(WolfController.class)){String value = ((WolfController)clazz.getAnnotation(WolfController.class)).value();
// 如果间接指定了 value 则取 value,否则取首字母小写类名作为 key 值存储类的实例对象
iocContainerMap.put(StringUtils.isBlank(value) ? toLowerFirstLetterCase(clazz.getSimpleName()) : value,clazz.newInstance());
}else if(clazz.isAnnotationPresent(WolfService.class)){String value = ((WolfService)clazz.getAnnotation(WolfService.class)).value();
iocContainerMap.put(StringUtils.isBlank(value) ? toLowerFirstLetterCase(clazz.getSimpleName()) : value,clazz.newInstance());
}else{System.out.println("不思考其余注解的状况");
}
} catch (Exception e) {e.printStackTrace();
System.out.println("初始化类失败,className 为" + className);
}
}
}
/**
* 将首字母转换为小写
* @param className
* @return
*/
private String toLowerFirstLetterCase(String className) {if (StringUtils.isBlank(className)){return "";}
String firstLetter = className.substring(0,1);
return firstLetter.toLowerCase() + className.substring(1);
}
/**
* 扫描包下所有文件获取全限定类名
* @param basePackages
*/
private void doScanPacakge(String basePackages) {if (StringUtils.isBlank(basePackages)){return;}
// 把包名的. 替换为 /
String scanPath = "/" + basePackages.replaceAll("\\.","/");
URL url = this.getClass().getClassLoader().getResource(scanPath);// 获取到以后包所在磁盘的全门路
File files = new File(url.getFile());// 获取以后门路下所有文件
for (File file : files.listFiles()){// 开始扫描门路下的所有文件
if (file.isDirectory()){// 如果是文件夹则递归
doScanPacakge(basePackages + "." + file.getName());
}else{// 如果是文件则增加到汇合。因为下面是通过类加载器获取到的文件门路,所以实际上是 class 文件所在门路
classNameList.add(basePackages + "." + file.getName().replace(".class",""));
}
}
}
/**
* 加载配置文件
* @param configPath - 配置文件所在门路
*/
private void doLoadConfig(String configPath) {InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(configPath);
Properties properties = new Properties();
try {properties.load(inputStream);
} catch (IOException e) {e.printStackTrace();
System.out.println("加载配置文件失败");
}
properties.forEach((k, v) -> {
try {Field field = myConfig.getClass().getDeclaredField((String)k);
field.setAccessible(true);
field.set(myConfig,v);
} catch (Exception e) {e.printStackTrace();
System.out.println("初始化配置类失败");
return;
}
});
}
}
5、这个 Servlet
相比拟于下面的 HelloServlet
多了一个 init
办法,这个办法中次要做了以下几件事件:
(1)初始化配置文件,拿到配置文件中配置的参数信息(对应办法:doLoadConfig
)。
(2)拿到第 1
步加载进去的配置文件,获取到须要扫描的包门路,而后将包门路进行转换成理论的磁盘门路,并开始遍历磁盘门路下的所有 class
文件,最终通过转换之后失去扫描门路下的所有类的全限定类型,存储到全局变量 classNameList
中(对应办法:doScanPacakge
)。
(3)依据第 2
步中失去的全局变量 classNameList
中的类通过反射进行初始化(须要留神的是只会初始化加了指定注解的类)并将失去的对应关系存储到全局变量 iocContainerMap
中(即传说中的 IOC
容器),其中 key
值为注解中的 value
属性,如 value
属性为空,则默认取首字母小写的类名作为 key
值进行存储(对应办法:doInitializedClass
)。
(4)这一步比拟要害,须要对 IOC
容器中的所有类的属性进行赋值并且须要对 Controller
中的申请门路进行映射存储,为了确保最初能顺利调用 Controller
中的办法,还须要将办法的参数进行存储。对属性进行映射时只会对加了注解的属性进行映射,映射时会从 IOC
容器中取出第 3
步中曾经初始化的实例对象进行赋值,最初将申请门路和 Controller
中办法的映射关系存入变量 handlerMappingMap
,key
值为申请门路,value
为办法的相干信息(对应办法:doDependencyInjection
)。
6、存储申请门路和办法的映射关系时,须要用到 HandlerMapping
类来进行存储:
package com.lonely.wolf.mini.spring.v1;
import java.lang.reflect.Method;
import java.util.Map;
// 省略了 getter/setter 办法
public class HandlerMapping {
private String requestUrl;
private Object target;// 保留办法对应的实例
private Method method;// 保留映射的办法
private Map<Integer,String> methodParams;// 记录办法参数
}
7、初始化实现之后,因为拦挡了 /*
,所以调用任意接口都会进入 MyDispatcherServlet
,而且最终都会执行办法 doDispatch
,执行这个办法时会拿到申请的门路,而后和全局变量 handlerMappingMap
进行匹配,匹配不上则返回 404
,匹配的上则取出必要的参数进行赋值,最初通过反射调用到 Controller
中的相干办法。
8、新建一个 HelloController
和 HelloService
来进行测试:
package com.lonely.wolf.mini.spring.controller;
import com.lonely.wolf.mini.spring.annotation.WolfAutowired;
import com.lonely.wolf.mini.spring.annotation.WolfController;
import com.lonely.wolf.mini.spring.annotation.WolfGetMapping;
import com.lonely.wolf.mini.spring.annotation.WolfRequestParam;
import com.lonely.wolf.mini.spring.service.HelloService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WolfController
public class HelloController {
@WolfAutowired
private HelloService helloService;
@WolfGetMapping("/hello")
public void query(HttpServletRequest request,HttpServletResponse response, @WolfRequestParam("name") String name) throws IOException {response.setContentType("text/html;charset=utf-8");
response.getWriter().write("Hello:" + name);
}
}
package com.lonely.wolf.mini.spring.service;
import com.lonely.wolf.mini.spring.annotation.WolfService;
@WolfService(value = "hello_service")// 为了演示是否失常取 value 属性
public class HelloService {}
9、输出测试门路:http://localhost:8080////hello?name= 双子孤狼
,进行测试发现能够失常输入:Hello:双子孤狼
。
下面这个例子只是一个简略的演示,通过这个例子只是心愿在没有任何框架的状况下,咱们也能晓得如何实现一个简略的利用开发。例子中很多细节都没有进行解决,仅仅只是为了体验一下 Spring
的核心思想,并理解 Spring
到底帮忙咱们做了什么,实际上 Spring
能帮咱们做的事件远比这个例子中多得多,Spring
体系宏大,设计优雅,通过了多年的迭代优化,是一款十分值得钻研的框架。
近期热文举荐:
1.1,000+ 道 Java 面试题及答案整顿(2022 最新版)
2. 劲爆!Java 协程要来了。。。
3.Spring Boot 2.x 教程,太全了!
4. 别再写满屏的爆爆爆炸类了,试试装璜器模式,这才是优雅的形式!!
5.《Java 开发手册(嵩山版)》最新公布,速速下载!
感觉不错,别忘了顺手点赞 + 转发哦!