详解Spring学习总结——Spring实现AOP的多种方式
发布时间 - 2026-01-10 22:19:25 点击率:次目录

一、基于XML配置的Spring AOP
二、使用注解配置AOP
三、AspectJ切点函数
四、AspectJ通知注解
五、零配置实现Spring IoC与AOP
AOP(Aspect Oriented Programming)面向切面编程,通过预编译方式和运行期动态代理实现程序功能的横向多模块统一控制的一种技术。AOP是OOP的补充,是spring框架中的一个重要内容。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。AOP可以分为静态织入与动态织入,静态织入即在编译前将需织入内容写入目标模块中,这样成本非常高。动态织入则不需要改变目标模块。Spring框架实现了AOP,使用注解配置完成AOP比使用XML配置要更加方便与直观。
一、基于XML配置的Spring AOP
在讲注解实现AOP功能前先用前面学习过的使用xml配置Spring AOP功能,这样是为了对比以便更好的理解。
1.1、新建一个Maven项目,添加引用,项目的pom.xml文件如下:
<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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhangguo</groupId>
<artifactId>Spring052</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Spring052</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.0.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
<version>4.10</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.4</version>
</dependency>
</dependencies>
</project>
1.2、创建要被代理的Math类,代码如下:
package com.zhangguo.Spring052.aop01;
/**
* 被代理的目标类
*/
public class Math{
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
1.3、编辑AOP中需要使用到的通知类Advices.Java代码如下:
package com.zhangguo.Spring052.aop01;
import org.aspectj.lang.JoinPoint;
/**
* 通知类,横切逻辑
*
*/
public class Advices {
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
}
1.4、配置容器初始化时需要的XML文件,aop01.xml文件内容如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <!-- 被代理对象 --> <bean id="math" class="com.zhangguo.Spring052.aop01.Math"></bean> <!-- 通知 --> <bean id="advices" class="com.zhangguo.Spring052.aop01.Advices"></bean> <!-- aop配置 --> <aop:config proxy-target-class="true"> <!--切面 --> <aop:aspect ref="advices"> <!-- 切点 --> <aop:pointcut expression="execution(* com.zhangguo.Spring052.aop01.Math.*(..))" id="pointcut1"/> <!--连接通知方法与切点 --> <aop:before method="before" pointcut-ref="pointcut1"/> <aop:after method="after" pointcut-ref="pointcut1"/> </aop:aspect> </aop:config> </beans>
1.5、测试代码Test.java如下:
package com.zhangguo.Spring052.aop01;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop01.xml");
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
运行结果:
二、使用注解配置AOP
2.1、在上一个示例中修改被代理的类Math,为了实现IOC扫描在Math类上注解了@Service并命名bean为math。相当于上一个示例中在xml配置文件中增加了一个bean,<!-- 被代理对象 --><bean id="math" class="com.zhangguo.Spring052.aop01.Math"></bean>,Math类的代码如下:
package com.zhangguo.Spring052.aop02;
import org.springframework.stereotype.Service;
/**
* 被代理的目标类
*/
@Service("math")
public class Math{
//加
public int add(int n1,int n2){
int result=n1+n2;
System.out.println(n1+"+"+n2+"="+result);
return result;
}
//减
public int sub(int n1,int n2){
int result=n1-n2;
System.out.println(n1+"-"+n2+"="+result);
return result;
}
//乘
public int mut(int n1,int n2){
int result=n1*n2;
System.out.println(n1+"X"+n2+"="+result);
return result;
}
//除
public int div(int n1,int n2){
int result=n1/n2;
System.out.println(n1+"/"+n2+"="+result);
return result;
}
}
2.2、修改通知类Advices,代码中有3个注解,@Component表示该类的实例会被Spring IOC容器管理;@Aspect表示声明一个切面;@Before表示before为前置通知,通过参数execution声明一个切点,Advices.java代码如下所示:
package com.zhangguo.Spring052.aop02;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知类,横切逻辑
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop02.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("execution(* com.zhangguo.Spring052.aop02.Math.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
}
上面的代码与下面的配置基本等同
<!-- 通知 --> <bean id="advices" class="com.zhangguo.Spring052.aop01.Advices"></bean> <!-- aop配置 --> <aop:config proxy-target-class="true"> <!--切面 --> <aop:aspect ref="advices"> <!-- 切点 --> <aop:pointcut expression="execution(* com.zhangguo.Spring052.aop01.Math.*(..))" id="pointcut1"/> <!--连接通知方法与切点 --> <aop:before method="before" pointcut-ref="pointcut1"/> <aop:after method="after" pointcut-ref="pointcut1"/> </aop:aspect> </aop:config>
2.3、新增配置文件aop02.xml,在配置IOC的基础上增加了aop:aspectj-autoproxy节点,Spring框架会自动为与AspectJ切面配置的Bean创建代理,proxy-target-class="true"属性表示被代理的目标对象是一个类,而非实现了接口的类,主要是为了选择不同的代理方式。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <context:component-scan base-package="com.zhangguo.Spring052.aop02"> </context:component-scan> <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy> </beans>
2.4、测试运行代码Test.java如下:
package com.zhangguo.Spring052.aop02;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop02.xml");
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
}
}
运行结果:
三、AspectJ切点函数
切点函数可以定位到准确的横切逻辑位置,在前面的示例中我们只使用过execution(* com.zhangguo.Spring052.aop02.Math.*(..)),execution就是一个切点函数,但该函数只什么方法一级,如果我们要织入的范围是类或某个注解则execution就不那么好用了,其实一共有9个切点函数,有不同的针对性。
@AspectJ使用AspectJ专门的切点表达式描述切面,Spring所支持的AspectJ表达式可分为四类:
方法切点函数:通过描述目标类方法信息定义连接点。
方法参数切点函数:通过描述目标类方法入参信息定义连接点。
目标类切点函数:通过描述目标类类型信息定义连接点。
代理类切点函数:通过描述代理类信息定义连接点。
常见的AspectJ表达式函数:
- execution():满足匹配模式字符串的所有目标类方法的连接点
- @annotation():任何标注了指定注解的目标方法链接点
- args():目标类方法运行时参数的类型指定连接点
- @args():目标类方法参数中是否有指定特定注解的连接点
- within():匹配指定的包的所有连接点
- target():匹配指定目标类的所有方法
- @within():匹配目标对象拥有指定注解的类的所有方法
- @target():匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解
- this():匹配当前AOP代理对象类型的所有执行方法
最常用的是:execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)切点函数,可以满足多数需求。
为了展示各切点函数的功能现在新增一个类StrUtil,类如下:
package com.zhangguo.Spring052.aop03;
import org.springframework.stereotype.Component;
@Component("strUtil")
public class StrUtil {
public void show(){
System.out.println("Hello StrUtil!");
}
}
测试代码如下:
package com.zhangguo.Spring052.aop03;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("aop03.xml");
IMath math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 5;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
math.div(n1, n2);
StrUtil strUtil=ctx.getBean("strUtil",StrUtil.class);
strUtil.show();
}
}
3.1、切点函数execution,通知与切面的定义如下:
package com.zhangguo.Spring052.aop03;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知类,横切逻辑
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop03.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
//execution切点函数
//com.zhangguo.Spring052.aop03包下所有类的所有方法被切入
@After("execution(* com.zhangguo.Spring052.aop03.*.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
}
运行结果如下:
execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
3.2、切点函数within
//within切点函数
//com.zhangguo.Spring052.aop03包下所有类的所有方法被切入
@After("within(com.zhangguo.Spring052.aop03.*)")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
3.3、this切点函数
//this切点函数
//实现了IMath接口的代理对象的任意连接点
@After("this(com.zhangguo.Spring052.aop03.IMath)")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
3.4、args切点函数
//args切点函数
//要求方法有两个int类型的参考才会被织入横切逻辑
@After("args(int,int)")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
如果参数类型不是基本数据类型则需要包名。
3.5、@annotation切点函数
先自定义一个可以注解在方法上的注解
package com.zhangguo.Spring052.aop03;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnno {
}
//@annotation切点函数
//要求方法必须被注解com.zhangguo.Spring052.aop03.MyAnno才会被织入横切逻辑
@After("@annotation(com.zhangguo.Spring052.aop03.MyAnno)")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
package com.zhangguo.Spring052.aop03;
import org.springframework.stereotype.Component;
@Component("strUtil")
public class StrUtil {
@MyAnno
public void show(){
System.out.println("Hello StrUtil!");
}
}
运行结果:
其它带@的切点函数都是针对注解的
四、AspectJ通知注解
AspectJ通知注解共有6个,常用5个,引介少用一些。
先解决定义切点复用的问题,如下代码所示,切点函数的内容完全一样:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* 通知类,横切逻辑
*
*/
@Component
@Aspect
public class Advices {
@Before("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
}
可以先定义一个切点然后复用,如下所示:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 通知类,横切逻辑
*/
@Component
@Aspect
public class Advices {
//切点
@Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.*(..))")
public void pointcut(){
}
@Before("pointcut()")
public void before(JoinPoint jp){
System.out.println("----------前置通知----------");
System.out.println(jp.getSignature().getName());
}
@After("pointcut()")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
}
修改Advices.java文件,增加各种通知类型如下:
package com.zhangguo.Spring052.aop04;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 通知类,横切逻辑
*/
@Component
@Aspect
public class Advices {
//切点
@Pointcut("execution(* com.zhangguo.Spring052.aop04.Math.a*(..))")
public void pointcut(){
}
//前置通知
@Before("pointcut()")
public void before(JoinPoint jp){
System.out.println(jp.getSignature().getName());
System.out.println("----------前置通知----------");
}
//最终通知
@After("pointcut()")
public void after(JoinPoint jp){
System.out.println("----------最终通知----------");
}
//环绕通知
@Around("execution(* com.zhangguo.Spring052.aop04.Math.s*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable{
System.out.println(pjp.getSignature().getName());
System.out.println("----------环绕前置----------");
Object result=pjp.proceed();
System.out.println("----------环绕后置----------");
return result;
}
//返回结果通知
@AfterReturning(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.m*(..))",returning="result")
public void afterReturning(JoinPoint jp,Object result){
System.out.println(jp.getSignature().getName());
System.out.println("结果是:"+result);
System.out.println("----------返回结果----------");
}
//异常后通知
@AfterThrowing(pointcut="execution(* com.zhangguo.Spring052.aop04.Math.d*(..))",throwing="exp")
public void afterThrowing(JoinPoint jp,Exception exp){
System.out.println(jp.getSignature().getName());
System.out.println("异常消息:"+exp.getMessage());
System.out.println("----------异常通知----------");
}
}
运行结果:
五、零配置实现Spring IoC与AOP
为了实现零配置在原有示例的基础上我们新增一个类User,如下所示:
package com.zhangguo.Spring052.aop05;
public class User {
public void show(){
System.out.println("一个用户对象");
}
}
该类并未注解,容器不会自动管理。因为没有xml配置文件,则使用一个作为配置信息,ApplicationCfg.java文件如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration //用于表示当前类为容器的配置类,类似<beans/>
@ComponentScan(basePackages="com.zhangguo.Spring052.aop05") //扫描的范围,相当于xml配置的结点<context:component-scan/>
@EnableAspectJAutoProxy(proxyTargetClass=true) //自动代理,相当于<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
public class ApplicationCfg {
//在配置中声明一个bean,相当于<bean id=getUser class="com.zhangguo.Spring052.aop05.User"/>
@Bean
public User getUser(){
return new User();
}
}
该类的每一部分内容基本都与xml 配置有一对一的关系,请看注释,这样做要比写xml方便,但不便发布后修改。测试代码如下:
package com.zhangguo.Spring052.aop05;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Test {
public static void main(String[] args) {
// 通过类初始化容器
ApplicationContext ctx = new AnnotationConfigApplicationContext(ApplicationCfg.class);
Math math = ctx.getBean("math", Math.class);
int n1 = 100, n2 = 0;
math.add(n1, n2);
math.sub(n1, n2);
math.mut(n1, n2);
try {
math.div(n1, n2);
} catch (Exception e) {
}
User user=ctx.getBean("getUser",User.class);
user.show();
}
}
advices.java 同上,没有任何变化,运行结果如下:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
# spring
# aop实现方式
# spring实现aop的方式
# aop实现
# Spring AOP的几种实现方式总结
# Spring AOP的使用详解
# 关于Spring AOP使用时的一些问题汇总
# 基于Spring AOP proxyTargetClass的行为表现总结
# 横切
# 所示
# 配置文件
# 基础上
# 才会
# 实现了
# 复用
# 的是
# 都是
# 增加了
# 是为了
# 是一个
# 不需要
# 就不
# 修饰符
# 没有任何
# 中有
# 这样做
# 自定义
# 要比
相关栏目:
【
网站优化151355 】
【
网络推广146373 】
【
网络技术251813 】
【
AI营销90571 】
相关推荐:
HTML5空格和margin有啥区别_空格与外边距的使用场景【说明】
如何用花生壳三步快速搭建专属网站?
宙斯浏览器视频悬浮窗怎么开启 边看视频边操作其他应用教程
Laravel如何连接多个数据库_Laravel多数据库连接配置与切换教程
网站制作壁纸教程视频,电脑壁纸网站?
海南网站制作公司有哪些,海口网是哪家的?
如何快速生成可下载的建站源码工具?
打造顶配客厅影院,这份100寸电视推荐名单请查收
如何在阿里云虚拟主机上快速搭建个人网站?
香港代理服务器配置指南:高匿IP选择、跨境加速与SEO优化技巧
Laravel如何处理异常和错误?(Handler示例)
Windows Hello人脸识别突然无法使用
晋江文学城电脑版官网 晋江文学城网页版直接进入
Laravel项目如何进行性能优化_Laravel应用性能分析与优化技巧大全
详解一款开源免费的.NET文档操作组件DocX(.NET组件介绍之一)
怎么制作一个起泡网,水泡粪全漏粪育肥舍冬季氨气超过25ppm,可以有哪些措施降低舍内氨气水平?
详解Nginx + Tomcat 反向代理 如何在高效的在一台服务器部署多个站点
湖南网站制作公司,湖南上善若水科技有限公司做什么的?
如何快速查询网址的建站时间与历史轨迹?
Laravel如何处理CORS跨域请求?(配置示例)
昵图网官方站入口 昵图网素材图库官网入口
Laravel如何使用Socialite实现第三方登录?(微信/GitHub示例)
如何在IIS管理器中快速创建并配置网站?
EditPlus中的正则表达式实战(5)
Laravel怎么防止CSRF攻击_Laravel CSRF保护中间件原理与实践
如何用AWS免费套餐快速搭建高效网站?
Laravel怎么实现搜索高亮功能_Laravel结合Scout与Algolia全文检索【实战】
Laravel全局作用域是什么_Laravel Eloquent Global Scopes应用指南
php读取心率传感器数据怎么弄_php获取max30100的心率值【指南】
Laravel表单请求验证类怎么用_Laravel Form Request分离验证逻辑教程
Claude怎样写结构化提示词_Claude结构化提示词写法【教程】
动图在线制作网站有哪些,滑动动图图集怎么做?
深圳网站制作的公司有哪些,dido官方网站?
网站视频制作书签怎么做,ie浏览器怎么将网站固定在书签工具栏?
零基础网站服务器架设实战:轻量应用与域名解析配置指南
浅谈redis在项目中的应用
Laravel如何使用Service Provider注册服务_Laravel服务提供者配置与加载
微信小程序制作网站有哪些,微信小程序需要做网站吗?
如何用IIS7快速搭建并优化网站站点?
网站建设要注意的标准 促进网站用户好感度!
UC浏览器如何设置启动页 UC浏览器启动页设置方法
制作企业网站建设方案,怎样建设一个公司网站?
怎么用AI帮你设计一套个性化的手机App图标?
如何在局域网内绑定自建网站域名?
高性能网站服务器配置指南:安全稳定与高效建站核心方案
什么是javascript作用域_全局和局部作用域有什么区别?
Laravel如何发送邮件_Laravel Mailables构建与发送邮件的简明教程
详解vue.js组件化开发实践
Laravel安装步骤详细教程_Laravel环境搭建指南
网站制作软件有哪些,制图软件有哪些?

