Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)
前言
在上一篇《Spring学习之——手写Spring源码(V1.0)》中,我实现了一个Mini版本的Spring框架,在这几天,博主又看了不少关于Spring源码解析的视频,受益匪浅,也对Spring的各组件有了自己的理解和认识,于是乎,在空闲时间把之前手写Spring的代码重构了一遍,遵循了单一职责的原则,使结构更清晰,并且实现了AOP,这次还是只引用一个servlet包,其他全部手写实现。
全部源码照旧放在文章末尾~
开发工具
环境:jdk8 + IDEA + maven
jar包:javax.servlet-2.5
项目结构
具体实现
配置文件
web.xml 与之前一样 并无改变
application.properties 增加了html页面路径和AOP的相关配置
#扫描路径#
scanPackage=com.wqfrw #模板引擎路径#
templateRoot=template #切面表达式#
pointCut=public .* com.wqfrw.service.impl..*ServiceImpl..*(.*)
#切面类#
aspectClass=com.wqfrw.aspect.LogAspect
#切面前置通知#
aspectBefore=before
#切面后置通知#
aspectAfter=after
#切面异常通知#
aspectAfterThrowing=afterThrowing
#切面异常类型#
aspectAfterThrowingName=java.lang.Exception
IOC与DI实现
1.在DispatcherServlet的init方法中初始化ApplicationContent;
2.ApplicationContent是Spring容器的主入口,通过创建BeanDefintionReader对象加载配置文件;
3.在BeanDefintionReader中将扫描到的类解析成BeanDefintion返回;
4.ApplicationContent中通过BeanDefintionMap这个缓存来关联BeanName与BeanDefintion对象之间的关系;
5.通过getBean方法,进行Bean的创建并封装为BeanWrapper对象,进行依赖注入,缓存到IoC容器中
/**
* 功能描述: 初始化MyApplicationContext
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月03日 18:54:01
* @param configLocations
* @return:
**/
public MyApplicationContext(String... configLocations) {
this.configLocations = configLocations; try {
//1.读取配置文件并解析BeanDefinition对象
beanDefinitionReader = new MyBeanDefinitionReader(configLocations);
List<MyBeanDefinition> beanDefinitionList = beanDefinitionReader.loadBeanDefinitions(); //2.将解析后的BeanDefinition对象注册到beanDefinitionMap中
doRegisterBeanDefinition(beanDefinitionList); //3.触发创建对象的动作,调用getBean()方法(Spring默认是延时加载)
doCreateBean();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 功能描述: 真正触发IoC和DI的动作 1.创建Bean 2.依赖注入
*
* @param beanName
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月03日 19:48:58
* @return: java.lang.Object
**/
public Object getBean(String beanName) {
//============ 创建实例 ============ //1.获取配置信息,只要拿到beanDefinition对象即可
MyBeanDefinition beanDefinition = beanDefinitionMap.get(beanName); //用反射创建实例 这个实例有可能是代理对象 也有可能是原生对象 封装成BeanWrapper统一处理
Object instance = instantiateBean(beanName, beanDefinition);
MyBeanWrapper beanWrapper = new MyBeanWrapper(instance); factoryBeanInstanceCache.put(beanName, beanWrapper); //============ 依赖注入 ============
populateBean(beanName, beanDefinition, beanWrapper); return beanWrapper.getWrapperInstance();
}
/**
* 功能描述: 依赖注入
*
* @param beanName
* @param beanDefinition
* @param beanWrapper
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月03日 20:09:01
* @return: void
**/
private void populateBean(String beanName, MyBeanDefinition beanDefinition, MyBeanWrapper beanWrapper) {
Object instance = beanWrapper.getWrapperInstance();
Class<?> clazz = beanWrapper.getWrapperClass(); //只有加了注解的类才需要依赖注入
if (!(clazz.isAnnotationPresent(MyController.class) || clazz.isAnnotationPresent(MyService.class))) {
return;
} //拿到bean所有的字段 包括private、public、protected、default
for (Field field : clazz.getDeclaredFields()) { //如果没加MyAutowired注解的属性则直接跳过
if (!field.isAnnotationPresent(MyAutowired.class)) {
continue;
} MyAutowired annotation = field.getAnnotation(MyAutowired.class);
String autowiredBeanName = annotation.value().trim();
if ("".equals(autowiredBeanName)) {
autowiredBeanName = field.getType().getName();
}
//强制访问
field.setAccessible(true);
try {
if (factoryBeanInstanceCache.get(autowiredBeanName) == null) { continue; }
//赋值
field.set(instance, this.factoryBeanInstanceCache.get(autowiredBeanName).getWrapperInstance());
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
MVC实现
1.在DispatcherServlet的init方法中调用initStrategies方法初始化九大核心组件;
2.通过循环BeanDefintionMap拿到每个接口的url、实例对象、对应方法封装成一个HandlerMapping对象的集合,并建立HandlerMapping与HandlerAdapter(参数适配器)的关联;
3.初始化ViewResolver(视图解析器),解析配置文件中模板文件路径(即html文件的路径,其作用类似于BeanDefintionReader);
4.在运行阶段,调用doDispatch方法,根据请求的url找到对应的HandlerMapping;
5.在HandlerMapping对应的HandlerAdapter中,调用handle方法,进行参数动态赋值,反射调用接口方法,拿到返回值与返回页面封装成一个MyModelAndView对象返回;
6.通过ViewResolver拿到View(模板页面文件),在View中通过render方法,通过正则将返回值与页面取值符号进行适配替换,渲染成html页面返回
/**
* 功能描述: 初始化核心组件 在Spring中有九大核心组件,这里只实现三种
*
* @param context
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月04日 11:51:55
* @return: void
**/
protected void initStrategies(MyApplicationContext context) {
//多文件上传组件
//initMultipartResolver(context);
//初始化本地语言环境
//initLocaleResolver(context);
//初始化模板处理器
//initThemeResolver(context);
//初始化请求分发处理器
initHandlerMappings(context);
//初始化参数适配器
initHandlerAdapters(context);
//初始化异常拦截器
//initHandlerExceptionResolvers(context);
//初始化视图预处理器
//initRequestToViewNameTranslator(context);
//初始化视图转换器
initViewResolvers(context);
//缓存管理器(值栈)
//initFlashMapManager(context);
}
/**
* 功能描述: 进行参数适配
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 19:41:38
* @param req
* @param resp
* @param mappedHandler
* @return: com.framework.webmvc.servlet.MyModelAndView
**/
public MyModelAndView handle(HttpServletRequest req, HttpServletResponse resp, MyHandlerMapping mappedHandler) throws Exception { //保存参数的名称和位置
Map<String, Integer> paramIndexMapping = new HashMap<>(); //获取这个方法所有形参的注解 因一个参数可以添加多个注解 所以是一个二维数组
Annotation[][] pa = mappedHandler.getMethod().getParameterAnnotations(); /**
* 获取加了MyRequestParam注解的参数名和位置 放入到paramIndexMapping中
*/
for (int i = 0; i < pa.length; i++) {
for (Annotation annotation : pa[i]) {
if (!(annotation instanceof MyRequestParam)) {
continue;
}
String paramName = ((MyRequestParam) annotation).value();
if (!"".equals(paramName.trim())) {
paramIndexMapping.put(paramName, i);
}
}
} //方法的形参列表
Class<?>[] parameterTypes = mappedHandler.getMethod().getParameterTypes(); /**
* 获取request和response的位置(如果有的话) 放入到paramIndexMapping中
*/
for (int i = 0; i < parameterTypes.length; i++) {
Class<?> parameterType = parameterTypes[i];
if (parameterType == HttpServletRequest.class || parameterType == HttpServletResponse.class) {
paramIndexMapping.put(parameterType.getName(), i);
}
} //拿到一个请求所有传入的实际实参 因为一个url上可以多个相同的name,所以此Map的结构为一个name对应一个value[]
//例如:request中的参数t1=1&t1=2&t2=3形成的map结构:
//key=t1;value[0]=1,value[1]=2
//key=t2;value[0]=3
Map<String, String[]> paramsMap = req.getParameterMap(); //自定义初始实参列表(反射调用Controller方法时使用)
Object[] paramValues = new Object[parameterTypes.length]; /**
* 从paramIndexMapping中取出参数名与位置 动态赋值
*/
for (Map.Entry<String, String[]> entry : paramsMap.entrySet()) {
//拿到请求传入的实参
String value = entry.getValue()[0]; //如果包含url参数上的key 则动态转型赋值
if (paramIndexMapping.containsKey(entry.getKey())) {
//获取这个实参的位置
int index = paramIndexMapping.get(entry.getKey());
//动态转型并赋值
paramValues[index] = caseStringValue(value, parameterTypes[index]);
}
} /**
* request和response单独赋值
*/
if (paramIndexMapping.containsKey(HttpServletRequest.class.getName())) {
int index = paramIndexMapping.get(HttpServletRequest.class.getName());
paramValues[index] = req;
}
if (paramIndexMapping.containsKey(HttpServletResponse.class.getName())) {
int index = paramIndexMapping.get(HttpServletResponse.class.getName());
paramValues[index] = resp;
} //方法调用 拿到返回结果
Object result = mappedHandler.getMethod().invoke(mappedHandler.getController(), paramValues);
if (result == null || result instanceof Void) {
return null;
} else if (mappedHandler.getMethod().getReturnType() == MyModelAndView.class) {
return (MyModelAndView) result;
}
return null;
} /**
* 功能描述: 动态转型
*
* @param value String类型的value
* @param clazz 实际对象的class
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月04日 16:34:40
* @return: java.lang.Object 实际对象的实例
**/
private Object caseStringValue(String value, Class<?> clazz) throws Exception {
//通过class对象获取一个入参为String的构造方法 没有此方法则抛出异常
Constructor constructor = clazz.getConstructor(new Class[]{String.class});
//通过构造方法new一个实例返回
return constructor.newInstance(value);
}
/**
* 功能描述: 对页面内容进行渲染
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月04日 17:54:40
* @param model
* @param req
* @param resp
* @return: void
**/
public void render(Map<String, ?> model, HttpServletRequest req, HttpServletResponse resp) throws Exception {
StringBuilder sb = new StringBuilder();
//只读模式 读取文件
RandomAccessFile ra = new RandomAccessFile(this.viewFile, "r"); String line = null;
while ((line = ra.readLine()) != null) {
line = new String(line.getBytes("ISO-8859-1"), "utf-8"); //%{name}
Pattern pattern = Pattern.compile("%\\{[^\\}]+\\}", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(line); while (matcher.find()) {
String paramName = matcher.group(); paramName = paramName.replaceAll("%\\{|\\}", "");
Object paramValue = model.get(paramName);
line = matcher.replaceFirst(makeStringForRegExp(paramValue.toString()));
matcher = pattern.matcher(line);
}
sb.append(line);
} resp.setCharacterEncoding("utf-8");
resp.getWriter().write(sb.toString());
}
html页面
404.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>页面没有找到</title>
</head>
<body>
<font size="25" color="red">Exception Code : 404 Not Found</font>
<br><br><br>
@我恰芙蓉王
</body>
</html>
500.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>服务器崩溃</title>
</head>
<body>
<font size="25" color="red">Exception Code : 500 <br/> 服务器崩溃了~</font>
<br/>
<br/>
<b>Message:%{message}</b>
<br/>
<b>StackTrace:%{stackTrace}</b>
<br/>
<br><br><br>
@我恰芙蓉王
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>自定义SpringMVC模板引擎Demo</title>
</head>
<center>
<h1>大家好,我是%{name}</h1>
<h2>我爱%{food}</h2>
<font color="red">
<h2>时间:%{date}</h2>
</font>
<br><br><br>
@我恰芙蓉王
</center>
</html>
测试接口调用返回页面
404.html 接口未找到
500.html 服务器错误
index.html 正常返回页面
AOP实现
1.参照IOC与DI实现第五点,在对象实例化之后,依赖注入之前,将配置文件中AOP的配置解析至AopConfig中;
2.通过配置的pointCut参数,正则匹配此实例对象的类名与方法名,如果匹配上,将配置的三个通知方法(Advice)与此方法建立联系,生成一个 Map<Method, Map<String, MyAdvice>> methodCache 的缓存;
3.将原生对象、原生对象class、原生对象方法与通知方法的映射关系封装成AdviceSupport对象;
4.如果需要代理,则使用JdkDynamicAopProxy中getProxy方法,获得一个此原生对象的代理对象,并将原生对象覆盖;
5.JdkDynamicAopProxy实现了InvocationHandler接口(使用JDK的动态代理),重写invoke方法,在此方法中执行切面方法与原生对象方法。
/**
* 功能描述: 反射实例化对象
*
* @param beanName
* @param beanDefinition
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月03日 20:08:50
* @return: java.lang.Object
**/
private Object instantiateBean(String beanName, MyBeanDefinition beanDefinition) {
String className = beanDefinition.getBeanClassName(); Object instance = null;
try {
Class<?> clazz = Class.forName(className);
instance = clazz.newInstance(); /**
* ===========接入AOP begin===========
*/
MyAdviceSupport support = instantiateAopConfig(beanDefinition);
support.setTargetClass(clazz);
support.setTarget(instance);
//如果需要代理 则用代理对象覆盖目标对象
if (support.pointCutMatch()) {
instance = new MyJdkDynamicAopProxy(support).getProxy();
}
/**
* ===========接入AOP end===========
*/ factoryBeanObjectCache.put(beanName, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
}
/**
* 功能描述: 解析配置 pointCut
*
* @param
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 11:20:21
* @return: void
**/
private void parse() {
String pointCut = aopConfig.getPointCut()
.replaceAll("\\.", "\\\\.")
.replaceAll("\\\\.\\*", ".*")
.replaceAll("\\(", "\\\\(")
.replaceAll("\\)", "\\\\)"); //public .*.com.wqfrw.service..*impl..*(.*)
String pointCutForClassRegex = pointCut.substring(0, pointCut.lastIndexOf("\\(") - 4);
this.pointCutClassPattern = Pattern.compile(pointCutForClassRegex.substring(pointCutForClassRegex.lastIndexOf(" ") + 1)); methodCache = new HashMap<>();
//匹配方法的正则
Pattern pointCutPattern = Pattern.compile(pointCut); //1.对回调通知进行缓存
Map<String, Method> aspectMethods = new HashMap<>();
try {
//拿到切面类的class com.wqfrw.aspect.LogAspect
Class<?> aspectClass = Class.forName(this.aopConfig.getAspectClass());
//将切面类的通知方法缓存到aspectMethods
Stream.of(aspectClass.getMethods()).forEach(v -> aspectMethods.put(v.getName(), v)); //2.扫描目标类的方法,去循环匹配
for (Method method : targetClass.getMethods()) {
String methodString = method.toString();
//如果目标方法有抛出异常 则截取
if (methodString.contains("throws")) {
methodString = methodString.substring(0, methodString.lastIndexOf("throws")).trim();
}
/**
* 匹配目标类方法 如果匹配上,就将缓存好的通知与它建立联系 如果没匹配上,则忽略
*/
Matcher matcher = pointCutPattern.matcher(methodString);
if (matcher.matches()) {
Map<String, MyAdvice> adviceMap = new HashMap<>();
//前置通知
if (!(null == aopConfig.getAspectBefore() || "".equals(aopConfig.getAspectBefore()))) {
adviceMap.put("before", new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectBefore())));
} //后置通知
if (!(null == aopConfig.getAspectAfter() || "".equals(aopConfig.getAspectAfter()))) {
adviceMap.put("after", new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectAfter())));
} //异常通知
if (!(null == aopConfig.getAspectAfterThrowing() || "".equals(aopConfig.getAspectAfterThrowing()))) {
MyAdvice advice = new MyAdvice(aspectClass.newInstance(), aspectMethods.get(aopConfig.getAspectAfterThrowing()));
advice.setThrowingName(aopConfig.getAspectAfterThrowingName());
adviceMap.put("afterThrowing", advice);
}
//建立关联
methodCache.put(method, adviceMap);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 功能描述: 返回一个代理对象
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 14:17:22
* @param
* @return: java.lang.Object
**/
public Object getProxy() {
return Proxy.newProxyInstance(this.getClass().getClassLoader(), this.support.getTargetClass().getInterfaces(), this);
} /**
* 功能描述: 重写invoke
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 20:29:19
* @param proxy
* @param method
* @param args
* @return: java.lang.Object
**/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Map<String, MyAdvice> advices = support.getAdvices(method, support.getTargetClass()); Object result = null;
try {
//调用前置通知
invokeAdvice(advices.get("before")); //执行原生目标方法
result = method.invoke(support.getTarget(), args); //调用后置通知
invokeAdvice(advices.get("after"));
} catch (Exception e) {
//调用异常通知
invokeAdvice(advices.get("afterThrowing"));
throw e;
} return result;
} /**
* 功能描述: 执行切面方法
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 11:09:32
* @param advice
* @return: void
**/
private void invokeAdvice(MyAdvice advice) {
try {
advice.getAdviceMethod().invoke(advice.getAspect());
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* @ClassName LogAspect
* @Description TODO(切面类)
* @Author 我恰芙蓉王
* @Date 2020年08月05日 10:03
* @Version 2.0.0
**/ public class LogAspect { /**
* 功能描述: 前置通知
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 17:24:30
* @param
* @return: void
**/
public void before(){
System.err.println("=======前置通知=======");
} /**
* 功能描述: 后置通知
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 17:24:40
* @param
* @return: void
**/
public void after(){
System.err.println("=======后置通知=======\n");
} /**
* 功能描述: 异常通知
*
* @创建人: 我恰芙蓉王
* @创建时间: 2020年08月05日 17:24:47
* @param
* @return: void
**/
public void afterThrowing(){
System.err.println("=======出现异常=======");
}
}
执行结果
总结
以上只贴出了部分核心实现代码,有兴趣的童鞋可以下载源码调试,具体的注释我都在代码中写得很清楚。
代码已经提交至Git : https://github.com/wqfrw/HandWritingSpringV2.0
Spring学习之——手写Spring源码V2.0(实现IOC、D、MVC、AOP)的更多相关文章
- Spring学习之——手写Mini版Spring源码
前言 Sping的生态圈已经非常大了,很多时候对Spring的理解都是在会用的阶段,想要理解其设计思想却无从下手.前些天看了某某学院的关于Spring学习的相关视频,有几篇讲到手写Spring源码,感 ...
- Spring源码 20 手写模拟源码
参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...
- 手写Redux-Saga源码
上一篇文章我们分析了Redux-Thunk的源码,可以看到他的代码非常简单,只是让dispatch可以处理函数类型的action,其作者也承认对于复杂场景,Redux-Thunk并不适用,还推荐了Re ...
- 手写koa-static源码,深入理解静态服务器原理
这篇文章继续前面的Koa源码系列,这个系列已经有两篇文章了: 第一篇讲解了Koa的核心架构和源码:手写Koa.js源码 第二篇讲解了@koa/router的架构和源码:手写@koa/router源码 ...
- 手写Tomcat源码
http://search.bilibili.com/all?keyword=%E6%89%8B%E5%86%99Tomcat%E6%BA%90%E7%A0%81 tomcat源码分析一:https: ...
- 织梦dedecms红黑配图片模板源码v2.0
dedecms红黑配风格美女图片站是采用dedecms程序搭建的图片网站源码,网站感觉很大气,简约但是不简单,适合做图片网站.网站模板是收集其他网站的模板,感谢原网站提供者.在安装过程中出现问题,现已 ...
- 手写Vuex源码
Vuex原理解析 Vuex是基于Vue的响应式原理基础,所以无法拿出来单独使用,必须在Vue的基础之上使用. 1.Vuex使用相关解析 main.js import store form './s ...
- Spring系列28:@Transactional事务源码分析
本文内容 @Transactional事务使用 @EnableTransactionManagement 详解 @Transactional事务属性的解析 TransactionInterceptor ...
- 从零开始手写 spring ioc 框架,深入学习 spring 源码
IoC Ioc 是一款 spring ioc 核心功能简化实现版本,便于学习和理解原理. 创作目的 使用 spring 很长时间,对于 spring 使用非常频繁,实际上对于源码一直没有静下心来学习过 ...
随机推荐
- 08 Flask源码剖析之flask拓展点
08 Flask源码剖析之flask拓展点 1. 信号(源码) 信号,是在flask框架中为我们预留的钩子,让我们可以进行一些自定义操作. pip3 install blinker 2. 根据flas ...
- unity-编辑器快捷按键
效果图 代码 [MenuItem("Custom/Run _F1")] static void PlayToggle() { EditorApplication.isPlaying ...
- Crystal Reports --报表设计
完整的报表解决方案 数据访问—>报表设计—>报表管理—>与应用系统集成 一.规划报表 设计报表的准备工作 谁看报表? 报表的数据是什么?(页眉页脚的内容?是否需要分组?是否需要汇总? ...
- kubernetes系列(十七) - 通过helm安装dashboard详细教程
1. 前提条件 2. 配置https证书为secret 3. dashboard安装 3.1 helm拉取dashboard的chart 3.2 配置dashboard的chart包配置 3.3 he ...
- 机器学习 | SVD矩阵分解算法,对矩阵做拆分,然后呢?
本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是机器学习专题第28篇文章,我们来聊聊SVD算法. SVD的英文全称是Singular Value Decomposition,翻译过来 ...
- 【揭秘】阿里测试框架,各大CTO良心力荐
自动化测试因其节约成本.提高效率.减少手动干预等优势已经日渐成为测试人员的“潮流”,从业人员日益清楚地明白实现自动化框架是软件自动化项目成功的关键因素之一.本篇文章将从 什么是真正的自动化测试框架.自 ...
- 干货分享丨玩转物联网IoTDA服务系列五-智能家居煤气检测联动
摘要:该场景主要描述的是设备可以通过LWM2M协议与物联网平台进行交互,用户可以在控制台或通过应用侧接口创建设备联动规则,把设备上报的属性转发,通过物联网平台规则引擎转变成命令下发给其他指定设备. 场 ...
- json:server 本地搭建
做个记录, 第一步,我们新建一个文件夹. 第二步,打开文件夹,执行git,没有git可以下载一个.或者用命令行工具进入到这个文件夹! 第三步,初始化json 在git里执行npm init --ye ...
- 关于Type-C扩展坞干扰路由器交换机的解决方案
近期看到网友反馈Type-C扩展坞干扰交换机的问题,具体表现为USB Type-C扩展坞在同时插上网线和PD充电的情况下,引起路由器或交换机死机,导致局域网断开的情况.经实测分析,原因为部分电脑默认设 ...
- 修改docker中mysql登入密码(包括容器内和本地远程登入的密码)
查看docker中正在运行的容器 docker ps 进入MySQL 容器中 sudo docker exec -it cd800a1cd503 /bin/bash 在容器中: /etc/mysql/ ...