在JavaWeb阶段,我们已经学习了如何使用Java进行Web应用程序开发,我们现在已经具有搭建Web网站的能力,但是,我们在开发的过程中,发现存在诸多的不便,在最后的图书管理系统编程实战中,我们发现虽然我们思路很清晰,知道如何编写对应的接口,但是这样的开发效率,实在是太慢了,并且对于对象创建的管理,存在诸多的不妥之处,因此,我们要去继续学习更多的框架技术,来简化和规范我们的Java开发。

Spring就是这样的一个框架(文档:https://docs.spring.io/spring-framework/docs/5.2.13.RELEASE/spring-framework-reference/),它就是为了简化开发而生,它是轻量级的IoCAOP的容器框架,主要是针对JavaBean的生命周期进行管理的轻量级容器,并且它的生态已经发展得极为庞大。那么,首先一问,什么是IoC和AOP,什么又是JavaBean呢?只是听起来满满的高级感,实际上没有多高级(很多东西都是这样,名字听起来很牛,实际上只是一个很容易理解的东西)

因此,一切的一切,我们还要从JavaBean说起,从这颗豆子生根发芽开始。

一、什么是JavaBean

JavaBean就是有一定规范的Java实体类,跟普通类差不多,不同的是类内部提供了一些公共的方法以便外界对该对象内部属性进行操作,比如set、get操作,实际上,就是我们之前一直在用的:

它的所有属性都是private所有的属性都可以通过get/set方法进行访问,同时还需要有一个无参构造(默认就有)

因此我们之前编写的很多类,其实都可以是一个JavaBean。

二、IoC理论基础

在我们之前的图书管理系统Web应用程序中,我们发现,整个程序其实是依靠各个部分相互协作,共同完成一个操作,比如要展示借阅信息列表,那么首先需要使用Servlet进行请求和响应的数据处理,然后请求的数据全部交给对应的Service(业务层)来处理,当Service发现要从数据库中获取数据时,再向对应的Mapper发起请求。

它们之间就像连接在一起的齿轮,谁也离不开谁:

就像一个团队,每个人的分工都很明确,流水线上的一套操作必须环环相扣,这是一种高度耦合的体系。

虽然这样的体系逻辑非常清晰,整个流程也能够让人快速了解,但是这样存在一个很严重的问题,我们现在的时代实际上是一个软件项目高速迭代的时代,我们发现很多App三天两头隔三差五地就更新,而且是什么功能当下最火,就马不停蹄地进行跟进开发,因此,就很容易出现,之前写好的代码,实现的功能,需要全部推翻,改成新的功能,那么我们就不得不去修改某些流水线上的模块,但是这样一修改,会直接导致整个流水线的引用关系大面积更新。

就像我不想用这个Service实现类了,我想使用其他的实现类用不同的逻辑做这些功能,那么这个时候,我们只能每个类都去挨个进行修改,当项目特别庞大时,光是改个类名就够你改一天。

因此,高耦合度带来的缺点是很明显的,也是现代软件开发中很致命的问题。如果要改善这种情况,我们只能将各个模块进行解耦,让各个模块之间的依赖性不再那么地强。也就是说,Service的实现类,不再由我们决定,而是让程序自己决定,所有的实现类对象,全部交给程序来管理,所有对象之间的关系,也由程序来动态决定,这样就引入了IoC理论。

IOC是Inversion of Control的缩写,翻译为:“控制反转”,把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。

我们可以将对象交给IoC容器进行管理,比如当我们需要一个接口的实现时,由它根据配置文件来决定到底给我们哪一个实现类,这样,我们就可以不用再关心我们要去使用哪一个实现类了,我们只需要关心,给到我的一定是一个可以正常使用的实现类,能用就完事了,反正接口定义了啥,我只管调,这样,我们就可以放心地让一个人去写视图层的代码,一个人去写业务层的代码,开发效率那是高的一匹啊。

高内聚,低耦合,是现代软件的开发的设计目标,而Spring框架就给我们提供了这样的一个IoC容器进行对象的管理。

三、使用IoC容器

首先一定要明确,使用Spring首要目的是为了使得软件项目进行解耦,而不是为了去简化代码!

Spring并不是一个独立的框架,它实际上包含了很多的模块:

而我们首先要去学习的就是Core Container,也就是核心容器模块

Spring是一个非入侵式的框架,就像一个工具库一样,因此,我们只需要直接导入其依赖就可以使用了。

0、配置

pom.xml

!!!!!
(前排提醒:后期增加更多spring相关依赖时,最好保证version版本全部一致!!!)
!!!!!

<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.13</version>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version>
</dependency>

test.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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean name="student" class="com.test.bean.Student"><property name="map"><map><entry key="语文" value="100.0"/><entry key="数学" value="80.0"/><entry key="英语" value="92.5"/></map></property></bean>
</beans>

Student.java

@ToString
public class Student {String name;int age;Map<String, Double> map;public Student() {}public void setMap(Map<String, Double> map) {this.map = map;}public void say(){System.out.println("我的成绩:"+ map);}
}

Main.java

public class Main {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");System.out.println(student); // Student(name=小明, age=10, list=[小明, 小红, 小刚])}
}

1、第一个Spring项目

我们创建一个新的Maven项目,并导入Spring框架的依赖(1),Spring框架的坐标:

<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.13</version>
</dependency>


(这里的Archetype我们选quickstart)
接着在resource中创建一个Spring配置文件(2),命名为test.xml,直接右键点击即可创建:
(可以直接“新建” -》 ”XML配置文件“ -〉“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"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"></beans>


然后,IDEA提示你配置应用程序上下文:(3)

直接点击创建,即可
然后图标就带了个叶子:,代表它是spring的配置文件

最后,在java包下创建com.test目录下的Main,在主方法中编写:

public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("text.xml");
}

(由于是xml文件,这里直接用ClassPath的即可)

这样,一个最基本的Spring项目创建完成了,接着我们来看看如何向IoC容器中注册JavaBean,首先在com.test.bean目录下创建一个Student(4):

//注意,这里还用不到值注入,只需要包含成员属性即可,不用Getter/Setter。
public class Student {String name;int age;
}

然后在配置文件test.xml添加这个bean(5):(注意到外面是beans)
注意这里name和id是一样的,都可以

<bean name="student" class="com.test.bean.Student"/>

也就是说变成了:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean name="student" class="com.test.bean.Student"/>
</beans>

现在,这个对象不需要我们再去生成了(不需要new),而是由IoC容器来提供:
(使用类型class或者name都可以!)

public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");// Student student = context.getBean(Student.class);Student student = (Student) context.getBean("student");System.out.println(student);  // com.test.bean.Student@e45f292,是Student的实例对象
}

实际上,这里得到的Student对象是由Spring通过反射机制帮助我们创建的,初学者会非常疑惑,为什么要这样来创建对象,我们直接new一个它不香吗?为什么要交给IoC容器管理呢?在后面的学习中,我们再慢慢进行体会。

2、将JavaBean交给IoC容器管理

通过前面的例子,我们发现只要将我们创建好的JavaBean通过配置文件编写,即可将其交给IoC容器进行管理,那么,我们来看看,一个JavaBean的详细配置:

<bean name="student" class="com.test.bean.Student"/>

其中name属性(也是id属性),全局唯一,不可出现重复的名称,我们发现,之前其实就是通过Bean的名称来向IoC容器索要对应的对象,也可以通过其他方式获取。

我们现在在主方法中连续获取两个对象:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
Student student2 = (Student) context.getBean("student");
System.out.println(student); // com.test.bean.Student@e45f292
System.out.println(student2); // com.test.bean.Student@e45f292
System.out.println(student == student2); // true

我们发现两次获取到的实际上是同一个对象(由注释发现地址相同),也就是说,默认情况下,通过IoC容器进行管理的JavaBean单例模式singleton的,无论怎么获取始终为那一个对象,那么如何进行修改呢?只需要修改其作用域即可,添加scope属性:

<bean name="student" class="com.test.bean.Student" scope="prototype"/>

通过将其设定为prototype原型模式)来使得其每次都会创建一个新的对象(因此,上面那个连续获取两个对象的程序会输出false,地址也不同)。我们接着来观察一下,这两种模式下Bean的生命周期,我们给构造方法(IoC容器会通过反射调用构造方法)添加一个输出:

public class Student {String name;int age;public Student(){System.out.println("我被构造了!");}
}

(注意到添加这个构造方法后,构造方法的前面也多了个绿色的叶子)
接着我们在mian方法中打上断点来查看对象分别是在什么时候被构造的。

我们发现,当Bean的作用域为单例模式,那么它会在一开始就被创建(context处),而处于原型模式下,只有在获取时才会被创建,也就是说,单例模式下,Bean会被IoC容器存储只要容器没有被销毁,那么此对象将一直存在,而原型模式才是相当于直接new了一个对象,并不会被保存

我们还可以通过配置文件,告诉创建一个对象需要执行此初始化方法,以及销毁一个对象的销毁方法

public class Student {String name;int age;private void init(){System.out.println("我是初始化方法!");}private void destroy(){System.out.println("我是销毁方法!");}public Student(){System.out.println("我被构造了!");}
}
public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");System.out.println(student);context.close();  //手动销毁容器
}

最后在XML文件中编写配置:(即此时是单例模式)

<bean name="student" class="com.test.bean.Student" init-method="init" destroy-method="destroy"/>

接下来测试一下即可。

我被构造了!
我是初始化方法!
com.test.bean.Student@e45f292
我是销毁方法!

我们还可以手动指定Bean的加载顺序,若某个Bean需要保证一定在另一个Bean加载之前加载,那么就可以使用depend-on属性(后面跟上bean的名称而不是类名)。

<bean name="student" class="com.test.bean.Student" depends-on="card"/>
<bean name="card" class="com.test.bean.Card"/>

这样的话,card就比student先构造了

3、依赖注入DI

现在我们已经了解了如何注册和使用一个Bean(刚才调用的都是无参构造),那么,如何向Bean的成员属性进行赋值呢?也就是说,IoC在创建对象时,需要将我们预先给定的属性注入到对象中,非常简单,我们可以使用property标签来实现,但是一定注意,此属性必须存在一个set方法,否则无法赋值:

<bean name="student" class="com.test.bean.Student"><property name="name" value="小明"/>
</bean>
public class Student {String name;int age;public void setName(String name) {this.name = name;}public void say(){System.out.println("我是:"+name);}
}

(这里可以使用lombok插件:

<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version>
</dependency>

然后在Student类上加 @ToString,这样的话在Main中直接sout就可以得到Student(name=小明, age=0)

public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");System.out.println(student);
}

那么,如果成员属性是一个非基本类型非String对象类型,我们该怎么注入呢?

public class Card {}
public class Student {String name;int age;Card card;public void setCard(Card card) {this.card = card;}public void setAge(int age) {this.age = age;}public void setName(String name) {this.name = name;}
}

我们只需要将对应的类型也注册为bean即可,然后直接使用ref属性来进行引用(后面跟上bean名):
(注意这里默认是singleton单例模式,如果student是prototype多例模式,那么每一个student用的都是同一个card对象!!!)

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student"><property name="name" value="小明"/><property name="age" value="10"/><property name="card" ref="card"/>
</bean>

比如,

<bean name="student" class="com.test.bean.Student" scope="prototype"><property name="name" value="小明"/><property name="age" value="10"/><property name="card" ref="card"/>
</bean>
<bean name="card" class="com.test.bean.Card"/>
public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");System.out.println(student); // Student(name=小明, age=10, card=com.test.bean.Card@23e028a9)Student student2 = (Student) context.getBean("student");System.out.println(student2); // Student(name=小明, age=10, card=com.test.bean.Card@23e028a9)
}

(如果student和card都是多例模式,那得到的card对象也不一样了)
比如:

<bean name="student" class="com.test.bean.Student" scope="prototype"><property name="name" value="小明"/><property name="age" value="10"/><property name="card" ref="card"/>
</bean>
<bean name="card" class="com.test.bean.Card" scope="prototype"/>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
System.out.println(student); // Student(name=小明, age=10, card=com.test.bean.Card@4f970963)
Student student2 = (Student) context.getBean("student");
System.out.println(student2); // Student(name=小明, age=10, card=com.test.bean.Card@61f8bee4)

那么,集合如何实现注入呢?我们需要在property内部进行编写:

package com.test.bean;import lombok.ToString;import java.util.List;@ToString
public class Student {String name;int age;List<String> list;public void setName(String name) {this.name = name;}public void setAge(int age) {this.age = age;}public void setList(List<String> list) {this.list = list;}
}
<bean name="student" class="com.test.bean.Student" scope="prototype"><property name="name" value="小明"/><property name="age" value="10"/><property name="list"><array><value>小明</value><value>小红</value><value>小刚</value></array></property>
</bean>
<bean name="card" class="com.test.bean.Card" scope="prototype"/>
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");
Student student = (Student) context.getBean("student");
System.out.println(student); // Student(name=小明, age=10, list=[小明, 小红, 小刚])

现在,我们就可以直接以一个数组的方式将属性注入,注意如果是List类型的话,我们也可以使用array数组。同样的,如果是一个Map类型,我们也可以使用entry来注入:

public class Student {String name;int age;Map<String, Double> map;public void setMap(Map<String, Double> map) {this.map = map;}public void say(){System.out.println("我的成绩:"+ map);}
}
<bean name="student" class="com.test.bean.Student"><property name="map"><map><entry key="语文" value="100.0"/><entry key="数学" value="80.0"/><entry key="英语" value="92.5"/></map></property>
</bean>
Student(name=null, age=0, map={语文=100.0, 数学=80.0, 英语=92.5})

我们还可以使用自动装配来实现属性值的注入:(使用autowire属性)

<bean name="card" class="com.test.bean.Card"/>
<bean name="student" class="com.test.bean.Student" autowire="byType"><property name="name" value="小明"/><property name="age" value="10"/>
</bean>

(然后就成功注入Card了)

如果autowire属性值为byName,就会根据名称去获取,这里bean的name为card,如果把Student中的setCard方法改成setXXX就无法自动注入了,否则就可以自动注入成功(即cardsetCard匹配成功)

自动装配会根据set方法中需要的类型,自动在容器中查找是否存在对应类型或是对应名称以及对应构造方法的Bean,比如我们上面指定的为byType(按类型),那么其中的card属性就会被自动注入类型为Card的Bean

我们已经了解了如何使用set方法来创建对象,那么能否不使用默认的无参构造方法(前面都是 使用默认的无参构造,然后用set),而是指定一个有参构造进行对象的创建呢?我们可以指定构造方法:
如果只有name一个参数的构造方法:

<bean name="student" class="com.test.bean.Student"><constructor-arg name="name" value="小明"/>
</bean>
<bean name="student" class="com.test.bean.Student"><constructor-arg name="name" value="小明"/><constructor-arg index="1" value="18"/> 表示第一个(下标从0开始)参数
</bean>
<bean name="student" class="com.test.bean.Student"><constructor-arg index="0" value="小明"/><constructor-arg index="1" value="18"/>
</bean>

(注意我们可以把value先写再写index或者name或者ref等)
(注意在我们在test.xml中写入constructor时,对应Student类中对应的构造方法前会有绿色叶子,而Student类中其他构造方法不会有叶子)

public class Student {String name;int age;public Student(String name, int age){this.name = name;this.age = age;}public void say(){System.out.println("我是:"+name+"今年"+age+"岁了!");}
}

通过手动指定构造方法参数,我们就可以直接告诉容器使用哪一个构造方法(但也因此,我们必须要有对应的构造方法才可以)来创建对象。

四、面向切面AOP

又是一个听起来很高大上的名词,AOP思想实际上就是:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。也就是说,我们可以使用AOP来帮助我们在方法执行或执行之,做一些额外的操作,实际上,就是代理

通过AOP我们可以在保证原有业务不变的情况下,添加额外的动作,比如我们的某些方法执行完成之后,需要打印日志,那么这个时候,我们就可以使用AOP来帮助我们完成,它可以批量地为这些方法添加动作。可以说,它相当于将我们原有的方法,在不改变源代码的基础上进行了增强(方法)处理。

相当于我们的整个业务流程,被直接斩断,并在断掉的位置添加了一个额外的操作,再连接起来,也就是在一个切点位置插入内容。它的原理实际上就是通过动态代理机制实现的,我们在JavaWeb阶段已经给大家讲解过动态代理了。不过Spring底层并不是使用的JDK提供的动态代理,而是使用的第三方库实现,它能够以父类的形式代理,而不是接口。

0、配置

test.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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"><bean name="student" class="com.test.bean.Student"><property name="map"><map><entry key="语文" value="100.0"/><entry key="数学" value="80.0"/><entry key="英语" value="92.5"/></map></property></bean><bean name="aopTest" class="com.test.aop.AopTest"/><aop:config><aop:aspect ref="aopTest"><aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/><aop:before method="before" pointcut-ref="test"/><aop:after-returning method="after" pointcut-ref="test"/></aop:aspect><aop:aspect ref="aopTest"><aop:pointcut id="test2" expression="@annotation(Deprecated)"/><aop:before method="before" pointcut-ref="test2"/></aop:aspect></aop:config>
</beans>

Main.java

public class Main {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");student.test2();    // 过时方法上会出现下划线}
}

AopTest.java

@Log
public class AopTest {public void after() {log.info("之后!");}public void before(){System.out.println("我是执行之前");}
}

Student.java

@ToString
public class Student {String name;int age;Map<String, Double> map;public Student() {}public void setMap(Map<String, Double> map) {this.map = map;}//分别在test方法执行前后切入public int test(String str) {System.out.println("我是一个测试方法:"+str);return str.length();}@Deprecatedpublic void test2() {System.out.println("我是过时方法!");}
}

1、使用SpringAOP

Spring是支持AOP编程的框架之一(实际上它整合了AspectJ框架的一部分),要使用AOP我们需要先导入一个依赖

<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.3.13</version>
</dependency>

那么,如何使用AOP呢?首先我们要明确,要实现AOP操作,我们需要知道这些内容:

  • 需要切入的,类的哪个方法需要被切入
  • 切入之后需要执行什么动作
  • 是在方法执行前切入还是在方法执行后切
  • 如何告诉Spring需要进行切入

那么我们依次来看,首先需要解决的问题是,找到需要切入的类:

public class Student {String name;int age;//分别在test方法执行前后切入public int test(String str) {System.out.println("我是一个测试方法:"+str);return str.length();}
}

现在我们希望在test方法执行前后添加我们的额外执行的内容,接着,我们来看看如何为方法执行前和执行后添加切入动作。比如现在我们想在方法返回之后,再执行我们的动作,首先定义我们要执行的操作:
(创建 com.test.aop下的AopTest类)

public class AopTest {//执行之后的方法public void after(){log.info("之后!");}//执行之前的方法public void before(){System.out.println("我是执行之前");}
}

那么,现在如何告诉Spring我们需要在方法执行之前和之后插入其他逻辑呢?首先我们将要进行AOP操作的类注册为Bean

<bean name="student" class="com.test.bean.Student"/>
<bean name="aopTest" class="com.test.aop.AopTest"/>

一个是Student类,还有一个就是包含我们要切入方法的AopTest类,注册为Bean后,他们就交给Spring进行管理,这样Spring才能帮助我们完成AOP操作。

接着,我们需要告诉Spring,我们需要添加切入点,首先将顶部修改为,引入aop相关标签
(直接在test.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:aop="http://www.springframework.org/schema/aop"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

(在test.xml文件中,与若干个bean同级)
通过使用aop:config来添加一个新的AOP配置

<aop:config></aop:config>

首先第一行,我们需要告诉Spring,我们要切入的是哪一个的哪个或是哪些方法
(在 < aop:config>的里面:)
(切点)

<aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/>

其中,expression属性的execution填写格式如下:

修饰符 包名.类名.方法名称(方法参数)
  • 修饰符:public、protected、private、包括返回值类型、static等等(使用*代表任意修饰符)
  • 包名:如com.test(*代表全部,比如com.*代表com包下的全部包)
  • 类名:使用*也可以代表包下的所有类
  • 方法名称:可以使用*代表全部方法
  • 方法参数:填写对应的参数即可,比如(String, String),也可以使用*来代表任意一个参数,使用…代表所有参数。

也可以使用其他属性来进行匹配,比如@annotation可以用于表示标记了哪些注解的方法被切入

<aop:config><aop:aspect ref="aopTest"><aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/><aop:before method="before" pointcut-ref="test"/><aop:after-returning method="after" pointcut-ref="test"/></aop:aspect><aop:aspect ref="aopTest"><aop:pointcut id="test2" expression="@annotation(Deprecated)"/><aop:before method="before" pointcut-ref="test2"/></aop:aspect>
</aop:config>

Student.java

@Deprecated
public void test2() {System.out.println("我是过时方法!");
}
public class Main {public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = (Student) context.getBean("student");student.test2();    // 过时方法上会出现下划线}
}
我是执行之前
我是过时方法!

接着,我们需要为此方法添加一个执行前动作和一个执行后动作:

(在 < aop:config>的里面:,与aop:pointcut同级)
(ref属性为 引用,就是 在哪个bean(其id)对应的行为)
(before是方法执行之前;after-returning是方法返回之后)
(after-returning的pointcut-ref就是引用哪一个切点(的id),注意我们这里用-ref的这个,如果不-ref就是单独定义了一个pointcut)

<aop:aspect ref="aopTest"><aop:before method="before" pointcut-ref="test"/><aop:after-returning method="after" pointcut-ref="test"/>
</aop:aspect>

最终结果:
(pointcut可以写在aspect里面)
(定义好后会出现一个图标)

<aop:config><aop:aspect ref="aopTest"><aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/><aop:before method="before" pointcut-ref="test"/><aop:after-returning method="after" pointcut-ref="test"/></aop:aspect>
</aop:config>

这样,我们就完成了全部的配置,现在来实验一下吧:

public static void main(String[] args) {ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("test.xml");Student student = context.getBean(Student.class);student.test("lbwnb");System.out.println(student.getClass());
}
我是执行之前
我是一个测试方法:lbwnb
class com.test.bean.Student$$EnhancerBySpringCGLIB$$36d692f0
九月 21, 2022 12:48:36 上午 com.test.aop.AopTest after
信息: 之后!

(且我们发现student的class名称很诡异, $$就是内部类,CGLIB是一个框架实现了动态代理,也就是说我们的student是一个已经被代理的对象,也就是说我们aop就是通过本质就是通过代理实现的)
(注意必须是由spring得到而不是new出来的,才能这样aop!!)

我们发现,方法执行前后,分别调用了我们对应的方法。但是仅仅这样还是不能满足一些需求,在某些情况下,我们可以需求方法执行的一些参数,比如方法执行之后返回了什么,或是方法开始之前传入了什么参数等等。

这个时候,我们可以为我们切入的方法添加一个参数,通过此参数就可以快速获取切点位置的一些信息

//执行之前的方法
public void before(JoinPoint point){System.out.println("我是执行之前");System.out.println(point.getTarget());  //获取执行方法的对象System.out.println(Arrays.toString(point.getArgs()));  //获取传入方法的实参
}

通过添加JoinPoint作为形参,Spring会自动给我们一个实现类对象,这样我们就能获取方法的一些信息了。

最后我们再来看环绕方法,环绕方法相当于完全代理了此方法,它完全将此方法包含在中间,需要我们手动调用才可以执行此方法,并且我们可以直接获取更多的参数

<aop:aspect ref="aopTest"><aop:pointcut id="test" expression="execution(* com.test.bean.Student.test(String))"/><aop:around method="around" pointcut-ref="test"/>
</aop:aspect>
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("方法开始之前");Object value = joinPoint.proceed();System.out.println("方法执行完成,结果为:"+value);return value;
}

注意,如果代理方法存在返回值,那么环绕方法也需要有一个返回值,通过proceed方法来执行代理的方法,也可以修改参数之后调用proceed(Object[]),使用我们给定的参数再去执行:

public Object around(ProceedingJoinPoint joinPoint) throws Throwable {System.out.println("方法开始之前");String arg = joinPoint.getArgs()[0] + "伞兵一号";Object value = joinPoint.proceed(new Object[]{arg});System.out.println("方法执行完成,结果为:"+value);return value;
}

2、使用接口实现AOP

前面我们介绍了如何使用xml配置一个AOP操作,这节课我们来看看如何使用Advice实现AOP

它与我们之前学习的动态代理更接近一些,比如在方法开始执行之前或是执行之后会去调用我们实现的接口,首先我们需要将一个实现Advice接口,只有实现此接口,才可以被通知,比如我们这里使用MethodBeforeAdvice表示是一个在方法执行之前的动作

public class AopTest implements MethodBeforeAdvice {@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {System.out.println("通过Advice实现AOP");}
}

我们发现,方法中包括了很多的参数,其中args代表的是方法执行前得到的实参列表,还有target表示执行此方法的实例对象。运行之后,效果和之前是一样的,但是在这里我们就可以快速获取到更多信息

<aop:config><aop:pointcut id="stu" expression="execution(* com.test.bean.Student.say(String))"/><aop:advisor advice-ref="before" pointcut-ref="stu"/>
</aop:config>

除了此接口以外,还有其他的接口,比如AfterReturningAdvice就需要实现一个方法执行之后的操作:

public class AopTest implements MethodBeforeAdvice, AfterReturningAdvice {@Overridepublic void before(Method method, Object[] args, Object target) throws Throwable {System.out.println("我是方法执行之前!");}@Overridepublic void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {System.out.println("我是方法执行之后!");}
}

其实,我们之前学习的操作正好对应了AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理,通知描述了切面何时执行以及如何执行增强处理,也就是我们上面编写的方法实现。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出,实际上就是我们在方法执行前或是执行后需要做的内容。
  • 切点(PointCut): 可以插入增强处理的连接点,可以是方法执行之前也可以方法执行之后,还可以是抛出异常之类的。
  • 切面(Aspect): 切面是通知和切点的结合,我们之前在xml中定义的就是切面,包括很多信息。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,我们之前都是在将我们的增强处理添加到目标对象,也就是织入(这名字挺有文艺范的)

五、使用注解开发

前面我们已经了解了IoC容器和AOP实现,但是我们发现,要使用这些功能,我们就不得不编写大量的配置,这是非常浪费时间和精力的,并且我们还只是演示了几个小的例子,如果是像之前一样去编写一个完整的Web应用程序,那么产生的配置可能会非常多。能否有一种更加高效的方法能够省去配置呢?当然还是注解了。

所以说,第一步先把你的xml配置文件删了吧,现在我们全部使用注解进行开发(哈哈,是不是感觉XML配置白学了)

直接新建一个项目(这里还是使用maven的quickstart)

pom.xml中还是需要引入spring-context的maven

<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.13</version>
</dependency>

然后创建com.test.Main

0、配置

pom.xml
(注意这里spring-context和spring-aspects的version版本最好要一致!!)

<dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.3.13</version>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.22</version>
</dependency>
<!-- aop -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.3.13</version>
</dependency>

AopTest

@Component
@Aspect
public class AopTest {@Before("execution(* com.test.bean.Student.say(..))")public void before(JoinPoint point){System.out.println("参数列表:"+ Arrays.toString(point.getArgs()));System.out.println("我是之前执行的内容!");}@AfterReturning(value = "execution(* com.test.bean.Student.say(..))", returning = "returnVal")public void after(Object returnVal){System.out.println("方法已返回,结果为:"+returnVal);}@Around("execution(* com.test.bean.Student.say(..))")public Object around(ProceedingJoinPoint point) throws Throwable {System.out.println("方法执行之前!");Object val = point.proceed();System.out.println("方法执行之后!");return val;}
}

Card

public class Card {}

Teacher

public class Teacher {}

Student

@Component
@ToString
public class Student {int age;String name;Card card;@Resourcepublic void setCard(Card card) {System.out.println("通过方法");this.card = card;}public String say(String text) {System.out.println("我叫:" + name + "今年:" + age + "我的card属性为:" + card);return text;}
}

MainConfiguration

@EnableAspectJAutoProxy
@ComponentScans({@ComponentScan("com.test.bean"),@ComponentScan("com.test.aop")
})
@Configuration
@Import({TestConfiguration.class, Date.class})
public class MainConfiguration {@Beanpublic Card card() {return new Card();}
}

TestConfiguration

public class TestConfiguration {@Beanpublic Teacher teacher() {return new Teacher();}
}

Main

public class Main {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);Student student = context.getBean(Student.class);System.out.println(student.say("执行了方法!"));System.out.println("================");Teacher teacher = context.getBean(Teacher.class);System.out.println(teacher);System.out.println("================");System.out.println(context.getBean(Date.class));}
}
通过方法
方法执行之前!
参数列表:[执行了方法!]
我是之前执行的内容!
我叫:null今年:0我的card属性为:com.test.bean.Card@65b3f4a4
方法已返回,结果为:执行了方法!
方法执行之后!
执行了方法!
================
com.test.bean.Teacher@f2ff811
================
Wed Sep 21 20:13:23 CST 2022

1、注解实现配置文件

那么,现在既然不使用XML文件了,那通过注解的方式就只能以实体类的形式进行配置了,我们在要作为配置的类上添加@Configuration注解,我们这里创建一个新的类MainConfiguration(在com.test.config下):

@Configuration
public class MainConfiguration {//没有配置任何Bean
}

(然后配置应用程序上下文,这样就可以在idea下面的spring栏目中看到mainConfiguration)

你可以直接把它等价于

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><!-- 没有配置任何Bean -->
</beans>

那么我们来看看,如何配置Bean,之前我们是直接在配置文件中编写Bean的一些信息,现在在配置类中,我们只需要编写一个方法,并返回我们要创建的Bean的对象即可,并在其上方添加@Bean注解:

(先在 com.test.bean创建Card类

@Bean
public Card card(){return new Card();
}

这样,等价于

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean class="com.test.bean.Card"></bean>
</beans>

我们还可以继续添加@Scope注解来指定作用域,这里我们就用原型模式

@Bean
@Scope("prototype")
public Card card(){return new Card();
}

采用这种方式,我们就可以更加方便地控制一个Bean对象的创建过程,现在相当于这个对象时由我们创建好了再交给Spring进行后续处理,我们可以在对象创建时做很多额外的操作,包括一些属性值的配置等。

既然现在我们已经创建好了配置类,接着我们就可以在主方法中加载此配置类,并创建一个基于配置类的容器:

public class Main {public static void main(String[] args) {//使用AnnotationConfigApplicationContext来实现注解配置AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class); //这里需要告诉Spring哪个类作为配置类Card card = context.getBean(Card.class);  //容器用法和之前一样System.out.println(card);}
}

在配置的过程中,我们可以点击IDEA底部的Spring标签,打开后可以对当前向容器中注册的Bean进行集中查看,并且会标注Bean之间的依赖关系,我们可以发现,Bean的默认名称实际上就是首字母小写的方法名称,我们也可以手动指定
(相当于是之前xml中的name熟悉)
(如果不手动指定,就是首字母小写的方法名称作为名称)

@Bean("lbwnb")
@Scope("prototype")
public Card card(){return new Card();
}

除了像原来一样在配置文件中创建Bean以外,我们还可以直接在类上添加@Component注解来将一个类进行注册(现在最常用的方式),不过要实现这样的方式,我们需要添加一个自动扫描,来告诉Spring需要在哪些包查找我们提供@Component声明的Bean。

只需要在配置类MainConfiguration上添加一个@ComponentScan注解即可,如果要添加多个包进行扫描,可以使用@ComponentScans来批量添加。这里我们演示将bean包下的所有类进行扫描:

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {}

现在删除类中的Bean定义,我们在Student类的上面添加@Component注解,来表示此类型需要作为Bean交给容器进行管理:
(注意@Component

@Component
@Scope("prototype")
public class Student {String name;int age;Card card;
}

同样的,在类上也可以直接添加@Scope来限定作用域。

效果和刚刚实际上是相同的,我们可以来测试一下:

public class Main {public static void main(String[] args) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MainConfiguration.class);System.out.println(context.getBean(Student.class));}
}

我们可以看到IDEA的Spring板块中也显示了我们刚刚通过直接在类上添加@Component声明的Bean。

@Component同样效果的还有@Controller@Service@Repository,但是现在暂时不提,讲到SpringMVC时再来探讨。

现在我们就有两种方式注册一个Bean了,那么如何实现像之前一样的自动注入呢,比如我们将Card也注册为Bean,我们希望Spring自动将其注入到Student的属性中

@Component
public class Student {String name;int sid;Card card;
}

因此,我们可以将此类型,通过这种方式注册为一个Bean

@Component
@Scope("prototype")
public class Card {}

现在,我们在需要注入的位置,添加一个@Resource注解来实现自动装配

@Component
public class Student {String name;int sid;@ResourceCard card;
}

这样的好处是,我们完全不需要创建任何的set方法,只需要添加这样的一个注解就可以了,Spring会跟之前配置文件的自动注入一样,在整个容器中进行查找,并将对应的Bean实例对象注入到此属性中,当然,如果还是需要通过set方法来注入,可以将注解添加到方法上:

@Component
public class Student {String name;int sid;Card card;@Resourcepublic void setCard(Card card) {System.out.println("通过方法");this.card = card;}
}

除了使用@Resource以外,我们还可以使用@Autowired(IDEA不推荐将其使用在字段上,会出现黄标,但是可以放在方法或是构造方法上),它们的效果是一样的,但是它们存在区别,虽然它们都是自动装配

  • @Resource默认ByName如果找不到则ByType,可以添加到set方法字段上。
  • @Autowired默认是byType,可以添加在构造方法、set方法、字段、方法参数上。
    并且@Autowired可以配合@Qualifier使用,来指定一个名称的Bean进行注入
@Autowired
@Qualifier("sxc")
public void setCard(Card card) {System.out.println("通过方法");this.card = card;
}

如果Bean是在配置文件中进行定义的,我们还可以在方法的参数中使用@Autowired来进行自动注入

@ComponentScan("com.test.bean")
@Configuration
public class MainConfiguration {@Beanpublic Student student(@Autowired Card card){Student student = new Student();student.setCard(card);return student;}
}

我们还可以通过@PostConstruct注解来添加构造后执行的方法,它等价于之前讲解的init-method

@PostConstruct
public void init(){System.out.println("我是初始化方法!1");
}

注意它们的顺序:Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct

同样的,如果需要销毁方法,也可以使用@PreDestroy注解,这里就不做演示了。

这样,两种通过注解进行Bean声明的方式就讲解完毕了,那么什么时候该用什么方式去声明呢?

  • 如果要注册为Bean的类是由其他框架提供,我们无法修改其源代码,那么我们就使用第一种方式进行配置。
  • 如果要注册为Bean的类是我们自己编写的,我们就可以直接在类上添加注解,并在配置添加扫描

2、注解实现AOP操作

了解了如何使用注解注册Bean之后,我们接着来看如何通过注解实现AOP操作,首先我们需要在主类添加@EnableAspectJAutoProxy注解,开启AOP注解支持
(并且要ComponentScan新包!)

@EnableAspectJAutoProxy
@ComponentScans({@ComponentScan("com.test.bean"),@ComponentScan("com.test.aop")
})
@Configuration
public class MainConfiguration {}

接着我们只需在定义AOP增强操作的上添加@Aspect注解和@Component将其注册为Bean即可,就像我们之前在配置文件中也要将其注册为Bean:
(com.test.aop下)
(Aspect要先导入依赖!!!

<dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>5.3.13</version>
</dependency>

)

@Component
@Aspect
public class AopTest {}

接着,我们直接在里面编写方法,并将此方法添加到一个切点中,比如我们希望在Student的test方法执行之前执行我们的方法:

public int test(String str){System.out.println("我被调用了:"+str);return str.length();
}

只需要添加@Before注解即可:
(AopTest.java中)

@Before("execution(* com.test.bean.Student.test(..))")
public void before(){System.out.println("我是之前执行的内容!");
}

同样的,我们可以为其添加JoinPoint参数来获取切入点信息

@Before("execution(* com.test.bean.Student.test(..))")
public void before(JoinPoint point){System.out.println("参数列表:"+ Arrays.toString(point.getArgs()));System.out.println("我是之前执行的内容!");
}

我们也可以使用@AfterReturning注解来指定方法返回后的操作

@AfterReturning(value = "execution(* com.test.bean.Student.test(..))", returning = "returnVal")
public void after(Object returnVal){System.out.println("方法已返回,结果为:"+returnVal);
}

我们还可以指定returning属性,并将其作为方法某个参数的实参。同样的,环绕也可以直接通过注解声明:
(环绕必须有ProceedingJoinPoint作为参数,不然不会被调用)
(环绕方法一定要返回一个值,相当于把方法是否执行完全交给了我们,不然结果得不到返回值,为null(void指的就是null))

@Around("execution(* com.test.bean.Student.test(..))")
public Object around(ProceedingJoinPoint point) throws Throwable {System.out.println("方法执行之前!");Object val = point.proceed();System.out.println("方法执行之后!");return val;
}

3、其他注解配置

配置文件可能不止一个,我们有可能会根据模块划分,定义多个配置文件,这个时候,可能会出现很多个配置类,如果我们需要@Import注解来快速将某个类加入到容器中,比如我们现在创建一个新的配置文件,并将数据库Bean也搬过去:

public class Test2Configuration {@Beanpublic Connection getConnection() throws SQLException {System.out.println("创建新的连接!");return DriverManager.getConnection("jdbc:mysql://localhost:3306/study","root","root");}
}
@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import(Test2Configuration.class)
public class TestConfiguration {@ResourceConnection connection;@PostConstructpublic void init(){System.out.println(connection);}
}

注意另一个配置类并没有添加任何注解,实际上,相当于导入的类强制注册为了一个Bean,到现在,我们一共了解了三种注册为Bean的方式,利用这种特性,我们还可以将其他的类型强制注册为Bean

@EnableAspectJAutoProxy
@Configuration
@ComponentScan("com.test")
@Import({Test2Configuration.class, Date.class})
public class TestConfiguration {@ResourceConnection connection;@ResourceDate date;@PostConstructpublic void init(){System.out.println(date+" -> "+connection);}
}

可以看到,日期直接作为一个Bean放入到IoC容器中了,并且时间永远都是被new的那个时间,也就是同一个对象(因为默认是单例模式)。

通过@Import方式最主要为了实现的目标并不是创建Bean,而是为了方便一些框架的Registrar进行Bean定义,在讲解到Spring原理时,我们再来详细讨论,目前只做了解即可。

到这里,关于Spring框架的大致内容就聊得差不多了,其余的内容,我们会在后面继续讲解。

4、spring-test

<dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version><scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-test -->
<dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>4.3.18.RELEASE</version><scope>test</scope>
</dependency>

test/java/com.exp.SpringTest
(其中,UserServiceImpl为一个已注册的容器中的类;MainConfiguration是在源码包下的配置类,也就是说不需要在test包下再写Configuration)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = MainConfiguration.class)
public class SpringTest {@AutowiredUserServiceI userService;@Testpublic void testLogin() {assertTrue(userService.login("testName", "testPassword"));}
}

JavaSSM笔记(一)Spring基础(JavaBean)(IoC理论)(AOP)(使用注解开发)相关推荐

  1. spring学习笔记 -- day02 spring基础、IOC控制反转

    一:srping的介绍 1.spring的体系结构 2.spring资源 a.spring-framework-4.2.4.RELEASE:Spring Framework的官方发行包 b.sprin ...

  2. Spring获取JavaBean的xml形式和注解形式

    Spring获取JavaBean的xml形式和注解形式 文章目录 一.用xml文件方式管理JavaBean 1. 创建一个xml配置文件 2. 将一个Bean交由spring创建并管理 3. 获取Sp ...

  3. Spring核心机制IoC与AoP梳理

    Spring核心机制IoC与AoP梳理 文章目录 Spring核心机制IoC与AoP梳理 IoC介绍 IoC案例介绍 pom文件中IoC环境引入 自己new对象方法举例(正转) IoC创建对象 基于X ...

  4. SSM—Spring框架,IOC理论推导,Hello Spring,IOC创建对象方式,Spring的配置,DI(依赖注入)

    文章目录 1.Spring 1.1.Spring简介(了解) 1.2.spring优点 1.3.组成(七大模块) 1.4.拓展 2.IOC理论推导 2.1.IOC本质 3.Hello Spring 4 ...

  5. spring学习笔记(spring概述和IOC)

    spring5 1.spring的概述 1.1.spring是什么 Spring 是于 2003 年兴起的一个轻量级的 Java 开发框架,它是为了解决企业应用开发的复杂性而创建的. Spring 的 ...

  6. Spring 原理初探——IoC、AOP

    前言 众所周知, 现在的 Spring 框架已经成为构建企业级 Java 应用事实上的标准了,众多的企业项目都构建在 Spring 项目及其子项目之上,特别是 Java Web 项目. Spring ...

  7. Spring总结(IOC、AOP原理以及Spring事务)

    一.概述 1.Spring是一个开源免费且轻量级的框架 , 非侵入式的 . 2.控制反转 IoC , 面向切面 Aop 3 .对事物的支持 , 对框架的支持 一句话概括: Spring 是一个轻量级的 ...

  8. 谈谈Spring中的IOC和AOP概念 - 倪升武的博客 - CSDN博客

    转载于https://blog.csdn.net/eson_15/article/details/51090040 IOC和AOP是Spring中的两个核心的概念,下面谈谈对这两个概念的理解. 1. ...

  9. Spring笔记——使用Spring进行面向切面(AOP)编程

    要进行AOP编程,首先我们要在spring的配置文件中引入aop命名空间: =================== Spring提供了两种切面声明方式,实际工作中我们可以选用其中一种: 1. 基于XM ...

  10. 笔记:Spring基础

    概述 Spring通过以下策略降低Java开发复杂性 基于POJO的轻量级和最小侵入性编程: 侵入式让用户代码产生对框架的依赖,这些代码不能在框架外使用,不利于代码的复用.但侵入式可以使用户跟框架更好 ...

最新文章

  1. OCH\OMS\OTS\MSP\RS\SPI解释
  2. day52 Django全流程
  3. STL-queue.back()队尾误区
  4. 母亲确诊的员工发烧 当当网通知全员在家办公
  5. ViewPager 中添加监听
  6. kvm虚拟机vnc和spice配置
  7. (大一)——自学计划
  8. Atitit spring单元测试 注解 获取服务名 Spring文件单独放在一个文件夹,去掉dubbo配置,方便测试 里面包含的mybatis 找不到,只好设置成相对于class绝对路径可以了
  9. xmind模板打包下载
  10. 虚拟机自省XenAccess和vmitools
  11. 如何从手机中提取system.img文件
  12. BugkuCTF –备份是个好习惯
  13. hbuilder+dcloud开发APP
  14. C# Base64编码、AES等编码加、解密
  15. 屏保问题(即背光灯的关闭)
  16. NVIDIA Jetson之TF卡系统镜像备份与恢复
  17. word选中段尾不选回车符
  18. 我的极限Scrum实践
  19. Bootstrap全部可用类名
  20. 手机威客php源码,最新威客众筹系统完整版PHP源码+支持支付宝和微信多种支付方式+PC+WAP+微信三端数据同步...

热门文章

  1. MySQL数据库实操教程(26)——数据库设计软件Power Designer
  2. 视觉SLAM综述(入门篇)
  3. 全球30米地表覆盖遥感制图关键技术与产品研发
  4. 中国企业一扎堆 这个产业就完蛋!
  5. 高新技术企业认定 国家高新技术企业复审指南
  6. 电脑微信双开功能的实现
  7. 键盘输入任意字符串,打乱里面的内容
  8. 电脑双开微信的脚本 (Windows系统)
  9. 自建SuSE Yast安装源
  10. PMP 项目资源管理