开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Spring Boot第八弹,一波带走,教你Spring B

发表于 2020-10-09

持续原创输出,点击上方蓝字关注我


前言
–

自从用了Spring Boot是否有一个感觉,以前MVC的配置都很少用到了,比如视图解析器,拦截器,过滤器等等,这也正是Spring Boot好处之一。

但是往往Spring Boot提供默认的配置不一定适合实际的需求,因此需要能够定制MVC的相关功能,这篇文章就介绍一下如何扩展和全面接管MVC。

Spring Boot 版本

本文基于的Spring Boot的版本是2.3.4.RELEASE。

如何扩展MVC?

在这里需要声明一个前提:配置类上没有标注@EnableWebMvc并且没有任何一个配置类继承了WebMvcConfigurationSupport。至于具体原因,下文会详细解释。

扩展MVC其实很简单,只需要以下步骤:

  1. 创建一个MVC的配置类,并且标注@Configuration注解。
  2. 实现WebMvcConfigurer这个接口,并且实现需要的方法。

WebMvcConfigurer这个接口中定义了MVC相关的各种组件,比如拦截器,视图解析器等等的定制方法,需要定制什么功能,只需要实现即可。

在Spring Boot之前的版本还可以继承一个抽象类WebMvcConfigurerAdapter,不过在2.3.4.RELEASE这个版本中被废弃了,如下:

1
2
复制代码@Deprecated
public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {}

举个栗子:现在要添加一个拦截器,使其在Spring Boot中生效,此时就可以在MVC的配置类重写addInterceptors()方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码/**
* MVC扩展的配置类,实现WebMvcConfigurer接口
*/
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;

/**
* 重写addInterceptors方法,注入自定义的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns("/error");
}
}

操作很简单,除了拦截器,还可以定制视图解析,资源映射处理器等等相关的功能,和Spring MVC很类似,只不过Spring MVC是在XML文件中配置,Spring Boot是在配置类中配置而已。

什么都不配置为什么依然能运行MVC相关的功能?

早期的SSM架构中想要搭建一个MVC其实挺复杂的,需要配置视图解析器,资源映射处理器,DispatcherServlet等等才能正常运行,但是为什么Spring Boot仅仅是添加一个WEB模块依赖即能正常运行呢?依赖如下:

1
2
3
4
复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

其实这已经涉及到了Spring Boot高级的知识点了,在这里就简单的说一下,Spring Boot的每一个starter都会有一个自动配置类,什么是自动配置类呢?自动配置类就是在Spring Boot项目启动的时候会自动加载的类,能够在启动期间就配置一些默认的配置。WEB模块的自动配置类是WebMvcAutoConfiguration。

WebMvcAutoConfiguration这个配置类中还含有如下一个子配置类WebMvcAutoConfigurationAdapter,如下:

1
2
3
4
5
复制代码@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}

WebMvcAutoConfigurationAdapter这个子配置类实现了WebMvcConfigurer这个接口,这个正是MVC扩展接口,这个就很清楚了。自动配置类是在项目启动的时候就加载的,因此Spring Boot会在项目启动时加载WebMvcAutoConfigurationAdapter这个MVC扩展配置类,提前完成一些默认的配置(比如内置了默认的视图解析器,资源映射处理器等等),这也就是为什么没有配置什么MVC相关的东西依然能够运行。

如何全面接管MVC?【不推荐】

全面接管MVC是什么意思呢?全面接管的意思就是不需要Spring Boot自动配置,而是全部使用自定义的配置。

全面接管MVC其实很简单,只需要在配置类上添加一个@EnableWebMvc注解即可。还是添加拦截器,例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码/**
* @EnableWebMvc:全面接管MVC,导致自动配置类失效
*/
@Configuration
@EnableWebMvc
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//添加拦截器
registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns("/error");
}
}

一个注解就能全面接口MVC,是不是很爽,不过,不建议使用。

为什么@EnableWebMvc一个注解就能够全面接管MVC?

what???为什么呢?上面刚说过自动配置类WebMvcAutoConfiguration会在项目启动期间加载一些默认的配置,这会怎么添加一个@EnableWebMvc注解就不行了呢?

其实很简单,@EnableWebMvc源码如下:

1
2
3
4
5
6
复制代码@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}

其实重要的就是这个@Import(DelegatingWebMvcConfiguration.class)注解了,Spring中的注解,快速导入一个配置类DelegatingWebMvcConfiguration,源码如下:

1
2
复制代码@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {}

明白了,@EnableWebMvc这个注解实际上就是导入了一个WebMvcConfigurationSupport子类型的配置类而已。

而WEB模块的自动配置类有这么一行注解@ConditionalOnMissingBean(WebMvcConfigurationSupport.class),源码如下:

1
2
3
4
5
6
7
8
复制代码@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

这个注解@ConditionalOnMissingBean什么意思呢?简单的说就是IOC容器中没有指定的Bean这个配置才会生效。

一切都已经揭晓了,@EnableWebMvc导入了一个WebMvcConfigurationSupport类型的配置类,导致了自动配置类WebMvcAutoConfiguration标注的@@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)判断为false了,从而自动配置类失效了。

总结

扩展和全面接管MVC都很简单,但是不推荐全面接管MVC,一旦全面接管了,WEb模块的这个starter将没有任何意义,一些全局配置文件中与MVC相关的配置也将会失效。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Google】 再见 SharedPreferences

发表于 2020-10-09

Google 新增加了一个新 Jetpack 的成员 DataStore,主要用来替换 SharedPreferences, DataStore 应该是开发者期待已久的库,DataStore 是基于 Flow 实现的,一种新的数据存储方案,它提供了两种实现方式:

  • Proto DataStore:存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地,protocol buffers 现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用,我们在部分业务场景中也用到了 protocol buffers,会在后续的文章详细分析
  • Preferences DataStore:以键值对的形式存储在本地和 SharedPreferences 类似,但是 DataStore 是基于 Flow 实现的,不会阻塞主线程,并且保证类型安全

因此 Jetpack DataStore 将会分为 2 篇文章来分析:

  • 再见 SharedPreferences 拥抱 Jetpack DataStore (一) : 主要来分析 Preferences DataStore
  • 再见 SharedPreferences 拥抱 Jetpack DataStore (二) : 主要来分析 Proto DataStore

今天这篇文章主要来介绍 Jetpack DataStore 其中一种实现方式 Preferences DataStore,文章中的示例代码,已经上传到 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple

GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

这篇文章会涉及到 Koltin flow 相关内容,如果不了解可以先去看另外一篇文章 Kotlin Flow 是什么?Channel 是什么?

通过这篇文章你将学习到以下内容:

  • 那些年我们所经历的 SharedPreferences 坑?
  • 为什么需要 DataStore?它为我们解决了什么问题?
  • 如何在项目中使用 DataStore?
  • 如何迁移 SharedPreferences 到 DataStore?
  • MMKV、DataStore、SharedPreferences 的不同之处?

一个新库的出现必定为我们解决了一些问题,那么 Jetpack DataStore 为我们解决什么问题呢,在分析之前,我们需要先来了解 SharedPreferences 都有那些坑。

那些年我们所经历的 SharedPreferences 坑

SharedPreference 是一个轻量级的数据存储方式,使用起来也非常方便,以键值对的形式存储在本地,初始化 SharedPreference 的时候,会将整个文件内容加载内存中,因此会带来以下问题:

  • 通过 getXXX() 方法获取数据,可能会导致主线程阻塞
  • SharedPreference 不能保证类型安全
  • SharedPreference 加载的数据会一直留在内存中,浪费内存
  • apply() 方法虽然是异步的,可能会发生 ANR,在 8.0 之前和 8.0 之后实现各不相同
  • apply() 方法无法获取到操作成功或者失败的结果

接下来我们逐个来分析一下 SharedPreferences 带来的这些问题,在文章中 SharedPreference 简称 SP。

getXXX() 方法可能会导致主线程阻塞

所有 getXXX() 方法都是同步的,在主线程调用 get 方法,必须等待 SP 加载完毕,会导致主线程阻塞,下面的代码,我相信小伙伴们并不陌生。

1
2
java复制代码val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 异步加载 SP 文件内容
sp.getString("jetpack", ""); // 等待 SP 加载完毕

调用 getSharedPreferences() 方法,最终会调用 SharedPreferencesImpl#startLoadFromDisk() 方法开启一个线程异步读取数据。

frameworks/base/core/java/android/app/SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码private final Object mLock = new Object();
private boolean mLoaded = false;
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}

正如你所看到的,开启一个线程异步读取数据,当我们正在读取一个比较大的数据,还没读取完,接着调用 getXXX() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}

private void awaitLoadedLocked() {
......
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
......
}

在同步方法内调用了 wait() 方法,会一直等待 getSharedPreferences() 方法开启的线程读取完数据才能继续往下执行,如果读取几 KB 的数据还好,假设读取一个大的文件,势必会造成主线程阻塞。

SP 不能保证类型安全

调用 getXXX() 方法的时候,可能会出现 ClassCastException 异常,因为使用相同的 key 进行操作的时候,putXXX 方法可以使用不同类型的数据覆盖掉相同的 key。

1
2
3
4
5
java复制代码val key = "jetpack"
val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE) // 异步加载 SP 文件内容

sp.edit { putInt(key, 0) } // 使用 Int 类型的数据覆盖相同的 key
sp.getString(key, ""); // 使用相同的 key 读取 Sting 类型的数据

使用 Int 类型的数据覆盖掉相同的 key,然后使用相同的 key 读取 Sting 类型的数据,编译正常,但是运行会出现以下异常。

1
vbnet复制代码java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

SP 加载的数据会一直留在内存中

通过 getSharedPreferences() 方法加载的数据,最后会将数据存储在静态的成员变量中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arduino复制代码// 调用 getSharedPreferences 方法,最后会调用 getSharedPreferencesCacheLocked 方法
public SharedPreferences getSharedPreferences(File file, int mode) {
......
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
return sp;
}

// 通过静态的 ArrayMap 缓存 SP 加载的数据
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

// 将数据保存在 sSharedPrefsCache 中
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
......

ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}

return packagePrefs;
}

通过静态的 ArrayMap 缓存每一个 SP 文件,而每个 SP 文件内容通过 Map 缓存键值对数据,这样数据会一直留在内存中,浪费内存。

apply() 方法是异步的,可能会发生 ANR

apply() 方法是异步的,为什么还会造成 ANR 呢?曾今的字节跳动就出现过这个问题,具体详情可以点击这里前去查看 剖析 SharedPreference apply 引起的 ANR 问题 而且 Google 也明确指出了 apply() 的问题。

简单总结一下:apply() 方法是异步的,本身是不会有任何问题,但是当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会一直等待 apply() 方法将数据保存成功,否则会一直等待,从而阻塞主线程造成 ANR,一起来分析一下为什么异步方法还会阻塞主线程,先来看看 apply() 方法的实现。

frameworks/base/core/java/android/app/SharedPreferencesImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public void apply() {
final long startTime = System.currentTimeMillis();

final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
mcr.writtenToDiskLatch.await(); // 等待
......
}
};
// 将 awaitCommit 添加到队列 QueuedWork 中
QueuedWork.addFinisher(awaitCommit);

Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 8.0 之前加入到一个单线程的线程池中执行
// 8.0 之后加入 HandlerThread 中执行写入任务
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
  • 将一个 awaitCommit 的 Runnable 任务,添加到队列 QueuedWork 中,在 awaitCommit 中会调用 await() 方法等待,在 handleStopService 、 handleStopActivity 等等生命周期会以这个作为判断条件,等待任务执行完毕
  • 将一个 postWriteRunnable 的 Runnable 写任务,通过 enqueueDiskWrite 方法,将写入任务加入到队列中,而写入任务在一个线程中执行

注意:在 8.0 之前和 8.0 之后 enqueueDiskWrite() 方法实现逻辑各不相同

在 8.0 之前调用 enqueueDiskWrite() 方法,将写入任务加入到 单个线程的线程池 中执行,如果 apply() 多次的话,任务将会依次执行,效率很低,android-7.0.0_r34 源码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码// android-7.0.0_r34: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
......
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}

// android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}

通过 Executors.newSingleThreadExecutor() 方法创建了一个 单个线程的线程池,因此任务是串行的,通过 apply() 方法创建的任务,都会添加到这个线程池内。

在 8.0 之后将写入任务加入到 LinkedList 链表中,在 HandlerThread 中执行写入任务,android-10.0.0_r14 源码如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码// android-10.0.0_r14: frameworks/base/core/java/android/app/SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
......
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}

// android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

private static final LinkedList<Runnable> sWork = new LinkedList<>();

public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler(); // 获取 handlerThread.getLooper() 生成 Handler 对象
synchronized (sLock) {
sWork.add(work); // 将写入任务加入到 LinkedList 链表中

if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}

在 8.0 之后通过调用 handlerThread.getLooper() 方法生成 Handler,任务都会在 HandlerThread 中执行,所有通过 apply() 方法创建的任务,都会添加到 LinkedList 链表中。

当生命周期处于 handleStopService() 、 handlePauseActivity() 、 handleStopActivity() 的时候会调用 QueuedWork.waitToFinish() 会等待写入任务执行完毕,我们以其中 handlePauseActivity() 方法为例。

1
2
3
4
5
6
7
8
arduino复制代码public void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving,
int configChanges, PendingTransactionActions pendingActions, String reason) {
......
// 确保写任务都已经完成
QueuedWork.waitToFinish();
......
}
}

正如你所看到的在 handlePauseActivity() 方法中,调用了 QueuedWork.waitToFinish() 方法,会等待所有的写入执行完毕,Google 在 8.0 之后对这个方法做了很大的优化,一起来看一下 8.0 之前和 8.0 之后的区别。

注意:在 8.0 之前和 8.0 之后 waitToFinish() 方法实现逻辑各不相同

在 8.0 之前 waitToFinish() 方法只做了一件事,会一直等待写入任务执行完毕,我先来看看在 android-7.0.0_r34 源码实现。

android-7.0.0_r34: frameworks/base/core/java/android/app/QueuedWork.java

1
2
3
4
5
6
7
8
9
csharp复制代码private static final ConcurrentLinkedQueue<Runnable> sPendingWorkFinishers =
new ConcurrentLinkedQueue<Runnable>();

public static void waitToFinish() {
Runnable toFinish;
while ((toFinish = sPendingWorkFinishers.poll()) != null) {
toFinish.run(); // 相当于调用 `mcr.writtenToDiskLatch.await()` 方法
}
}
  • sPendingWorkFinishers 是 ConcurrentLinkedQueue 实例,apply 方法会将写入任务添加到 sPendingWorkFinishers 队列中,在 单个线程的线程池 中执行写入任务,线程的调度并不由程序来控制,也就是说当生命周期切换的时候,任务不一定处于执行状态
  • toFinish.run() 方法,相当于调用 mcr.writtenToDiskLatch.await() 方法,会一直等待
  • waitToFinish() 方法就做了一件事,会一直等待写入任务执行完毕,其它什么都不做,当有很多写入任务,会依次执行,当文件很大时,效率很低,造成 ANR 就不奇怪了,尤其像字节跳动这种大规模的 App

在 8.0 之后 waitToFinish() 方法做了很大的优化,当生命周期切换的时候,会主动触发任务的执行,而不是一直在等着,我们来看看 android-10.0.0_r14 源码实现。

android-10.0.0_r14: frameworks/base/core/java/android/app/QueuedWork.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码private static final LinkedList<Runnable> sFinishers = new LinkedList<>();
public static void waitToFinish() {
......
try {
processPendingWork(); // 主动触发任务的执行
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}

try {
// 等待任务执行完毕
while (true) {
Runnable finisher;

synchronized (sLock) {
finisher = sFinishers.poll(); // 从 LinkedList 中取出任务
}

if (finisher == null) { // 当 LinkedList 中没有任务时会跳出循环
break;
}

finisher.run(); // 相当于调用 `mcr.writtenToDiskLatch.await()`
}
}

......
}

在 waitToFinish() 方法中会主动调用 processPendingWork() 方法触发任务的执行,在 HandlerThread 中执行写入任务。

另外还做了一个很重要的优化,当调用 apply() 方法的时候,执行磁盘写入,都是全量写入,在 8.0 之前,调用 N 次 apply() 方法,就会执行 N 次磁盘写入,在 8.0 之后,apply() 方法调用了多次,只会执行最后一次写入,通过版本号来控制的。

SharedPreferences 的另外一个缺点就是 apply() 方法无法获取到操作成功或者失败的结果,而 commit() 方法是可以接收 MemoryCommitResult 里面的一个 boolean 参数作为结果,来看一下它们的方法签名。

1
2
3
typescript复制代码public void apply() { ... }

public boolean commit() { ... }

SP 不能用于跨进程通信

我们在创建 SP 实例的时候,需要传入一个 mode,如下所示:

1
ini复制代码val sp = getSharedPreferences("ByteCode", Context.MODE_PRIVATE)

Context 内部还有一个 mode 是 MODE_MULTI_PROCESS,我们来看一下这个 mode 做了什么

1
2
3
4
5
6
7
8
scss复制代码public SharedPreferences getSharedPreferences(File file, int mode) {
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
// 重新读取 SP 文件内容
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}

在这里就做了一件事,当遇到 MODE_MULTI_PROCESS 的时候,会重新读取 SP 文件内容,并不能用 SP 来做跨进程通信。

到这里关于 SharedPreferences 部分分析完了,接下来分析一下 DataStore 为我们解决什么问题?

DataStore 解决了什么问题

Preferences DataStore 主要用来替换 SharedPreferences,Preferences DataStore 解决了 SharedPreferences 带来的所有问题

Preferences DataStore 相比于 SharedPreferences 优点

  • DataStore 是基于 Flow 实现的,所以保证了在主线程的安全性
  • 以事务方式处理更新数据,事务有四大特性(原子性、一致性、 隔离性、持久性)
  • 没有 apply() 和 commit() 等等数据持久的方法
  • 自动完成 SharedPreferences 迁移到 DataStore,保证数据一致性,不会造成数据损坏
  • 可以监听到操作成功或者失败结果

另外 Jetpack DataStore 提供了 Proto DataStore 方式,用于存储类的对象(typed objects ),通过 protocol buffers 将对象序列化存储在本地,protocol buffers 现在已经应用的非常广泛,无论是微信还是阿里等等大厂都在使用,我们在部分场景中也使用了 protocol buffers,在后续的文章会详细的分析。

注意:

Preferences DataStore 只支持 Int , Long , Boolean , Float , String 键值对数据,适合存储简单、小型的数据,并且不支持局部更新,如果修改了其中一个值,整个文件内容将会被重新序列化,可以运行 AndroidX-Jetpack-Practice/DataStoreSimple 体验一下,如果需要局部更新,建议使用 Room。

在项目中使用 Preferences DataStore

Preferences DataStore 主要应用在 MVVM 当中的 Repository 层,在项目中使用 Preferences DataStore 非常简单,只需要 4 步。

1. 需要添加 Preferences DataStore 依赖

1
arduino复制代码implementation "androidx.datastore:datastore-preferences:1.0.0-alpha01"

2. 构建 DataStore

1
2
3
ini复制代码private val PREFERENCE_NAME = "DataStore"
var dataStore: DataStore<Preferences> = context.createDataStore(
name = PREFERENCE_NAME

3. 从 Preferences DataStore 中读取数据

Preferences DataStore 以键值对的形式存储在本地,所以首先我们应该定义一个 Key.

1
ini复制代码val KEY_BYTE_CODE = preferencesKey<Boolean>("ByteCode")

这里和我们之前使用 SharedPreferences 的有点不一样,在 Preferences DataStore 中 Key 是一个 Preferences.Key<T> 类型,只支持 Int , Long , Boolean , Float , String,源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
kotlin复制代码inline fun <reified T : Any> preferencesKey(name: String): Preferences.Key<T> {
return when (T::class) {
Int::class -> {
Preferences.Key<T>(name)
}
String::class -> {
Preferences.Key<T>(name)
}
Boolean::class -> {
Preferences.Key<T>(name)
}
Float::class -> {
Preferences.Key<T>(name)
}
Long::class -> {
Preferences.Key<T>(name)
}
...... // 如果是其他类型就会抛出异常
}
}

当我们定义好 Key 之后,就可以通过 dataStore.data 来获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码override fun readData(key: Preferences.Key<Boolean>): Flow<Boolean> =
dataStore.data
.catch {
// 当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
// 但是如果是其他的异常,最好将它抛出去,不要隐藏问题
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map { preferences ->
preferences[key] ?: false
}
  • Preferences DataStore 是基于 Flow 实现的,所以通过 dataStore.data 会返回一个 Flow<T>,每当数据变化的时候都会重新发出
  • catch 用来捕获异常,当读取数据出现异常时会抛出一个异常,如果是 IOException 异常,会发送一个 emptyPreferences() 来重新使用,如果是其他异常,最好将它抛出去

4. 向 Preferences DataStore 中写入数据

在 Preferences DataStore 中是通过 DataStore.edit() 写入数据的,DataStore.edit() 是一个 suspend 函数,所以只能在协程体内使用,每当遇到 suspend 函数以挂起的方式运行,并不会阻塞主线程。

以挂起的方式运行,不会阻塞主线程 :也就是协程作用域被挂起, 当前线程中协程作用域之外的代码不会阻塞。

首先我们需要创建一个 suspend 函数,然后调用 DataStore.edit() 写入数据即可。

1
2
3
4
5
6
kotlin复制代码override suspend fun saveData(key: Preferences.Key<Boolean>) {
dataStore.edit { mutablePreferences ->
val value = mutablePreferences[key] ?: false
mutablePreferences[key] = !value
}
}

到这里关于 Preferences DataStore 读取数据和写入数据就已经分析完了,接下来分析一下如何迁移 SharedPreferences 到 DataStore。

迁移 SharedPreferences 到 DataStore

迁移 SharedPreferences 到 DataStore 只需要 2 步。

  • 在构建 DataStore 的时候,需要传入一个 SharedPreferencesMigration
1
2
3
4
5
6
7
8
9
ini复制代码dataStore = context.createDataStore(
name = PREFERENCE_NAME,
migrations = listOf(
SharedPreferencesMigration(
context,
SharedPreferencesRepository.PREFERENCE_NAME
)
)
)
  • 当 DataStore 对象构建完了之后,需要执行一次读取或者写入操作,即可完成 SharedPreferences 迁移到 DataStore,当迁移成功之后,会自动删除 SharedPreferences 使用的文件

注意: 只从 SharedPreferences 迁移一次,因此一旦迁移成功之后,应该停止使用 SharedPreferences。

相比于 MMKV 有什么不同之处

最后用一张表格来对比一下 MMKV、DataStore、SharedPreferences 的不同之处,如果发现错误,或者有其他不同之处,期待你来一起完善。

另外在附上一张 Google 分析的 SharedPreferences 和 DataStore 的区别

全文到这里就结束了,这篇文章主要分析了 SharedPreferences 和 DataStore 的优缺点,以及为什么需要引入 DataStore 和如何使用 DataStore,为了节省篇幅源码分析部分会在后续的文章中分析。

关于 SharedPreferences 和 DataStore 相关的代码,已经上传到了 GitHub 欢迎前去查看 AndroidX-Jetpack-Practice/DataStoreSimple ,可以运行一下示例项目,体验一下 SharedPreferences 和 DataStore 效果。

  • GitHub 地址:https://github.com/hi-dhl/AndroidX-Jetpack-Practice

参考文献

  • Preferences DataStore codelab
  • Now in Android #25
  • Prefer Storing Data with Jetpack DataStore
  • 剖析 SharedPreference 引起的 ANR 问题
  • SharedPreferences 问题分析和解决

结语

关注公众号:ByteCode,查看一系列 Android 系统源码、逆向分析、算法、译文、Kotlin、Jetpack 源码相关的文章,如果这篇文章对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。

在国庆期间我梳理了 LeetCode / 剑指 offer 及国内外大厂面试题解,截止到目前为止我已经在 LeetCode 上 AC 了 124+ 题,每题都会用 Java 和 kotlin 去实现,并且每题都有多种解法、解题思路、时间复杂度、空间复杂度分析,题库逐渐完善中,欢迎前去查看。

  • 剑指 offer 及国内外大厂面试题解:在线阅读
  • LeetCode 系列题解:在线阅读


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:AndroidX-Jetpack-Practice
  • LeetCode / 剑指 Offer / 国内外大厂面试题,涵盖: 多线程、数组、栈、队列、字符串、链表、树,查找算法、搜索算法、位运算、排序等等,每道题目都会用 Java 和 kotlin 去实现,仓库持续更新,欢迎前去查看 Leetcode-Solutions-with-Java-And-Kotlin,剑指 offer 及国内外大厂面试题解:在线阅读,LeetCode 系列题解:在线阅读
  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看 Android10-Source-Analysis
  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看 Technical-Article-Translation
  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看 为互联网人而设计导航网站

精选文章

  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(一)
  • 为数不多的人知道的 Kotlin 技巧以及 原理解析(二)
  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 再见吧 buildSrc, 拥抱 Composing builds 提升 Android 编译速度
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Google 推荐在项目中使用 sealed 和 RemoteMediator
  • Kotlin Sealed 是什么?为什么 Google 都用
  • Kotlin StateFlow 搜索功能的实践 DB + NetWork

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java8中的方法引用

发表于 2020-10-08

本文旨在用通俗易懂的语言讲解Java8中引入的一个语法糖–方法引用(method reference)

什么是方法引用?

对一个类某个方法进行引用。形式大致为:

  • 类型::方法名(构造方法为类型::new)
  • 对象::方法名

例子:

  • String的静态方法valueOf对应的方法引用为String::valueOf
  • Object的构造方法对应的方法引用为Object::new
  • 调用对象o的实例方法hashCode对应的方法引用为o::hashCode

方法引用有什么用?

在讲方法引用有什么用之前,先介绍一下lambda表达式

Lambda表达式

在Java8出现之前,当我们需要创建某个接口的一个实例时,通常的做法是:

  • 创建接口的一个实现类,然后通过该类来创建实例
+ 
1
2
3
4
5
6
7
8
9
10
11
typescript复制代码interface A {
void say(String s);
}

class AImpl implemnets A {
@Override
public void say(String s) {
System.out.println(s);
}
}
A a = new AImpl();
  • 通过匿名内部类的形式
+ 
1
2
3
4
5
6
7
8
9
typescript复制代码interface A {
void say(String s);
}
A a = new A() {
@Override
public void say(String s) {
System.out.println(s);
}
}

如果实现类只会被使用一次,通过匿名内部类的方式更简洁。

有相当一部分接口实际上只有一个抽象方法(default方法不是抽像方法),对于这样的接口,我们称之为函数式接口。比如Comparator、Runnable等接口。

jdk中的函数式接口的声明处一般都有@FunctionalInterface注解,加上这个注解的接口,如果不满足函数式接口的规范(只有一个抽象方法),编译器就会报错。

对于函数式接口,Java8引入lambda表达式来进一步简化匿名内部类的写法,因此非函数式接口是不能用lambda表达式的形式来创建接口的实例。

lambda表达式在许多语言中都有,比如在JavaScript中是=>表示的函数写法,在Java中则是->。

Lambda表达式的形式:

1
2
3
4
rust复制代码(参数1,参数2,...) -> {
// 抽象方法实现的代码块
...
}
  1. 如果参数只有1个,则可以省略掉括号
  2. 如果代码块中只有一行代码,则可以省略掉花括号和代码块结尾的分号
  3. 如果代码块中只有一条语句,且该语句为return语句,则可以将return省略

一个比较简短的lambda表达式长这样:

1
2
less复制代码a -> a+1
() -> System.out.println("hello world!")

进一步简化lambda表达式

方法引用的引出是为了简化代码,简化什么代码呢?答案就是简化lambda表达式,而且是对于只有一行代码的lambda表达式。下面来看几个案例:

  1. 将一个整型数字转换成对应的字符串
1
2
3
4
5
6
7
8
css复制代码// 接口
interface A {
String m(Integer i);
}

// 创建A的一个实例,lambda表达式写法
A a = i -> String.valueOf(i);
a.m(1); // 输出 "1"
  1. 将一个整型字符串转换成整型数字
1
2
3
4
5
6
7
8
less复制代码// 接口
interface A {
Integer m(String s);
}

// 创建A的一个实例,lambda表达式写法
A a = s -> Integer.valueOf(s);
a.m("1"); // 输出 1

对于上面的两个lambda表达式,都是这样的一种情况:lambda的形参作为某个静态方法的实参传入,在实际编程中有太多类似的这样的情况,因此对于这种代码,引入了方法引用进行简化,以上两个lambda表达式用方法引用的写法如下:

1
2
ini复制代码A a = String::valueOf
A a = Integer::valueOf

方法引用的几种形式

下面介绍我总结的几种方法引用的转换形式:

  • 类名::静态方法名
  • 对象::实例方法名
  • 类名::实例方法名
  • 类型::new(构造方法的引用)

类名::静态方法名

lambda表达式中调用某个类的静态方法,且lambda的形参作为静态方法的参数传入,并且lambda的方法返回类型要和静态方法的返回类型对应上。如:

1
2
3
rust复制代码str -> Integer.parseInt(str) 
对应的方法引用:
Integer::parseInt

采用擦除法,去掉左右两边一致的参数表

对象::实例方法名

lambda表达式中调用某个对象的某个方法,并且lambda的形参作为该方法的实参,并且lambda的方法返回类型要和对象的实例方法的返回类型对应。如:

1
2
3
4
5
6
7
8
9
10
11
12
css复制代码class A {
void a(String s) {
System.out.println(s);
}
}

interface B {
void b(String s);
}

A a = new A();
B b = s -> a.a(s); // 等价于 B b = a::a;

同样是擦除法记忆,去掉左右两边一致的参数表

类名::实例方法名

lambda表达式中调用lambda形参中第一个参数的某个实例方法,并且lambda形参剩余的n-1个参数作为这个实例方法的实参,并且lambda的方法返回类型要和对象的实例方法的返回类型对应。

1
rust复制代码str -> str.toLowerCase(); // 对应方法引用写法:String::toLowerCase

类型::new

类型我们分为基本数据类型和引用类型.

  1. 基本数据类型

对于普通基本数据类型没有new的操作,但是创建对应的数组则是通过关键字new来完成,lambda的形参作为数组的长度传入,比如:

  1. 引用类型

lambda的形参作为某个类的构造方法的实参,如:

总结

方法引用可使得Java代码编写起来更加简短,所有方法引用的写法都需要满足lambda方法的返回值类型与方法引用的返回值类型一致,即:

  • lambda 方法返回值类型为void ,则方法引用的返回值可以是void或者非void
  • lambda方法返回值非void,则方法引用的返回值类型要保持相同或者符合里氏置换原则(LSP)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot第六弹,拦截器如何配置,看这儿~

发表于 2020-10-08

持续原创输出,点击上方蓝字关注我吧


前言
–

上篇文章讲了Spring Boot的WEB开发基础内容,相信读者朋友们已经有了初步的了解,知道如何写一个接口。

今天这篇文章来介绍一下拦截器在Spring Boot中如何自定义以及配置。

Spring Boot 版本

本文基于的Spring Boot的版本是2.3.4.RELEASE。

什么是拦截器?

Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理。例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。

如何自定义一个拦截器?

自定义一个拦截器非常简单,只需要实现HandlerInterceptor这个接口即可,该接口有三个可以实现的方法,如下:

  1. preHandle()方法:该方法会在控制器方法前执行,其返回值表示是否知道如何写一个接口。中断后续操作。当其返回值为true时,表示继续向下执行;当其返回值为false时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等)。
  2. postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。可以通过此方法对请求域中的模型和视图做出进一步的修改。
  3. afterCompletion()方法:该方法会在整个请求完成,即视图渲染结束之后执行。可以通过此方法实现一些资源清理、记录日志信息等工作。

如何使其在Spring Boot中生效?

其实想要在Spring Boot生效其实很简单,只需要定义一个配置类,实现WebMvcConfigurer这个接口,并且实现其中的addInterceptors()方法即可,代码演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private XXX xxx;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//不拦截的uri
final String[] commonExclude = {}};
registry.addInterceptor(xxx).excludePathPatterns(commonExclude);
}
}

举个栗子

开发中可能会经常遇到短时间内由于用户的重复点击导致几秒之内重复的请求,可能就是在这几秒之内由于各种问题,比如网络,事务的隔离性等等问题导致了数据的重复等问题,因此在日常开发中必须规避这类的重复请求操作,今天就用拦截器简单的处理一下这个问题。

思路

在接口执行之前先对指定接口(比如标注某个注解的接口)进行判断,如果在指定的时间内(比如5秒)已经请求过一次了,则返回重复提交的信息给调用者。

根据什么判断这个接口已经请求了?

根据项目的架构可能判断的条件也是不同的,比如IP地址,用户唯一标识、请求参数、请求URI等等其中的某一个或者多个的组合。

这个具体的信息存放在哪里?

由于是短时间内甚至是瞬间并且要保证定时失效,肯定不能存在事务性数据库中了,因此常用的几种数据库中只有Redis比较合适了。

如何实现?

第一步,先自定义一个注解,可以标注在类或者方法上,如下:

1
2
3
4
5
6
7
8
复制代码@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
/**
* 默认失效时间5秒
*/
long seconds() default 5;
}

第二步,创建一个拦截器,注入到IOC容器中,实现的思路很简单,判断controller的类或者方法上是否标注了@RepeatSubmit这个注解,如果标注了,则拦截判断,否则跳过,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
复制代码/**
* 重复请求的拦截器
* @Component:该注解将其注入到IOC容器中
*/
@Component
public class RepeatSubmitInterceptor implements HandlerInterceptor {

/**
* Redis的API
*/
@Autowired
private StringRedisTemplate stringRedisTemplate;

/**
* preHandler方法,在controller方法之前执行
*
* 判断条件仅仅是用了uri,实际开发中根据实际情况组合一个唯一识别的条件。
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
//只拦截标注了@RepeatSubmit该注解
HandlerMethod method=(HandlerMethod)handler;
//标注在方法上的@RepeatSubmit
RepeatSubmit repeatSubmitByMethod = AnnotationUtils.findAnnotation(method.getMethod(),RepeatSubmit.class);
//标注在controler类上的@RepeatSubmit
RepeatSubmit repeatSubmitByCls = AnnotationUtils.findAnnotation(method.getMethod().getDeclaringClass(), RepeatSubmit.class);
//没有限制重复提交,直接跳过
if (Objects.isNull(repeatSubmitByMethod)&&Objects.isNull(repeatSubmitByCls))
return true;

// todo: 组合判断条件,这里仅仅是演示,实际项目中根据架构组合条件
//请求的URI
String uri = request.getRequestURI();

//存在即返回false,不存在即返回true
Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "", Objects.nonNull(repeatSubmitByMethod)?repeatSubmitByMethod.seconds():repeatSubmitByCls.seconds(), TimeUnit.SECONDS);

//如果存在,表示已经请求过了,直接抛出异常,由全局异常进行处理返回指定信息
if (ifAbsent!=null&&!ifAbsent)
throw new RepeatSubmitException();
}
return true;
}
}

第三步,在Spring Boot中配置这个拦截器,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
//不拦截的uri
final String[] commonExclude = {"/error", "/files/**"};
registry.addInterceptor(repeatSubmitInterceptor).excludePathPatterns(commonExclude);
}
}

OK,拦截器已经配置完成,只需要在需要拦截的接口上标注@RepeatSubmit这个注解即可,如下:

1
2
3
4
5
6
7
8
9
10
11
复制代码@RestController
@RequestMapping("/user")
//标注了@RepeatSubmit注解,全部的接口都需要拦截
@RepeatSubmit
public class LoginController {

@RequestMapping("/login")
public String login(){
return "login success";
}
}

此时,请求这个URI:http://localhost:8080/springboot-demo/user/login在5秒之内只能请求一次。

注意:标注在方法上的超时时间会覆盖掉类上的时间,因为如下一段代码:

1
复制代码Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(uri, "", Objects.nonNull(repeatSubmitByMethod)?repeatSubmitByMethod.seconds():repeatSubmitByCls.seconds(), TimeUnit.SECONDS);

这段代码的失效时间先取值repeatSubmitByMethod中配置的,如果为null,则取值repeatSubmitByCls配置的。

总结

至此,拦截器的内容就介绍完了,其实配置起来很简单,没什么重要的内容。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring 和 Spring Boot 之间到底有啥区别?

发表于 2020-10-08

Spring 和 Spring Boot 之间到底有啥区别?

  • 概述
  • 什么是Spring
  • 什么是Spring Boot
  • 应用程序启动引导配置
  • 打包和部署
  • 结论

概述

对于Spring和SpringBoot到底有什么区别,我听到了很多答案,刚开始迈入学习SpringBoot的我当时也是一头雾水,随着经验的积累、我慢慢理解了这两个框架到底有什么区别,相信对于用了SpringBoot很久的同学来说,还不是很理解SpringBoot到底和Spring有什么区别,看完文章中的比较,或许你有了不同的答案和看法!

什么是Spring

作为Java开发人员,大家都Spring都不陌生,简而言之,Spring框架为开发Java应用程序提供了全面的基础架构支持。它包含一些很好的功能,如依赖注入和开箱即用的模块,如:Spring JDBC 、Spring MVC 、Spring Security、 Spring AOP 、Spring ORM 、Spring Test,这些模块缩短应用程序的开发时间,提高了应用开发的效率例如,在Java Web开发的早期阶段,我们需要编写大量的代码来将记录插入到数据库中。但是通过使用Spring JDBC模块的JDBCTemplate,我们可以将操作简化为几行代码。

什么是Spring Boot

Spring Boot基本上是Spring框架的扩展,它消除了设置Spring应用程序所需的XML配置,为更快,更高效的开发生态系统铺平了道路。

Spring Boot中的一些特征:

  1. 创建独立的Spring应用。
  2. 嵌入式Tomcat、Jetty、 Undertow容器(无需部署war文件)。
  3. 提供的starters 简化构建配置
  4. 尽可能自动配置spring应用。
  5. 提供生产指标,例如指标、健壮检查和外部化配置
  6. 完全没有代码生成和XML配置要求

从配置分析

Maven依赖

首先,让我们看一下使用Spring创建Web应用程序所需的最小依赖项

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.1.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.1.0.RELEASE</version>
</dependency>

与Spring不同,Spring Boot只需要一个依赖项来启动和运行Web应用程序:

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>

在进行构建期间,所有其他依赖项将自动添加到项目中。

另一个很好的例子就是测试库。我们通常使用Spring Test,JUnit,Hamcrest和Mockito库。在Spring项目中,我们应该将所有这些库添加为依赖项。但是在Spring Boot中,我们只需要添加spring-boot-starter-test依赖项来自动包含这些库。

Spring Boot为不同的Spring模块提供了许多依赖项。一些最常用的是:

1
2
3
4
5
go复制代码spring-boot-starter-data-jpa`
`spring-boot-starter-security`
`spring-boot-starter-test`
`spring-boot-starter-web`
`spring-boot-starter-thymeleaf

有关starter的完整列表,请查看Spring文档。

MVC配置

让我们来看一下Spring和Spring Boot创建JSP Web应用程序所需的配置。

Spring需要定义调度程序servlet,映射和其他支持配置。我们可以使用 web.xml 文件或Initializer类来完成此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class MyWebAppInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext container) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.setConfigLocation("com.pingfangushi");
container.addListener(new ContextLoaderListener(context));
ServletRegistration.Dynamic dispatcher = container
.addServlet("dispatcher", new DispatcherServlet(context));
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");
}
}

还需要将@EnableWebMvc注释添加到@Configuration类,并定义一个视图解析器来解析从控制器返回的视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@EnableWebMvc
@Configuration
public class ClientWebConfig implements WebMvcConfigurer {
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver bean
= new InternalResourceViewResolver();
bean.setViewClass(JstlView.class);
bean.setPrefix("/WEB-INF/view/");
bean.setSuffix(".jsp");
return bean;
}
}

再来看SpringBoot一旦我们添加了Web启动程序,Spring Boot只需要在application配置文件中配置几个属性来完成如上操作:

1
2
ini复制代码spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

上面的所有Spring配置都是通过一个名为auto-configuration的过程添加Boot web starter来自动包含的。

这意味着Spring Boot将查看应用程序中存在的依赖项,属性和bean,并根据这些依赖项,对属性和bean进行配置。当然,如果我们想要添加自己的自定义配置,那么Spring Boot自动配置将会退回。

配置模板引擎

现在我们来看下如何在Spring和Spring Boot中配置Thymeleaf模板引擎。

在Spring中,我们需要为视图解析器添加thymeleaf-spring5依赖项和一些配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码@Configuration
@EnableWebMvc
public class MvcWebConfig implements WebMvcConfigurer {

@Autowired
private ApplicationContext applicationContext;

@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(applicationContext);
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
return templateResolver;
}

@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(templateEngine());
registry.viewResolver(resolver);
}
}

SpringBoot1X只需要spring-boot-starter-thymeleaf的依赖项来启用Web应用程序中的Thymeleaf支持。 但是由于Thymeleaf3.0中的新功能,我们必须将thymeleaf-layout-dialect 添加为SpringBoot2XWeb应用程序中的依赖项。配置好依赖,我们就可以将模板添加到src/main/resources/templates文件夹中,SpringBoot将自动显示它们。

Spring Security 配置

为简单起见,我们使用框架默认的HTTP Basic身份验证。让我们首先看一下使用Spring启用Security所需的依赖关系和配置。 Spring首先需要依赖 spring-security-web和spring-security-config 模块。接下来, 我们需要添加一个扩展WebSecurityConfigurerAdapter的类,并使用@EnableWebSecurity注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scala复制代码@Configuration
@EnableWebSecurity
public class CustomWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password(passwordEncoder()
.encode("password"))
.authorities("ROLE_ADMIN");
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.httpBasic();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

这里我们使用inMemoryAuthentication来设置身份验证。同样,Spring Boot也需要这些依赖项才能使其工作。但是我们只需要定义spring-boot-starter-security的依赖关系,因为这会自动将所有相关的依赖项添加到类路径中。

Spring Boot中的安全配置与上面的相同 。

应用程序启动引导配置

Spring和Spring Boot中应用程序引导的基本区别在于servlet。Spring使用web.xml 或SpringServletContainerInitializer作为其引导入口点。Spring Boot仅使用Servlet 3功能来引导应用程序,下面让我们详细来了解下

Spring 引导配置

Spring支持传统的web.xml引导方式以及最新的Servlet 3+方法。

配置web.xml方法启动的步骤

  • Servlet容器(服务器)读取web.xml
  • web.xml中定义的DispatcherServlet由容器实例化
  • DispatcherServlet通过读取WEB-INF / {servletName} -servlet.xml来创建WebApplicationContext。最后,DispatcherServlet注册在应用程序上下文中定义的bean

使用Servlet 3+方法的Spring启动步骤

容器搜索实现ServletContainerInitializer的类并执行SpringServletContainerInitializer找到实现所有类WebApplicationInitializer``WebApplicationInitializer创建具有XML或上下文@Configuration类WebApplicationInitializer创建DispatcherServlet与先前创建的上下文。

SpringBoot 引导配置

Spring Boot应用程序的入口点是使用@SpringBootApplication注释的类

1
2
3
4
5
6
typescript复制代码@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

默认情况下,Spring Boot使用嵌入式容器来运行应用程序。在这种情况下,Spring Boot使用public static void main入口点来启动嵌入式Web服务器。此外,它还负责将Servlet,Filter和ServletContextInitializer bean从应用程序上下文绑定到嵌入式servlet容器。Spring Boot的另一个特性是它会自动扫描同一个包中的所有类或Main类的子包中的组件。

Spring Boot提供了将其部署到外部容器的方式。我们只需要扩展SpringBootServletInitializer即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scala复制代码/**
* War部署
*
*/
public class ServletInitializer extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}

@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
servletContext.addListener(new HttpSessionEventPublisher());
}
}

这里外部servlet容器查找在war包下的META-INF文件夹下MANIFEST.MF文件中定义的Main-class,SpringBootServletInitializer将负责绑定Servlet,Filter和ServletContextInitializer。

打包和部署

最后,让我们看看如何打包和部署应用程序。这两个框架都支持Maven和Gradle等通用包管理技术。但是在部署方面,这些框架差异很大。例如,Spring Boot Maven插件在Maven中提供Spring Boot支持。它还允许打包可执行jar或war包并就地运行应用程序。

在部署环境中Spring Boot 对比Spring的一些优点包括:

  • 提供嵌入式容器支持
  • 使用命令java -jar独立运行jar
  • 在外部容器中部署时,可以选择排除依赖关系以避免潜在的jar冲突
  • 部署时灵活指定配置文件的选项
  • 用于集成测试的随机端口生成

结论

简而言之,我们可以说Spring Boot只是Spring本身的扩展,使开发,测试和部署更加方便。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一次线上JVM调优实践,FullGC40次/天到10天一次的

发表于 2020-10-07

通过这一个多月的努力,将FullGC从40次/天优化到近10天才触发一次,而且YoungGC的时间也减少了一半以上,这么大的优化,有必要记录一下中间的调优过程。

对于JVM垃圾回收,之前一直都是处于理论阶段,就知道新生代,老年代的晋升关系,这些知识仅够应付面试使用的。前一段时间,线上服务器的FullGC非常频繁,平均一天40多次,而且隔几天就有服务器自动重启了,这表明的服务器的状态已经非常不正常了,得到这么好的机会,当然要主动请求进行调优了。未调优前的服务器GC数据,FullGC非常频繁。

image.png

首先服务器的配置非常一般(2核4G),总共4台服务器集群。每台服务器的FullGC次数和时间基本差不多。其中JVM几个核心的启动参数为:

1
ruby复制代码-Xms1000M -Xmx1800M -Xmn350M -Xss300K -XX:+DisableExplicitGC -XX:SurvivorRatio=4 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:LargePageSizeInBytes=128M -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC

-Xmx1800M:设置JVM最大可用内存为1800M。

-Xms1000m:设置JVM初始化内存为1000m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn350M:设置年轻代大小为350M。整个JVM内存大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss300K:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

第一次优化

一看参数,马上觉得新生代为什么这么小,这么小的话怎么提高吞吐量,而且会导致YoungGC的频繁触发,如上如的新生代收集就耗时830s。初始化堆内存没有和最大堆内存一致,查阅了各种资料都是推荐这两个值设置一样的,可以防止在每次GC后进行内存重新分配。基于前面的知识,于是进行了第一次的线上调优:提升新生代大小,将初始化堆内存设置为最大内存

1
2
3
diff复制代码-Xmn350M -> -Xmn800M
-XX:SurvivorRatio=4 -> -XX:SurvivorRatio=8
-Xms1000m ->-Xms1800m

将SurvivorRatio修改为8的本意是想让垃圾在新生代时尽可能的多被回收掉。就这样将配置部署到线上两台服务器(prod,prod2另外两台不变方便对比)上后,运行了5天后,观察GC结果,YoungGC减少了一半以上的次数,时间减少了400s,但是FullGC的平均次数增加了41次。YoungGC基本符合预期设想,但是这个FullGC就完全不行了。

image.png

就这样第一次优化宣告失败。

第二次优化

在优化的过程中,我们的主管发现了有个对象T在内存中有一万多个实例,而且这些实例占据了将近20M的内存。于是根据这个bean对象的使用,在项目中找到了原因:匿名内部类引用导致的,伪代码如下:

1
2
3
4
5
6
7
8
9
typescript复制代码public void doSmthing(T t){
redis.addListener(new Listener(){
public void onTimeout(){
if(t.success()){
//执行操作
}
}
});
}

由于listener在回调后不会进行释放,而且回调是个超时的操作,当某个事件超过了设定的时间(1分钟)后才会进行回调,这样就导致了T这个对象始终无法回收,所以内存中会存在这么多对象实例。

通过上述的例子发现了存在内存泄漏后,首先对程序中的error log文件进行排查,首先先解决掉所有的error事件。然后再次发布后,GC操作还是基本不变,虽然解决了一点内存泄漏问题,但是可以说明没有解决根本原因,服务器还是继续莫名的重启。

内存泄漏调查

经过了第一次的调优后发现内存泄漏的问题,于是大家都开始将进行内存泄漏的调查,首先排查代码,不过这种效率是蛮低的,基本没发现问题。于是在线上不是很繁忙的时候继续进行dump内存,终于抓到了一个大对象。

image.png

image.png

这个对象竟然有4W多个,而且都是清一色的ByteArrowRow对象,可以确认这些数据是数据库查询或者插入时产生的了。于是又进行一轮代码分析,在代码分析的过程中,通过运维的同事发现了在一天的某个时候入口流量翻了好几倍,竟然高达83MB/s,经过一番确认,目前完全没有这么大的业务量,而且也不存在文件上传的功能。咨询了阿里云客服也说明完全是正常的流量,可以排除攻击的可能。

image.png

就在我还在调查入口流量的问题时,另外一个同事找到了根本的原因,原来是在某个条件下,会查询表中所有未处理的指定数据,但是由于查询的时候where条件中少加了模块这个条件,导致查询出的数量达40多万条,而且通过log查看当时的请求和数据,可以判断这个逻辑确实是已经执行了的,dump出的内存中只有4W多个对象,这个是因为dump时候刚好查询出了这么多个,剩下的还在传输中导致的。而且这也能非常好的解释了为什么服务器会自动重启的原因。

解决了这个问题后,线上服务器运行完全正常了,使用未调优前的参数,运行了3天左右FullGC只有5次。

image.png

第二次调优

内存泄漏的问题已经解决了,剩下的就可以继续调优了,经过查看GC log,发现前三次GullGC时,老年代占据的内存还不足30%,却发生了FullGC。于是进行各种资料的调查,在blog.csdn.net/zjwstz/arti… 博客中非常清晰明了的说明metaspace导致FullGC的情况,服务器默认的metaspace是21M,在GC log中看到了最大的时候metaspace占据了200M左右,于是进行如下调优,以下分别为prod1和prod2的修改参数,prod3,prod4保持不变

1
2
3
4
diff复制代码-Xmn350M -> -Xmn800M
-Xms1000M ->1800M
-XX:MetaspaceSize=200M
-XX:CMSInitiatingOccupancyFraction=75

和

1
2
3
4
diff复制代码-Xmn350M -> -Xmn600M
-Xms1000M ->1800M
-XX:MetaspaceSize=200M
-XX:CMSInitiatingOccupancyFraction=75

prod1和2只是新生代大小不一样而已,其他的都一致。到线上运行了10天左右,进行对比:

prod1:

image.png

prod2:

image.png

prod3:

image.png

prod4:

image.png

对比来说,1,2两台服务器FullGC远远低于3,4两台,而且1,2两台服务器的YounGC对比3,4也减少了一半左右,而且第一台服务器效率更为明显,除了YoungGC次数减少,而且吞吐量比多运行了一天的3,4两台的都要多(通过线程启动数量),说明prod1的吞吐量提升尤为明显。

通过GC的次数和GC的时间,本次优化宣告成功,且prod1的配置更优,极大提升了服务器的吞吐量和降低了GC一半以上的时间。

prod1中的唯一一次FullGC:

image.png

image.png

通过GC log上也没看出原因,老年代在cms remark的时候只占据了660M左右,这个应该还不到触发FullGC的条件,而且通过前几次的YoungGC调查,也排除了晋升了大内存对象的可能,通过metaspace的大小,也没有达到GC的条件。这个还需要继续调查,有知道的欢迎指出下,这里先行谢过了。

总结

通过这一个多月的调优总结出以下几点:

  • FullGC一天超过一次肯定就不正常了。
  • 发现FullGC频繁的时候优先调查内存泄漏问题。
  • 内存泄漏解决后,jvm可以调优的空间就比较少了,作为学习还可以,否则不要投入太多的时间。
  • 如果发现CPU持续偏高,排除代码问题后可以找运维咨询下阿里云客服,这次调查过程中就发现CPU 100%是由于服务器问题导致的,进行服务器迁移后就正常了。
  • 数据查询的时候也是算作服务器的入口流量的,如果访问业务没有这么大量,而且没有攻击的问题的话可以往数据库方面调查。
  • 有必要时常关注服务器的GC,可以及早发现问题。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀

出处:club.perfma.com/article/185…

作者:coffeeboy

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

spring源码编译就是这么简单

发表于 2020-10-07
众所周知,spring已然成为javaEE开发的企业标准,面对这个神秘的黑盒,我们无不望而却步。但是“问渠哪得清如许,唯有源头活水来“,想要透彻的理解spring的原理,源码的学习是我门java工程师绕不开的坎。学习spring源码的第一步就是本地构建spring的源码环境,也是笔者今天所有讨论的主题。话不多说,让我们直入今天的主题。

[环境准备]

一、jdk1.8

二、构建工具gradle

 spring在spring4之后都是依托于[gradle](https://gradle.org/)构建,gradle是一款类似maven的现代化项目构建工具,在此就不再多赘述。gradle的社区十分活跃,从版本发布就可一窥其态势,面对如此多的gradle版本,在spring源码构建时如何选择gradle版本是困扰广大初学者一大难题。第二就是如何配置gradle可以提高构建速度?


gradle的版本选择是有矩可循的,打开spring的githuab主页(国内这个网站速度稍慢,可以使用gitee将github的项目导入到gitee中提高访问速度,见附录A),选择你想要分支,笔者以5.1.x为例查看/gradle/wrapper/gradle-wrapper.properties该文件。
1
2
3
4
5
ini复制代码distributionBase=GRADLE_USER_HOME  
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
可以看到distributionUrl中要求的gradle版本为\*\*4.10.3\*\*,去官网下载该版本的发行版,然后安装(解压),配置环境变量(笔者使用的是ubuntu20.04,windows的环境变量的配置可自行百度就不再多赘述)
1
2
3
4
bash复制代码export GRADLE_HOME=/home/mojito/application/gradle/gradle-4.10.3(gradle的主目录)
export PATH=$GRADLE_HOME/bin:$PATH
export GRADLE_USER_HOME=/home/mojito/application/gradle/repository(grale下载的jar
存放的位置)

配置完环境变量,需要配置从阿里云的镜像仓库下载jar包这样可以大大加快下载速度

1
2
bash复制代码cd /home/mojito/application/gradle/gradle-4.10.3/init.d(以自己安装gradle的目录为准)
touch init.gradle

init.gradle文件的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码allprojects{
repositories {
def REPOSITORY_URL = 'http://maven.aliyun.com/nexus/content/groups/public/'
all { ArtifactRepository repo ->
if(repo instanceof MavenArtifactRepository){
def url = repo.url.toString()
if (url.startsWith('https://repo1.maven.org/maven2') || url.startsWith('https://jcenter.bintray.com/')) {
project.logger.lifecycle "Repository ${repo.url} replaced by $REPOSITORY_URL."
remove repo
}
}
}
maven {
url REPOSITORY_URL
}
}
}

一切搞定后测试下是否ready,执行命令出现如下图所示即可

1
css复制代码gradle --version

[开始构建]

构建可参考[spring build from source](https://github.com/spring-projects/spring-framework/wiki/Build-from-Source)(这里不推荐使用gradlew命令,因为这个命令不需要你本地安装gradle,会自动从网上帮你下载gradle对应版本的二进制文件,作为构建项目的基础,但是这个下载及慢,大大影响构建速度,推荐按下面的方式进行)

一、源码下载

spring已被我们导入gitee,clone或下载zip包速度都很快,先将spring源码下载到本地。笔者目录为/home/mojito/workspace/opensource/spring-framework

二、开始构建

1
2
3
4
5
6
7
ruby复制代码1.进入源码目录 cd /home/mojito/workspace/opensource/spring-framework
2.编译oxm模块 gradle cleanIdea :spring-oxm:compileTestJava
3.导入idea 1).新建一个项目(项目随意)
2).建好项目后 File -> New -> Project from Existing Sources
-> Navigate to directory -> Select build.gradle
4.配置当前项目的gradle环境(不配置的会用wrapper中的配置从网上下载gradle,
这里下载很慢,所以建议手动配置gradle,因为本地安装了gradle)
完成上面几步,只需等待构建完成即可,网速快的话耗时二十多分钟即可享受香喷喷的spring源码啦。

[附录]

A:github项目导入gitee

  • 获取项目的github的clone地址(下图clone处的https地址)

  • gitee上选择从github导入项目

  • 将复制的url填在第一个输入框内,点击导入,等待完成即可

完成之后即可获得飞一般的访问速度

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官:连Spring AOP都说不明白,自己走还是我送你?

发表于 2020-10-07

前言

因为假期原因,有一段时间没给大家更新了!和大家说个事吧,放假的时候一位粉丝和我说了下自己的被虐经历,在假期前他去某互联网公司面试,结果直接被人家面试官Spring AOP三连问给问的一脸懵逼!其实我觉着吧,这玩意不是挺简单的吗?

大家在学习 AOP 之前,如果清楚代理模式的话,则学习起来非常轻松,接下来就由我为大家介绍 AOP 这个重要的知识点!

代理模式

代理模式在 Java 开发中是一种比较常见的设计模式。设计目的旨在为服务类与客户类之间插入其他功能,插入的功能对于调用者是透明的,起到伪装控制的作用。如租房的例子:房客、中介、房东。对应于代理模式中即:客户类、代理类 、委托类(被代理类)。

为某一个对象(委托类)提供一个代理(代理类),用来控制对这个对象的访问。委托类和代理类有一个共同的父类或父接口。代理类会对请求做预处理、过滤,将请求分配给指定对象。

生活中常见的代理情况: 租房中介、婚庆公司等

代理模式的两个设计原则:

  1. 代理类与委托类具有相似的行为(共同)
  2. 代理类增强委托类的行为

常用的代理模式:

  1. 静态代理
  2. 动态代理

静态代理

某个对象提供一个代理,代理角色固定,以控制对这个对象的访问。 代理类和委托类有共同的父类或父接口,这样在任何使用委托类对象的地方都可以用代理对象替代。代理类负责请求的预处理、过滤、将请求分派给委托类处理、以及委托类执行完请求后的后续处理。

代理的三要素

  • 有共同的行为(结婚) - 接口
  • 目标角色(新人) - 实现行为
  • 代理角色(婚庆公司) - 实现行为 增强目标对象行为

静态代理的特点

  1. 目标角色固定
  2. 在应用程序执行前就得到目标角色
  3. 代理对象会增强目标对象的行为
  4. 有可能存在多个代理,引起”类爆炸”(缺点)

静态代理的实现

定义行为(共同)定义接口

1
2
3
4
5
6
csharp复制代码/**
* 定义⾏为
*/
public interface Marry {
public void toMarry();
}

目标对象(实现行为)

1
2
3
4
5
6
7
8
9
10
csharp复制代码/**
* 静态代理 ——> ⽬标对象
*/
public class You implements Marry {
// 实现⾏为
@Override
public void toMarry() {
System.out.println("我要结婚了...");
}
}

代理对象(实现行为、增强目标对象的行为)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
csharp复制代码/**
* 静态代理 ——> 代理对象
*/
public class MarryCompanyProxy implements Marry {
// ⽬标对象
private Marry marry;
// 通过构造器将⽬标对象传⼊
public MarryCompanyProxy(Marry marry) {
this.marry = marry;
}
// 实现⾏为
@Override
public void toMarry() {
// 增强⾏为
before();

// 执⾏⽬标对象中的⽅法
marry.toMarry();
// 增强⾏为
after();
}
/**
* 增强⾏为
*/
private void after() {
System.out.println("新婚快乐,早⽣贵⼦!");
}
/**
* 增强⾏为
*/
private void before() {
System.out.println("场地正在布置中...");
}
}

通过代理对象实现目标对象的功能

1
2
3
4
5
6
scss复制代码// ⽬标对象
You you = new You();
// 构造代理⻆⾊同时传⼊真实⻆⾊
MarryCompanyProxy marryCompanyProxy = new MarryCompanyProxy(you);
// 通过代理对象调⽤⽬标对象中的⽅法
marryCompanyProxy.toMarry();

静态代理对于代理的角色是固定的,如 dao 层有20个 dao 类,如果要对方法的访问权限进行代理,此时需要创建20个静态代理角色,引起类爆炸,无法满足生产上的需要,于是就催生了动态代理的思想。

动态代理

相比于静态代理,动态代理在创建代理对象上更加的灵活,动态代理类的字节码在程序运行时,由 Java 反射机制动态产生。它会根据需要,通过反射机制在程序运行期,动态的为目标对象创建代理对象,无需程序员手动编写它的源代码。动态代理不仅简化了编程工作,二且提高了软件系统的可扩展性,因为反射机制可以生成任意类型的动态代理类。代理的行为可以代理多个方法,即满足生产需要的同时又达到代码通用的目的。

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

  1. JDK 动态代理
  2. CGLIB动态代理

动态代理的特点

  1. 目标对象不固定
  2. 在应用程序执行时动态创建目标对象
  3. 代理对象会增强目标对象的行为

JDK动态代理

注:JDK动态代理的目标对象必须有接口实现

newProxyInstance

Proxy 类:

Proxy类是专门完成代理的操作类,可以通过此类为一个或多个接口动态地生成实现类,此类提供了如下操作方法:

1
2
3
4
5
6
7
8
9
10
11
12
vbnet复制代码/*
返回⼀个指定接⼝的代理类的实例⽅法调⽤分派到指定的调⽤处理程序。 (返回代理对象)
loader:⼀个ClassLoader对象,定义了由哪个ClassLoader对象来对⽣成的代理对象进⾏加载
interfaces:⼀个Interface对象的数组,表示的是我将要给我需要代理的对象提供⼀组什么接⼝,如果
我提供了⼀组接⼝给它,那么这个代理对象就宣称实现了该接⼝(多态),这样我就能调⽤这组接⼝中的⽅法了
h:⼀个InvocationHandler接⼝,表示代理实例的调⽤处理程序实现的接⼝。每个代理实例都具有⼀个关联
的调⽤处理程序。对代理实例调⽤⽅法时,将对⽅法调⽤进⾏编码并将其指派到它的调⽤处理程序的 invoke ⽅法
(传⼊InvocationHandler接⼝的⼦类)
*/
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)

获取代理对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
typescript复制代码public class JdkHandler implements InvocationHandler {
// ⽬标对象
private Object target; // ⽬标对象的类型不固定,创建时动态⽣成
// 通过构造器将⽬标对象赋值
public JdkHandler(Object target) {
this.target = target;
}
/**
* 1、调⽤⽬标对象的⽅法(返回Object)
* 2、增强⽬标对象的⾏为
* @param proxy 调⽤该⽅法的代理实例
* @param method ⽬标对象的⽅法
* @param args ⽬标对象的⽅法形参
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
// 增强⾏为
System.out.println("==============⽅法前执⾏");
// 调⽤⽬标对象的⽅法(返回Object)
Object result = method.invoke(target,args);
// 增强⾏为
System.out.println("⽅法后执⾏==============");
return result;
}
/**
* 得到代理对象
* public static Object newProxyInstance(ClassLoader loader,
* Class<?>[] interfaces,
* InvocationHandler h)
* loader:类加载器
* interfaces:接⼝数组
* h:InvocationHandler接⼝ (传⼊InvocationHandler接⼝的实现类)
*
*
* @return
*/
public Object getProxy() {
return
Proxy.newProxyInstance(this.getClass().getClassLoader(),target.getClass().getInterface
s(),this);
}
}

通过代理对象实现目标对象的功能

1
2
3
4
5
6
7
ini复制代码// ⽬标对象
You you = new You();
// 获取代理对象
JdkHandler jdkHandler = new JdkHandler(you);
Marry marry = (Marry) jdkHandler.getProxy();
// 通过代理对象调⽤⽬标对象中的⽅法
marry.toMarry();

问:Java 动态代理类中的 invoke 是怎么调用的?

答:在生成的动态代理类 Proxy0.class中,构造方法调用了父类Proxy.class的构造方法,给成员变量invocationHandler赋值,Proxy0.class 中,构造方法调用了父类Proxy.class 的构造方法,给成员变量 invocationHandler 赋值,Proxy0.class中,构造方法调用了父类Proxy.class的构造方法,给成员变量invocationHandler赋值,Proxy0.class的 static 模块中创建了被代理类的方法,调用相应方法时方法体中调用了父类中的成员变量 InvocationHandler 的 invoke ()方法。

注:JDK 的动态代理依靠接口实现,如果有些类并没有接口实现,则不能使用 JDK 代理。

CGLIB 动态代理

JDK 的动态代理机制只能代理实现了接口的类,而不能实现接口的类就不能使用 JDK 的动态代理,cglib 是针对类来实现代理的,它的原理是对指定的目标类生成一个子类,并覆盖其中方法实现增强,但因为采用的是继承,所以不能对 final 修饰的类进行代理。

添加依赖

在 pom.xml 文件中引入 cglib 的相关依赖

1
2
3
4
5
6
xml复制代码<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>

定义类

实现 MethodInterceptor 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
typescript复制代码public class CglibInterceptor implements MethodInterceptor {
// ⽬标对象
private Object target;
// 通过构造器传⼊⽬标对象
public CglibInterceptor(Object target) {
this.target = target;
}
/**
* 获取代理对象
* @return
*/
public Object getProxy() {
// 通过Enhancer对象的create()⽅法可以⽣成⼀个类,⽤于⽣成代理对象
Enhancer enhancer = new Enhancer();
// 设置⽗类 (将⽬标类作为其⽗类)
enhancer.setSuperclass(target.getClass());
// 设置拦截器 回调对象为本身对象
enhancer.setCallback(this);
// ⽣成⼀个代理类对象,并返回
return enhancer.create();
}
/**
* 拦截器
* 1、⽬标对象的⽅法调⽤
* 2、增强⾏为
* @param object 由CGLib动态⽣成的代理类实例
* @param method 实体类所调⽤的被代理的⽅法引⽤
* @param objects 参数值列表
* @param methodProxy ⽣成的代理类对⽅法的代理引⽤
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object object, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
// 增强⾏为
System.out.println("==============⽅法前执⾏");
// 调⽤⽬标对象的⽅法(返回Object)
Object result = methodProxy.invoke(target,objects);
// 增强⾏为
System.out.println("⽅法后执⾏==============");
return result;
}
}

调用方法

1
2
3
4
5
6
7
8
9
ini复制代码// ⽬标对象
You you = new You();
CglibInterceptor cglibInterceptor = new CglibInterceptor(you);
Marry marry = (Marry) cglibInterceptor.getProxy();
marry.toMarry();
User user = new User();
CglibInterceptor cglibInterceptor = new CglibInterceptor(user);
User u = (User) cglibInterceptor.getProxy();
u.test();

JDK代理与CGLIB代理的区别

  • JDK 动态代理实现接口,Cglib 动态代理继承思想
  • JDK 动态代理(目标对象存在接口时)执行效率高于 Ciglib
  • 如果目标对象有接口实现,选择 JDK 代理,如果没有接口实现选择 Cglib 代理

Spring AOP

日志处理带来的问题

我们有一个 Pay (接口) 然后两个实现类 DollarPay 和 RmbPay,都需要重写 pay ()方法, 这时我们需要对 pay 方法进行性能监控,日志的添加等等怎么做?

最容易想到的方法

对每个字符方法均做日志代码的编写处理,如下面方式

缺点: 代码重复太多, 添加的日志代码耦合度太高(如果需要更改日志记录代码功能需求,类中方法需要全部改动,工程量浩大)

使用装饰器模式 /代理模式改进解决方案

装饰器模式:动态地给一个对象添加一些额外的职责。

代理模式:以上刚讲过。于是得出以下结构:

仔细考虑过后发现虽然对原有内部代码没有进行改动,对于每个类做日志处理,并引用目标类,但是如果待添加日志的业务类的数量很多,此时手动为每个业务类实现一个装饰器或创建对应的代理类,同时代码的耦合度也加大,需求一旦改变,改动的工程量也是可想而知的。

有没有更好的解决方案,只要写一次代码,对想要添加日志记录的地方能够实现代码的复用,达到松耦合的同时,又能够完美完成功能?

答案是肯定的,存在这样的技术,aop 已经对其提供了完美的实现!

什么是AOP?

Aspect Oriented Programing 面向切面编程,相比较 oop 面向对象编程来说,Aop 关注的不再是程序代码中某个类,某些方法,而 aop 考虑的更多的是一种面到面的切入,即层与层之间的一种切入,所以称之为切面。联想大家吃的汉堡(中间夹肉)。那么 aop 是怎么做到拦截整个面的功能呢?考虑前面学到的 servlet filter /* 的配置 ,实际上也是 aop 的实现。

AOP能做什么?

AOP 主要应用于日志记录,性能统计,安全控制,事务处理等方面,实现公共功能性的重复使用。

AOP的特点

  1. 降低模块与模块之间的耦合度,提高业务代码的聚合度。(高内聚低耦合)
  2. 提高了代码的复用性
  3. 提高了代码的复用性
  4. 可以在不影响原有的功能基础上添加新的功能

AOP的底层实现

动态代理(JDK + CGLIB)

AOP基本概念

被拦截到的每个点,spring 中指被拦截到的每一个方法,spring aop 一个连接点即代表一个方法的执行。

Pointcut(切入点)

对连接点进行拦截的定义(匹配规则定义 规定拦截哪些方法,对哪些方法进行处理),spring 有专门的表达式语言定义。

Advice(通知)

拦截到每一个连接点即(每一个方法)后所要做的操作

  1. 前置通知 (前置增强)— before() 执行方法前通知
  2. 返回通知(返回增强)— afterReturn 方法正常结束返回后的通知
  3. 异常抛出通知(异常抛出增强)— afetrThrow()
  4. 最终通知 — after 无论方法是否发生异常,均会执行该通知。
  5. 环绕通知 — around 包围一个连接点(join point)的通知,如方法调用。这是最强大的一种通知类型。 环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它们自己的返回值或抛出异常来结束执行。

Aspect(切面)

切入点与通知的结合,决定了切面的定义,切入点定义了要拦截哪些类的哪些方法,通知则定义了拦截过方法后要做什么,切面则是横切关注点的抽象,与类相似,类是对物体特征的抽象,切面则是横切关注点抽象。

Target(目标对象)

被代理的目标对象

Weave(织入)

将切面应用到目标对象并生成代理对象的这个过程即为织入

Introduction(引入)

在不修改原有应用程序代码的情况下,在程序运行期为类动态添加方法或者字段的过程称为引入

Spring AOP的实现

Spring AOP环境搭建

坐标依赖引入

1
2
3
4
5
6
xml复制代码<!--Spring AOP-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.9</version>
</dependency>

添加 spring.xml 的配置

添加命名空间

1
2
3
ruby复制代码xmlns:aop="http://www.springframework.org/schema/aop"
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

注解实现

定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
typescript复制代码/**
* 切⾯
* 切⼊点和通知的抽象 (与⾯向对象中的 类 相似)
* 定义 切⼊点和通知 (切⼊点定义了要拦截哪些类的哪些⽅法,通知则定义了拦截过⽅法后要做什么)
*/
@Component // 将对象交给IOC容器去实例化
@Aspect // 声明当前类是⼀个切⾯
public class LogCut {
/**
* 切⼊点:
* 匹配规则。规定什么⽅法被拦截、需要处理什么⽅法
* 定义切⼊点
* @Pointcut("匹配规则")
*
* Aop 切⼊点表达式简介
* 1. 执⾏任意公共⽅法:
* execution(public *(..))
* 2. 执⾏任意的set⽅法
* execution(* set*(..))
* 3. 执⾏com.xxxx.service包下任意类的任意⽅法
* execution(* com.xxxx.service.*.*(..))
* 4. 执⾏com.xxxx.service 包 以及⼦包下任意类的任意⽅法
* execution(* com.xxxx.service..*.*(..))
*
* 注:表达式中的第⼀个* 代表的是⽅法的修饰范围
* 可选值:private、protected、public (* 表示所有范围)
*/
@Pointcut("execution (* com.xxxx.service..*.*(..) )")
public void cut(){}
/**
* 声明前置通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法执⾏前 执⾏该通知
*
*/
@Before(value = "cut()")
public void before() {
System.out.println("前置通知.....");
}
/**
* 声明返回通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法(⽆异常)执⾏后 执⾏该通知
*
*/
@AfterReturning(value = "cut()")
public void afterReturn() {
System.out.println("返回通知.....");
}
/**
* 声明最终通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法(⽆异常或有异常)执⾏后 执⾏该通知
*
*/
@After(value = "cut()")
public void after() {
System.out.println("最终通知.....");
}
/**
* 声明异常通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法出现异常时 执⾏该通知
*/
@AfterThrowing(value="cut()",throwing = "e")
public void afterThrow(Exception e) {
System.out.println("异常通知....." + " 异常原因:" + e.getCause());
}
/**
* 声明环绕通知 并将通知应⽤到切⼊点上
* ⽅法执⾏前后 通过环绕通知定义相应处理
* 需要通过显式调⽤对应的⽅法,否则⽆法访问指定⽅法 (pjp.proceed();)
* @param pjp
* @return
*/
@Around(value = "cut()")
public Object around(ProceedingJoinPoint pjp) {
System.out.println("前置通知...");
Object object = null;
try {
object = pjp.proceed();
System.out.println(pjp.getTarget() + "======" + pjp.getSignature());
// System.out.println("返回通知...");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("异常通知...");
}
System.out.println("最终通知...");
return object;
}
}

配置文件(spring.xml)

1
2
xml复制代码<!--配置AOP代理-->
<aop:aspectj-autoproxy/>

XML实现

定义切面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
csharp复制代码**
* 切⾯
* 切⼊点和通知的抽象 (与⾯向对象中的 类 相似)
* 定义 切⼊点和通知 (切⼊点定义了要拦截哪些类的哪些⽅法,通知则定义了拦截过⽅法后要做什么)
*/
@Component // 将对象交给IOC容器去实例化
public class LogCut02 {
public void cut(){}
/**
* 声明前置通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法执⾏前 执⾏该通知
*/
public void before() {
System.out.println("前置通知.....");
}
/**
* 声明返回通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法(⽆异常)执⾏后 执⾏该通知
*
*/
public void afterReturn() {
System.out.println("返回通知.....");
}
/**
* 声明最终通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法(⽆异常或有异常)执⾏后 执⾏该通知
*
*/
public void after() {
System.out.println("最终通知.....");
}
/**
* 声明异常通知 并将通知应⽤到定义的切⼊点上
* ⽬标类⽅法出现异常时 执⾏该通知
*/
public void afterThrow(Exception e) {
System.out.println("异常通知....." + " 异常原因:" + e.getCause());
}
/**
* 声明环绕通知 并将通知应⽤到切⼊点上
* ⽅法执⾏前后 通过环绕通知定义相应处理
* 需要通过显式调⽤对应的⽅法,否则⽆法访问指定⽅法 (pjp.proceed();)
* @param pjp
* @return
*/
public Object around(ProceedingJoinPoint pjp) {
System.out.println("前置通知...");
Object object = null;
try {
object = pjp.proceed();
System.out.println(pjp.getTarget() + "======" + pjp.getSignature());
// System.out.println("返回通知...");
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("异常通知...");
}
System.out.println("最终通知...");
return object;
}
}

配置文件(spring.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<!--aop相关配置-->
<aop:config>
<!--aop切⾯-->
<aop:aspect ref="logCut02">
<!-- 定义aop 切⼊点 -->
<aop:pointcut id="cut" expression="execution(* com.xxxx.service..*.*(..))"/>
<!-- 配置前置通知 指定前置通知⽅法名 并引⽤切⼊点定义 -->
<aop:before method="before" pointcut-ref="cut"/>
<!-- 配置返回通知 指定返回通知⽅法名 并引⽤切⼊点定义 -->
<aop:after-returning method="afterReturn" pointcut-ref="cut"/>
<!-- 配置异常通知 指定异常通知⽅法名 并引⽤切⼊点定义 -->
<aop:after-throwing method="afterThrow" throwing="e" pointcut-ref="cut"/>
<!-- 配置最终通知 指定最终通知⽅法名 并引⽤切⼊点定义 -->
<aop:after method="after" pointcut-ref="cut"/>
<!-- 配置环绕通知 指定环绕通知⽅法名 并引⽤切⼊点定义 -->
<aop:around method="around" pointcut-ref="cut"/>
</aop:aspect>
</aop:config>

Spring AOP总结

代理模式实现三要素

  1. 接口定义
  2. 目标对象与代理对象必须实现统一接口
  3. 代理对象持有目标对象的引用,增强目标对象行为

代理模式实现分类以及对应区别

  1. 静态代理:手动为目标对象制作代理对象,即在程序编译阶段完成代理对象的创建
  2. 动态代理:在程序运行期动态创建目标对象对应代理对象。
  3. jdk 动态代理:被代理目标对象必须实现某一或某一组接口实现方式通过回调创建代理对象。
  4. cglib 动态代理:被代理目标对象可以不必实现接口,继承的方式实现

动态代理相比较静态代理,提高开发效率,可以批量化创建代理,提高代码复用率。

Aop 理解

  1. 面向切面,相比 oop 关注的是代码中的层或面
  2. 解耦,提高系统扩展性
  3. 提高代码复用

Aop 关键词

  1. 连接点:每一个方法
  2. 切入点:匹配的方法集合
  3. 切面:连接点与切入点的集合决定了切面,横切关注点的抽象
  4. 通知:几种通知
  5. 目标对象:被代理对象
  6. 织入:程序运行期将切面应用到目标对象并生成代理对象的过程
  7. 引入:在不修改原始代码情况下,在程序运行期为程序动态引入方法或字段的过程

最后

感谢你看到这里,文章有什么不足还请指正,觉得文章对你有帮助的话记得给我点个赞!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一篇通俗易懂的长文,带你从零认识IoC和AOP 写在前面 I

发表于 2020-10-05

写在前面

本篇博客共一万七百字左右,从 IOC到 AOP。

算是对自己学习 Spring的一个验收,同时也分享出来供大家查漏补缺。

在我写这篇博客时自己也是第一次接触 Spring,也不敢保证写的东西绝对正确,可能存在谬误和歧义,如发现请务必指出 会及时更正。

写这篇博客前前后后花了一个礼拜多,期间有通过视频来了解 Spring,也有参考其他的博客,对自己无法表述的部分来进行完善,可以说是每一段都有经过反复修改。自己也感觉到,在一次次的修改中 自己也对这些反复敲的概念理解更进了一步。

IoC 思想

IOC(控制反转)是一种依赖倒置原则的代码设计的思路,它主要采用(DI)依赖注入的方式来实现

不使用IoC思想的传统模式

  • 在传统模式中,对象由程序员主动创建,控制权在程序员手中。
  • 程序可以做到正常工作,但仍有一个难以避免的问题。
  • 如果用户需求变更,程序员就要修改对应的代码,代码量不大还好,如果代码量巨大的话 修改一次的成本…
  • 这个问题就是耦合性过高引起的,修改一次需求,或多或少会造成代码的修改,工作量先不说,维护起来也是极其不便的啊。

  • 就如上图中这四个齿轮(对象)一样,互相啮合,如果有一方停止或更换 其他的齿轮也就没办法工作,这自然不是我们希望看到的。

为了解决对象间耦合过高的问题,软件专家Michael Mattson提出了IoC理论,用来实现对象之间的“解耦”。

那么应当如何达到理想的效果呢?

使用IoC思想后的模式

IoC的主要思想是借助一个“第三方”来拆开原本耦合的对象,并将这些对象都与“第三方”建立联系,由第三方来创建、操作 这些对象,进而达到解耦的目的。

因此IoC容器也就成了整个程序的核心,对象之间没有了联系(但都和 IoC容器有联系)。

这里引用一句知乎上看到的话

IoC的思想最核心的地方在于,资源不由使用资源的双方管理,而由不使用资源的第三方管理,这可以带来很多好处。第一,资源集中管理,实现资源的可配置和易管理。第二,降低了使用资源双方的依赖程度,也就是我们说的耦合度。

什么是控制反转

这里我们引入一个场景, “如果 A对象想调用 B对象”

传统模式中该如何操作 大家都很熟悉了,在A对象中创建一个 B对象实例,就可以满足 A对象调用 B对象的需求。这是我们在 A对象中主动的去创建 B对象实例

而引入 IoC后,A对象如果想调用 B对象,IoC容器会创建一个 B对象注入到 A对象中,这样也可以满足 A对象的调用需求。但是过程由我们的主动创建,变成了 A对象被动的去接收 IoC容器注入的 B对象

A对象依赖 B对象的过程,由程序员的主动创建 B对象供其依赖,变为了被动的接收 IoC容器注入的对象。控制权从程序员手中交到了 IoC容器手中。A对象获得依赖的过程也由主动变为被动,这就是所谓的控制反转

什么是依赖注入(DI)

依赖注入是 IoC思想最主要的实现方式,也就是上文提到的 “ A对象如果想调用 B对象,IoC容器会创建一个 B对象注入到 A对象中,这样就可以满足 A对象对 B对象的依赖需求”。 这个行为就是依赖注入。

DI ≠ IOC

IoC的概念更宽广一些,而 DI是 IoC的主要实现方式,但这并不意味着 DI就是 IoC,将二者混为一谈 这是不对的,很容易误导他人。

就比如你想要阅读,最主要的实现方式自然是“用眼睛长时间的去看”,但你不能把这个“眼睛长时间去看”的行为 理解为阅读。(可能例子有点不恰当)

注入方式

setter方法注入

我们需要在类中生成一个set方法和一个空构造(空构造在没有声明有参构造时会隐式声明,无需再进行多余的操作)

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class Hello {

private String name;
// 一定要生成set方法
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
}

在 Spring配置文件中注入 Bean(对象),在 Bean中使用 property标签为 name属性赋值

1
2
3
4
xml复制代码    <bean id="hello" class="com.molu.pojo.Hello">
<!--setter方法注入使用property标签-->
<property name="name" value="陌路"/>
</bean>

写一个简单的测试方法,测试 property标签是否成功赋值。

构造器注入

构造器注入我们需要手动的生成一个有参构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.molu.pojo;

public class Hello {

private String name;
// 生成有参构造
public Hello(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

这个时候再切回 applicationContext.xml中可以看到已经报错了

因为显式的定义了有参构造后,无参构造就不存在了

我们需要将property标签改为 constructor-arg

constructor-arg标签赋值的方式更为多样化。

  • 通过下标赋值
+ 
1
2
3
xml复制代码<bean id="hello" class="com.molu.pojo.Hello">
<constructor-arg index="0" value="陌路"/>
</bean>
  • 通过参数类型赋值(不推荐,参数类型容易重合)
+ 
1
2
3
xml复制代码<bean id="hello" class="com.molu.pojo.Hello">
<constructor-arg type="java.lang.String" value="陌路"/>
</bean>
  • 通过参数名赋值(推荐)
+ 
1
2
3
ini复制代码<bean id="hello" class="com.molu.pojo.Hello">
<constructor-arg name="name" value="陌路"/>
</bean>
结果都是一样的,这里就不再展示测试结果了

拓展注入

P(Property)命名空间注入
  • 在使用P命名空间之前要在引入它的约束
+ 
1
xml复制代码xmlns:p="http://www.springframework.org/schema/p"
  • P命名空间的使用必须要有一个空构造。(类似于setter方法注入)但前面也说了,没有声明有参构造时 空构造会隐式声明。
  • P命名空间注入 可以直接在 Bean标签中进行注入

首先创建一个 Me类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码package com.molu.pojo;

public class Me {

private String name;

private int age;

public String getName() { return name; }

public void setName(String name) { this.name = name; }

public int getAge() { return age; }

public void setAge(int age) { this.age = age; }

在 applicationContext.xml中用 P命名空间进行注入

1
2
3
4
5
6
7
8
9
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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--在Bean标签中,使用P命名空间进行简单的属性注入-->
<bean id="me" class="com.molu.pojo.Me" p:name="陌路" p:age="18"/>
</beans>

测试

略……

C(constructor-arg)命名标签注入
  • 使用 C命名空间之前也需要引入约束
+ 
1
xml复制代码xmlns:c="http://www.springframework.org/schema/c"
  • C命名空间不同于 P命名空间,他必须要有一个构造方法(类似构造器注入)
  • P命名空间也可以直接在 Bean标签中进行简单的注入操作

在 Me类中添加构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码package com.molu.pojo;

public class Me {
private String name;

private int age;
// 生成有参构造 供C命名空间调用
public Me(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() { return name; }

public void setName(String name) { this.name = name; }

public int getAge() { return age; }

public void setAge(int age) { this.age = age; }
}

在 applicationContext.xml中用 C命名空间进行注入

1
2
xml复制代码<!--C命名空间注入,通过构造器注入-->
<bean id="me" class="com.molu.pojo.Me" c:name="陌路" c:age="18" />

测试

略

关于其他的注入方式这里就不再一一列举了,官网上写的很详细,有能力的朋友可以移步官网,在官网上寻求答案

Spring官网

补充

我们再写一个 HelloTwo类,里面和 Hello类一样,唯一不同是显式的定义了无参构造而不是有参构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码package com.molu.pojo;

public class HelloTwo {
private String name;
// 显式的定义了无参构造
public HelloTwo() {
// 简单写一个测试输出语句
System.out.println("HelloTwo的无参构造被调用了");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

在 Application Context.xml中注册 Bean,不对其进行其他任何操作。

1
xml复制代码<bean id="helloTwo" class="com.molu.pojo.HelloTwo"></bean>

使用刚刚用过的 Hello类测试方法 原封不动进行测试

1
2
3
4
5
6
7
8
java复制代码public class MyTest {
@Test
public void test(){
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
Hello hello = (Hello) context.getBean("hello");
System.out.println(hello.getName());
}
}

控制台输出了我们在 HelloTwo无参构造中写的输出语句,奇怪的是我们并没有在测试类中写任何关于 HelloTwo的代码。

由此能够得到一些信息:“注册进 applicationContext.xml中的 Bean,无论你调用与否,他都会被初始化”

AOP 编程

在软件业,AOP为 Aspect Oriented Programming的缩写,意为:面向切面编程, 通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术

AOP是 OOP的延续,是软件开发中的一个热点,也是 Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

Spring的关键组件之一是 AOP框架。尽管 Spring IoC容器不依赖于 AOP,但 AOP是对Spring IoC的补充,以提供功能非常强大的中间件解决方案。

在涉及 AOP之前我们先简单了解一下代理模式,因为代理模式是 SpringAOP的底层实现。

代理模式

代理模式是23种设计模式之一,它分为动态代理和静态代理,代理模式可以使客户端的访问对象从真实对象变为代理对象。

为什么这么做呢?

  • 代理模式可以屏蔽用户对真实对象的访问,这样可以避免一些安全上的问题
  • 能够做到不改变真实对象,对真实对象的功能进行扩展
  • 使得真实对象的功能更加纯粹,业务的分工更加明确

那么如何实现代理模式呢?

  • 首先需要一个抽象主题(接口或者抽象类)
  • 创建代理对象和真实对象
  • 代理对象和真实对象都实现该抽象主题
  • 客户端访问代理对象

下面让我们从代码中来简单的理解代理模式

静态代理

引入场景:“我喜欢一双鞋,但在中国地区买不到,需要托朋友从国外代购”

这里“我”可以理解为客户端、朋友是代理对象、出售鞋的商店为真实对象、抽象主题为卖这双鞋。

1
2
3
java复制代码// 我
public class Me {
}
1
2
3
4
java复制代码// 抽象主题,卖鞋(接口)
public interface Subject {
public void sellShoes();
}
1
2
3
4
5
6
java复制代码// 商店
public class Store implements Subject{
public void sellShoes() {
System.out.println("鞋子售价为90刀");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码// 朋友
public class Friend implements Subject{

// 朋友拿到商店对象,对应朋友去商店这一场景(代理对象拿到真实对象)。
private Store store;

public void setStore(Store store) {
this.store = store;
}

// 代理对象附加操作
public void returnHome(){
System.out.println("朋友回到国内,来到我家");
}
// 代理对象附加操作
public void giveMe(){
System.out.println("我付给了朋友一百刀");
}
public void sellShoes() {
// 朋友在商店里买下了这双鞋子(代理对象调用真实对象的方法)
store.sellShoes();
// 朋友回国
returnHome();
// 朋友把这双鞋交给我,我付给它相应的费用(含关税)
giveMe();
}
}

可以看到,朋友和商店都实现了 Subject这个接口。
原本我想买到这双鞋应该直接访问商店对象。但因为没办法访问到该对象,我只能通过访问“朋友”对象来实现我的需求。

访问“朋友”对象

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码// 我
public class Me {
public static void main(String[] args) {
// 创建真实对象
Store store = new Store();
// 创建代理对象
Friend friend = new Friend();
// 将真实对象传给代理对象
friend.setStore(store);
//调用代理方法
friend.sellShoes();
}
}

输出结果

到这里简单的代理操作就实现了,我们通过访问“朋友”对象,确实解决了 原本需要去访问“商店对象”才能拿到鞋的困扰。对应到代理模式中就是,我们绕过了真实对象,通过访问代理对象实现了调用真实对象功能的操作。且代理对象的两个附加操作也实现了对真实对象功能的扩展!

可能栗子举的不太恰当,大家不要太去深究,明白这一代理操作的具体实现和思想才是主要。

代理模式有没有弊端?

静态代理模式中,每有一个真实对象 就会有一个代理对象,如果真实对象十分多的话…

动态代理

动态代理可以根据需要,通过反射机制在程序运行时,动态的为目标对象生成代理对象。

动态代理主要分为两大类,一种是基于接口的(JDK),一种是基于类的(CGLIB)

jdk动态代理:

了解jdk动态代理之前我们需要了解两个类:java.lang.reflect.Proxy 和 java.lang.reflect.InvocationHandler接口。

InvocationHandler: 该接口仅定义了一个方法

  • public object invoke(Object proxy,Method method,Object[] args)
    • 第一个参数为调用该方法的代理实例
    • 第二个参数为目标对象的方法
    • 第三个参数为目标对象方法的参数
  • 当我们使用Proxy的静态方法生成动态代理实例后,使用该实例调用接口中的任意方法,都会将调用的方法替换为 invoke方法。

Proxy: 该类是为我们生成动态代理的类

Proxy提供了很多方法,我们最常用的是 newProxyInstance方法
1
java复制代码static Object newProxyInstanc(ClassLoader loader,Class[] interface,InvocationHandler h)

该静态方法会返回一个 Object,返回的 Object就可以被当做代理类使用
它的三个参数

  • loader:一个类加载器对象,我们通过反射来获取目标对象(真实)的类加载器
  • Class[] interface: 接口对象数组,也是通过反射获取的,生成的代理对象会实现这些接口,并可以调用接口中声明的所有方法。
  • h: InvocationHandler的对象实例,如果我们用来的生成代理类 的 类(Friend)实现了这个接口(InvocationHandler),可以直接传入这个类本身(this)。

我们通过 newProxyInstance就可以得到真实对象所需要的代理对象

用代码进行简单的演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码// 首先实现 InvocationHandler接口
public class Friend implements InvocationHandler {
// 被代理的接口对象
private Object target;

public Friend(Object target) {
this.target = target;
}

// 写一个获取代理对象实例的方法
public Object getProxy(){
// Proxy中的newProxyInstance方法会创建一个动态的代理类
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
}
// InvocationHandler接口中的invoke方法:
// 该方法在使用getProxy方法 生成代理类并调用接口中的方法时会被自动调用
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 附加操作
returnHome();
// method的invoke方法,通过反射获取到目标对象中的方法。
// 由于我们没有将目标对象写死,所有我们传入动态的target。
Object object = method.invoke(target,args);
// 附加操作
giveMe();
return object;
}
//附加操作
public void returnHome(){
System.out.println("朋友回国后来到我家");
}
// 同上
public void giveMe(){

System.out.println("我付给了朋友指定的钱");
}
}

在 Me类中进行动态代理的调用测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public class Me {
public static void main(String[] args) {
// 创建真实对象
Store store = new Store();
// 创建 InvocationHandler对象的实例,并传入目标对象(真实对象)
Friend friend = new Friend(store);
// 通过InvocationHandler的实例(friend)调用getProxy方法
// 该方法会返回一个代理对象的实例,我们只需要将我们写好的Object类型转换为需要的接口类型即可
Subject proxy = (Subject) friend.getProxy();
// invoke方法会在我们调用接口中的方法时,将该方法替换为它。
// invoke方法会通过method.invoke拿到目标对象中的方法
// 也就是Store中的方法,从而实现代理的操作。
proxy.sellShoes();
}
}

运行结果:

1
2
3
4
5
java复制代码朋友回国后来到我家
售价为90刀
我付给了朋友一百刀

进程已结束,退出代码0

为了凸显动态代理的作用,我们再编写一个代购的栗子

引入场景: 我想要一台 Mac,但是国内……所以又托朋友….

1
2
3
4
java复制代码// 公共主题
public interface Mac {
public void sellMac();
}
1
2
3
4
5
6
7
8
java复制代码// 被代理的对象
package com.molu.proxy;

public class MacStore implements Mac{
public void sellMac() {
System.out.println("Mac售价为1899刀");
}
}

Me类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class Me {
public static void main(String[] args) {
// 创建真实对象
Store store = new Store();
// 创建 InvocationHandler对象的实例,并传入目标对象(真实对象)
Friend friend = new Friend(store);
// 通过InvocationHandler的实例调用getProxy方法
//该方法会返回一个代理对象的实例,我们只需要指定该实例需要实现的接口即可(强转)
Subject proxy = (Subject) friend.getProxy();
// 通过这个动态生成代理实例来调用真实对象中的sellShoes()方法
// proxy.sellShoes();


// 生成第二个栗子的动态代理类,步骤同上一模一样
MacStore macStore = new MacStore();
Friend friendMac = new Friend(macStore);
Mac proxyMac = (Mac) friendMac.getProxy();
proxyMac.sellMac();
}
}

运行结果

没有任何问题,又成功的生成了 MacStore的代理对象。这样我们就避免了反复写代理类的问题。

jdk动态代理原理剖析

我们主要分析Friend类中的具体实现

  1. 首先来看一下我们手动写的 getProxy方法,它主要使用了 Proxy类中的newProxyInstance方法。
1. 这个方法返回一个 Object对象,这个 Object对象有三个参数,这三个参数具体是什么,上文已经说过了。
2. 返回的这个对象通过 **Proxy的静态方法**生成,生成后就可以被当作一个代理对象来使用。
1
2
3
java复制代码    public Object getProxy(){
return Proxy.newProxyInstance(this.getClass().getClassLoader(), target.getClass().getInterfaces(),this);
}
  1. InvocationHandler中的invoke()方法
1. 这个方法有三个参数,分别是代理对象的实例(com.sun.proxy.$Proxy0),目标对象的方法,方法的参数。
2. 我们如果通过 getProxy来生成代理实例,使用该实例调用接口中的方法——就会执行 invoke方法。
3. invoke通过反射拿到真实对象中的方法。真正执行的也就是这个通过反射拿到的方法。
1
2
3
4
java复制代码    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object object = method.invoke(target,args);
return object;
}
3. Me类


    1. 首先我们创建目标对象(真实对象)的实例和 InvocationHandler的实例,将目标对象传入**处理程序的实例**中(也就是传入了 friend == this 中)![](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14e4de9e98644bccbe76a76b07dd8152~tplv-k3u1fbpfcp-zoom-in-crop-mark:1512:0:0:0.awebp)


打上断点后,确实看到 Friend的实例中的 target变成了 MacStore,之后 invoke方法会通过反射`method.invoke()`拿到 MacStore中的方法。


    2. 使用**处理程序的实例**调用 getPorxy方法创建代理对象实例。该对象创建后类型默认为 Object(因为我们在写getProxy方法的时候返回值写的是Object),我们将它强转为需要的接口类型即可。
    3. 通过生成的代理实例来调用接口中的方法时,**处理程序的实例会自动调用 invoke()方法。**
    4. `invoke()`方法中的`method.invoke(target,args)`已经拿到了目标对象中的方法及参数(我们这没有写参数),所以**调用invoke方法就等于是调用了目标对象中的方法**。再将增强行为写在`method.invoke(target,args)`上下,就可以实现一次代理的操作。
1
2
3
4
java复制代码        MacStore macStore = new MacStore();
Friend friendMac = new Friend(macStore);
Mac proxyMac = (Mac) friendMac.getProxy();
proxyMac.sellMac();
invoke方法自动调用

我们再来聊聊为什么invoke方法会被自动调用的问题

1
2
css复制代码public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;

invoke() 方法来自 InvocationHandler 接口

我们先来看看它的第一个参数 proxy参数,直接输出它的字节码文件名。

1
2
java复制代码    System.out.println(proxy.getClass().getName());
// 输出结果为: com.sun.proxy.$Proxy0

这个 $Proxy0 实际上就是我们的代理类实例,感兴趣的朋友可以去将newProxyInstance()返回的 object对象的字节码文件名打印出来看一下。也会是 $Proxy0

要明白为什么会自动调用invoke()方法,我们需要查看一下$Proxy0对象反编译文件的源码。

在main方法最前面添加该配置,运行后会生成代理类反编译的 class文件

1
java复制代码System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

生成在 IDEA工作空间下的com\sun\Proxy$Proxy0.class文件和源代码不在一个目录下

点开代理对象反编译的class文件源码可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public final class $Proxy0 extends Proxy implements Subject {
// 发现它继承了Proxy类,且实现了Subject接口(在生成该反编译文件时我将Mac相关代码都注了,所以是Subject)
..........
// 重写了 Subject 接口的 sellShoes方法
public final void sellShoes() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
..........

点进去第一行能获得两个信息

1
java复制代码public final class $Proxy0 extends Proxy implements Subject {
  • 代理类的反编译文件继承了Proxy类

也就是说它的父类是Proxy,那么它就会关联一个 InvocationHandler方法调用处理器

  • 实现了我们写的Subject接口

可能这就是为什么Jdk动态代理为什么必须要有接口才能使用。(单继承的局限性)

再往下看,可以看到 $Proxy0 重写了sellShoes()方法,该方法调用了super(Proxy).h.invoke()方法。

关于 h 我们前面只在Proxy.newProxyInstance()有所涉及,也就是我们传入的第三个参数,一个InvocationHandler实例(this)。

而 $Proxy0 重写的 sellShoes() 方法中的 h也是从 Proxy类中取的参数极有可能就是我们传进去的 this

接下来要做的就很明显了,我们要看看 newProxyInstance()方法的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码   @CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
/* 我们通过 newProxyInstance
传进来的InvocationHandler实例 h */
throws IllegalArgumentException
{
Objects.requireNonNull(h);

final Class<?>[] intfs = interfaces.clone();

final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
}
Class<?> cl = getProxyClass0(loader, intfs);
// (o゚v゚)ノ这里
try {
if (sm != null) {
checkNewProxyPermission(Reflection.getCallerClass(), cl);
}

final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
// (o゚v゚)ノ还有这里
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}

不难看出我们的 $Proxy0 就是该方法创建的,c1 为 $Proxy0 的引用对象

1
2
java复制代码Class<?> cl = getProxyClass0(loader, intfs);
// 需要传入一个类加载器和一个接口数组 传入的接口数组在创建$Proxy0时会被自动实现

再往下看,有这么两行代码

1
2
java复制代码/* final Constructor<?> cons = cl.getConstructor(constructorParams); 这行不管 */
final InvocationHandler ih = h;

第一行我们不细细展开,篇幅有限,我觉得我也没办法在源码上讲的比较能够让人理解,所以我们将目光放到第二行。

  • 我们通过Proxy.newProxyInstance(…. , ….. ,h )传进来的h,被赋值给了InvocationHandler实例。
  • InvocationHandler ih = h,这个h实际上就是 Friend实例。
  • 而在代理对象的反编译文件中又看到这么几行代码
1
2
3
4
5
6
java复制代码public final void sellShoes() throws  {
try {
// super 是继承的Proxy类
// h 是我们传进来的InvocationHandler实例(friend),
// invoke方法就是我们写在Friend类中的invoke方法。
super.h.invoke(this, m3, (Object[])null);

很明了了,我们通过getProxy();生成的代理对象实例 $Proxy0 ,调用sellShoes()方法时它最终会执行:

1
java复制代码public final void sellShoes() throws  { try {  **super.h.invoke**(this, m3, (Object[])null);  } catch (RuntimeException | Error var2) { ....... }

继而就调用了 Friend中的invoke()方法。也就实现了invoke()方法的自动调用。

所以我们原以为的 通过代理对象实例调用接口中的方法实际上是通过$Proxy0调用了源码中的sellShoes()方法才对

1
2
3
4
5
6
7
8
9
java复制代码// 使用代理对象的实例调用sellShoes方法
proxy.sellShoes();
// 你以为的
public void sellShoes();

// 实际上的
public final void sellShoes() throws {
try { super.h.invoke(this, m3, (Object[])null);
..........

再捋一捋

首先我们通过newProxyInstance()中的Class<?> cl = getProxyClass0(loader, intfs);方法得到$Proxy0,通过代理实例调用接口中的方法时,实际上就是通过 $Proxy0 调用源码里重写过的接口方法

重写过的接口方法

1
2
3
4
5
6
7
8
9
java复制代码public final void sellShoes() throws  {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
..........

super 很容易理解,$Proxy0 继承的父类。也就是 Proxy,

Proxy中的 h,不就是我们通过 Proxy.newProxyInstance()传进去的 this吗。

这个this ?就是实现了InvocationHandler接口的Friend实例啊,最后通过这个实例调用了invoke方法。

“super.h.invoke(this, m3, (Object[])null);”

到这里,为什么invoke方法会被自动调用,不就图样了嘛

cglib动态代理

在 jdk动态代理生成的代理对象实例$Proxy0的源码中我们看到,jdk动态代理必须要有接口实现才能使用。这就造成了一定的局限性,所以在目标类没有接口实现的情况下我们就会使用 cglib动态代理。

cglib动态代理采用的是继承思想,它针对类来实现代理,它会给目标类生成一个对应的子类,并覆盖其方法。

简单点说就是:代理类会继承目标类,并重写目标类中的方法(由于使用了继承,所以要避免使用final来修饰目标类)。

使用cglib动态代理

导入pom依赖

1
2
3
4
5
6
xml复制代码<!--导入cglib依赖-->  
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

这里导入pom依赖时需要注意版本问题,可能会有无法加载依赖的错误,根本原因是 ASM支持与当前的 cglib版本不一致。

可以选择降低版本,来快速解决该问题,使用低版本的 cglib,Mavan会自动导入版本符合的ASM支持。

cglib实现动态代理首选需要准备一个目标对象和一个生成动态代理的类

这里我们使用MacStore来充当目标对象,唯一的不同是没有再继承一个公共主题接口。

1
2
3
4
5
6
7
java复制代码package com.molu.cglib;

public class MacStore {
public void sellMac(){
System.out.println("Mac售价为1899刀");
}
}

编写Friend类,写一个生成代理类的方法,重写拦截器方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码// 继承cglib中的MethodInterceptor接口
public class Friend implements MethodInterceptor
{
// 创建目标对象的实例
private Object target;
// 通过构造器传入目标对象
public Friend(Object target) {
this.target = target;
}
// 使用该方法来创建代理类
public Object getProxy(){
// 创建Enhancer对象
Enhancer enhancer = new Enhancer();
// 使用Enhancer对象中的方法设置父类(将目标类设置为代理类的父类)
enhancer.setSuperclass(target.getClass());
// 这里需要传入一个CallBack对象,因为MethodInterceptor接口继承了CallBack
// 而我们的Friend又实现了CallBack所以我们直接传入 this。这行代码的意思是:
// 设置拦截器,回调对象为本身对象。
enhancer.setCallback(this);
// 返回Enhancer中的create()方法拿到代理对象实例给调用者
return enhancer.create();
}
// 重写intercept方法
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
// 假装有增强行为 ˋ(°▽°)`
System.out.println("增强行为");
// 使用代理类对方法的代理引用,来调用invoke方法
Object object = methodProxy.invoke(target,objects);
return object;
}
}

在Me类中调用getProxy方法获取动态代理实例**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class Me {
public static void main(String[] args) {
MacStore macStore = new MacStore();
Friend friend = new Friend(macStore);
MacStore proxy = (MacStore) friend.getProxy();
proxy.sellMac();
}
}


测试结果:

增强行为
Mac售价为1899刀

Process finished with exit code 0

引入AOP

在理解了AOP的底层”代理模式”后我们来正式的引入 AOP

Spring AOP默认将标准 JDK动态代理用于 AOP代理,在业务类没有接口的实现时,也可以使用 cglib动态代理。

AspectJ

Spring使用 AspectJ提供的用于切入点解析和匹配的库来解释与 AspectJ 5相同的注释。但是,AOP运行时仍然是纯 Spring AOP,并且不依赖于 AspectJ编译器或编织器。

常见的术语和概念

在实现AOP操作之前我们先对下面的这些术语和概念有一个比较粗浅的认识

  • 横切关注点:跨越应用程序多个模块的方法或者功能,即是 与我们业务逻辑毫无关系的部分 也是我们需要关注的部分。如日志、安全、缓存、事务等等…..
  • 切面(ASPECT):横切关注点 被模块化 的特殊对象。即 它是一个类。
  • 通知(Advice):切面必须要完成的工作 即 它是类中的一个方法
  • 目标(Target): 被通知的对象
  • 代理(Proxy):向目标对象应用通知之后创建的对象
  • 切入点(PointCut):切面通知执行的”地点”的定义
  • 连接点(JoinPoint):与切入点匹配的执行点

SpringAOP中, 通过 Advice(通知) 定义横切逻辑,Spring支持五种类型的 Advice

  • 前置通知 [Before advice]:方法(连接点)前执行的通知,它会不阻止执行流程前进到连接点(除非它引发异常)
  • 正常返回(后置)通知 [After returning advice]:方法(连接点)正常执行完后运行的通知(没有引发异常的情况)
  • 环绕通知 [Around advice]:环绕通知围绕在方法(连接点)执行前后运行。这是最强大的通知类型,能在方法调用前后自定义一些操作。
  • 异常返回通知 [After throwing advice]:方法(连接点)抛出异常时运行的通知
  • 最终通知 [Final advice]:在方法(连接点)执行完成后执行的通知,与后置通知不同的是,它会无视抛出异常的情况,即抛出异常仍然会执行该通知,用人话说就是,无论如何都会执行的通知(后置通知可以通过配置得到返回值,而最终通知不行)

更多内容可以移步Spring官网

aop概念在5.1处

实现AOP

原生API接口实现

1
2
3
4
5
6
xml复制代码        <dependency>
<!--导入织入依赖-->
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>

导入依赖后我们写一个简单的业务类

业务接口

1
2
3
4
5
6
java复制代码public interface UserService {
public void add();
public void delete();
public void update();
public void select();
}

接口实现类

1
2
3
4
5
6
java复制代码public class UserServiceImpl implements UserService{
public void add() { System.out.println("增加了一个用户"); }
public void delete() { System.out.println("删除了一个用户"); }
public void update() { System.out.println("更新了用户"); }
public void select() { System.out.println("查询用户"); }
}

写一个前置通知,这个通知类只做一件事情:在我们调用接口实现类中的方法时 打印当前时间和调用的方法名

1
2
3
4
5
6
7
8
9
java复制代码// 继承Spring原生的API接口MethodBeforeAdvice
public class Log implements MethodBeforeAdvice {
// 在MethodBeforeAdvice接口的 before 方法写我们具体的操作
public void before(Method method, Object[] objects, Object o) throws Throwable {
Date date = new Date();
System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date)
+ " 执行了" + method.getName() + "方法");
}
}

在 Spring配置文件中注册以上两个 Bean

1
2
xml复制代码<bean id="userService" class="com.molu.service.UserServiceImpl"/>
<bean id="log" class="com.molu.service.Log"/>

之后我们在 applicationContext.xml中引入 AOP的命名空间

1
xml复制代码xmlns:aop="http://www.springframework.org/schema/aop"
1
2
xml复制代码http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd

引入命名空间后我们对 AOP进行配置

1
2
3
4
5
6
xml复制代码    <aop:config>
<!--定义切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.molu.service.UserServiceImpl.*(..))"/>
<!--将通知与切入点绑定-->
<aop:advisor advice-ref="log" pointcut-ref="pointcut"/>
</aop:config>

切入点中的 execution表达式很好理解,它用来确定我们的通知会在哪些地方执行。

  • expression=”execution(* com.molu.service.UserServiceImpl.*(..))”
    • 第一个 * 为所有的返回类型
    • com.molu.service.UserServiceImpl. * (..) 表示com.molu.service包下的UserServiceImpl类的所有方法(所有的参数)

配置完成后我们调用UserServiceImpl中的任意方法,都会在方法前执行我们的log前置通知

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class MyTest {
public static void main(String[] args) {
// 获取Spring上下文环境对象
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
// 使用上下文环境对象拿到我们的UserServiceImpl Bean的实例
// 因为AOP默认使用标准JDK动态代理,所以我们还需要将类型强转为UserService接口
UserService userService = (UserService) context.getBean("userService");
// 调用add方法
userService.add();
}
}

测试结果

可以看到,在我们调用 UserServiceImpl中的方法时,前置通知成功的被执行了。

自定义切面实现

切面(ASPECT):横切关注点 被模块化 的特殊对象。即 它是一个类。

我们还可以通过自定义一个类,将该类标记为一个切面,使用该类中的方法来实现通知的功能。

写一个自定义类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class DiyAspect {
// 前置通知
public void Before(){
Date date = new Date();
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH-mm-ss").format(date)+ "时执行了该通知");
}
// 后置通知
public void After(){
System.out.println("方法执行完毕");
}
}

在配置文件中注册 Bean,并将该类定义为一个切面,使用该类中的方法来执行通知功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
xml复制代码<!--注册Bean-->
<bean id="diyAspect" class="com.molu.diy.DiyAspect"/>
<!--进行AOP配置 -->
<aop:config>
<!--自定义切面-->
<aop:aspect id="aspect" ref="diyAspect">
<!--定义切入点-->
<aop:pointcut id="pointcut" expression="execution(* com.molu.service.UserServiceImpl.*(..))"/>
<!--前置通知设置为我们写在 diyAspect 中的 Before方法-->
<aop:before method="Before" pointcut-ref="pointcut"/>
<!--后置通知设置为我们写在 diyAspect 中的 After方法-->
<aop:after method="After" pointcut-ref="pointcut"/>
</aop:aspect>

</aop:config>

MyTest测试类不进行任何改动,直接运行测试。

这种通过自定义切面的方式 相对来说会更加简单一些也更容易理解,但因为我们写的只是普通方法,功能上自然是不如实现接口的方式强大

注解实现

使用注解实现之前,我们需要开启 AOP注解的支持 和自动扫描包

1
2
3
4
xml复制代码<!--自动扫描包,使该包下的注解能够生效-->
<context:component-scan base-package="com.molu.diy"/>
<!--开启AOP注解支持-->
<aop:aspectj-autoproxy/>

写一个 Annotation类,在该类中定义一个方法为前置通知,使用注解进行标记。

1
2
3
4
5
6
7
8
9
10
java复制代码@Component // 使用注解注册Bean
@Aspect // 使用注解标记该类为一个切面
public class Annotation {

@Before("execution(* com.molu.service.UserServiceImpl.*(..))")
// 标记为前置通知,由于类中没办法引用切入点,所以切入点需要我们手动写。这也是注解来实现AOP的一个不便之处。
public void before(){
System.out.println("我是前置通知~~~");
}
}

Mytest测试类不进行任何改动,直接进行测试

到这里 AOP的三种常见的实现方式 就介绍的差不多了,三种方式各自有各自的好处。使用方面哪种简单用哪种即可

AOP并没有我们想象中的那么难,主要的是理解这种面向切面的思想。

使用 AOP后我们在业务中插入日志等功能会更加的便捷,且不会对业务类造成太多的影响,对日志等功能进行修改或删除也大多不会对业务类本身造成影响。

也就做到了所谓的高内聚低耦合,能够熟练的运用 AOP 对写出优质的代码多多少少也会有一些帮助

声明式事务

什么是事务

事务可以简单的理解为,将一组业务当作一个业务来处理。要么都成功要么都失败

事务在开发中十分的重要,它涉及数据的完整性和一致性

事务的ACID原则

事务的ACID原则在面试中会被经常问到,分别是,原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )。这四个特性简称为 ACID 特性。

  • 原子性
    • 简单的说就是要么都成功要么都失败
  • 一致性
    • 事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。
    • 因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。
    • 如果数据库系统 运行中发生故障,个别事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于不一致的状态。
  • 隔离性
    • 多个业务可能操作同一个资源
    • 我们需要保证这些业务操作数据时是互相隔离的,不会造成数据的损坏等问题。
    • 确保完整性和一致性
  • 持久性
    • 指事务一旦提交,它对数据库中的数据的改变就应该是永久性的,不能回滚。
    • 之后的其它操作或故障不应该对其执行结果有任何影响

开启事务

Spring支持声明式事务和编程式事务两种事务管理模式,我们一般使用声明式事务。

  • 编程式事务管理: 通过Transaction Template手动管理事务,实际应用中很少使用
  • 使用XML配置声明式事务: 推荐使用(代码侵入性最小),实际是通过AOP实现

由于篇幅至此已经一万字了,所以我们只浅显的涉及声明式事务,来作为这篇文章的收尾

声明式事务会使用 AOP,在指定的切入点中织入事务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码 <!--配置c3p0连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--注入属性-->
<property name="driverClass" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&amp;useSSL=true&amp;useUnicode=true&amp;characterEncoding=UTF-8"/>
<property name="user" value="root"/>
<property name="password" value="手动马赛克"/>
</bean>
<!--配置事务管理器-->
<bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<constructor-arg ref="dataSource"/>
</bean>
<!--配置事务通知-->
<tx:advice id="interceptor" transaction-manager="dataSourceTransactionManager">
<tx:attributes>
<!-- * 表示我们每一个方法都会被织入事务-->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>

<!--配置事务切入-->
<aop:config>
<aop:pointcut id="txPointCut" expression="execution(* com.molu.service.*.*(..))"/>
<aop:advisor advice-ref="interceptor" pointcut-ref="txPointCut"/>
</aop:config>
<!--配置完成后我们service中的所有类的所有方法,都会被织入事务-->
</beans>

以上就是如何开启声明式事务的全部操作,到这里我们从IOC到AOP的这篇博客也要结束了。非常感谢你能看到这里,如果有帮到你的话。


放松一下眼睛

pixiv地址

画师主页


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

微服务降级方案

发表于 2020-10-03

在微服务调用的中,为了防止服务的雪崩和业务方案的降级,要事先准备好降级方案。

业务降级

比如优惠券的推荐方案,在上线后,有可能发现不是很合适,有大量的客诉。
这时就要回滚到之前的方案。
有两种选择

  • 回滚代码,重新上线,浪费时间和人力
  • Apollo配置开关,开关关闭,回到之前的逻辑,简单高效,新老接口逻辑耦合在一起

熔断降级

feign调用其他服务,有hystrix降级,超时熔断,保护本服务。

Hystrix整个工作流如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
!复制代码  1.构造一个 HystrixCommand或HystrixObservableCommand对象,用于封装请求,并在构造方法配置请求被执行需要的参数;

2.执行命令,Hystrix提供了4种执行命令的方法,后面详述;

3.判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix支持请求缓存,但需要用户自定义启动;

4.判断熔断器是否打开,如果打开,跳到第8步;

5.判断线程池/队列/信号量是否已满,已满则跳到第8步;

6.执行HystrixObservableCommand.construct()或HystrixCommand.run(),如果执行失败或者超时,跳到第8步;否则,跳到第9步;

7.统计熔断器监控指标;

8.走Fallback备用逻辑

9.返回请求响应

从流程图上可知道,第5步线程池/队列/信号量已满时,还会执行第7步逻辑,更新熔断器统计信息,而第6步无论成功与否,都会更新熔断器统计信息。

hystrix隔离策略:线程池(默认),信号量

策略 线程切换 支持异步 支持超时 支持熔断 限流 开销
信号量 否 否 否 是 是 小
线程池 是 是 是 是 是 大

手写超时降级策略

基于spring aop,一个注解加上一个切面类就可以了。使用线程池来执行业务代码,超时降级,执行降级方法,没有熔断功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@RestController
public class HelloController {

private Random random = new Random();

@RequestMapping("/hello")
@MyHystrixCommand(fallback = "errorMethod")
public String hello(String message) throws InterruptedException {
int time = random.nextInt(200);
System.out.println("spend time : " + time + "ms");
Thread.sleep(time);
System.out.println("hhhhhhhhhhhhhhhhhhhhhhhhh");
return "hello world:" + message;
}

public String errorMethod(String message) {
return "error message";
}
}
1
2
3
4
5
6
7
java复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyHystrixCommand {
int value() default 100;
String fallback() default "";
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码@Aspect
@Component
public class MyHystrixCommandAspect {

ExecutorService executor = Executors.newFixedThreadPool(10);


@Pointcut(value = "@annotation(MyHystrixCommand)")
public void pointCut(){

}

// @Around(value = "pointCut()&&@annotation(hystrixCommand)")
@Around(value = "@annotation(hystrixCommand)")
public Object doPointCut(ProceedingJoinPoint joinPoint, MyHystrixCommand hystrixCommand) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
int timeout = hystrixCommand.value();
Future<?> future = executor.submit(() -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
throw new RuntimeException("方法执行异常");
}
});
Object result;
try {
result = future.get(timeout, TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
future.cancel(true);
//执行降级方法
if (StringUtils.isBlank(hystrixCommand.fallback())){
throw new RuntimeException("无降级方法");
}
System.out.println("执行降级方法");
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
Class[] parameterTypes = methodSignature.getParameterTypes();

Class<?> aClass = joinPoint.getTarget().getClass();
Method method = aClass.getMethod(hystrixCommand.fallback(), parameterTypes);
result = method.invoke(joinPoint.getTarget(), joinPoint.getArgs());
return result;
}
return result;
}

}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…776777778…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%