前言

在上一篇《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)的更多相关文章

  1. Spring学习之——手写Mini版Spring源码

    前言 Sping的生态圈已经非常大了,很多时候对Spring的理解都是在会用的阶段,想要理解其设计思想却无从下手.前些天看了某某学院的关于Spring学习的相关视频,有几篇讲到手写Spring源码,感 ...

  2. Spring源码 20 手写模拟源码

    参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...

  3. 手写Redux-Saga源码

    上一篇文章我们分析了Redux-Thunk的源码,可以看到他的代码非常简单,只是让dispatch可以处理函数类型的action,其作者也承认对于复杂场景,Redux-Thunk并不适用,还推荐了Re ...

  4. 手写koa-static源码,深入理解静态服务器原理

    这篇文章继续前面的Koa源码系列,这个系列已经有两篇文章了: 第一篇讲解了Koa的核心架构和源码:手写Koa.js源码 第二篇讲解了@koa/router的架构和源码:手写@koa/router源码 ...

  5. 手写Tomcat源码

    http://search.bilibili.com/all?keyword=%E6%89%8B%E5%86%99Tomcat%E6%BA%90%E7%A0%81 tomcat源码分析一:https: ...

  6. 织梦dedecms红黑配图片模板源码v2.0

    dedecms红黑配风格美女图片站是采用dedecms程序搭建的图片网站源码,网站感觉很大气,简约但是不简单,适合做图片网站.网站模板是收集其他网站的模板,感谢原网站提供者.在安装过程中出现问题,现已 ...

  7. 手写Vuex源码

    Vuex原理解析 Vuex是基于Vue的响应式原理基础,所以无法拿出来单独使用,必须在Vue的基础之上使用. 1.Vuex使用相关解析 main.js   import store form './s ...

  8. Spring系列28:@Transactional事务源码分析

    本文内容 @Transactional事务使用 @EnableTransactionManagement 详解 @Transactional事务属性的解析 TransactionInterceptor ...

  9. 从零开始手写 spring ioc 框架,深入学习 spring 源码

    IoC Ioc 是一款 spring ioc 核心功能简化实现版本,便于学习和理解原理. 创作目的 使用 spring 很长时间,对于 spring 使用非常频繁,实际上对于源码一直没有静下心来学习过 ...

随机推荐

  1. 04 drf源码剖析之版本

    04 drf源码剖析之版本 目录 04 drf源码剖析之版本 1. 版本简述 2. 版本使用 3.源码剖析 4. 总结 1. 版本简述 API版本控制使您可以更改不同客户端之间的行为.REST框架提供 ...

  2. Drf06 /drf总结

    Drf06 /drf总结 目录 Drf06 /drf总结 1. restful规范 2. drf组件认证的实现过程? 3. drf组件中权限的实现过程? 4. drf组件中节流的实现方式? 5. 什么 ...

  3. hihoCoder 1062 最近公共祖先·一 最详细的解题报告

    题目来源:最近公共祖先·一 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 题目描述 小Ho最近发现了一个神奇的网站!虽然还不够像58同城那样神奇,但这个网站仍然让小Ho乐在其 ...

  4. Spring Boot中Tomcat是怎么启动的

    Spring Boot一个非常突出的优点就是不需要我们额外再部署Servlet容器,它内置了多种容器的支持.我们可以通过配置来指定我们需要的容器. 本文以我们平时最常使用的容器Tomcat为列来介绍以 ...

  5. bzoj2456mode

    bzoj2456mode 题意: 给你一个n个数的数列,求出现次数超过n div 2的数(只有1个). 题解: 注意空间只有1M,显然不能开数组.用两个变量,一个存“当前数”,另一个存“当前数”的个数 ...

  6. Bash 脚本编程

    概述 Bash (GNU Bourne-Again Shell) 是许多Linux发行版的默认Shell. shell语法 变量 定义:your_name="hellohhy" 使 ...

  7. 并发编程AQS--------ReentrantLock

    同步框架AbstractQueuedSynchronizer Java并发编程核心在于java.concurrent.util包 而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列. ...

  8. 高效C++:序

    C++的语法全而复杂,如何简洁高效的使用C++的各种语法,是一个值得研究的问题,特别是对于刚入门或是有小几年开发经历的同学,了解或是熟悉这个问题,所得到的提升无疑是巨大的.向前人学习,站在巨人的肩膀上 ...

  9. EF Code 如何输出sql语句

    首先写拷贝下面类 public class EFLoggerProvider : ILoggerProvider { public ILogger CreateLogger(string catego ...

  10. js用正则表达式查找中文

    var content = "awfjawf测试wfewef"; var ret = /\p{Unified_Ideograph}+/u.exec(content);