当前位置: 首页 > news >正文

做网站 提交源码 论坛wordpress单页留言聊天

做网站 提交源码 论坛,wordpress单页留言聊天,建设企业网站公积金,同一个地方做几个网站文章目录概要Spring启动以及扫描流程实现基础环境搭建扫描逻辑实现bean创建的简单实现依赖注入实现BeanNameAware回调实现初始化机制模拟实现BeanPostProcessor模拟实现AOP模拟实现概要 手写Spring启动以及扫描流程手写getBean流程手写Bean生命周期流程手写依赖注入流程手写Be… 文章目录概要Spring启动以及扫描流程实现基础环境搭建扫描逻辑实现bean创建的简单实现依赖注入实现BeanNameAware回调实现初始化机制模拟实现BeanPostProcessor模拟实现AOP模拟实现概要 手写Spring启动以及扫描流程手写getBean流程手写Bean生命周期流程手写依赖注入流程手写BeanPostProcessor机制手写Aop机制 Spring启动以及扫描流程实现 我们平时都是使用这两种方法获得spring容器上面的是通过加载类路径上的配置文件来获得容器。下面的方式和上面的原理相同只不过是通过注解的形式去实现我们传入的也是一个配置类的class文件我们可以把这个文件类比成第一种方法中的xml文件然后这个xml文件里的一个个标签都变成了注解。 基础环境搭建 首先搭建好基础环境 我们的测试类 public class MySpringTest {public static void main(String[] args) {MyApplicationContext applicationContext new MyApplicationContext(AppConfig.class);Object bean applicationContext.getBean();System.out.println(bean);} }我们的容器类MyApplicationContext public class MyApplicationContext {private Class configClass;public MyApplicationContext(Class configClass) {this.configClass configClass;}public Object getBean(String beanName){return null;} }我们原来在编写Spring的配置文件的时候会使用一个注解ComponentScan来定义一个扫描路径。所以我们这里也定义了一个。 Retention(RetentionPolicy.RUNTIME) Target({ElementType.TYPE}) public interface ComponentScan {String value(); }Retention指定该注解的生命周期设置成RUNTIME便于反射获取 Target指定注解的使用位置设置为TYPE表示能在类上使用 我们的AppConfig ComponentScan(com.zyb.service) public class AppConfig { }根据扫描包我们再创建一个业务层类UseService这个UseService我们一般会使用Component注解进行标记这里我们也是如此 Retention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) public interface Component {String value(); }Component(userService) public class UserService {}扫描逻辑实现 我们的思路就是 解析配置类拿到ComponentScan注解得到扫描路径进行扫描 首先我们可以通过如下代码拿到AppConfig中ComponentScan的内容 public MyApplicationContext(Class configClass) {this.configClass configClass;//解析配置类//ComponentScan注解 --》 扫描路径 --》 扫描ComponentScan componentScanAnnotation (ComponentScan)configClass.getDeclaredAnnotation(ComponentScan.class);String path componentScanAnnotation.value();System.out.println(path);}我们测试一下 拿到路径之后我们就可以开始扫描了。而扫描的目的并不是包下的所有类而是那些带有Component注解的类而Spring会将他们的对象当作Spring中的一个bean。 这里我们扫描的思路如下 通过类加载器得到类路径上的class文件对文件进行筛选 是否以class结尾(判断是否为class文件)对class文件名进行处理 替换\为.截取全限定名 然后将类加载进jvm虚拟机判断运行时类是否有Component注解如果有则进行相关的创建bean对象操作 代码如下 public MyApplicationContext(Class configClass) {this.configClass configClass;//解析配置类//ComponentScan注解 --》 扫描路径 --》 扫描ComponentScan componentScanAnnotation (ComponentScan)configClass.getDeclaredAnnotation(ComponentScan.class);String path componentScanAnnotation.value();String searchPath path.replace(., /);ClassLoader classLoader MyApplicationContext.class.getClassLoader();URL resource classLoader.getResource(searchPath);File file new File(resource.getFile());if (file.isDirectory()) {//首先筛选末尾是.class文件的File[] files file.listFiles();for (File classFile: files) {String absolutePath classFile.getAbsolutePath();if (absolutePath.endsWith(.class)) {//拼接全限定名String fullyQualifiedClassName absolutePath.substring(absolutePath.indexOf(com), absolutePath.indexOf(.class));fullyQualifiedClassName fullyQualifiedClassName.replace(\\, .);//使用app类加载器加载这个类Class? aClass null;try {aClass classLoader.loadClass(fullyQualifiedClassName);if (aClass.isAnnotationPresent(Component.class)) {//然后加载bean}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}} 注意 classLoader.loadClass接收的是类的全限定名 当我们确认当前类有Component注解的时候并不是急着去给其创建bean我们在使用spring的时候是可以决定该bean是否为单例的。我们在这里还是创建一个同名注解Scope Retention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) public interface Scope {String value() default single; }默认是single单例的如果是原型bean则传入prototype。 而我们实现单例bean是通过一个Map单例池不考虑懒加载。 现在我们的大致思路就是在通过Scope注解确定bean是单例模式还是原型模式之后在进行相应的bean的创建但是这里我们考虑到一个问题我们在getBean的时候如果每次都去解析这个类获得Scope是很麻烦的。所以我们在这里引入一个新的概念BeanDefinition。 BeanDefinition中包含了当前bean的所有定义例如bean的类型、作用域、是否懒加载等等。注意bean的name不在BeanDefinition中 public class BeanDefinition {private Class beanClass;private String scope;public Class getBeanClass() {return beanClass;}public void setBeanClass(Class beanClass) {this.beanClass beanClass;}public String getScope() {return scope;}public void setScope(String scope) {this.scope scope;} }在Spring中每扫描到一个bean都会对其进行解析然后生成各自的BeanDefinition实例随后将这个BeanDefinition对象放在map中key为bean的nameBeanDefinition对象作为value。以后当我们要获取一个bean时先会去map中查看如果查找不到bean的BeanDefinition才会去解析类以此来减少解析类的次数提高效率。 接下来我们完善一下代码思路如下 判断运行时类是否有Component注解如果有再获取该类的Scope 如果没有注解则为默认的单例模式如果有此注解则为原型模式 然后再把对应的模式添加到BeanDefinition将此BeanDefinition和对应的bean name添加到beanDefinitionMap中 getBean方法思路 判断要获取的bean的BeanDefinition是否存在于beanDefinitionMap 如果不存在说明容器中不存在该bean则直接报错如果存在则在BeanDefinition中拿到该bean的scope根据scope再去具体的创建bean 代码如下 public class MyApplicationContext {private Class configClass;private ConcurrentHashMapString,Object singleBeanHashMap new ConcurrentHashMap();private HashMapString,BeanDefinition beanDefinitionHashMap new HashMap();public MyApplicationContext(Class configClass) {this.configClass configClass;//解析配置类//ComponentScan注解 --》 扫描路径 --》 扫描ComponentScan componentScanAnnotation (ComponentScan)configClass.getDeclaredAnnotation(ComponentScan.class);String path componentScanAnnotation.value();String searchPath path.replace(., /);ClassLoader classLoader MyApplicationContext.class.getClassLoader();URL resource classLoader.getResource(searchPath);File file new File(resource.getFile());if (file.isDirectory()) {//首先筛选末尾是.class文件的File[] files file.listFiles();for (File classFile: files) {String absolutePath classFile.getAbsolutePath();if (absolutePath.endsWith(.class)) {//拼接全限定名System.out.println(classFile);String fullyQualifiedClassName absolutePath.substring(absolutePath.indexOf(com), absolutePath.indexOf(.class));fullyQualifiedClassName fullyQualifiedClassName.replace(\\, .);//使用app类加载器加载这个类Class? aClass null;try {aClass classLoader.loadClass(fullyQualifiedClassName);if (aClass.isAnnotationPresent(Component.class)) {BeanDefinition beanDefinition new BeanDefinition();if (aClass.isAnnotationPresent(Scope.class)) {Scope scopeAnnotation aClass.getAnnotation(Scope.class);String scope scopeAnnotation.value();beanDefinition.setScope(scope);}else{beanDefinition.setScope(single);}Component componentAnnotation aClass.getDeclaredAnnotation(Component.class);String beanName componentAnnotation.value();beanDefinitionHashMap.put(beanName,beanDefinition);}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}}public Object getBean(String beanName){//首先判断是否存在该bean的BeanDefinitionif (beanDefinitionHashMap.containsKey(beanName)) {BeanDefinition beanDefinition beanDefinitionHashMap.get(beanName);if (beanDefinition.getScope().equals(single)) {//单例模式创建bean}else {//原型模式创建bean}}else {throw new NullPointerException(不存在该bean);}} } 最后我们可以把扫描这部分逻辑提取出来重新建立一个scan方法 public class MyApplicationContext {private Class configClass;private ConcurrentHashMapString,Object singleBeanHashMap new ConcurrentHashMap();private HashMapString,BeanDefinition beanDefinitionHashMap new HashMap();public MyApplicationContext(Class configClass) {this.configClass configClass;scan(configClass);}private void scan(Class configClass) {//解析配置类//ComponentScan注解 --》 扫描路径 --》 扫描ComponentScan componentScanAnnotation (ComponentScan)configClass.getDeclaredAnnotation(ComponentScan.class);String path componentScanAnnotation.value();String searchPath path.replace(., /);ClassLoader classLoader MyApplicationContext.class.getClassLoader();URL resource classLoader.getResource(searchPath);File file new File(resource.getFile());if (file.isDirectory()) {//首先筛选末尾是.class文件的File[] files file.listFiles();for (File classFile: files) {String absolutePath classFile.getAbsolutePath();if (absolutePath.endsWith(.class)) {//拼接全限定名System.out.println(classFile);String fullyQualifiedClassName absolutePath.substring(absolutePath.indexOf(com), absolutePath.indexOf(.class));fullyQualifiedClassName fullyQualifiedClassName.replace(\\, .);//使用app类加载器加载这个类Class? aClass null;try {aClass classLoader.loadClass(fullyQualifiedClassName);if (aClass.isAnnotationPresent(Component.class)) {BeanDefinition beanDefinition new BeanDefinition();if (aClass.isAnnotationPresent(Scope.class)) {Scope scopeAnnotation aClass.getAnnotation(Scope.class);String scope scopeAnnotation.value();beanDefinition.setScope(scope);}else{beanDefinition.setScope(single);}Component componentAnnotation aClass.getDeclaredAnnotation(Component.class);String beanName componentAnnotation.value();beanDefinitionHashMap.put(beanName,beanDefinition);}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}}public Object getBean(String beanName){//首先判断是否存在该bean的BeanDefinitionif (beanDefinitionHashMap.containsKey(beanName)) {BeanDefinition beanDefinition beanDefinitionHashMap.get(beanName);if (beanDefinition.getScope().equals(single)) {//单例模式创建bean}else {//原型模式创建bean}}else {throw new NullPointerException(不存在该bean);}} } bean创建的简单实现 spring容器启动之后先进行扫描步骤而这个扫描的主要作用就是得到BeanDefinition这样我们就有了BeanDefinitionMap然后我们就可以根据BeanDefinitionMap去创建单例池singleBeanHashMap为我们的getBean方法提供支持。 在getBean方法中 如果是单例模式的bean就直接在singleBeanHashMap中去拿如果是原型模式的bean就直接使用creatBean方法创建bean 我们这里的creatBean方法就是用来创建bean的方法我们这里暂时只对此方法进行一个简单的实现。 整体代码如下 public class MyApplicationContext {private Class configClass;private ConcurrentHashMapString,Object singleBeanHashMap new ConcurrentHashMap();private HashMapString,BeanDefinition beanDefinitionHashMap new HashMap();public MyApplicationContext(Class configClass) {this.configClass configClass;scan(configClass);//scan之后就得到了beanDefinitionHashMap然后我们根据此来构建singleBeanHashMapfor (String beanName:beanDefinitionHashMap.keySet()) {BeanDefinition beanDefinition beanDefinitionHashMap.get(beanName);if (beanDefinition.getScope().equals(single)) {singleBeanHashMap.put(beanName,createBean(beanName));}}}public Object createBean(String name){BeanDefinition beanDefinition beanDefinitionHashMap.get(name);Class beanClass beanDefinition.getBeanClass();Object instance null;try {instance beanClass.getConstructor().newInstance();} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {e.printStackTrace();}return instance;}private void scan(Class configClass) {//解析配置类//ComponentScan注解 --》 扫描路径 --》 扫描ComponentScan componentScanAnnotation (ComponentScan)configClass.getDeclaredAnnotation(ComponentScan.class);String path componentScanAnnotation.value();String searchPath path.replace(., /);ClassLoader classLoader MyApplicationContext.class.getClassLoader();URL resource classLoader.getResource(searchPath);File file new File(resource.getFile());if (file.isDirectory()) {//首先筛选末尾是.class文件的File[] files file.listFiles();for (File classFile: files) {String absolutePath classFile.getAbsolutePath();if (absolutePath.endsWith(.class)) {//拼接全限定名System.out.println(classFile);String fullyQualifiedClassName absolutePath.substring(absolutePath.indexOf(com), absolutePath.indexOf(.class));fullyQualifiedClassName fullyQualifiedClassName.replace(\\, .);//使用app类加载器加载这个类Class? aClass null;try {aClass classLoader.loadClass(fullyQualifiedClassName);if (aClass.isAnnotationPresent(Component.class)) {BeanDefinition beanDefinition new BeanDefinition();beanDefinition.setBeanClass(aClass);if (aClass.isAnnotationPresent(Scope.class)) {Scope scopeAnnotation aClass.getAnnotation(Scope.class);String scope scopeAnnotation.value();beanDefinition.setScope(scope);}else{beanDefinition.setScope(single);}Component componentAnnotation aClass.getDeclaredAnnotation(Component.class);String beanName componentAnnotation.value();beanDefinitionHashMap.put(beanName,beanDefinition);}} catch (ClassNotFoundException e) {e.printStackTrace();}}}}}public Object getBean(String beanName){//首先判断是否存在该bean的BeanDefinitionif (beanDefinitionHashMap.containsKey(beanName)) {BeanDefinition beanDefinition beanDefinitionHashMap.get(beanName);if (beanDefinition.getScope().equals(single)) {//单例模式创建beanreturn singleBeanHashMap.get(beanName);}else {//原型模式创建beanreturn createBean(beanName);}}else {throw new NullPointerException(不存在该bean);}} }我们测试一下效果 当UserService类上的Scope Vablue值为prototype 当UserService类上的Scope Vablue值为single或者去掉Scope注解 依赖注入实现 前面我们对createBean方法进行了一个简单地实现这里依赖注入的实现就是将createBean写完整。我们知道不管是单例bean还是原型bean他们的作用范围可能不同但是他们在创建某一个对象的时候步骤是一样的这也就是我们为什么要单独抽取一个方法出来专门用来创建bean。 这里我们只考虑使用Autowired注解进行注入createBean方法思路 首先通过beanName在BeanDefinition中拿到Class对象通过空参构造器创建instance对象遍历当前Class所有成员变量检查是否有Autowired注解 如果没有则直接调用空参构造器返回bean如果有那么我们此时只能根据当前属性的属性名以及类型进行依赖注入这里我们选择简单一点的按照名称注入思路就是将属性名传入到getBean方法中尝试去获取bean在getBean方法我们会对当前依赖进行一个判断 如果是单例模式则将依赖直接从单例池中拿出来对依赖进行赋值如果不是单例模式则递归调用createBean方法创建依赖再对依赖进行赋值 代码如下 public Object createBean(String name){BeanDefinition beanDefinition beanDefinitionHashMap.get(name);Class beanClass beanDefinition.getBeanClass();Object instance null;try {instance beanClass.getConstructor().newInstance();for (Field field:beanClass.getDeclaredFields()) {if (field.isAnnotationPresent(Autowired.class)) {field.setAccessible(true);//拿到属性名String fieldName field.getName();field.set(instance,getBean(fieldName));}}} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {e.printStackTrace();}return instance;}我们测试一下 结果 BeanNameAware回调实现 我们思考一个场景 如果我们想获得当前的beanName这里是不能使用Autowired的。Spring为我们提供了一种接口BeanNameAware将当前的beanName返回给我们。 我们将这个功能实现一下 首先写一个BeanNameAware接口 public interface BeanNameAware {void setBeanNameAware(String beanName); }然后我们在createBean方法中调用接口方法传入bean的name public Object createBean(String name){···//进行依赖注入···//BeanNameAware返回bean nameif (instance instanceof BeanNameAware) {BeanNameAware beanNameAware (BeanNameAware) instance;beanNameAware.setBeanNameAware(name);}···} 初始化机制模拟实现 在bean的属性默认初始化设置完之后我们程序员可以自定义进行业务方面的初始化例如bean中的属性是否为空或者说是否符合业务方面的要求或者你想对某一个属性赋值。那么这个功能如何完成呢 在Spring中我们可以去实现InitializingBean接口来完成这一个功能Spring在检测到当前类实现了InitializingBean接口之后就会帮我们调用接口方法完成我们自定义的初始化。 InitializingBean接口定义如下 public interface InitializingBean {void afterPropertiesSet(); }然后我们在createBean方法中调用接口方法 public Object createBean(String name){···//进行依赖注入···//BeanNameAware返回bean nameif (instance instanceof BeanNameAware) {BeanNameAware beanNameAware (BeanNameAware) instance;beanNameAware.setBeanNameAware(name);}//自定义初始化if (instance instanceof InitializingBean) {InitializingBean initializingBean (InitializingBean) instance;initializingBean.afterPropertiesSet();}···}BeanPostProcessor模拟实现 BeanPostProcessorbean的后置处理器 我们在前文中提到的初始化机制、Aware回调机制都是Spring提供给我们的拓展功能我们只需要在需求类上实现相应接口Spring即可帮我们完成需求。 那么Spring能否给我们提供帮助在实例化之前或之后给我们提供额外的功能追加或者说在初始化之前或初始化之后给我们提供额外的功能追加。 为了实现这一需求Spring为我们提供了BeanPostProcessor。 我们来看一下Spring的源码 public interface BeanPostProcessor {Nullabledefault Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {return bean;}Nullabledefault Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {return bean;} }postProcessBeforeInitialization初始化前追加功能postProcessAfterInitialization初始化后追加功能 BeanPostProcessor有很多子接口其中InstantiationAwareBeanPostProcessor的源码如下 public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor {Nullabledefault Object postProcessBeforeInstantiation(Class? beanClass, String beanName) throws BeansException {return null;}default boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException {return true;}Nullabledefault PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException {return null;}/** deprecated */DeprecatedNullabledefault PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException {return pvs;} }postProcessBeforeInstantiation实例化之前追加功能postProcessAfterInstantiation实例化之后追加功能 我们在Spring框架中要想使用这一个功能一般是新创建一个类然后去实现相应的接口而不会去在业务层的类中去实现。因为初始化、实例化都是相对于每一个bean来说的我们理应将他们单独抽离出来而且这样不会污染业务代码。 接下来我们开始BeanPostProcessor的代码实现 实现思路如下 扫描类的时候(也就是scan方法中) 看是否有类实现了BeanPostProcessor接口如果实现了该接口则创建该类的实例将实例放入beanPostProcessorList中在createBean方法的相应的位置将列表中的实例一个个取出来然后调用他们的方法 代码实现 public interface BeanPostProcessor {Object postProcessBeforeInitialization(Object bean, String beanName);Object postProcessAfterInitialization(Object bean, String beanName); } public class MyBeanPostProcessor implements BeanPostProcessor {Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) {System.out.println(初始化之前);if (beanName.equals(userService)) {System.out.println(为userService初始化之前追加的功能);}return bean;}Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {System.out.println(初始化之后);return bean;} }在MyApplicationContext中新建一个成员属性beanPostProcessorList scan方法 注意 这里的isAssignableFrom方法和instanceof要区别开 instanceof是判断某一个对象与父类或者接口的关系isAssignableFrom是判断某一个类与父类或者接口的关系 同时isAssignableFrom方法与instanceof的用法也不一样例如 判断a对象是否实现b接口a instanceof b判断a类是否实现b接口b.isAssignableFrom(a) 这里的代码逻辑与Spring源码并不相同在Spring源码中这里创建BeanPostProcessor的实例对象时使用的getBean方法这样走的是Spring内部的逻辑可以处理当BeanPostProcessor的实现类里面存在使用Autowired进行依赖注入的情况。但是我们这里并没有考虑这样的情况所以我们直接得到类的构造器去创建BeanPostProcessor的实例对象 createBean方法 我们测试一下 public class MySpringTest {public static void main(String[] args) {MyApplicationContext applicationContext new MyApplicationContext(AppConfig.class);// UserService userService (UserService) applicationContext.getBean(userService); // System.out.println(userService.getOrderService()); // System.out.println(userService.getBeanName());} }结果 我们的扫描范围时service包下而我们的service包下有三个类与结果相吻合。 AOP模拟实现 在spring框架中使用代理模式比较典型的场景就是AOP的实现了代理逻辑核心要点如下 默认使用 JDK 动态代理这样可以代理所有的接口类型如果目标对象没有实现任何接口则默认采用CGLIB代理可强制使用CGLIB指定proxy-target-class “true” 或者 基于注解EnableAspectJAutoProxy(proxyTargetClass true) AOP的工作流程可以直接看我的另一篇文章 [Spring Framework]AOP工作流程 除了动态代理技术之外AOP在Spring中的实现还需要借助BeanPostProcessor。我们知道BeanPostProcessor是bean的后置处理器其可以在bean初始化的前后对bean造成额外的影响。 这里我们可以借助BeanPostProcessor接口中的postProcessAfterInitialization方法返回需要增强类的代理对象。 接下来我们进行代码实现 这里我们实现JDK动态代理的情况所以先要为委托类编写接口方法方便根据接口实现代理对象 public interface ProxyInterface {void show(); }Component(userService) Scope(single) public class UserService implements BeanNameAware, InitializingBean,ProxyInterface {Autowiredprivate OrderService orderService;private String beanName;public String getBeanName() {return beanName;}Overridepublic void afterPropertiesSet() {System.out.println(自定义初始化);}Overridepublic void setBeanNameAware(String beanName) {this.beanName beanName;}public OrderService getOrderService() {return orderService;}public void show(){System.out.println(这里是UserService的实例);}}然后我们重写一下postProcessAfterInitialization方法 Component(myBeanPostProcessor) public class MyBeanPostProcessor implements BeanPostProcessor {Overridepublic Object postProcessBeforeInitialization(Object bean, String beanName) {System.out.println(初始化之前);if (beanName.equals(userService)) {System.out.println(为userService初始化之前追加的功能);}return bean;}Overridepublic Object postProcessAfterInitialization(Object bean, String beanName) {System.out.println(初始化之后);if (beanName.equals(userService)) {return Proxy.newProxyInstance(MyBeanPostProcessor.class.getClassLoader(),bean.getClass().getInterfaces(),new InvocationHandler() {Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {if (method.getName().equals(show)) {System.out.println(这里是对show方法的前置增强);}return method.invoke(bean, args);}});}return bean;} }我们这里省略了是否进行AOP的判断以及找切点的逻辑 测试 public class MySpringTest {public static void main(String[] args) {MyApplicationContext applicationContext new MyApplicationContext(AppConfig.class);ProxyInterface proxyInterface (ProxyInterface) applicationContext.getBean(userService);proxyInterface.show();} }结果 我们在Spring中使用AOP时会在配置类上使用一个注解EnableAspectJAutoProxy。这个注解会向Spring的容器中注册一个bean 而这个bean你往上一直寻找会发现他其实就是一个BeanPostProcessor
http://www.w-s-a.com/news/158336/

相关文章:

  • 河北省建设机械会网站首页刚做的网站怎么收录
  • 什么网站专门做自由行的framework7做网站
  • 网页设计与网站建设书籍包头住房与城乡建设局网站
  • 重庆网站建设平台免费猎头公司收费收费标准和方式
  • 形象设计公司网站建设方案书打开一个不良网站提示创建成功
  • 网站手机页面如何做网站关键字 优帮云
  • 免费的黄冈网站有哪些下载软件系统软件主要包括网页制作软件
  • 企业微站系统重庆高端网站建设价格
  • 有没有做衣服的网站吗网站自适应开发
  • 青海省制作网站专业专业定制网吧桌椅
  • 网站开发的项目17岁高清免费观看完整版
  • 手机网站建设多少钱一个门网站源码
  • 重庆 网站开发天津住房和城乡建设厅官方网站
  • 泰安高级网站建设推广厦门高端网站建设定制
  • jsp网站开发引用文献手机seo排名
  • 创建一家网站如何创设计网页的快捷网站
  • 1688代加工官方网站h5开发教程
  • 静态网站源码下载网站怎么显示备案号
  • 网站代码设计网站开发维护任职要求
  • 长寿做网站的电话怎么快速刷排名
  • 上海市中学生典型事例网站邯郸全网推广
  • 厦门网站建设680元好男人的最好的影院
  • 石家庄网站建设设计产品设计专业就业前景
  • 网站移动排名做最好最全的命理网站
  • 网站怎么防黑客杭州市做外贸网站的公司
  • 网站推广公司认准乐云seo易语言做网站登录
  • 配色设计网站推荐网站下拉菜单重叠
  • 内容展示型网站特点在北京注册公司需要多少钱
  • h5网站源代码创意设计理念
  • 岳阳网站开发服务推广运营平台