1、Spring的AOP编程

什么是AOP?  ----- 在软件行业AOP为Aspect Oriented Programming  也就是面向切面编程,使用AOP编程的好处就是:在不修改源代码的情况下,可以实现代码功能的增强

AOP的实现原理(掌握)

JDK的动态代理(注意JDK的动态代理只能对实现了接口的类产生代理)

/**

* Jdk的动态代理

* @author lilong

*/

public class JdkProxy implements InvocationHandler{

//要代理的对象

private CustomerDao customerDao;

public JdkProxy(CustomerDao customerDao){

this.customerDao = customerDao;

}

/**

* 生成代理对象的方法

* @return

*/

public CustomerDao createProxy(){

CustomerDao proxy = (CustomerDao) Proxy.newProxyInstance(customerDao.getClass().getClassLoader(), customerDao.getClass().getInterfaces(), this);

return proxy;

}

/**

* 增强的方法

*/

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

System.out.println("权限校验...");

return method.invoke(customerDao, args);

}

}

编写测试代码:

@Test

public void test1(){

CustomerDao customerDao = new CustomerDaoImpl();

JdkProxy jdkProxy = new JdkProxy(customerDao);

CustomerDao proxy = jdkProxy.createProxy();

proxy.save();

}

第二种代理方式Cglib方式(了解或者欣赏)

注意的是:Cglib可以对没有实现接口的类产生代理,生成子类来实现功能的增强

public class CglibProxy implements MethodInterceptor{

//要代理的对象

private LinkManDao linkManDao;

public CglibProxy(LinkManDao linkManDao) {

this.linkManDao = linkManDao;

}

public LinkManDao createProxy(){

//创建Cglib核心类

Enhancer enhancer = new Enhancer();

//设置父类

enhancer.setSuperclass(linkManDao.getClass());

//设置回调

enhancer.setCallback(this);

//生成代理

LinkManDao proxy = (LinkManDao) enhancer.create();

return proxy;

}

@Override

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {

System.out.println("日志记录");

Object obj = methodProxy.invokeSuper(proxy, args);

return obj;

}

}

n  编写测试代码

@Test

public void test2(){

LinkManDao linkManDao = new LinkManDao();

CglibProxy cglibProxy = new CglibProxy(linkManDao);

LinkManDao proxy = cglibProxy.createProxy();

proxy.save();

}

2、Spring的AOP开发方式(xml方式)

其中相关术语的介绍

AOP开发所需要的jar介绍

n  AOP联盟的jar包:com.springsource.org.aopalliance-1.0.0.jar

n  Spring提供的AOP的jar包:spring-aop-4.2.4.RELEASE.jar

n  AspectJ的jar包:com.springsource.org.aspectj.weaver-1.6.8.RELEASE.jar

n  Spring整合AspectJ的jar包:spring-aspects-4.2.4.RELEASE.jar

编写接口和实现类

ProductDao接口:

public interface ProductDao {

/**

* 持久层:产品保存

*/

public void save();

}

ProductDaoImpl实现类:

public class ProductDaoImpl implements ProductDao {

@Override

public void save() {

System.out.println("持久层:产品保存...");

}

}

配置相关类到Spring中

<?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: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.xsd">

<bean id="productDao" class="cn.itcast.dao.impl.ProductDaoImpl"></bean>

</beans>

编写切面类

/**

* 自定义切面类

* @author kevin

*/

public class MyAspectXml {

public void checkPrivilege(){

System.out.println("权限校验...");

}

}

配置切面类

<?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: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.xsd">

<bean id="productDao" class="cn.itcast.dao.impl.ProductDaoImpl"></bean>

<bean id="myAspectXml" class="cn.itcast.aspect.MyAspectXml"></bean>

</beans>

进行AOP配置

<?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:context="http://www.springframework.org/schema/context"

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/context

        http://www.springframework.org/schema/context/spring-context.xsd

        http://www.springframework.org/schema/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd">

<bean id="productDao" class="cn.itcast.dao.impl.ProductDaoImpl"></bean>

<bean id="myAspectXml" class="cn.itcast.aspect.MyAspectXml"></bean>

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

</aop:aspect>

</aop:config>

</beans>

编写测试类

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration("classpath:applicationContext.xml")

public class TestAOP {

@Autowired

private ProductDao productDao;

@Test

public void test1(){

productDao.save();

}

}

总结:对切入点表达式语法进行总结

语法:[修饰符] 返回类型 包名.类名.方法名(形式参数)

常见写法:

n  execution(public * *(..))                                                                                      所有的public方法

n  execution(* set(..))                                                                                               所有set开头的方法

execution(* com.xyz.service.AccountService.*(..))                              AccountService类中的所有方法

execution(* com.xyz.service.*.*(..))                                                              com.xyz.service包下所有的方法

n  execution(* com.xyz.service..*.*(..))                                                           com.xyz.service包及其子包下所有的方法

3、Spring中AOP的通知类型

1.1.1.   前置通知:在方法执行之前增强。可以获得切入点信息。

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<!-- 前置通知 -->

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

</aop:aspect>

</aop:config>

public class MyAspectXml {

public void checkPrivilege(JoinPoint point){

System.out.println("权限校验..." + point);

}

}

1.1.2.   后置通知:在方法执行完之后增强。可以获取返回值信息。

/**

* 自定义切面类

* @author kevin

*/

public class MyAspectXml {

public void checkPrivilege(JoinPoint point){

System.out.println("权限校验..." + point);

}

public void afterReturn(Object result){

System.out.println("后置通知:" + result);

}

}

public class ProductDaoImpl implements ProductDao {

@Override

public void save() {

System.out.println("持久层:产品保存...");

}

public int delete(){

System.out.println("持久层:产品删除...");

return 100;

}

}

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))" id="pointcut2"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<!-- 前置通知 -->

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

<!-- 后置通知 -->

<aop:after-returning method="afterReturn" pointcut-ref="pointcut2" returning="result"/>

</aop:aspect>

</aop:config>

1.1.3.   环绕通知:在方法执行前后都进行增强。可以阻止方法的执行。

public class ProductDaoImpl implements ProductDao {

@Override

public void save() {

System.out.println("持久层:产品保存...");

}

public int delete(){

System.out.println("持久层:产品删除...");

return 100;

}

@Override

public void update() {

System.out.println("持久层:产品更新");

}

}

public class MyAspectXml {

public void checkPrivilege(JoinPoint point){

System.out.println("权限校验..." + point);

}

public void afterReturn(Object result){

System.out.println("后置通知:" + result);

}

public Object around(ProceedingJoinPoint joinpoint){

System.out.println("环绕前执行");

Object obj = null;

try {

obj = joinpoint.proceed();

} catch (Throwable e) {

e.printStackTrace();

}

System.out.println("环绕后执行");

return obj;

}

}

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))" id="pointcut2"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.update(..))" id="pointcut3"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<!-- 前置通知 -->

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

<!-- 后置通知 -->

<aop:after-returning method="afterReturn" pointcut-ref="pointcut2" returning="result"/>

<!-- 环绕通知 -->

<aop:around method="around" pointcut-ref="pointcut3"/>

</aop:aspect>

</aop:config>

1.1.4.   异常抛出通知:当发生异常之后增强,可以获取异常信息。

public class ProductDaoImpl implements ProductDao {

@Override

public void save() {

System.out.println("持久层:产品保存...");

}

public int delete(){

System.out.println("持久层:产品删除...");

return 100;

}

@Override

public void update() {

System.out.println("持久层:产品更新");

}

@Override

public void find() {

System.out.println("持久层:查询");

int i = 10/0;

}

}

public class MyAspectXml {

public void checkPrivilege(JoinPoint point){

System.out.println("权限校验..." + point);

}

public void afterReturn(Object result){

System.out.println("后置通知:" + result);

}

public Object around(ProceedingJoinPoint joinpoint){

System.out.println("环绕前执行");

Object obj = null;

try {

obj = joinpoint.proceed();

} catch (Throwable e) {

e.printStackTrace();

}

System.out.println("环绕后执行");

return obj;

}

public void afterThrowing(Exception ex){

System.out.println("抛出异常通知:" + ex.getMessage());

}

}

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))" id="pointcut2"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.update(..))" id="pointcut3"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))" id="pointcut4"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<!-- 前置通知 -->

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

<!-- 后置通知 -->

<aop:after-returning method="afterReturn" pointcut-ref="pointcut2" returning="result"/>

<!-- 环绕通知 -->

<aop:around method="around" pointcut-ref="pointcut3"/>

<!-- 抛出异常通知 -->

<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>

</aop:aspect>

</aop:config>

1.1.5.   最终通知:不管是否有异常,都会执行的

public class MyAspectXml {

public void checkPrivilege(JoinPoint point){

System.out.println("权限校验..." + point);

}

public void afterReturn(Object result){

System.out.println("后置通知:" + result);

}

public Object around(ProceedingJoinPoint joinpoint){

System.out.println("环绕前执行");

Object obj = null;

try {

obj = joinpoint.proceed();

} catch (Throwable e) {

e.printStackTrace();

}

System.out.println("环绕后执行");

return obj;

}

public void afterThrowing(Exception ex){

System.out.println("抛出异常通知:" + ex.getMessage());

}

public void after(){

System.out.println("最终通知");

}

}

<!-- AOP配置 -->

<aop:config>

<!-- 配置切入点 -->

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))" id="pointcut1"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))" id="pointcut2"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.update(..))" id="pointcut3"/>

<aop:pointcut expression="execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))" id="pointcut4"/>

<!-- 配置切面 -->

<aop:aspect ref="myAspectXml">

<!-- 前置通知 -->

<aop:before method="checkPrivilege" pointcut-ref="pointcut1"/>

<!-- 后置通知 -->

<aop:after-returning method="afterReturn" pointcut-ref="pointcut2" returning="result"/>

<!-- 环绕通知 -->

<aop:around method="around" pointcut-ref="pointcut3"/>

<!-- 抛出异常通知 -->

<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut4" throwing="ex"/>

<!-- 最终通知 -->

<aop:after method="after" pointcut-ref="pointcut4"/>

</aop:aspect>

</aop:config>

注意:最终通知和后置通知的区别:最终通知,不管异常与否,都执行;而后置通知在异常时不执行。

4、Spring的AOP注解

4.1. 创建工程,引入jar包,创建核心配置文件

<?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: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/aop

        http://www.springframework.org/schema/aop/spring-aop.xsd

        http://www.springframework.org/schema/context

        http://www.springframework.org/schema/context/spring-context.xsd">

<context:component-scan base-package="cn.itcast"></context:component-scan>

<!-- 开启自动代理注解 -->

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

</beans>

4.2. 创建接口和实现类

public interface ProductDao {

/**

* 持久层:产品保存

*/

public void save();

public int delete();

public void update();

public void find();

}

@Repository("productDao")

public class ProductDaoImpl implements ProductDao {

@Override

public void save() {

System.out.println("持久层:产品保存...");

}

public int delete(){

System.out.println("持久层:产品删除...");

return 100;

}

@Override

public void update() {

System.out.println("持久层:产品更新");

}

@Override

public void find() {

System.out.println("持久层:查询");

}

}

4.3. 编写切面类

@Component("myAspectAnnotation")

@Aspect

public class MyAspect {

@Before("execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))")

public void checkPrivilege(JoinPoint joinPoint){

System.out.println("权限校验..." + joinPoint.toString());

}

}

提示:此处的切面类可以不取id.

5、Spring的AOP 中注解通知

5.1. 前置通知

/**

* 前置通知

* @param joinPoint

*/

@Before("execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))")

public void checkPrivilege(JoinPoint joinPoint){

System.out.println("权限校验..." + joinPoint.toString());

}

5.2. 后置通知

@Aspect

public class MyAspect {

/**

* 前置通知

* @param joinPoint

*/

@Before("execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))")

public void checkPrivilege(JoinPoint joinPoint){

System.out.println("权限校验..." + joinPoint.toString());

}

@AfterReturning(value="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))",returning="result")

public void afterReturning(Object result){

System.out.println("后置通知:" + result);

}

}

5.3. 环绕通知

@Around("execution(* cn.itcast.dao.impl.ProductDaoImpl.update(..))")

public Object after(ProceedingJoinPoint joinpoint) throws Throwable{

System.out.println("环绕通知前增强");

Object obj = joinpoint.proceed();

System.out.println("环绕通知后增强");

return obj;

}

5.4. 异常通知

@AfterThrowing(value="execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))",throwing="ex")

public void afterThrowing(Exception ex){

System.out.println("抛出异常通知");

}

5.5. 最终通知

@After("execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))")

public void after(){

System.out.println("最终通知");

}

5.6. PointCut注解(了解)

作用:用于定义切入点表达式的一个注解。

@Component("myAspectAnnotation")

@Aspect

public class MyAspect {

/**

* 前置通知

* @param joinPoint

*/

@Before("execution(* cn.itcast.dao.impl.ProductDaoImpl.save(..))")

public void checkPrivilege(JoinPoint joinPoint){

System.out.println("权限校验..." + joinPoint.toString());

}

@AfterReturning(value="execution(* cn.itcast.dao.impl.ProductDaoImpl.delete(..))",returning="result")

public void afterReturning(Object result){

System.out.println("后置通知:" + result);

}

@Around("execution(* cn.itcast.dao.impl.ProductDaoImpl.update(..))")

public Object aroung(ProceedingJoinPoint joinpoint) throws Throwable{

System.out.println("环绕通知前增强");

Object obj = joinpoint.proceed();

System.out.println("环绕通知后增强");

return obj;

}

// @AfterThrowing(value="execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))",throwing="ex")

@AfterThrowing(value="MyAspect.pointcut()",throwing="ex")

public void afterThrowing(Exception ex){

System.out.println("抛出异常通知");

}

// @After("execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))")

@After("MyAspect.pointcut()")

public void after(){

System.out.println("最终通知");

}

@Pointcut("execution(* cn.itcast.dao.impl.ProductDaoImpl.find(..))")

public void pointcut(){

}

}

spring框架总结(03)重点介绍(Spring框架的第二种核心掌握)的更多相关文章

  1. Spring官方文档翻译——15.1 介绍Spring Web MVC框架

    Part V. The Web 文档的这一部分介绍了Spring框架对展现层的支持(尤其是基于web的展现层) Spring拥有自己的web框架--Spring Web MVC.在前两章中会有介绍. ...

  2. 是时候给大家介绍 Spring Boot/Cloud 背后豪华的研发团队了。

    看了 Pivotal 公司的发展历史,这尼玛就是一场商业大片呀. 我们刚开始学习 Spring Boot 的时候肯定都会看到这么一句话: Spring Boot 是由 Pivotal 团队提供的全新框 ...

  3. 【转帖】是时候给大家介绍 Spring Boot/Cloud 背后豪华的研发团队了。

    是时候给大家介绍 Spring Boot/Cloud 背后豪华的研发团队了. 2019/01/03 http://www.ityouknow.com/springboot/2019/01/03/spr ...

  4. Spring入门篇——第7章 Spring对AspectJ的支持

    第7章 Spring对AspectJ的支持 介绍Spring对AspectJ的支持 7-1 AspectJ介绍及Pointcut注解应用 实例 完成了在xml文件的配置 7-2 Advice定义及实例 ...

  5. 带你手写基于 Spring 的可插拔式 RPC 框架(一)介绍

    概述 首先这篇文章是要带大家来实现一个框架,听到框架大家可能会觉得非常高大上,其实这和我们平时写业务员代码没什么区别,但是框架是要给别人使用的,所以我们要换位思考,怎么才能让别人用着舒服,怎么样才能让 ...

  6. 关于如何介绍spring框架。

    一.介绍Spring 1.Spring是一个分层的JavaSE/EEfull-stack(一站式) 轻量级开源框架. 2.概念:轻量级的IOC(控制反转或者依赖注入).AOP(面向切面或者面向方面) ...

  7. Spring框架学习03——Spring Bean 的详解

    1.Bean 的配置 Spring可以看做一个大型工厂,用于生产和管理Spring容器中的Bean,Spring框架支持XML和Properties两种格式的配置文件,在实际开发中常用XML格式的配置 ...

  8. Spring4- 01 - Spring框架简介及官方压缩包目录介绍- Spring IoC 的概念 - Spring hello world环境搭建

    一. Spring 框架简介及官方压缩包目录介绍 主要发明者:Rod Johnson 轮子理论推崇者: 2.1 轮子理论:不用重复发明轮子. 2.2 IT 行业:直接使用写好的代码. Spring 框 ...

  9. Spring框架IOC和AOP介绍

    说明:本文部分内容参考其他优秀博客后结合自己实战例子改编如下 Spring框架是个轻量级的Java EE框架.所谓轻量级,是指不依赖于容器就能运行的.Struts.Hibernate也是轻量级的. 轻 ...

随机推荐

  1. [转]linux中颜色的含义

    绿色文件---------- 可执行文件,可执行的程序 红色文件-----------压缩文件或者包文件 蓝色文件----------目录  www.2cto.com     白色文件-------- ...

  2. epoll全面讲解:从实现到应用

    多路复用的适用场合 •     当客户处理多个描述符时(例如同时处理交互式输入和网络套接口),必须使用I/O复用. •     如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用 ...

  3. SQL菜鸟学习札记(一)

    刚开始学SQL,从最基础的语句开始写,用一个LOL数据库做实验.目前使用的工具是MySQL Workbench,感觉比较顺手,界面没花多久时间就读懂的差不多了,所以目前就使用这个工具来做SQL的学习了 ...

  4. js封装成插件-------Canvas统计图插件编写

    之前就说过,我想写一个canvas画统计图的插件,现在写好了 先说下实现的功能吧: 1.可以通过自定义X轴坐标属性和Y轴坐标属性按比例画出统计图 2.可以选择画折现图还是柱形统计图,或者两者都实现 3 ...

  5. 【C++小白成长撸】--(续)双偶数N阶魔阵

    原理: 把双偶数N阶魔阵均分为(N/4)^2个4阶魔阵(4*4) 每个魔阵的对角线都标为"-1",其余位置标为"0" 从第一个位置(a[0][0])从左到右,从 ...

  6. 【C++小白成长撸】--N阶幻方(魔阵)矩阵

    解决方法:1.第一个元素放在第一行中间一列 2.下一个元素存放在当前元素的上一行.下一列. 3.如果上一行.下一列已经有内容,则下一个元素的存放位置为当前列的下一行. 在找上一行.下一行或者下一列的时 ...

  7. OSI与TCP/IP网络模型分层

      学习linux的人,都会接触到一些网络方面的知识.作为一个linux方面的萌新,今天,小编就接触了OSI模型和TCP/IP协议栈,那么什么是OSI模型呢?     OSI模型,开放式系统互联通信参 ...

  8. 移动端图片放大滑动查看-插件photoswipe的使用

    最近在开发项目的时候,遇到一个需求,需要移动端实现放大查看图片的功能,然后我就在网上搜索了一下资料,看到了photoswipe这个插件,后来试了试,确实挺好用的,它可以实现手势放大缩小查看图片,左右滑 ...

  9. PHP初入--表单元素

    <!DOCTYPE html><html> <head> <meta charset="UTF-8"> <title>& ...

  10. 软工+C(2017第5期) 工具和结构化

    // 上一篇:Alpha/Beta换人 // 下一篇:最近发展区/脚手架 工具/轮子 软件工程/计算机相关专业的一个特点是会使用到众多的工具,工具的使用是从程序猿进化到程序员的一个关键要素.软件工程师 ...