转载
作者:程序员技术圈
链接:https://www.jianshu.com/p/2c30f3e6b664
用不到 300 行代码来描述 Spring IOC、DI、MVC 的精华设计思想,并保证基本功能完整。
Spring 的三个阶段,配置阶段、初始化阶段和运行阶段
image.png配置阶段:主要是完成 application.xml 配置和 Annotation 配置。
初始化阶段:主要是加载并解析配置信息,然后,初始化 IOC 容器,完成容器的 DI 操作,已经完成 HandlerMapping 的初始化。
运行阶段:主要是完成 Spring 容器启动以后,完成用户请求的内部调度,并返回响应结果。
项目结构
image.png一、配置阶段
1、pom.xml配置
我采用的是 maven 管理项目。先来看 pom.xml 文件中的配置,我只引用了 servlet-api 的依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>sz-spring</artifactId>
<groupId>com.suzao.spring</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-demo-01</artifactId>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
</dependency>
</dependencies>
<packaging>war</packaging>
<build>
<finalName>webapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.2.2</version>
<configuration>
<webResources>
<resource>
<!-- 原配置文件的目录,相对于pom.xml文件的路径 -->
<directory>src/main/webapp/WEB-INF</directory>
<!-- 目标路径 -->
<targetPath>WEB-INF</targetPath>
</resource>
</webResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
2、SZDispatcherServlet 类
然后,创建 SZDispatcherServlet 类并继承 HttpServlet,重写 init()、doGet() 和 doPost() 方法。
package com.suzao.mvcframework.servlet.v2;
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.IOException;
/**
* @ClassName SZDispatchServlet
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public class SZDispatchServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
}
}
3、web.xml配置
在 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>SuZao Web Application</display-name>
<servlet>
<servlet-name>szmvc</servlet-name>
<servlet-class>com.suzao.mvcframework.servlet.v2.SZDispatchServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>application.properties</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>szmvc</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
</web-app>
4、application.properties配置
我们配置了一个初始化加载的 Spring 主配置文件路径,在原生框架中,我们应该配置的是 classpath:application.xml。在这里,我们为了简化操作,用 properties 文件代替 xml 文件。以下是 properties 文件中的内容:
scanPackage=com.suzao.demo
5、创建SZController 注解:
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZController {
String value() default "";
}
6、创建SZService 注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZService {
String value() default "";
}
7、创建SZRequestMapping注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZRequestMapping {
String value() default "";
}
8、创建SZRequestParam注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZRequestParam {
String value() default "";
}
9、创建SZAutowired 注解
package com.suzao.mvcframework.annotation;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SZAutowired {
String value() default "";
}
10、使用自定义注解进行配置DemoAction
package com.suzao.demo.action;
import com.suzao.demo.service.IDemoService;
import com.suzao.mvcframework.annotation.SZAutowired;
import com.suzao.mvcframework.annotation.SZController;
import com.suzao.mvcframework.annotation.SZRequestMapping;
import com.suzao.mvcframework.annotation.SZRequestParam;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName DemoAction
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
@SZController
@SZRequestMapping("/demo")
public class DemoAction {
@SZAutowired
private IDemoService demoService;
@SZRequestMapping("query")
public void query(HttpServletRequest req, HttpServletResponse resp,
@SZRequestParam("name") String name){
String result = demoService.get(name);
try {
resp.getWriter().write(result);
}catch (Exception e){
e.printStackTrace();
}
}
@SZRequestMapping("add")
public void add(HttpServletRequest request , HttpServletResponse resp,
@SZRequestParam("a") Integer a, @SZRequestParam("b") Integer b){
try {
resp.getWriter().write(a+"+" +b +"=" +(a+b));
}catch (Exception e){
e.printStackTrace();
}
}
@SZRequestMapping("remove")
public void remove(HttpServletRequest req , HttpServletResponse resp,
@SZRequestParam("id") Integer id){
}
}
11、使用自定义注解进行配置DemoService
package com.suzao.demo.service;
import com.suzao.mvcframework.annotation.SZService;
/**
* @ClassName DemoService
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
@SZService
public class DemoService implements IDemoService {
@Override
public String get(String name) {
return "My name is "+ name;
}
}
12、IDemoService
package com.suzao.demo.service;
/**
* @ClassName IDemoService
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public interface IDemoService {
String get(String name);
}
二、初始化阶段
1、先在 SZDispatcherServlet 中声明几个成员变量
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描的所有的类名
private List<String> classNames = new ArrayList<>();
//IOC容器
private Map<String,Object> ioc = new HashMap<>();
//保存url和Method的对应关系
private Map<String,Method> handleMapping = new HashMap<>();
2、SZDispatcherServlet 的init()方法
当 Servlet 容器启动时,会调用 SZDispatcherServlet 的 init()方法,从 init 方法的参数中,我们可以拿到主配置文件的路径,从能够读取到配置文件中的信息。前面我们已经介绍了 Spring 的三个阶段,现在来完成初始化阶段的代码。在 init() 方法中,定义好执行步骤,如下:
@Override
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化扫描到的类,并且放入到IOC容器中
doInstance();
//4.完成自动化的依赖注入
doAutowired();
//5.初始化HandlerMapping
doInitHandlerMapping();
System.out.println("SZ Spring framework is init.");
}
3、doLoadConfig() 方法
doLoadConfig() 方法的实现,将文件读取到 Properties 对象中
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.suzao.demo从文件中保存到内存中
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
4、doScanner() 方法
doScanner() 方法,递归扫描出所有的 Class 文件
//扫描出相关的类
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\.","/" ));
//scanPackage= com.suzao.demo 存储的包路径
//转换为文件路径,实际上就是把 .替换成/
//classpath下不仅有.class 文件, .xml文件 .properties文件
File classPath = new File(url.getFile());
for (File file :classPath.listFiles()){
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());
}else {
//变成包名.类名
//Class.forname()
if(!file.getName().endsWith(".class")){
continue;
}
classNames.add(scanPackage + "." + file.getName().replace(".class","" ));
}
}
}
5、doInstance() 方法
doInstance() 方法,初始化所有相关的类,并放入到 IOC 容器之中。IOC 容器的 key 默认是类名首字母小写,如果是自己设置类名,则优先使用自定义的。因此,要先写一个针对类名首字母处理的工具方法
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
private void doInstance() {
if(classNames.isEmpty()){
return;
}
try {
for(String className : classNames){
Class<?> clazz = Class.forName(className);
//什么样的类才需要初始化呢
//加了注解的类,才初始化,怎么判断
//为了简化代码逻辑,主要体会设计思想,只举例@Controller @Service
if(clazz.isAnnotationPresent(SZController.class)){
Object instance = clazz.newInstance();
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance );
} else if (clazz.isAnnotationPresent(SZService.class)) {
//1.默认根据beanName类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
//2.使用自定义的beanName
SZService service = clazz.getAnnotation(SZService.class);
if(!"".equals(service.value())){
beanName = service.value();
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3.根据包名.类名作为beanName
for (Class<?> i : clazz.getInterfaces()){
if(ioc.containsKey(i.getName())){
throw new Exception("The beanName is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(),instance );
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
doAutowired() 方法
doAutowired() 方法,将初始化到 IOC 容器中的类,需要赋值的字段进行赋值
private void doAutowired() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry<String,Object> entry : ioc.entrySet()){
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields){
if(!field.isAnnotationPresent(SZAutowired.class)){
continue;
}
SZAutowired autowired = field.getAnnotation(SZAutowired.class);
//如果用户没有自定义beanname ,默认就根据类型注入
String beanName = autowired.value().trim();
if("".equals(beanName)){
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//暴力访问
field.setAccessible(true);
//反射调用
//给entry.getValue()这个对象的field字段,赋ioc.get(beanName)的值
try {
field.set(entry.getValue(),ioc.get(beanName) );
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
6、doInitHandlerMapping() 方法
doInitHandlerMapping() 方法,将 SZRequestMapping 中配置的信息和 Method 进行关联,并保存这些关系。
//初始化url和method的一对一对应关系
private void doInitHandlerMapping() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry<String,Object> entry : ioc.entrySet()){
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SZController.class)){
continue;
}
//保存写在类上面的@GPRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(SZRequestMapping.class)){
SZRequestMapping requestMapping = clazz.getAnnotation(SZRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for(Method method : clazz.getMethods()){
if(!method.isAnnotationPresent(SZRequestMapping.class)){
continue;
}
SZRequestMapping requestMapping = method.getAnnotation(SZRequestMapping.class);
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/" );
handleMapping.put(url,method );
System.out.println("Mapped " + url + "," + method);
}
}
}
到此,初始化阶段的所有代码全部写完。
三、运行阶段
1、doPost()方法
来到运行阶段,当用户发送请求被 Servlet 接受时,都会统一调用 doPost 方法,我先在 doPost 方法中再调用 doDispach() 方法
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req,resp);
}catch (Exception e){
e.printStackTrace();
resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
}
}
2、doDispatch() 方法
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath,"" ).replaceAll("/+","/" );
if(!this.handleMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!");
return;
}
Method method = this.handleMapping.get(url);
Map<String,String[]> paramsMap = req.getParameterMap();
//实参列表要根据形参列表才能决定,首先得拿到形参列表
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] parameValus = new Object[parameterTypes.length];
for (int i = 0 ;i< parameterTypes.length ; i++){
Class paramterType = parameterTypes[i];
if(paramterType == HttpServletRequest.class){
parameValus[i] = req;
continue;
}else if(paramterType == HttpServletResponse.class){
parameValus[i] = resp;
continue;
}else if(paramterType == String.class){
Annotation[][] pa = method.getParameterAnnotations();
for (Annotation a : pa[i]){
if(a instanceof SZRequestParam){
String paramName = ((SZRequestParam) a).value();
if(!"".equals(paramName.trim())){
String value = Arrays.toString(paramsMap.get(paramName))
.replaceAll("\\[|\\]", "")
.replaceAll("\\s","," );
parameValus[i] = value;
}
}
}
/*for(int j =0 ;j < pa.length ;j++){
}*/
}
}
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
method.invoke(ioc.get(beanName),parameValus);
}
我们把服务发布到 web 容器中,然后,在浏览器输入:http://localhost:8080/demo/query?name=MC
结果:
当然,真正的 Spring 要复杂很多,但核心设计思路基本如此。
4、SZDispatchServlet完整代码
package com.suzao.mvcframework.servlet.v2;
import com.suzao.mvcframework.annotation.*;
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.*;
/**
* @ClassName SZDispatchServlet
* @Description: TODO
* @Author mc
* @Date 2020
* @Version V1.0
**/
public class SZDispatchServlet extends HttpServlet {
//保存application.properties配置文件中的内容
private Properties contextConfig = new Properties();
//保存扫描的所有的类名
private List<String> classNames = new ArrayList<>();
//IOC容器
private Map<String,Object> ioc = new HashMap<>();
//保存url和Method的对应关系
private Map<String,Method> handleMapping = new HashMap<>();
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req,resp);
}catch (Exception e){
e.printStackTrace();
resp.getWriter().write("500 Exception " + Arrays.toString(e.getStackTrace()));
}
}
private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws Exception{
String url = req.getRequestURI();
String contextPath = req.getContextPath();
url = url.replace(contextPath,"" ).replaceAll("/+","/" );
if(!this.handleMapping.containsKey(url)){
resp.getWriter().write("404 Not Found!");
return;
}
Method method = this.handleMapping.get(url);
Map<String,String[]> paramsMap = req.getParameterMap();
//实参列表要根据形参列表才能决定,首先得拿到形参列表
Class<?>[] parameterTypes = method.getParameterTypes();
Object[] parameValus = new Object[parameterTypes.length];
for (int i = 0 ;i< parameterTypes.length ; i++){
Class paramterType = parameterTypes[i];
if(paramterType == HttpServletRequest.class){
parameValus[i] = req;
continue;
}else if(paramterType == HttpServletResponse.class){
parameValus[i] = resp;
continue;
}else if(paramterType == String.class){
Annotation[][] pa = method.getParameterAnnotations();
for (Annotation a : pa[i]){
if(a instanceof SZRequestParam){
String paramName = ((SZRequestParam) a).value();
if(!"".equals(paramName.trim())){
String value = Arrays.toString(paramsMap.get(paramName))
.replaceAll("\\[|\\]", "")
.replaceAll("\\s","," );
parameValus[i] = value;
}
}
}
/*for(int j =0 ;j < pa.length ;j++){
}*/
}
}
String beanName = toLowerFirstCase(method.getDeclaringClass().getSimpleName());
method.invoke(ioc.get(beanName),parameValus);
}
@Override
public void init(ServletConfig config) throws ServletException {
//1.加载配置文件
doLoadConfig(config.getInitParameter("contextConfigLocation"));
//2.扫描相关的类
doScanner(contextConfig.getProperty("scanPackage"));
//3.初始化扫描到的类,并且放入到IOC容器中
doInstance();
//4.完成自动化的依赖注入
doAutowired();
//5.初始化HandlerMapping
doInitHandlerMapping();
System.out.println("SZ Spring framework is init.");
}
//初始化url和method的一对一对应关系
private void doInitHandlerMapping() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry<String,Object> entry : ioc.entrySet()){
Class<?> clazz = entry.getValue().getClass();
if(!clazz.isAnnotationPresent(SZController.class)){
continue;
}
//保存写在类上面的@GPRequestMapping("/demo")
String baseUrl = "";
if(clazz.isAnnotationPresent(SZRequestMapping.class)){
SZRequestMapping requestMapping = clazz.getAnnotation(SZRequestMapping.class);
baseUrl = requestMapping.value();
}
//默认获取所有的public方法
for(Method method : clazz.getMethods()){
if(!method.isAnnotationPresent(SZRequestMapping.class)){
continue;
}
SZRequestMapping requestMapping = method.getAnnotation(SZRequestMapping.class);
String url = ("/" + baseUrl + "/" + requestMapping.value()).replaceAll("/+","/" );
handleMapping.put(url,method );
System.out.println("Mapped " + url + "," + method);
}
}
}
private void doAutowired() {
if(ioc.isEmpty()){
return;
}
for (Map.Entry<String,Object> entry : ioc.entrySet()){
Field[] fields = entry.getValue().getClass().getDeclaredFields();
for (Field field : fields){
if(!field.isAnnotationPresent(SZAutowired.class)){
continue;
}
SZAutowired autowired = field.getAnnotation(SZAutowired.class);
//如果用户没有自定义beanname ,默认就根据类型注入
String beanName = autowired.value().trim();
if("".equals(beanName)){
beanName = field.getType().getName();
}
//如果是public以外的修饰符,只要加了@Autowired注解,都要强制赋值
//暴力访问
field.setAccessible(true);
//反射调用
//给entry.getValue()这个对象的field字段,赋ioc.get(beanName)的值
try {
field.set(entry.getValue(),ioc.get(beanName) );
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
}
}
}
private void doInstance() {
if(classNames.isEmpty()){
return;
}
try {
for(String className : classNames){
Class<?> clazz = Class.forName(className);
//什么样的类才需要初始化呢
//加了注解的类,才初始化,怎么判断
//为了简化代码逻辑,主要体会设计思想,只举例@Controller @Service
if(clazz.isAnnotationPresent(SZController.class)){
Object instance = clazz.newInstance();
String beanName = toLowerFirstCase(clazz.getSimpleName());
ioc.put(beanName,instance );
} else if (clazz.isAnnotationPresent(SZService.class)) {
//1.默认根据beanName类名首字母小写
String beanName = toLowerFirstCase(clazz.getSimpleName());
//2.使用自定义的beanName
SZService service = clazz.getAnnotation(SZService.class);
if(!"".equals(service.value())){
beanName = service.value();
}
Object instance = clazz.newInstance();
ioc.put(beanName, instance);
//3.根据包名.类名作为beanName
for (Class<?> i : clazz.getInterfaces()){
if(ioc.containsKey(i.getName())){
throw new Exception("The beanName is exists!!");
}
//把接口的类型直接当成key了
ioc.put(i.getName(),instance );
}
}else {
continue;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
private String toLowerFirstCase(String simpleName) {
char[] chars = simpleName.toCharArray();
chars[0] += 32;
return String.valueOf(chars);
}
//扫描出相关的类
private void doScanner(String scanPackage) {
URL url = this.getClass().getClassLoader().getResource("/"+scanPackage.replaceAll("\\.","/" ));
//scanPackage= com.suzao.demo 存储的包路径
//转换为文件路径,实际上就是把 .替换成/
//classpath下不仅有.class 文件, .xml文件 .properties文件
File classPath = new File(url.getFile());
for (File file :classPath.listFiles()){
if(file.isDirectory()){
doScanner(scanPackage + "." + file.getName());
}else {
//变成包名.类名
//Class.forname()
if(!file.getName().endsWith(".class")){
continue;
}
classNames.add(scanPackage + "." + file.getName().replace(".class","" ));
}
}
}
//加载配置文件
private void doLoadConfig(String contextConfigLocation) {
//直接从类路径下找到Spring主配置文件所在的路径
//并且将其读取出来放到Properties对象中
//相对于scanPackage=com.suzao.demo从文件中保存到内存中
InputStream is = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
try {
contextConfig.load(is);
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
转载
作者:程序员技术圈
链接:https://www.jianshu.com/p/2c30f3e6b664
网友评论