Java最全Spring 学习,看松哥这一篇万余字干货就够了!,java中级面试题库weixin

最后

我想问下大家当初选择做程序员的初衷是什么?有思考过这个问题吗?高薪?热爱?

既然入了这行就应该知道,这个行业是靠本事吃饭的,你想要拿高薪没有问题,请好好磨练自己的技术,不要抱怨。有的人通过培训可以让自己成长,有些人可以通过自律强大的自学能力成长,如果你两者都不占,还怎么拿高薪?

架构师是很多程序员的职业目标,一个好的架构师是不愁所谓的35岁高龄门槛的,到了那个时候,照样大把的企业挖他。为什么很多人想进阿里巴巴,无非不是福利待遇好以及优质的人脉资源,这对个人职业发展是有非常大帮助的。

如果你也想成为一名好的架构师,那或许这份Java核心架构笔记你需要阅读阅读,希望能够对你的职业发展有所帮助。

中高级开发必知必会:

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

this.price = price;

}

}

2.在 xml 文件中注入 Bean

<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd”>

这里需要注意的是,constructor-arg 中的 index 和 Book 中的构造方法参数一一对应。写的顺序可以颠倒,但是 index 的值和 value 要一一对应。

另一种构造方法中的属性注入,则是通过直接指定参数名来注入:

如果有多个构造方法,则会根据给出参数个数以及参数类型,自动匹配到对应的构造方法上,进而初始化一个对象。

3.3.2 set 方法注入

除了构造方法之外,我们也可以通过 set 方法注入值。

set 方法注入,有一个很重要的问题,就是属性名。很多人会有一种错觉,觉得属性名就是你定义的属性名,这个是不对的。在所有的框架中,凡是涉及到反射注入值的,属性名统统都不是 Bean 中定义的属性名,而是通过 Java 中的内省机制分析出来的属性名,简单说,就是根据 get/set 方法分析出来的属性名。

3.3.3 p 名称空间注入

p 名称空间注入,使用的比较少,它本质上也是调用了 set 方法。

3.3.4 外部 Bean 的注入

有时候,我们使用一些外部 Bean,这些 Bean 可能没有构造方法,而是通过 Builder 来构造的,这个时候,就无法使用上面的方式来给它注入值了。

例如在 OkHttp 的网络请求中,原生的写法如下:

public class OkHttpMain {

public static void main(String[] args) {

OkHttpClient okHttpClient = new OkHttpClient.Builder()

.build();

Request request = new Request.Builder()

.get()

.url(“http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg”)

.build();

Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {

@Override

public void onFailure(@NotNull Call call, @NotNull IOException e) {

System.out.println(e.getMessage());

}

@Override

public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

FileOutputStream out = new FileOutputStream(new File(“E:\123.jpg”));

int len;

byte[] buf = new byte[1024];

InputStream is = response.body().byteStream();

while ((len = is.read(buf)) != -1) {

out.write(buf, 0, len);

}

out.close();

is.close();

}

});

}

}

这个 Bean 有一个特点,OkHttpClient 和 Request 两个实例都不是直接 new 出来的,在调用 Builder 方法的过程中,都会给它配置一些默认的参数。这种情况,我们可以使用 静态工厂注入或者实例工厂注入来给 OkHttpClient 提供一个实例。

1.静态工厂注入

首先提供一个 OkHttpClient 的静态工厂:

public class OkHttpUtils {

private static OkHttpClient OkHttpClient;

public static OkHttpClient getInstance() {

if (OkHttpClient == null) {

OkHttpClient = new OkHttpClient.Builder().build();

}

return OkHttpClient;

}

}

在 xml 文件中,配置该静态工厂:

这个配置表示 OkHttpUtils 类中的 getInstance 是我们需要的实例,实例的名字就叫 okHttpClient。然后,在 Java 代码中,获取到这个实例,就可以直接使用了。

public class OkHttpMain {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(“applicationContext.xml”);

OkHttpClient okHttpClient = ctx.getBean(“okHttpClient”, OkHttpClient.class);

Request request = new Request.Builder()

.get()

.url(“http://b.hiphotos.baidu.com/image/h%3D300/sign=ad628627aacc7cd9e52d32d909032104/32fa828ba61ea8d3fcd2e9ce9e0a304e241f5803.jpg”)

.build();

Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {

@Override

public void onFailure(@NotNull Call call, @NotNull IOException e) {

System.out.println(e.getMessage());

}

@Override

public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {

FileOutputStream out = new FileOutputStream(new File(“E:\123.jpg”));

int len;

byte[] buf = new byte[1024];

InputStream is = response.body().byteStream();

while ((len = is.read(buf)) != -1) {

out.write(buf, 0, len);

}

out.close();

is.close();

}

});

}

}

2.实例工厂注入

实例工厂就是工厂方法是一个实例方法,这样,工厂类必须实例化之后才可以调用工厂方法。

这次的工厂类如下:

public class OkHttpUtils {

private OkHttpClient OkHttpClient;

public OkHttpClient getInstance() {

if (OkHttpClient == null) {

OkHttpClient = new OkHttpClient.Builder().build();

}

return OkHttpClient;

}

}

此时,在 xml 文件中,需要首先提供工厂方法的实例,然后才可以调用工厂方法:

自己写的 Bean 一般不会使用这两种方式注入,但是,如果需要引入外部 jar,外部 jar 的类的初始化,有可能需要使用这两种方式。

3.4 复杂属性的注入


3.4.1 对象注入

可以通过 xml 注入对象,通过 ref 来引用一个对象。

3.4.2 数组注入

数组注入和集合注入在 xml 中的配置是一样的。如下:

足球

篮球

乒乓球

注意,array 节点,也可以被 list 节点代替。

当然,array 或者 list 节点中也可以是对象。

足球

篮球

乒乓球

注意,即可以通过 ref 使用外部定义好的 Bean,也可以直接在 list 或者 array 节点中定义 bean。

3.4.3 Map 注入

3.4.4 Properties 注入

99

javaboy

以上 Demo,定义的 User 如下:

public class User {

private Integer id;

private String name;

private Integer age;

private Cat cat;

private String[] favorites;

private List cats;

private Map<String,Object> map;

private Properties info;

@Override

public String toString() {

return “User{” +

“id=” + id +

“, name='” + name + ‘’’ +

“, age=” + age +

“, cat=” + cat +

“, favorites=” + Arrays.toString(favorites) +

“, cats=” + cats +

“, map=” + map +

“, info=” + info +

‘}’;

}

public Properties getInfo() {

return info;

}

public void setInfo(Properties info) {

this.info = info;

}

public Map<String, Object> getMap() {

return map;

}

public void setMap(Map<String, Object> map) {

this.map = map;

}

public List getCats() {

return cats;

}

public void setCats(List cats) {

this.cats = cats;

}

public String[] getFavorites() {

return favorites;

}

public void setFavorites(String[] favorites) {

this.favorites = favorites;

}

public User() {

}

public User(Integer id, String name, Integer age, Cat cat) {

this.id = id;

this.name = name;

this.age = age;

this.cat = cat;

}

public Cat getCat() {

return cat;

}

public void setCat(Cat cat) {

this.cat = cat;

}

public Integer getId() {

return id;

}

public void setId(Integer id) {

this.id = id;

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public Integer getAge() {

return age;

}

public void setAge(Integer age) {

this.age = age;

}

}

3.5 Java 配置


在 Spring 中,想要将一个 Bean 注册到 Spring 容器中,整体上来说,有三种不同的方式。

  • XML 注入,如前文所说

  • Java 配置(通过 Java 代码将 Bean 注册到 Spring 容器中)

  • 自动化扫描

这里我们来看 Java 配置。

Java 配置这种方式在 Spring Boot 出现之前,其实很少使用,自从有了 Spring Boot,Java 配置开发被广泛使用,因为在 Spring Boot 中,不使用一行 XML 配置。

例如我有如下一个 Bean:

public class SayHello {

public String sayHello(String name) {

return "hello " + name;

}

}

在 Java 配置中,我们用一个 Java 配置类去代替之前的 applicationContext.xml 文件。

@Configuration

public class JavaConfig {

@Bean

SayHello sayHello() {

return new SayHello();

}

}

首先在配置类上有一个 @Configuration 注解,这个注解表示这个类不是一个普通类,而是一个配置类,它的作用相当于 applicationContext.xml。

然后,定义方法,方法返回对象,方法上添加 @Bean 注解,表示将这个方法的返回值注入的 Spring 容器中去。也就是说,@Bean 所对应的方法,就相当于 applicationContext.xml 中的 bean 节点。

既然是配置类,我们需要在项目启动时加载配置类。

public class Main {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);

SayHello hello = ctx.getBean(SayHello.class);

System.out.println(hello.sayHello(“javaboy”));

}

}

注意,配置的加载,是使用 AnnotationConfigApplicationContext 来实现。

关于 Java 配置,这里有一个需要注意的问题:Bean 的名字是什么?

Bean 的默认名称是方法名。以上面的案例为例,Bean 的名字是 sayHello。

如果开发者想自定义方法名,也是可以的,直接在 @Bean 注解中进行过配置。如下配置表示修改 Bean 的名字为 javaboy:

@Configuration

public class JavaConfig {

@Bean(“javaboy”)

SayHello sayHello() {

return new SayHello();

}

}

3.6 自动化配置


在我们实际开发中,大量的使用自动配置。

自动化配置既可以通过 Java 配置来实现,也可以通过 xml 配置来实现。

3.6.1 准备工作

例如我有一个 UserService,我希望在自动化扫描时,这个类能够自动注册到 Spring 容器中去,那么可以给该类添加一个 @Service,作为一个标记。

和 @Service 注解功能类似的注解,一共有四个:

  • @Component

  • @Repository

  • @Service

  • @Controller

这四个中,另外三个都是基于 @Component 做出来的,而且从目前的源码来看,功能也是一致的,那么为什么要搞三个呢?主要是为了在不同的类上面添加时方便。

  • 在 Service 层上,添加注解时,使用 @Service

  • 在 Dao 层,添加注解时,使用 @Repository

  • 在 Controller 层,添加注解时,使用 @Controller

  • 在其他组件上添加注解时,使用 @Component

@Service

public class UserService {

public List getAllUser() {

List users = new ArrayList<>();

for (int i = 0; i < 10; i++) {

users.add(“javaboy:” + i);

}

return users;

}

}

添加完成后,自动化扫描有两种方式,一种就是通过 Java 代码配置自动化扫描,另一种则是通过 xml 文件来配置自动化扫描。

3.6.2 Java 代码配置自动扫描

@Configuration

@ComponentScan(basePackages = “org.javaboy.javaconfig.service”)

public class JavaConfig {

}

然后,在项目启动中加载配置类,在配置类中,通过 @ComponentScan 注解指定要扫描的包(如果不指定,默认情况下扫描的是配置类所在的包下载的 Bean 以及配置类所在的包下的子包下的类),然后就可以获取 UserService 的实例了:

public class Main {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);

UserService userService = ctx.getBean(UserService.class);

System.out.println(userService.getAllUser());

}

}

这里有几个问题需要注意:

1.Bean 的名字叫什么?

默认情况下,Bean 的名字是类名首字母小写。例如上面的 UserService,它的实例名,默认就是 userService。如果开发者想要自定义名字,就直接在 @Service 注解中添加即可。

2.有几种扫描方式?

上面的配置,我们是按照包的位置来扫描的。也就是说,Bean 必须放在指定的扫描位置,否则,即使你有 @Service 注解,也扫描不到。

除了按照包的位置来扫描,还有另外一种方式,就是根据注解来扫描。例如如下配置:

@Configuration

@ComponentScan(basePackages = “org.javaboy.javaconfig”,useDefaultFilters = true,excludeFilters = {@ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Controller.class)})

public class JavaConfig {

}

这个配置表示扫描 org.javaboy.javaconfig 下的所有 Bean,但是除了 Controller。

3.6.3 XML 配置自动化扫描

<context:component-scan base-package=“org.javaboy.javaconfig”/>

上面这行配置表示扫描 org.javaboy.javaconfig 下的所有 Bean。当然也可以按照类来扫描。

XML 配置完成后,在 Java 代码中加载 XML 配置即可。

public class XMLTest {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(“applicationContext.xml”);

UserService userService = ctx.getBean(UserService.class);

List list = userService.getAllUser();

System.out.println(list);

}

}

也可以在 XML 配置中按照注解的类型进行扫描:

<context:component-scan base-package=“org.javaboy.javaconfig” use-default-filters=“true”>

<context:exclude-filter type=“annotation” expression=“org.springframework.stereotype.Controller”/>

</context:component-scan>

3.6.4 对象注入

自动扫描时的对象注入有三种方式:

  1. @Autowired

  2. @Resources

  3. @Injected

@Autowired 是根据类型去查找,然后赋值,这就有一个要求,这个类型只可以有一个对象,否则就会报错。@Resources 是根据名称去查找,默认情况下,定义的变量名,就是查找的名称,当然开发者也可以在 @Resources 注解中手动指定。所以,如果一个类存在多个实例,那么就应该使用 @Resources 去注入,如果非常使用 @Autowired,也是可以的,此时需要配合另外一个注解,@Qualifier,在 @Qualifier 中可以指定变量名,两个一起用(@Qualifier 和 @Autowired)就可以实现通过变量名查找到变量。

@Service

public class UserService {

@Autowired

UserDao userDao;

public String hello() {

return userDao.hello();

}

public List getAllUser() {

List users = new ArrayList<>();

for (int i = 0; i < 10; i++) {

users.add(“javaboy:” + i);

}

return users;

}

}

3.7 条件注解


条件注解就是在满足某一个条件的情况下,生效的配置。

3.7.1 条件注解

首先在 Windows 中如何获取操作系统信息?Windows 中查看文件夹目录的命令是 dir,Linux 中查看文件夹目录的命令是 ls,我现在希望当系统运行在 Windows 上时,自动打印出 Windows 上的目录展示命令,Linux 运行时,则自动展示 Linux 上的目录展示命令。

首先定义一个显示文件夹目录的接口:

public interface ShowCmd {

String showCmd();

}

然后,分别实现 Windows 下的实例和 Linux 下的实例:

public class WinShowCmd implements ShowCmd {

@Override

public String showCmd() {

return “dir”;

}

}

public class LinuxShowCmd implements ShowCmd {

@Override

public String showCmd() {

return “ls”;

}

}

接下来,定义两个条件,一个是 Windows 下的条件,另一个是 Linux 下的条件。

public class WindowsCondition implements Condition {

@Override

public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

return context.getEnvironment().getProperty(“os.name”).toLowerCase().contains(“windows”);

}

}

public class LinuxCondition implements Condition {

@Override

public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

return context.getEnvironment().getProperty(“os.name”).toLowerCase().contains(“linux”);

}

}

接下来,在定义 Bean 的时候,就可以去配置条件注解了。

@Configuration

public class JavaConfig {

@Bean(“showCmd”)

@Conditional(WindowsCondition.class)

ShowCmd winCmd() {

return new WinShowCmd();

}

@Bean(“showCmd”)

@Conditional(LinuxCondition.class)

ShowCmd linuxCmd() {

return new LinuxShowCmd();

}

}

这里,一定要给两个 Bean 取相同的名字,这样在调用时,才可以自动匹配。然后,给每一个 Bean 加上条件注解,当条件中的 matches 方法返回 true 的时候,这个 Bean 的定义就会生效。

public class JavaMain {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);

ShowCmd showCmd = (ShowCmd) ctx.getBean(“showCmd”);

System.out.println(showCmd.showCmd());

}

}

条件注解有一个非常典型的使用场景,就是多环境切换。

3.7.2 多环境切换

开发中,如何在 开发/生产/测试 环境之间进行快速切换?Spring 中提供了 Profile 来解决这个问题,Profile 的底层就是条件注解。这个从 @Profile 注解的定义就可以看出来:

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Documented

@Conditional(ProfileCondition.class)

public @interface Profile {

/**

  • The set of profiles for which the annotated component should be registered.

*/

String[] value();

}

class ProfileCondition implements Condition {

@Override

public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());

if (attrs != null) {

for (Object value : attrs.get(“value”)) {

if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {

return true;

}

}

return false;

}

return true;

}

}

我们定义一个 DataSource:

public class DataSource {

private String url;

private String username;

private String password;

@Override

public String toString() {

return “DataSource{” +

“url='” + url + ‘’’ +

“, username='” + username + ‘’’ +

“, password='” + password + ‘’’ +

‘}’;

}

public String getUrl() {

return url;

}

public void setUrl(String url) {

this.url = url;

}

public String getUsername() {

return username;

}

public void setUsername(String username) {

this.username = username;

}

public String getPassword() {

return password;

}

public void setPassword(String password) {

this.password = password;

}

}

然后,在配置 Bean 时,通过 @Profile 注解指定不同的环境:

@Bean(“ds”)

@Profile(“dev”)

DataSource devDataSource() {

DataSource dataSource = new DataSource();

dataSource.setUrl(“jdbc:mysql://127.0.0.1:3306/dev”);

dataSource.setUsername(“root”);

dataSource.setPassword(“123”);

return dataSource;

}

@Bean(“ds”)

@Profile(“prod”)

DataSource prodDataSource() {

DataSource dataSource = new DataSource();

dataSource.setUrl(“jdbc:mysql://192.158.222.33:3306/dev”);

dataSource.setUsername(“jkldasjfkl”);

dataSource.setPassword(“jfsdjflkajkld”);

return dataSource;

}

最后,在加载配置类,注意,需要先设置当前环境,然后再去加载配置类:

public class JavaMain {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();

ctx.getEnvironment().setActiveProfiles(“dev”);

ctx.register(JavaConfig.class);

ctx.refresh();

DataSource ds = (DataSource) ctx.getBean(“ds”);

System.out.println(ds);

}

}

这个是在 Java 代码中配置的。环境的切换,也可以在 XML 文件中配置,如下配置在 XML 文件中,必须放在其他节点后面。

启动类中设置当前环境并加载配置:

public class Main {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext();

ctx.getEnvironment().setActiveProfiles(“prod”);

ctx.setConfigLocation(“applicationContext.xml”);

ctx.refresh();

DataSource dataSource = (DataSource) ctx.getBean(“dataSource”);

System.out.println(dataSource);

}

}

3.8 其他


3.8.1 Bean 的作用域

在 XML 配置中注册的 Bean,或者用 Java 配置注册的 Bean,如果我多次获取,获取到的对象是否是同一个?

public class Main {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(“applicationContext.xml”);

User user = ctx.getBean(“user”, User.class);

User user2 = ctx.getBean(“user”, User.class);

System.out.println(user==user2);

}

}

如上,从 Spring 容器中多次获取同一个 Bean,默认情况下,获取到的实际上是同一个实例。当然我们可以自己手动配置。

通过在 XML 节点中,设置 scope 属性,我们可以调整默认的实例个数。scope 的值为 singleton(默认),表示这个 Bean 在 Spring 容器中,是以单例的形式存在,如果 scope 的值为 prototype,表示这个 Bean 在 Spring 容器中不是单例,多次获取将拿到多个不同的实例。

除了 singleton 和 prototype 之外,还有两个取值,request 和 session。这两个取值在 web 环境下有效。这是在 XML 中的配置,我们也可以在 Java 中配置。

@Configuration

public class JavaConfig {

@Bean

@Scope(“prototype”)

SayHello sayHello() {

return new SayHello();

}

}

在 Java 代码中,我们可以通过 @Scope 注解指定 Bean 的作用域。

当然,在自动扫描配置中,也可以指定 Bean 的作用域。

@Repository

@Scope(“prototype”)

public class UserDao {

public String hello() {

return “userdao”;

}

}

3.8.2 id 和 name 的区别

在 XML 配置中,我们可以看到,即可以通过 id 给 Bean 指定一个唯一标识符,也可以通过 name 来指定,大部分情况下这两个作用是一样的,有一个小小区别:

name 支持取多个。多个 name 之间,用 , 隔开:

此时,通过 user、user1、user2、user3 都可以获取到当前对象:

public class Main {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(“applicationContext.xml”);

User user = ctx.getBean(“user”, User.class);

User user2 = ctx.getBean(“user2”, User.class);

System.out.println(user);

System.out.println(user2);

}

}

而 id 不支持有多个值。如果强行用 , 隔开,它还是一个值。例如如下配置:

这个配置表示 Bean 的名字为 user,user1,user2,user3,具体调用如下:

public class Main {

public static void main(String[] args) {

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(“applicationContext.xml”);

User user = ctx.getBean(“user,user1,user2,user3”, User.class);

User user2 = ctx.getBean(“user,user1,user2,user3”, User.class);

System.out.println(user);

System.out.println(user2);

}

}

3.8.3 混合配置

混合配置就是 Java 配置+XML 配置。混用的话,可以在 Java 配置中引入 XML 配置。

@Configuration

@ImportResource(“classpath:applicationContext.xml”)

public class JavaConfig {

}

在 Java 配置中,通过 @ImportResource 注解可以导入一个 XML 配置。

4. Aware 接口


Aware 接口,从字面上理解就是感知捕获。单纯的一个 Bean 是没有知觉的。

在 3.6.4 节的场景中,之所以 UserDao 能够注入到 UserService ,有一个前提,就是它两个都是被 Spring 容器管理的。如果直接 new 一个 UserService,这是没用的,因为 UserService 没有被 Spring 容器管理,所以也不会给它里边注入 Bean。

在实际开发中,我们可能会遇到一些类,需要获取到容器的详细信息,那就可以通过 Aware 接口来实现。

Aware 是一个空接口,有很多实现类:

这些实现的接口,有一些公共特性:

  1. 都是以 Aware 结尾

  2. 都继承自 Aware

  3. 接口内均定义了一个 set 方法

每一个子接口均提供了一个 set 方法,方法的参数就是当前 Bean 需要感知的内容,因此我们需要在 Bean 中声明相关的成员变量来接受这个参数。接收到这个参数后,就可以通过这个参数获取到容器的详细信息了。

@Component

public class SayHello implements ApplicationContextAware {

private ApplicationContext applicationContext;

public String sayHello(String name) {

//判断容器中是否存在某个 Bean

boolean userDao = applicationContext.containsBean(“userDao333”);

System.out.println(userDao);

return "hello " + name;

}

@Override

public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

this.applicationContext = applicationContext;

}

}

5.1 Aop


Aop(Aspect Oriented Programming),面向切面编程,这是对面向对象思想的一种补充。

面向切面编程,就是在程序运行时,不改变程序源码的情况下,动态的增强方法的功能,常见的使用场景非常多:

  1. 日志

  2. 事务

  3. 数据库操作

这些操作中,无一例外,都有很多模板化的代码,而解决模板化代码,消除臃肿就是 Aop 的强项。

在 Aop 中,有几个常见的概念:

| 概念 | 说明 |

| :-- | :-- |

| 切点 | 要添加代码的地方,称作切点 |

| 通知(增强) | 通知就是向切点动态添加的代码 |

| 切面 | 切点+通知 |

| 连接点 | 切点的定义 |

5.1.1 Aop 的实现

在 Aop 实际上集基于 Java 动态代理来实现的。

Java 中的动态代理有两种实现方式:

  • cglib

  • jdk

5.2 动态代理


基于 JDK 的动态代理。

1.定义一个计算器接口:

public interface MyCalculator {

int add(int a, int b);

}

2.定义计算机接口的实现:

public class MyCalculatorImpl implements MyCalculator {

public int add(int a, int b) {

return a+b;

}

}

3.定义代理类

public class CalculatorProxy {

public static Object getInstance(final MyCalculatorImpl myCalculator) {

return Proxy.newProxyInstance(CalculatorProxy.class.getClassLoader(), myCalculator.getClass().getInterfaces(), new InvocationHandler() {

/**

  • @param proxy 代理对象

  • @param method 代理的方法

  • @param args 方法的参数

  • @return

  • @throws Throwable

*/

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

System.out.println(method.getName()+“方法开始执行啦…”);

Object invoke = method.invoke(myCalculator, args);

System.out.println(method.getName()+“方法执行结束啦…”);

return invoke;

}

});

}

}

Proxy.newProxyInstance 方法接收三个参数,第一个是一个 classloader,第二个是代理多项实现的接口,第三个是代理对象方法的处理器,所有要额外添加的行为都在 invoke 方法中实现。

5.3 五种通知


Spring 中的 Aop 的通知类型有 5 种:

  • 前置通知

  • 后置通知

  • 异常通知

  • 返回通知

  • 环绕通知

具体实现,这里的案例和 5.2 中的一样,依然是给计算器的方法增强功能。

首先,在项目中,引入 Spring 依赖(这次需要引入 Aop 相关的依赖):

org.springframework

spring-context

5.1.9.RELEASE

org.aspectj

aspectjweaver

1.9.5

org.aspectj

aspectjrt

1.9.5

接下来,定义切点,这里介绍两种切点的定义方式:

  • 使用自定义注解

  • 使用规则

其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解,另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。

自定义注解

首先自定义一个注解:

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.RUNTIME)

public @interface Action {

}

然后在需要拦截的方法上,添加该注解,在 add 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响。

@Component

public class MyCalculatorImpl {

@Action

public int add(int a, int b) {

return a + b;

}

public void min(int a, int b) {

System.out.println(a + “-” + b + “=” + (a - b));

}

}

接下来,定义增强(通知、Advice):

@Component

@Aspect//表示这是一个切面

public class LogAspect {

/**

  • @param joinPoint 包含了目标方法的关键信息

  • @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点

*/

@Before(value = “@annotation(Action)”)

public void before(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法开始执行了…”);

}

/**

  • 后置通知

  • @param joinPoint 包含了目标方法的所有关键信息

  • @After 表示这是一个后置通知,即在目标方法执行之后执行

*/

@After(“@annotation(Action)”)

public void after(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法执行结束了…”);

}

/**

  • @param joinPoint

  • @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object

*/

@AfterReturning(value = “@annotation(Action)”,returning = “r”)

public void returing(JoinPoint joinPoint,Integer r) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法返回:”+r);

}

/**

  • 异常通知

  • @param joinPoint

  • @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception

*/

@AfterThrowing(value = “@annotation(Action)”,throwing = “e”)

public void afterThrowing(JoinPoint joinPoint,Exception e) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法抛异常了:”+e.getMessage());

}

/**

  • 环绕通知

  • 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法

  • @param pjp

  • @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配

*/

@Around(“@annotation(Action)”)

public Object around(ProceedingJoinPoint pjp) {

Object proceed = null;

try {

//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知

proceed = pjp.proceed();

} catch (Throwable throwable) {

throwable.printStackTrace();

}

return proceed;

}

}

通知定义完成后,接下来在配置类中,开启包扫描和自动代理:

@Configuration

@ComponentScan

@EnableAspectJAutoProxy//开启自动代理

public class JavaConfig {

}

然后,在 Main 方法中,开启调用:

public class Main {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);

MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);

myCalculator.add(3, 4);

myCalculator.min(3, 4);

}

}

再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。

@Component

@Aspect//表示这是一个切面

public class LogAspect {

/**

  • 可以统一定义切点

*/

@Pointcut(“@annotation(Action)”)

public void pointcut() {

}

/**

  • @param joinPoint 包含了目标方法的关键信息

  • @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点

*/

@Before(value = “pointcut()”)

public void before(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法开始执行了…”);

}

/**

  • 后置通知

  • @param joinPoint 包含了目标方法的所有关键信息

  • @After 表示这是一个后置通知,即在目标方法执行之后执行

*/

@After(“pointcut()”)

public void after(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法执行结束了…”);

}

/**

  • @param joinPoint

  • @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object

*/

@AfterReturning(value = “pointcut()”,returning = “r”)

public void returing(JoinPoint joinPoint,Integer r) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法返回:”+r);

}

/**

  • 异常通知

最后

小编精心为大家准备了一手资料

以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术

【附】架构书籍

  1. BAT面试的20道高频数据库问题解析
  2. Java面试宝典
  3. Netty实战
  4. 算法

BATJ面试要点及Java架构师进阶资料

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

try {

//这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知

proceed = pjp.proceed();

} catch (Throwable throwable) {

throwable.printStackTrace();

}

return proceed;

}

}

通知定义完成后,接下来在配置类中,开启包扫描和自动代理:

@Configuration

@ComponentScan

@EnableAspectJAutoProxy//开启自动代理

public class JavaConfig {

}

然后,在 Main 方法中,开启调用:

public class Main {

public static void main(String[] args) {

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);

MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);

myCalculator.add(3, 4);

myCalculator.min(3, 4);

}

}

再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。

@Component

@Aspect//表示这是一个切面

public class LogAspect {

/**

  • 可以统一定义切点

*/

@Pointcut(“@annotation(Action)”)

public void pointcut() {

}

/**

  • @param joinPoint 包含了目标方法的关键信息

  • @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点

*/

@Before(value = “pointcut()”)

public void before(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法开始执行了…”);

}

/**

  • 后置通知

  • @param joinPoint 包含了目标方法的所有关键信息

  • @After 表示这是一个后置通知,即在目标方法执行之后执行

*/

@After(“pointcut()”)

public void after(JoinPoint joinPoint) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法执行结束了…”);

}

/**

  • @param joinPoint

  • @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object

*/

@AfterReturning(value = “pointcut()”,returning = “r”)

public void returing(JoinPoint joinPoint,Integer r) {

Signature signature = joinPoint.getSignature();

String name = signature.getName();

System.out.println(name + “方法返回:”+r);

}

/**

  • 异常通知

最后

小编精心为大家准备了一手资料

[外链图片转存中…(img-vuppRNBo-1715324959989)]

[外链图片转存中…(img-GPZB7Ld8-1715324959989)]

以上Java高级架构资料、源码、笔记、视频。Dubbo、Redis、设计模式、Netty、zookeeper、Spring cloud、分布式、高并发等架构技术

【附】架构书籍

  1. BAT面试的20道高频数据库问题解析
  2. Java面试宝典
  3. Netty实战
  4. 算法

[外链图片转存中…(img-JL5tUHDM-1715324959990)]

BATJ面试要点及Java架构师进阶资料

[外链图片转存中…(img-WMYoJ1a3-1715324959990)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值