how2j.cn

-->
下载区
文件名 文件大小
请先登录 13m
增值内容 13m
13m
请先登录 1m
增值内容 1m
1m

解压rar如果失败,请用5.21版本或者更高版本的winrar

点击下载 winrar5.21

3分59秒
本视频采用html5方式播放,如无法正常播放,请将浏览器升级至最新版本,推荐火狐,chrome,360浏览器。 如果装有迅雷,播放视频呈现直接下载状态,请调整 迅雷系统设置-基本设置-启动-监视全部浏览器 (去掉这个选项)。 chrome 的 视频下载插件会影响播放,如 IDM 等,请关闭或者切换其他浏览器

步骤 1 : 拓扑图点亮   
步骤 2 : Context 监听器   
步骤 3 : javaweb 中的 ContextListener   
步骤 4 : web.xml   
步骤 5 : Context   
步骤 6 : 测试   
步骤 7 : 比较可运行项目,快速定位问题   

增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
拓扑图点亮
步骤 2 :

Context 监听器

edit
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
步骤 3 :

javaweb 中的 ContextListener

edit
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
package cn.how2j.javaweb; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; public class ContextListener implements ServletContextListener { @Override public void contextDestroyed(ServletContextEvent e) { System.out.println("监听到web 应用 "+ e.getSource() +" 的销毁事件 "); } @Override public void contextInitialized(ServletContextEvent e) { System.out.println("监听到web 应用 "+ e.getSource() +" 的初始化事件 "); } }
package cn.how2j.javaweb;

import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
 
public class ContextListener implements ServletContextListener {
 
    @Override
    public void contextDestroyed(ServletContextEvent e) {
        System.out.println("监听到web 应用 "+ e.getSource() +" 的销毁事件  ");
    }
 
    @Override
    public void contextInitialized(ServletContextEvent e) {
        System.out.println("监听到web 应用 "+ e.getSource() +" 的初始化事件  ");
    }
}
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>cn.how2j.javaweb.HelloServlet</servlet-class> <init-param> <param-name>author</param-name> <param-value>how2j(servlet)</param-value> </init-param> <init-param> <param-name>site</param-name> <param-value>http://how2j.cn(servlet)</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <servlet> <servlet-name>ParamServlet</servlet-name> <servlet-class>cn.how2j.javaweb.ParamServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ParamServlet</servlet-name> <url-pattern>/param</url-pattern> </servlet-mapping> <servlet> <servlet-name>HeaderServlet</servlet-name> <servlet-class>cn.how2j.javaweb.HeaderServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HeaderServlet</servlet-name> <url-pattern>/header</url-pattern> </servlet-mapping> <servlet> <servlet-name>SetCookieServlet</servlet-name> <servlet-class>cn.how2j.javaweb.SetCookieServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>SetCookieServlet</servlet-name> <url-pattern>/setCookie</url-pattern> </servlet-mapping> <servlet> <servlet-name>GetCookieServlet</servlet-name> <servlet-class>cn.how2j.javaweb.GetCookieServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>GetCookieServlet</servlet-name> <url-pattern>/getCookie</url-pattern> </servlet-mapping> <servlet> <servlet-name>SetSessionServlet</servlet-name> <servlet-class>cn.how2j.javaweb.SetSessionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>SetSessionServlet</servlet-name> <url-pattern>/setSession</url-pattern> </servlet-mapping> <servlet> <servlet-name>GetSessionServlet</servlet-name> <servlet-class>cn.how2j.javaweb.GetSessionServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>GetSessionServlet</servlet-name> <url-pattern>/getSession</url-pattern> </servlet-mapping> <servlet> <servlet-name>ClientJumpServlet</servlet-name> <servlet-class>cn.how2j.javaweb.ClientJumpServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ClientJumpServlet</servlet-name> <url-pattern>/jump1</url-pattern> </servlet-mapping> <servlet> <servlet-name>ServerJumpServlet</servlet-name> <servlet-class>cn.how2j.javaweb.ServerJumpServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>ServerJumpServlet</servlet-name> <url-pattern>/jump2</url-pattern> </servlet-mapping> <filter> <filter-name>URLFilter</filter-name> <filter-class>cn.how2j.javaweb.URLFilter</filter-class> </filter> <filter-mapping> <filter-name>URLFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>PfmFilter</filter-name> <filter-class>cn.how2j.javaweb.PfmFilter</filter-class> <init-param> <param-name>author</param-name> <param-value>how2j(filter)</param-value> </init-param> <init-param> <param-name>site</param-name> <param-value>http://how2j.cn(filter)</param-value> </init-param> </filter> <filter-mapping> <filter-name>PfmFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <listener> <listener-class>cn.how2j.javaweb.ContextListener</listener-class> </listener> </web-app>
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
package cn.how2j.diytomcat.catalina; import cn.how2j.diytomcat.classloader.WebappClassLoader; import cn.how2j.diytomcat.exception.WebConfigDuplicatedException; import cn.how2j.diytomcat.http.ApplicationContext; import cn.how2j.diytomcat.http.StandardServletConfig; import cn.how2j.diytomcat.util.ContextXMLUtil; import cn.how2j.diytomcat.watcher.ContextFileChangeWatcher; import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.TimeInterval; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.log.LogFactory; import org.apache.jasper.compiler.JspRuntimeContext; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import javax.servlet.*; import javax.servlet.http.HttpServlet; import java.io.File; import java.util.*; import org.apache.jasper.JspC; public class Context { private String path; private String docBase; private File contextWebXmlFile; private Map<String, String> url_servletClassName; private Map<String, String> url_ServletName; private Map<String, String> servletName_className; private Map<String, String> className_servletName; private Map<String, Map<String, String>> servlet_className_init_params; private Map<String, List<String>> url_filterClassName; private Map<String, List<String>> url_FilterNames; private Map<String, String> filterName_className; private Map<String, String> className_filterName; private Map<String, Map<String, String>> filter_className_init_params; private List<String> loadOnStartupServletClassNames; private WebappClassLoader webappClassLoader; private Host host; private boolean reloadable; private ContextFileChangeWatcher contextFileChangeWatcher; private ServletContext servletContext; private Map<Class<?>, HttpServlet> servletPool; private Map<String, Filter> filterPool; private List<ServletContextListener> listeners; public Context(String path, String docBase, Host host, boolean reloadable) { TimeInterval timeInterval = DateUtil.timer(); this.host = host; this.reloadable = reloadable; this.path = path; this.docBase = docBase; this.contextWebXmlFile = new File(docBase, ContextXMLUtil.getWatchedResource()); this.url_servletClassName = new HashMap<>(); this.url_ServletName = new HashMap<>(); this.servletName_className = new HashMap<>(); this.className_servletName = new HashMap<>(); this.servlet_className_init_params = new HashMap<>(); this.url_filterClassName = new HashMap<>(); this.url_FilterNames = new HashMap<>(); this.filterName_className = new HashMap<>(); this.className_filterName = new HashMap<>(); this.filter_className_init_params = new HashMap<>(); this.loadOnStartupServletClassNames = new ArrayList<>(); this.servletContext = new ApplicationContext(this); ClassLoader commonClassLoader = Thread.currentThread().getContextClassLoader(); this.webappClassLoader = new WebappClassLoader(docBase, commonClassLoader); this.servletPool = new HashMap<>(); this.filterPool = new HashMap<>(); listeners=new ArrayList<ServletContextListener>(); LogFactory.get().info("Deploying web application directory {}", this.docBase); deploy(); LogFactory.get().info("Deployment of web application directory {} has finished in {} ms", this.docBase,timeInterval.intervalMs()); } public void reload() { host.reload(this); } private void deploy() { loadListeners(); init(); if(reloadable){ contextFileChangeWatcher = new ContextFileChangeWatcher(this); contextFileChangeWatcher.start(); } JspC c = new JspC(); new JspRuntimeContext(servletContext, c); } private void init() { if (!contextWebXmlFile.exists()) return; try { checkDuplicated(); } catch (WebConfigDuplicatedException e) { // TODO Auto-generated catch block e.printStackTrace(); return; } String xml = FileUtil.readUtf8String(contextWebXmlFile); Document d = Jsoup.parse(xml); parseServletMapping(d); parseFilterMapping(d); parseServletInitParams(d); parseFilterInitParams(d); initFilter(); parseLoadOnStartup(d); handleLoadOnStartup(); fireEvent("init"); } private void parseServletMapping(Document d) { // url_ServletName Elements mappingurlElements = d.select("servlet-mapping url-pattern"); for (Element mappingurlElement : mappingurlElements) { String urlPattern = mappingurlElement.text(); String servletName = mappingurlElement.parent().select("servlet-name").first().text(); url_ServletName.put(urlPattern, servletName); } // servletName_className / className_servletName Elements servletNameElements = d.select("servlet servlet-name"); for (Element servletNameElement : servletNameElements) { String servletName = servletNameElement.text(); String servletClass = servletNameElement.parent().select("servlet-class").first().text(); servletName_className.put(servletName, servletClass); className_servletName.put(servletClass, servletName); } // url_servletClassName Set<String> urls = url_ServletName.keySet(); for (String url : urls) { String servletName = url_ServletName.get(url); String servletClassName = servletName_className.get(servletName); url_servletClassName.put(url, servletClassName); } } private void checkDuplicated(Document d, String mapping, String desc) throws WebConfigDuplicatedException { Elements elements = d.select(mapping); // 判断逻辑是放入一个集合,然后把集合排序之后看两临两个元素是否相同 List<String> contents = new ArrayList<>(); for (Element e : elements) { contents.add(e.text()); } Collections.sort(contents); for (int i = 0; i < contents.size() - 1; i++) { String contentPre = contents.get(i); String contentNext = contents.get(i + 1); if (contentPre.equals(contentNext)) { throw new WebConfigDuplicatedException(StrUtil.format(desc, contentPre)); } } } private void checkDuplicated() throws WebConfigDuplicatedException { String xml = FileUtil.readUtf8String(contextWebXmlFile); Document d = Jsoup.parse(xml); checkDuplicated(d, "servlet-mapping url-pattern", "servlet url 重复,请保持其唯一性:{} "); checkDuplicated(d, "servlet servlet-name", "servlet 名称重复,请保持其唯一性:{} "); checkDuplicated(d, "servlet servlet-class", "servlet 类名重复,请保持其唯一性:{} "); } public String getServletClassName(String uri) { return url_servletClassName.get(uri); } public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getDocBase() { return docBase; } public void setDocBase(String docBase) { this.docBase = docBase; } public WebappClassLoader getWebappClassLoader() { return webappClassLoader; } public void stop() { webappClassLoader.stop(); contextFileChangeWatcher.stop(); destroyServlets(); fireEvent("destroy"); } public boolean isReloadable() { return reloadable; } public void setReloadable(boolean reloadable) { this.reloadable = reloadable; } public ServletContext getServletContext() { return servletContext; } public synchronized HttpServlet getServlet(Class<?> clazz) throws InstantiationException, IllegalAccessException, ServletException { HttpServlet servlet = servletPool.get(clazz); if (null == servlet) { servlet = (HttpServlet) clazz.newInstance(); ServletContext servletContext = this.getServletContext(); String className = clazz.getName(); String servletName = className_servletName.get(className); Map<String, String> initParameters = servlet_className_init_params.get(className); ServletConfig servletConfig = new StandardServletConfig(servletContext, servletName, initParameters); servlet.init(servletConfig); servletPool.put(clazz, servlet); } return servlet; } private void parseServletInitParams(Document d) { Elements servletClassNameElements = d.select("servlet-class"); for (Element servletClassNameElement : servletClassNameElements) { String servletClassName = servletClassNameElement.text(); Elements initElements = servletClassNameElement.parent().select("init-param"); if (initElements.isEmpty()) continue; Map<String, String> initParams = new HashMap<>(); for (Element element : initElements) { String name = element.select("param-name").get(0).text(); String value = element.select("param-value").get(0).text(); initParams.put(name, value); } servlet_className_init_params.put(servletClassName, initParams); } // System.out.println("class_name_init_params:" + servlet_className_init_params); } private void destroyServlets() { Collection<HttpServlet> servlets = servletPool.values(); for (HttpServlet servlet : servlets) { servlet.destroy(); } } public void parseLoadOnStartup(Document d) { Elements es = d.select("load-on-startup"); for (Element e : es) { String loadOnStartupServletClassName = e.parent().select("servlet-class").text(); loadOnStartupServletClassNames.add(loadOnStartupServletClassName); } } public void handleLoadOnStartup() { for (String loadOnStartupServletClassName : loadOnStartupServletClassNames) { try { Class<?> clazz = webappClassLoader.loadClass(loadOnStartupServletClassName); getServlet(clazz); } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ServletException e) { e.printStackTrace(); } } } public WebappClassLoader getWebClassLoader() { return webappClassLoader; } public void parseFilterMapping(Document d) { // filter_url_name Elements mappingurlElements = d.select("filter-mapping url-pattern"); for (Element mappingurlElement : mappingurlElements) { String urlPattern = mappingurlElement.text(); String filterName = mappingurlElement.parent().select("filter-name").first().text(); List<String> filterNames= url_FilterNames.get(urlPattern); if(null==filterNames) { filterNames = new ArrayList<>(); url_FilterNames.put(urlPattern, filterNames); } filterNames.add(filterName); } // class_name_filter_name Elements filterNameElements = d.select("filter filter-name"); for (Element filterNameElement : filterNameElements) { String filterName = filterNameElement.text(); String filterClass = filterNameElement.parent().select("filter-class").first().text(); filterName_className.put(filterName, filterClass); className_filterName.put(filterClass, filterName); } // url_filterClassName Set<String> urls = url_FilterNames.keySet(); for (String url : urls) { List<String> filterNames = url_FilterNames.get(url); if(null == filterNames) { filterNames = new ArrayList<>(); url_FilterNames.put(url, filterNames); } for (String filterName : filterNames) { String filterClassName = filterName_className.get(filterName); List<String> filterClassNames = url_filterClassName.get(url); if(null==filterClassNames) { filterClassNames = new ArrayList<>(); url_filterClassName.put(url, filterClassNames); } filterClassNames.add(filterClassName); } } } private void parseFilterInitParams(Document d) { Elements filterClassNameElements = d.select("filter-class"); for (Element filterClassNameElement : filterClassNameElements) { String filterClassName = filterClassNameElement.text(); Elements initElements = filterClassNameElement.parent().select("init-param"); if (initElements.isEmpty()) continue; Map<String, String> initParams = new HashMap<>(); for (Element element : initElements) { String name = element.select("param-name").get(0).text(); String value = element.select("param-value").get(0).text(); initParams.put(name, value); } filter_className_init_params.put(filterClassName, initParams); } } private void initFilter() { Set<String> classNames = className_filterName.keySet(); for (String className : classNames) { try { Class clazz = this.getWebClassLoader().loadClass(className); Map<String,String> initParameters = filter_className_init_params.get(className); String filterName = className_filterName.get(className); FilterConfig filterConfig = new StandardFilterConfig(servletContext, filterName, initParameters); Filter filter = filterPool.get(clazz); if(null==filter) { filter = (Filter) ReflectUtil.newInstance(clazz); filter.init(filterConfig); filterPool.put(className, filter); } } catch (Exception e) { throw new RuntimeException(e); } } } public List<Filter> getMatchedFilters(String uri) { List<Filter> filters = new ArrayList<>(); Set<String> patterns = url_filterClassName.keySet(); Set<String> matchedPatterns = new HashSet<>(); for (String pattern : patterns) { if(match(pattern,uri)) { matchedPatterns.add(pattern); } } Set<String> matchedFilterClassNames = new HashSet<>(); for (String pattern : matchedPatterns) { List<String> filterClassName = url_filterClassName.get(pattern); matchedFilterClassNames.addAll(filterClassName); } for (String filterClassName : matchedFilterClassNames) { Filter filter = filterPool.get(filterClassName); filters.add(filter); } return filters; } private boolean match(String pattern, String uri) { // 完全匹配 if(StrUtil.equals(pattern, uri)) return true; // /* 模式 if(StrUtil.equals(pattern, "/*")) return true; // 后缀名 /*.jsp if(StrUtil.startWith(pattern, "/*.")) { String patternExtName = StrUtil.subAfter(pattern, '.', false); String uriExtName = StrUtil.subAfter(uri, '.', false); if(StrUtil.equals(patternExtName, uriExtName)) return true; } // 其他模式就懒得管了 return false; } public void addListener(ServletContextListener listener){ listeners.add(listener); } public void removeListener(ServletContextListener listener){ listeners.remove(listener); } private void loadListeners() { try { if(!contextWebXmlFile.exists()) return; String xml = FileUtil.readUtf8String(contextWebXmlFile); Document d = Jsoup.parse(xml); Elements es = d.select("listener listener-class"); for (Element e : es) { String listenerClassName = e.text(); Class<?> clazz= this.getWebClassLoader().loadClass(listenerClassName); ServletContextListener listener = (ServletContextListener) clazz.newInstance(); addListener(listener); } } catch (IORuntimeException | ClassNotFoundException | InstantiationException | IllegalAccessException e) { throw new RuntimeException(e); } } private void fireEvent(String type) { ServletContextEvent event = new ServletContextEvent(servletContext); for (ServletContextListener servletContextListener : listeners) { if("init".equals(type)) servletContextListener.contextInitialized(event); if("destroy".equals(type)) servletContextListener.contextDestroyed(event); } } }
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢
测试
步骤 7 :

比较可运行项目,快速定位问题

edit
增值内容,请先登录
自己写一个Tomcat, 几乎使用到了除开框架外的所有Java 技术,如多线程,Socket, J2EE, 反射,Log4j, JSoup, JUnit, Html 等一整套技术栈, 从无到有,循序渐进涵盖全部74个知识点,549个开发步骤, 为竞争高薪资职位加上一个有吸引力的砝码.
增值内容,点击购买
使用爬虫已经被系统记录,请勿使用爬虫,增大封号风险。 如果是误封 ,请联系站长,谢谢


HOW2J公众号,关注后实时获知最新的教程和优惠活动,谢谢。


问答区域    
2021-07-25 结束了!!期待站长新的产品!!!
凉蛋

总的来说,体验不错。结合着其他视频一起学习,收获颇多!!感谢站长




1 个答案

how2j
答案时间:2021-07-27
加油!



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2021-07-18 首先,得提醒,不重新编译javaweb是出现不了预期效果的,其次,追码完结啦,不看视频纯看文字还是很刺激的!
chuancey

首先,得提醒,不重新编译javaweb是出现不了预期效果的,其次,追码完结啦,不看视频纯看文字还是很刺激的!




1 个答案

how2j
答案时间:2021-07-27
是的,需要编译生成 .class才行的。 不看视频。。。你厉害!



回答已经提交成功,正在审核。 请于 我的回答 处查看回答记录,谢谢
答案 或者 代码至少填写一项, 如果是自己有问题,请重新提问,否则站长有可能看不到




2020-07-02 热加载过程中出现ClassNotFound的问题
2020-06-28 完结撒花,hutool里面那个文件改变监听器watcher怎么实现的有待研究
2020-06-15 单独rebuild某一个java类没问题,如果rebuild整个javaweb,就会报错


提问太多,页面渲染太慢,为了加快渲染速度,本页最多只显示几条提问。还有 1 条以前的提问,请 点击查看

提问之前请登陆
提问已经提交成功,正在审核。 请于 我的提问 处查看提问记录,谢谢
关于 实践项目-DiyTomcat-Context 监听器 的提问

尽量提供截图代码异常信息,有助于分析和解决问题。 也可进本站QQ群交流: 578362961
提问尽量提供完整的代码,环境描述,越是有利于问题的重现,您的问题越能更快得到解答。
对教程中代码有疑问,请提供是哪个步骤,哪一行有疑问,这样便于快速定位问题,提高问题得到解答的速度
在已经存在的几千个提问里,有相当大的比例,是因为使用了和站长不同版本的开发环境导致的,比如 jdk, eclpise, idea, mysql,tomcat 等等软件的版本不一致。
请使用和站长一样的版本,可以节约自己大量的学习时间。 站长把教学中用的软件版本整理了,都统一放在了这里, 方便大家下载: https://how2j.cn/k/helloworld/helloworld-version/1718.html

上传截图