0%

这篇博客是我在阅读 Spring 英文文档过程中记录的对 Spring 的新认知,或者说是之前比较模糊的概念。总而言之,算是我个人的随笔记录,方便以后查看,如果感觉对你帮助不大,可以跳过不看本篇博客。

1. Container Overview

The org.springframework.context.ApplicationContext interface represents the Spring IoC container and is responsible for instantiating, configuring, and assembling the beans. The container gets its instructions on what objects to instantiate, configure, and assemble by reading configuration metadata. The configuration metadata is represented in XML, Java annotations, or Java code. It lets you express the objects that compose your application and the rich interdependencies between those objects.

applicationcontext接口表示Spring IoC容器,并负责实例化、配置和组装bean。容器通过读取配置元数据来获取关于实例化、配置和组装哪些对象的指令。配置元数据以XML、Java注释或Java代码表示。它允许您表达组成应用程序的对象以及这些对象之间的丰富的相互依赖关系。

2. Configuration Metadata

For information about using other forms of metadata with the Spring container, see:

  • Annotation-based configuration: Spring 2.5 introduced support for annotation-based configuration metadata.
  • Java-based configuration: Starting with Spring 3.0, many features provided by the Spring JavaConfig project became part of the core Spring Framework. Thus, you can define beans external to your application classes by using Java rather than XML files. To use these new features, see the @Configuration, @Bean, @Import, and @DependsOn annotations.

Spring configuration consists of at least one and typically more than one bean definition that the container must manage. XML-based configuration metadata configures these beans as <bean/> elements inside a top-level <beans/> element. Java configuration typically uses @Bean-annotated methods within a @Configuration class.

这段我认为需要理解好最后一句话:Java 配置通常在@Configuration 类中使用@Bean 注释的方法。

Spring3.0开始,@Configuration用于定义配置类,定义的配置类可以替换xml文件,一般和@Bean注解联合使用。@Configuration注解主要标注在某个类上,相当于xml配置文件中的@Bean注解主要标注在某个方法上,相当于xml配置文件中的

D:\development\myblog\source\_posts\阅读Spring文档摘录\202004161021529.png

2.1 the basic structure of XML-based configuration metadata

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>

</beans>
  • The id attribute is a string that identifies the individual bean definition.
  • The class attribute defines the type of the bean and uses the fully qualified classname.

3. Composing XML-based Configuration Metadata

It can be useful to have bean definitions span multiple XML files. Often, each individual XML configuration file represents a logical layer or module in your architecture.

You can use the application context constructor to load bean definitions from all these XML fragments. This constructor takes multiple Resource locations, as was shown in the previous section. Alternatively, use one or more occurrences of the <import/> element to load bean definitions from another file or files. The following example shows how to do so:

1
2
3
4
5
6
7
8
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>

<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>

In the preceding example, external bean definitions are loaded from three files: services.xml, messageSource.xml, and themeSource.xml. All location paths are relative to the definition file doing the importing, so services.xml must be in the same directory or classpath location as the file doing the importing, while messageSource.xml and themeSource.xml must be in a resources location below the location of the importing file. As you can see, a leading slash is ignored. However, given that these paths are relative, it is better form not to use the slash at all. The contents of the files being imported, including the top level <beans/> element, must be valid XML bean definitions, according to the Spring Schema.

上述内容主要就是描述了XML如何定义,其中黑体字标出的相对重要些:所有的位置路径都是相对于执行导入的定义文件的,因此services.xml必须与执行导入的文件位于相同的目录或类路径位置,而messageSource.xml和themeSource.xml必须位于导入文件位置下方的资源位置。

It is possible, but not recommended, to reference files in parent directories using a relative “../“ path. Doing so creates a dependency on a file that is outside the current application. In particular, this reference is not recommended for classpath: URLs (for example, classpath:../services.xml), where the runtime resolution process chooses the “nearest” classpath root and then looks into its parent directory. Classpath configuration changes may lead to the choice of a different, incorrect directory.

You can always use fully qualified resource locations instead of relative paths: for example, file:C:/config/services.xml or classpath:/config/services.xml. However, be aware that you are coupling your application’s configuration to specific absolute locations. It is generally preferable to keep an indirection for such absolute locations — for example, through “${…}” placeholders that are resolved against JVM system properties at runtime.

上述大概意思就是不推荐使用 “../” 这种形式的路径配置,这样做会在当前应用程序之外的文件上创建一个依赖项。特别是不建议classpath: URLs (比如 classpath:../services.xml) 这种形式的引用,运行时解析进程选择“最近的”类路径根,然后查看其父目录。

可以使用完全限定的资源位置而不是绝对位置: 比如 file:C:/config/services.xml 或者classpath:/config/services.xml。但是,请注意,您正在将应用程序的配置耦合到特定的绝对位置。对于这种绝对位置,通常更可取的做法是保持间接性ーー例如,通过在运行时根据 JVM 系统属性解析的“ ${ … }”占位符。

4. Naming Beans

Every bean has one or more identifiers. These identifiers must be unique within the container that hosts the bean. A bean usually has only one identifier. However, if it requires more than one, the extra ones can be considered aliases.

In XML-based configuration metadata, you use the id attribute, the name attribute, or both to specify the bean identifiers. The id attribute lets you specify exactly one id.

The convention is to use the standard Java convention for instance field names when naming beans. That is, bean names start with a lowercase letter and are camel-cased from there. Examples of such names include accountManager, accountService, userDao, loginController, and so forth.

Naming beans consistently makes your configuration easier to read and understand. Also, if you use Spring AOP, it helps a lot when applying advice to a set of beans related by name.

With component scanning in the classpath, Spring generates bean names for unnamed components, following the rules described earlier: essentially, taking the simple class name and turning its initial character to lower-case. However, in the (unusual) special case when there is more than one character and both the first and second characters are upper case, the original casing gets preserved. These are the same rules as defined by java.beans.Introspector.decapitalize (which Spring uses here).

上述前两段主要就是描述每个 bean 都有一个或多个标识符。这些标识符在承载 bean 的容器中必须是唯一的。一个 bean 通常只有一个标识符。但是,如果它需要多于一个,那么额外的那些可以被认为是别名。Bean的命名方式通常有 idname 命名,

中间两段主要介绍了并推荐使用驼峰命名。但是这部分最后一句加粗的文字目前我并不理解,暂且搁置。

最后一段描述通过在类路径中进行组件扫描,Spring 为未命名的组件生成 bean 名称,遵循前面描述的规则: 本质上,使用简单的类名并将其初始字符转换为小写。但是,在(不寻常的)特殊情况下,当有多个字符且第一个和第二个字符都是大写字母时,原始大小写将得到保留。这些规则与 java.beans 定义的规则相同。这部分加粗文字和Java内省机制相关类 Introspector 有关,其中该类相关源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
public static String decapitalize(String name) {
if (name == null || name.length() == 0) {
return name;
}
if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) &&
Character.isUpperCase(name.charAt(0))){
return name;
}
char chars[] = name.toCharArray();
chars[0] = Character.toLowerCase(chars[0]);
return new String(chars);
}

一般情况下,把字符串第一个字母变为小写,如把“FooBah”变为“fooBah”。但在特殊情况下,即字符串前两个字母都是大写的时候,什么也不做,如,遇到“URL”,原样返回。

decapitalize()的bug是:如果一个字符串,前两个字母大写,但后面还有小写字母,它仍然返回原字符串!

Hibernate的开发者注意到decapitalize()的特点,所以才在判断语句中使用一个或运算(不然只需要判断方法名截掉“get”,再改第一个字母为小写后的字符串与属性名是否相等即可,这也是按照JavaBean Specification定义的标准做法)。但是,Hibernate没有解决这个bug,可能是他们没有碰到我遇到的情况。

类似sAddress(一般性地说,第一个字母小写,第二个字母大写)属性命名就是bug的诱因。

那么,解决方法有三种:

把属性名改成SAddress,这样就满足上面匹配判断的第二个条件(方法名截掉“get”后,与属性名匹配)。但是,这样做不符合Java命名规范;

把getSAddress()改成getsAddress(),这样也满足上面匹配判断的第二个条件(方法名截掉“get”后,与属性名匹配)。但是,这样做不符合JavaBean命名规范;

把属性名改成strAddress,并形成一种约定:命名属性时,第二个字符只能是小写字母。这个方法不需要做更多地修改,符合所有规范,最为稳妥。

4.1 Aliasing a Bean outside the Bean Definition

子系统 a 的配置元数据可以通过 subsystemA-DataSource 的名称引用 DataSource。子系统 b 的配置元数据可以通过 subsystemB-DataSource 的名称来引用 DataSource。当组合使用这两个子系统的主应用程序时,主应用程序通过 myapp-DataSource 的名称引用 DataSource。要让所有三个名称都指向同一个对象,可以向配置元数据添加以下别名定义:

1
2
<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

现在,每个组件和主应用程序都可以通过一个惟一的名称引用 dataSource,并保证不会与任何其他定义发生冲突(有效地创建名称空间) ,但它们引用的是同一个 bean。

If you use Javaconfiguration, the @Bean annotation can be used to provide aliases. See Using the @Bean Annotation for details.

4.2 Instantiation with a Static Factory Method(用静态工厂方法实例化)

调用这个方法(带有可选参数,如后面所述)并返回一个活动对象,随后该对象将被视为通过构造函数创建的。这种 bean 定义的一个用途是在遗留代码中调用静态工厂。

下面的 bean 定义指定通过调用 factory 方法创建 bean。该定义不指定返回对象的类型(类) ,只指定包含工厂方法的类。在本例中,createInstance ()方法必须是静态方法。下面的示例演示如何指定工厂方法:

1
2
3
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>

下面的示例展示了一个与前面的 bean 定义一起工作的类:

1
2
3
4
5
6
7
8
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}

public static ClientService createInstance() {
return clientService;
}
}

遗留代码: Legacy Code

遗留代码是指不再受支持的应用程序系统源代码类型。遗留代码也可以指不支持的操作系统、硬件和格式。在大多数情况下,遗留代码被转换为现代软件语言和平台。然而,为了保留熟悉的用户功能,遗留代码有时会被带入新的环境中。

这里可以参考我的另一篇博客来理解静态工厂方法创建对象:Effective Java–创建和销毁对象

4.3 Instantiation by Using an Instance Factory Method (使用实例工厂方法实例化)

与通过静态工厂方法进行的实例化类似,使用实例工厂方法进行的实例化,从容器中调用现有 bean 的非静态方法来创建新 bean。要使用这种机制,保留 class 属性为空,并在 factory-bean 属性中,在当前(或父或祖先)容器中指定 bean 的名称,该容器包含要调用来创建对象的实例方法。使用 factory-method 属性设置 factory 方法本身的名称。下面的例子展示了如何配置这样一个 bean:

1
2
3
4
5
6
7
8
9
<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>

下面的示例展示了相应的类:

1
2
3
4
5
6
7
8
public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}
}

一个工厂类也可以容纳多个工厂方法,如下面的示例所示:

1
2
3
4
5
6
7
8
9
10
11
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
<!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>

<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>

下面的示例展示了相应的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class DefaultServiceLocator {

private static ClientService clientService = new ClientServiceImpl();

private static AccountService accountService = new AccountServiceImpl();

public ClientService createClientServiceInstance() {
return clientService;
}

public AccountService createAccountServiceInstance() {
return accountService;
}
}

这种方法表明,工厂 bean 本身可以通过依赖注入管理器(DI)来管理和配置。详见依赖关系和配置

在 Spring 文档中,”factory bean” 指的是在 Spring 容器中配置并通过实例或静态工厂方法创建对象的 bean。相比之下,FactoryBean (注意大写)引用了一个 spring 特定的 FactoryBean 实现类。

4.4 Determining a Bean’s Runtime Type

确定特定 bean 的运行时类型并不简单。Bean 元数据定义中的指定类仅仅是一个初始类引用,它可能与已声明的工厂方法或 FactoryBean 类结合在一起,后者可能导致 bean 的不同运行时类型,或者在实例级工厂方法(可以通过指定的工厂 bean 名称解析)的情况下根本不设置类。此外,AOP 代理可以使用基于接口的代理来包装 bean 实例,对目标 bean 的实际类型(仅仅是实现的接口)进行有限的公开。

查找特定 bean 的实际运行时类型的推荐方法是 BeanFactory.getType 调用指定的 bean 名称。这将考虑上述所有情况,并返回 BeanFactory.getBean 调用将返回的对象类型。

5. Dependencies

5.1 Constructor Argument Resolution

使用参数的类型进行构造函数参数解析匹配。如果 bean 定义的构造函数参数中没有潜在的歧义,那么在 bean 定义中定义构造函数参数的顺序就是在 bean 被实例化时这些参数被提供给相应的构造函数的顺序。参考下面的类:

1
2
3
4
5
6
public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}

假设 ThingTwo 和 thingThree类不通过继承关联,则不存在潜在的歧义。因此,下面的配置可以很好地工作,不需要在 < constructor-arg/> 元素中显式地指定构造函数参数索引或类型。

1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

当引用另一个 bean 时,该类型是已知的,并且可以进行匹配(与前面的示例一样)。当使用简单类型时,如 < value > true </value> ,Spring 无法确定值的类型,因此在没有帮助的情况下无法按类型进行匹配。参考下面的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ExampleBean {

// Number of years to calculate the Ultimate Answer
private final int years;

// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;

public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}

类型匹配

在前面的场景中,如果使用 type 属性显式指定构造函数参数的类型,那么容器可以对简单类型使用类型匹配,如下面的示例所示:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

构造函数参数的顺序

可以使用 index 属性显式指定构造函数参数的索引,如下面的示例所示:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>

除了解决多个简单值的不确定性,指定索引还可以解决构造函数具有两个相同类型的参数时的不确定性。

5.2 Constructor-based or setter-based DI?

Since you can mix constructor-based and setter-based DI, it is a good rule of thumb to use constructors for mandatory dependencies and setter methods or configuration methods for optional dependencies. Note that use of the @Required annotation on a setter method can be used to make the property be a required dependency; however, constructor injection with programmatic validation of arguments is preferable.

The Spring team generally advocates constructor injection, as it lets you implement application components as immutable objects and ensures that required dependencies are not null. Furthermore, constructor-injected components are always returned to the client (calling) code in a fully initialized state. As a side note, a large number of constructor arguments is a bad code smell, implying that the class likely has too many responsibilities and should be refactored to better address proper separation of concerns.

Setter injection should primarily only be used for optional dependencies that can be assigned reasonable default values within the class. Otherwise, not-null checks must be performed everywhere the code uses the dependency. One benefit of setter injection is that setter methods make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is therefore a compelling use case for setter injection.

Use the DI style that makes the most sense for a particular class. Sometimes, when dealing with third-party classes for which you do not have the source, the choice is made for you. For example, if a third-party class does not expose any setter methods, then constructor injection may be the only available form of DI.

译:由于可以混合使用基于构造函数和基于 setter 的 DI,因此对于强制依赖项使用构造函数和对于可选依赖项使用 setter 方法或配置方法是一个很好的经验法则。注意,在 setter 方法上使用@Required 注释可以使该属性成为必需的依赖项; 但是,带有参数编程验证的构造函数注入更可取。

Spring 团队通常提倡构造函数注入,因为它允许您将应用程序组件实现为不可变对象,并确保所需的依赖项不为空。此外,构造函数注入的组件总是以完全初始化的状态返回给客户机(调用)代码。作为一个旁注,大量的构造函数参数是糟糕的代码,(本博客作者注:在《代码整洁之道》书中,作者建议一般不超过三个参数,超过的话意味着要对函数进行拆分),这意味着类可能有太多的责任,应该重构以更好地解决适当的关注点分离/代码。

Setter 注入应该主要用于可选的依赖项,这些依赖项可以在类中分配合理的默认值。否则,必须在代码使用依赖项的所有地方执行非空检查。Setter 注入的一个好处是 setter 方法使该类的对象容易在以后重新配置或重新注入。因此,通过 JMX MBeans进行管理是 setter 注入的一个引人注目的用例。

使用对特定类最有意义的DI样式。有时,在处理没有源代码的第三方类时,可以自行选择。例如,如果一个第三方类不公开任何setter方法,那么构造函数注入可能是DI的唯一可用形式。

构造注入是必须把一个对象的所有依赖的对象都进行实例化,才能实例化这个对象

Setter注入是先实例化这个对象,然后找到依赖的对象,对依赖的对象进行实例化。

setter注入: 一般情况下所有的java bean, 我们都会使用setter方法和getter方法去设置和获取属性的值,示例如下:

1
2
3
4
5
6
7
8
9
public class namebean {
String name;
public void setName(String a) {
name = a;
}
public String getName() {
return name;
}
}

我们会创建一个bean的实例然后设置属性的值,spring的配置文件如下:

我们会创建一个bean的实例然后设置属性的值,spring的配置文件如下:

1
2
3
4
5
<bean id="bean1″ >
<property name="name" >
<value>tom</value>
</property>
</bean>

Spring会调用setName方法来只是name属性为tom
构造方法注入:构造方法注入中,我们使用带参数的构造方法如下:

Spring会调用setName方法来只是name属性为tom
构造方法注入:构造方法注入中,我们使用带参数的构造方法如下:

1
2
3
4
5
6
public class namebean {
String name;
public namebean(String a) {
name = a;
}
}

我们会在创建bean实例的时候以new namebean(”tom”)的方式来设置name属性, Spring配置文件如下:

我们会在创建bean实例的时候以new namebean(”tom”)的方式来设置name属性, Spring配置文件如下:

1
2
3
4
5
<bean id="bean1″ >
<constructor-arg>
<value>My Bean Value</value>
</constructor-arg>
</bean>

使用constructor-arg标签来设置构造方法的参数。

使用constructor-arg标签来设置构造方法的参数。

引用:构造注入和Setter注入

5.3 Dependency Resolution Process(依赖解析的过程)

The container performs bean dependency resolution as follows:

  • The ApplicationContext is created and initialized with configuration metadata that describes all the beans. Configuration metadata can be specified by XML, Java code, or annotations.
  • For each bean, its dependencies are expressed in the form of properties, constructor arguments, or arguments to the static-factory method (if you use that instead of a normal constructor). These dependencies are provided to the bean, when the bean is actually created.
  • Each property or constructor argument is an actual definition of the value to set, or a reference to another bean in the container.
  • Each property or constructor argument that is a value is converted from its specified format to the actual type of that property or constructor argument. By default, Spring can convert a value supplied in string format to all built-in types, such as int, long, String, boolean, and so forth.

The Spring container validates the configuration of each bean as the container is created. However, the bean properties themselves are not set until the bean is actually created. Beans that are singleton-scoped and set to be pre-instantiated (the default) are created when the container is created. Scopes are defined in Bean Scopes. Otherwise, the bean is created only when it is requested. Creation of a bean potentially causes a graph of beans to be created, as the bean’s dependencies and its dependencies’ dependencies (and so on) are created and assigned. Note that resolution mismatches among those dependencies may show up late — that is, on first creation of the affected bean.

容器执行 bean 依赖项解析如下:

创建 ApplicationContext 并使用描述所有 bean 的配置元数据进行初始化。可以通过 XML、 Java 代码或注释指定配置元数据。

对于每个 bean,它的依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示(如果您使用静态工厂方法而不是普通的构造函数)。在实际创建 bean 时,这些依赖项被提供给 bean。

每个属性或构造函数参数都是要设置的值的实际定义,或对容器中另一个 bean 的引用。

作为值的每个属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,如 int、 long、 String、 boolean 等。

Spring 容器在创建容器时验证每个 bean 的配置。但是,在 bean 实际创建之前,不会设置 bean 属性本身。在创建容器时,将创建单一作用域并设置为预实例化(默认情况)的 bean。作用域在 Bean 作用域中定义。否则,只有在请求 bean 时才会创建它。创建 bean 可能会导致创建 bean 图,因为创建和分配 bean 的依赖项及其依赖项的依赖项(等等)。在首次创建受影响的 bean 时,请注意,这些依赖项之间的解析不匹配可能会延迟出现。

You can generally trust Spring to do the right thing. It detects configuration problems, such as references to non-existent beans and circular dependencies, at container load-time. Spring sets properties and resolves dependencies as late as possible, when the bean is actually created. This means that a Spring container that has loaded correctly can later generate an exception when you request an object if there is a problem creating that object or one of its dependencies — for example, the bean throws an exception as a result of a missing or invalid property. This potentially delayed visibility of some configuration issues is why ApplicationContext implementations by default pre-instantiate singleton beans. At the cost of some upfront time and memory to create these beans before they are actually needed, you discover configuration issues when the ApplicationContext is created, not later. You can still override this default behavior so that singleton beans initialize lazily, rather than being eagerly pre-instantiated.

If no circular dependencies exist, when one or more collaborating beans are being injected into a dependent bean, each collaborating bean is totally configured prior to being injected into the dependent bean. This means that, if bean A has a dependency on bean B, the Spring IoC container completely configures bean B prior to invoking the setter method on bean A. In other words, the bean is instantiated (if it is not a pre-instantiated singleton), its dependencies are set, and the relevant lifecycle methods (such as a configured init method or the InitializingBean callback method) are invoked.

通常可以相信 Spring 会做正确的事情。它在容器加载时检测配置问题,例如对不存在的 bean 和循环依赖项的引用。Spring 在实际创建 bean 时设置属性并尽可能晚地解析依赖项。这意味着,如果创建对象或其某个依赖项时出现问题,那么正确加载的 Spring 容器随后可以在请求对象时生成异常ーー例如,由于缺少或无效属性,bean 抛出异常。一些配置问题的可见性可能会延迟,这就是为什么默认情况下 ApplicationContext 实现会预先实例化单例 bean。在实际需要这些 bean 之前,需要花费一些前期时间和内存来创建它们,因此在创建 ApplicationContext 时(而不是以后)会发现配置问题。您仍然可以覆盖这个默认行为,以便单例 bean 以惰性方式初始化,而不是急切地预先实例化。

如果不存在循环依赖关系,当一个或多个合作 bean 被注入到依赖 bean 中时,每个合作 bean 在被注入到依赖 bean 之前都会被完全配置。这意味着,如果 bean a 对 bean b 有依赖关系,那么在调用 bean a 上的 setter 方法之前,Spring IoC 容器将完全配置 bean b。换句话说,bean 被实例化(如果它不是预实例化的单例) ,它的依赖关系被设置,相关的生命周期方法(如配置的 init 方法或 InitializingBean 回调方法)被调用。

6. Dependencies and Configuration in Detail

6.1 Straight Values (Primitives, Strings, and so on)

元素的 value 属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring 的转换服务用于将这些值从 String 转换为属性或参数的实际类型。下面的示例显示了正在设置的各种值:

1
2
3
4
5
6
7
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="misterkaoli"/>
</bean>

下面的示例使用 p 名称空间进行更简洁的 XML 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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 id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close"
p:driverClassName="com.mysql.jdbc.Driver"
p:url="jdbc:mysql://localhost:3306/mydb"
p:username="root"
p:password="misterkaoli"/>

</beans>

还可以配置 java.util.Properties 实例,如下所示:

1
2
3
4
5
6
7
8
9
10
11
<bean id="mappings"
class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">

<!-- typed as a java.util.Properties -->
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
</value>
</property>
</bean>

Spring 容器将 < value/> 元素内的文本转换为 java.util。属性实例,使用 javabean PropertyEditor 机制。这是一个很好的快捷方式,也是 Spring 团队确实喜欢使用嵌套的 < value/> 元素而不是 value 属性样式的少数几个地方之一。

6.2 The idref element

Idref 元素只是一种防错的方法,用于将容器中另一个 bean 的 id (字符串值——而不是引用)传递给一个 < constructor-arg/> 或 < property/> 元素。下面的例子展示了如何使用它:

1
2
3
4
5
6
7
<bean id="theTargetBean" class="..."/>

<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean"/>
</property>
</bean>

前面的 bean 定义片段与下面的片段完全等效(在运行时) :

1
2
3
4
5
<bean id="theTargetBean" class="..." />

<bean id="client" class="...">
<property name="targetName" value="theTargetBean"/>
</bean>

第一种形式比第二种更可取,因为使用 idref 标记可以让容器在部署时验证所引用的命名 bean 是否确实存在。在第二个变体中,不对传递给客户端 bean 的 targetName 属性的值执行验证。只有在实际实例化客户端 bean 时才会发现输入错误(很可能会导致致命的结果)。如果客户端 bean 是一个原型 bean,那么只有在部署容器之后很长时间才能发现这个排版错误和由此产生的异常。

Idref 元素的本地属性在4.0 beans XSD 中不再受支持,因为它不再在常规 bean 引用上提供值。升级到4.0模式时,将现有的 idref 本地引用更改为 idref bean。

6.3 Collections

<list/><set/><map/><props/>元素分别设置Java集合类型list、set、map和properties的属性和参数。下面的例子展示了如何使用它们:

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
<bean id="moreComplexObject" class="example.ComplexObject">
<!-- results in a setAdminEmails(java.util.Properties) call -->
<property name="adminEmails">
<props>
<prop key="administrator">administrator@example.org</prop>
<prop key="support">support@example.org</prop>
<prop key="development">development@example.org</prop>
</props>
</property>
<!-- results in a setSomeList(java.util.List) call -->
<property name="someList">
<list>
<value>a list element followed by a reference</value>
<ref bean="myDataSource" />
</list>
</property>
<!-- results in a setSomeMap(java.util.Map) call -->
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
<entry key="a ref" value-ref="myDataSource"/>
</map>
</property>
<!-- results in a setSomeSet(java.util.Set) call -->
<property name="someSet">
<set>
<value>just some string</value>
<ref bean="myDataSource" />
</set>
</property>
</bean>

6.4 Strongly-typed collection

随着Java 5中泛型类型的引入,您可以使用强类型集合。也就是说,可以声明一个Collection类型,使其只能包含(例如)String元素。如果您使用Spring将强类型集合的依赖项注入到bean中,那么您可以利用Spring的类型转换支持,以便在将强类型集合实例的元素添加到集合之前将其转换为适当的类型。下面的Java类和bean定义展示了如何做到这一点:

1
2
3
4
5
6
7
8
public class SomeClass {

private Map<String, Float> accounts;

public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
1
2
3
4
5
6
7
8
9
10
11
<beans>
<bean id="something" class="x.y.SomeClass">
<property name="accounts">
<map>
<entry key="one" value="9.99"/>
<entry key="two" value="2.75"/>
<entry key="six" value="3.99"/>
</map>
</property>
</bean>
</beans>

something bean的 accounts 属性准备注入时,关于强类型 Map<String, Float> 的元素类型的泛型信息通过反射可用。因此,Spring的类型转换基础设施将各种值元素识别为Float类型,并将字符串值(9.992.753.99)转换为实际的Float类型。

6.5 XML Shortcut with the p-namespace

p名称空间允许使用bean元素的属性(而不是嵌套的元素)来描述协作bean的属性值,或者两者都使用。

Spring支持具有名称空间的可扩展配置格式,名称空间基于XML Schema定义。本章中讨论的bean配置格式是在XML Schema文档中定义的。但是,p名称空间并没有在XSD文件中定义,它只存在于Spring的核心中。

下面的例子展示了两个XML片段(第一个使用标准XML格式,第二个使用p-命名空间),它们解析到相同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 name="classic" class="com.example.ExampleBean">
<property name="email" value="someone@somewhere.com"/>
</bean>

<bean name="p-namespace" class="com.example.ExampleBean"
p:email="someone@somewhere.com"/>
</beans>

该示例显示了bean定义中p-namespace中的一个名为email的属性。这告诉Spring包含一个属性声明。如前所述,p-namespace没有模式定义,因此可以将属性的名称设置为属性名称。

下一个例子包含另外两个bean定义,它们都有对另一个bean的引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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 name="john-classic" class="com.example.Person">
<property name="name" value="John Doe"/>
<property name="spouse" ref="jane"/>
</bean>

<bean name="john-modern"
class="com.example.Person"
p:name="John Doe"
p:spouse-ref="jane"/>

<bean name="jane" class="com.example.Person">
<property name="name" value="Jane Doe"/>
</bean>
</beans>

这个示例不仅包含使用p-namespace的属性值,而且还使用一种特殊的格式来声明属性引用。第一个bean定义使用<property name="spouse" ref="jane"/>来创建从bean johnbean jane的引用,而第二个bean定义使用p:spouse-ref="jane"作为属性来做完全相同的事情。在本例中,spouse是属性名,而-ref部分表明这不是一个直接的值,而是对另一个bean的引用。

6.6 XML Shortcut with the c-namespace

与带有p-namespace的XML Shortcut类似,在Spring 3.1中引入的c-namespace允许内联属性来配置构造函数参数,而不是嵌套constructor-arg元素。

下面的例子使用了c: namespace来做和from基于构造函数的依赖注入一样的事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="beanTwo" class="x.y.ThingTwo"/>
<bean id="beanThree" class="x.y.ThingThree"/>

<!-- traditional declaration with optional argument names -->
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg name="thingTwo" ref="beanTwo"/>
<constructor-arg name="thingThree" ref="beanThree"/>
<constructor-arg name="email" value="something@somewhere.com"/>
</bean>

<!-- c-namespace declaration with argument names -->
<bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>

c:命名空间使用与p: one相同的约定(后面的-ref表示bean引用),通过它们的名称设置构造函数参数。类似地,它需要在XML文件中声明,即使它没有在XSD模式中定义(它存在于Spring核心中)。

对于构造函数参数名不可用的罕见情况(通常是在没有调试信息的情况下编译字节码),你可以使用回退到参数索引,如下所示:

1
2
3
<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
c:_2="something@somewhere.com"/>

由于XML语法的原因,索引表示法要求前面有_,因为XML属性名称不能以数字开头(尽管有些ide允许)。对于元素也可以使用相应的索引表记法,但并不常用,因为这里的声明顺序通常就足够了。

7. Method Injection

在大多数应用程序场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性来处理依赖关系。当bean生命周期不同时,问题就出现了。假设单例bean A需要使用非单例(原型)bean B,也许是在对A的每个方法调用中。容器只创建一次单例bean A,因此只得到一次设置属性的机会。容器不能在每次需要bean B的新实例时为bean A提供新实例。

一种解决方案是放弃某些控制倒置。你可以通过实现ApplicationContextAware接口让bean A知道容器,并且每次bean A需要它时,通过getBean(“B”)调用容器来请求(典型的新)bean B实例。下面的例子展示了这种方法:

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
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

private ApplicationContext applicationContext;

public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}

protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}

public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

8. Request, Session, Application, and WebSocket Scopes

8.1 Request scope

Spring容器通过为每个HTTP请求使用LoginAction bean定义来创建LoginAction bean的新实例。也就是说,loginAction bean的作用域为HTTP请求级别。您可以随心所欲地更改创建的实例的内部状态,因为从相同的loginAction bean定义创建的其他实例不会在状态中看到这些更改。它们是针对个人要求的。当请求完成处理时,将范围限定在请求的bean丢弃。

在使用注释驱动的组件或Java配置时,可以使用@RequestScope注释将组件分配给请求范围。下面的例子展示了如何做到这一点:

1
2
3
4
5
@RequestScope
@Component
public class LoginAction {
// ...
}

8.2 Session Scope

Spring容器通过在单个HTTP会话的生命周期中使用UserPreferences bean定义来创建UserPreferences bean的新实例。换句话说,userPreferences bean有效地限定在HTTP Session级别。与请求范围内bean一样,你可以改变内部状态的实例创建尽可能多的你想要的,知道其他HTTP会话实例也使用相同的实例创建userPreferences bean定义看不到这些变化状态,因为他们是特定于一个单独的HTTP会话。当HTTP会话最终被丢弃时,限定在该特定HTTP会话范围内的bean也被丢弃。

在使用注释驱动的组件或Java配置时,可以使用@SessionScope注释将组件分配给会话作用域。

1
2
3
4
5
@SessionScope
@Component
public class UserPreferences {
// ...
}

8.3 Application Scope

Spring容器通过对整个web应用程序使用一次AppPreferences bean定义来创建一个AppPreferences bean的新实例。也就是说,appPreferences bean的作用域在ServletContext级别,并存储为一个常规ServletContext属性。这有点类似于弹簧单例bean,但在两个重要方面不同:它是一个单例每ServletContext不是每Spring ApplicationContext(可能有几个在任何给定的web应用程序),它实际上是暴露,因此可见ServletContext属性。

在使用注释驱动的组件或Java配置时,可以使用@ApplicationScope注释将组件分配给应用程序范围。下面的例子展示了如何做到这一点:

1
2
3
4
5
@ApplicationScope
@Component
public class AppPreferences {
// ...
}

8.4 WebSocket Scope

WebSocket作用域与WebSocket会话的生命周期相关,适用于WebSocket上的STOMP应用程序,更多细节请参阅WebSocket作用域

9. Customizing the Nature of a Bean(定制Bean的性质)

Spring框架提供了许多接口,您可以使用这些接口来定制bean的性质。本节将它们分组如下:

9.1 Lifecycle Callbacks

要与容器对bean生命周期的管理交互,你可以实现Spring InitializingBean和DisposableBean接口。容器对前者调用afterPropertiesSet(),对后者调用destroy(),让bean在初始化和销毁bean时执行某些操作。

JSR-250 @PostConstruct@PreDestroy注释通常被认为是现代Spring应用程序中接收生命周期回调的最佳实践。使用这些注释意味着您的bean没有耦合到特定于spring的接口。详细信息请参见使用Using @PostConstruct and @PreDestroy

如果你不想使用JSR-250注释,但是你仍然想要移除耦合,考虑 init-methoddestroy-method bean定义元数据。

在内部,Spring框架使用BeanPostProcessor实现来处理它可以找到并调用适当方法的任何回调接口。如果您需要定制特性或Spring默认不提供的其他生命周期行为,您可以自己实现BeanPostProcessor。有关更多信息,请参见Container Extension Points

除了初始化和销毁回调,spring管理的对象还可以实现Lifecycle接口,这样这些对象就可以参与启动和关闭过程,这是由容器自己的生命周期驱动的。

本节介绍生命周期回调接口。

9.1.1 Initialization Callbacks

initializingbean接口允许bean在容器设置完bean上所有必要的属性后执行初始化工作。InitializingBean接口指定了一个单一的方法:

1
void afterPropertiesSet() throws Exception;

我们建议您不要使用InitializingBean接口,因为它不必要地将代码耦合到Spring。另外,我们建议使用@PostConstruct注释或指定POJO初始化方法。在基于xml的配置元数据的情况下,可以使用init-method属性指定具有空无参数签名的方法的名称。在Java配置中,您可以使用@BeaninitMethod属性。请参见接收生命周期回调。考虑以下例子:

1
<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
1
2
3
4
5
6
public class ExampleBean {

public void init() {
// do some initialization work
}
}

上面的例子和下面的例子(包含两个清单)效果几乎完全相同:

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
1
2
3
4
5
6
7
public class AnotherExampleBean implements InitializingBean {

@Override
public void afterPropertiesSet() {
// do some initialization work
}
}

然而,前面两个示例中的第一个并没有将代码与Spring耦合起来。(后一个继承了InitializingBean)

9.1.2 Destruction Callbacks

实现这个接口( org.springframework.beans.factory.DisposableBean)可以让bean在容器销毁时获得回调函数。DisposableBean接口指定了一个单独的方法:

1
void destroy() throws Exception;

我们建议您不要使用DisposableBean回调接口,因为它不必要地将代码与Spring耦合起来。另外,我们建议使用@PreDestroy注释或指定bean定义支持的泛型方法。使用基于xml的配置元数据,可以在上使用destroy-method属性。在Java配置中,您可以使用@Bean的destroyMethod属性。请参见接收生命周期回调。考虑以下定义:

1
<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
1
2
3
4
5
6
public class ExampleBean {

public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}

前面的定义与下面的定义具有几乎完全相同的效果:

1
<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
1
2
3
4
5
6
7
public class AnotherExampleBean implements DisposableBean {

@Override
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}

然而,前面两个定义中的第一个并没有将代码与Spring耦合起来。

……此外本节还有一些没有摘录

9.1.3 Combining Lifecycle Mechanisms(结合生命周期机制)

在Spring 2.5中,你有三个控制bean生命周期行为的选项:

  • InitializingBeanDisposableBean回调接口
  • 自定义init()destroy()方法
  • @PostConstruct@PreDestroy注释。您可以组合这些机制来控制给定的bean。
9.1.4 Startup and Shutdown Callbacks

Lifecycle接口为任何有自己生命周期需求的对象定义了基本方法(比如启动和停止一些后台进程):

1
2
3
4
5
6
7
8
public interface Lifecycle {

void start();

void stop();

boolean isRunning();
}

任何spring管理的对象都可以实现Lifecycle接口。然后,当ApplicationContext本身接收到启动和停止信号时(例如,对于运行时的停止/重启场景),它将这些调用级联到在该上下文中定义的所有Lifecycle实现。它通过委托给LifecycleProcessor来实现,如下所示:

1
2
3
4
5
6
public interface LifecycleProcessor extends Lifecycle {

void onRefresh();

void onClose();
}
9.1.5 Shutting Down the Spring IoC Container Gracefully in Non-Web Applications

本节讲述了在非 web 应用程序中优雅地关闭 Spring IoC 容器,我个人认为这里暂时用的不多,以后有需要再看。

10 . Container Extension Points(扩展容器)

10.1 Customizing Beans by Using a BeanPostProcessor

BeanPostProcessor接口定义了回调方法,您可以实现这些方法来提供您自己的(或覆盖容器的默认值)实例化逻辑、依赖关系解析逻辑,等等。如果您想在Spring容器完成实例化、配置和初始化bean之后实现一些自定义逻辑,您可以插入一个或多个自定义BeanPostProcessor实现。

您可以配置多个BeanPostProcessor实例,并且可以通过设置order属性来控制这些BeanPostProcessor实例运行的顺序。只有当BeanPostProcessor实现了Ordered接口时,才能设置此属性。如果编写自己的BeanPostProcessor,还应该考虑实现Ordered接口。要了解更多细节,请参阅BeanPostProcessorOrdered接口的javadoc。请参见有关BeanPostProcessor实例的编程式注册的说明。

10.2 Example: Hello World, BeanPostProcessor-style

下面的例子展示了如何在ApplicationContext中编写、注册和使用BeanPostProcessor实例。

第一个示例演示了基本用法。这个例子展示了一个自定义的BeanPostProcessor实现,它在容器创建每个bean时调用它的toString()方法,并将结果字符串打印到系统控制台。

下面的清单显示了自定义的BeanPostProcessor实现类定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean; // we could potentially return any object reference here...
}

public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}

下面的bean元素使用了InstantiationTracingBeanPostProcessor:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?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:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/lang
https://www.springframework.org/schema/lang/spring-lang.xsd">

<lang:groovy id="messenger"
script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
<lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
</lang:groovy>

<!--
when the above bean (messenger) is instantiated, this custom
BeanPostProcessor implementation will output the fact to the system console
-->
<bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意InstantiationTracingBeanPostProcessor是如何定义的。它甚至没有名称,而且,因为它是一个bean,所以可以像其他任何bean一样进行依赖注入。(前面的配置还定义了一个由Groovy脚本支持的bean。Spring的动态语言支持将在“动态语言支持”一章中详细介绍。)

以下Java应用程序运行上述代码和配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;

public final class Boot {

public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = ctx.getBean("messenger", Messenger.class);
System.out.println(messenger);
}

}

上述应用程序的输出如下所示:

1
2
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961

10.3 Example: The Class Name Substitution PropertySourcesPlaceholderConfigurer

您可以使用propertysourcesconfigururer来通过使用标准Java Properties格式将bean定义中的属性值外部化到单独的文件中。这样一来,部署应用程序的人员就可以定制特定于环境的属性,如数据库url和密码,而无需修改主XML定义文件或容器文件,从而避免了复杂性或风险。

参考以下基于xml的配置元数据片段,其中定义了一个具有占位符值的数据源:

1
2
3
4
5
6
7
8
9
10
11
<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
<property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>

该示例显示了从外部properties文件配置的属性。在运行时,propertysourcesconfigururer被应用于元数据,用来替换数据源的一些属性。要替换的值被指定为${property-name}形式的占位符,它遵循Ant、log4j和JSP EL风格。

实际值来自另一个标准Java Properties格式的文件:

1
2
3
4
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

11. Annotation-based Container Configuration

注解是否比XML更适合配置Spring?

基于注释的配置的引入提出了这样一个问题:这种方法是否比XML“更好”。简短的回答是“视情况而定”。长一点的答案是,每种方法都有其优点和缺点,通常,这取决于开发人员决定哪种策略更适合他们。由于注解的定义方式,注解在其声明中提供了大量的上下文,从而使配置更短、更简洁。但是,XML擅长在不修改源代码或重新编译它们的情况下连接组件。一些开发人员更喜欢将连接连接到源代码附近,而另一些开发人员则认为,带注释的类不再是pojo,而且,配置变得分散,更难控制。

无论选择什么,Spring都可以容纳这两种风格,甚至可以将它们混合在一起。值得指出的是,通过它的JavaConfig选项,Spring允许以一种非侵入性的方式使用注释,而不涉及目标组件源代码。

一如既往,您可以将后处理器注册为单独的 bean 定义,但也可以通过在基于 xml 的 Spring 配置中包含以下标记来隐式注册它们(注意包含上下文名称空间) :

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

</beans>

<context:annotation-config/>元素隐式地注册了以下的后处理器:

<仅在定义注解的应用程序上下文中查找bean上的注解。这意味着,如果你把<context:annotation-config/>放在一个DispatcherServletWebApplicationContext中,它只检查你的控制器中的@Autowired bean,而不是你的服务。更多信息请参见DispatcherServlet

注释的相关讲解参考Spring官方文档

11.1 @Resource@Autowired的区别

参考:https://blog.csdn.net/weixin_40906484/article/details/113937179

11.2 基于自定义注解的依赖注入

您可以创建自己的自定义限定符注释。要做到这一点,请定义一个注释,并在定义中提供@Qualifier注释,如下所示:

1
2
3
4
5
6
7
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {

String value();
}

然后你可以在自动连接的字段和参数上提供自定义限定符,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MovieRecommender {

@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;

private MovieCatalog comedyCatalog;

@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}

// ...
}

接下来,您可以为候选bean定义提供信息。您可以将<qualifier/>标记添加为<bean/>标记的子元素,然后指定类型和值,以匹配您的自定义限定符注释。该类型与注释的完全限定类名相匹配。另外,如果不存在名称冲突的风险,为了方便起见,可以使用简短的类名。下面的例子演示了这两种方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

<bean class="example.SimpleMovieCatalog">
<qualifier type="Genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<qualifier type="example.Genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>

<bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

还可以定义自定义限定符注释,在简单的value属性之外或之外接受命名属性。如果在一个要自动连接的字段或参数上指定了多个属性值,那么bean定义必须匹配所有这些属性值,才能被视为自动连接候选属性。例如,参考以下注释定义:

1
2
3
4
5
6
7
8
9
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {

String genre();

Format format();
}

在这种情况下,Format是一个enum,定义如下:

1
2
3
public enum Format {
VHS, DVD, BLURAY
}

要自动连接的字段用自定义限定符标注,并包含两个属性的值:genreformat,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MovieRecommender {

@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;

@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;

@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;

@Autowired
@MovieQualifier(format=Format.BLURAY, genre="Comedy")
private MovieCatalog comedyBluRayCatalog;

// ...
}

最后,bean定义应该包含匹配的限定符值。这个例子还演示了您可以使用bean的元属性而不是<qualifier/>元素。如果可用,<qualifier/>元素及其属性优先,但如果没有这样的限定符,自动组合机制将返回到<meta/>标记中提供的值,就像下面示例中的最后两个bean定义一样:

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">

<context:annotation-config/>

<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Action"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<qualifier type="MovieQualifier">
<attribute key="format" value="VHS"/>
<attribute key="genre" value="Comedy"/>
</qualifier>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<meta key="format" value="DVD"/>
<meta key="genre" value="Action"/>
<!-- inject any dependencies required by this bean -->
</bean>

<bean class="example.SimpleMovieCatalog">
<meta key="format" value="BLURAY"/>
<meta key="genre" value="Comedy"/>
<!-- inject any dependencies required by this bean -->
</bean>

</beans>

12. AOP Concepts

相关术语:

1.通知(Advice)

  就是你想要的功能,也就是上面说的 安全,事物,日志等。你给先定义好把,然后在想用的地方用一下。

2.连接点(JoinPoint)

  这个更好解释了,就是spring允许你使用通知的地方,那可真就多了,基本每个方法的前,后(两者都有也行),或抛出异常时都可以是连接点,spring只支持方法连接点.其他如aspectJ还可以让你在构造器或属性注入时都行,不过那不是咱关注的,只要记住,和方法有关的前前后后(抛出异常),都是连接点。

3.切入点(Pointcut)

  上面说的连接点的基础上,来定义切入点,你的一个类里,有15个方法,那就有几十个连接点了对把,但是你并不想在所有方法附近都使用通知(使用叫织入,以后再说),你只想让其中的几个,在调用这几个方法之前,之后或者抛出异常时干点什么,那么就用切点来定义这几个方法,让切点来筛选连接点,选中那几个你想要的方法。

4.切面(Aspect)

  切面是通知和切入点的结合。现在发现了吧,没连接点什么事情,连接点就是为了让你好理解切点,搞出来的,明白这个概念就行了。通知说明了干什么和什么时候干(什么时候通过方法名中的before,after,around等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。

5.引入(introduction)

  允许我们向现有的类添加新方法属性。这不就是把切面(也就是新方法属性:通知定义的)用到目标类中吗

6.目标(target)

  引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑,他可以在毫不知情的情况下,被咱们织入切面。而自己专注于业务本身的逻辑。

7.代理(proxy)

  怎么实现整套aop机制的,都是通过代理,这个一会给细说。

8.织入(weaving)

  把切面应用到目标对象来创建新的代理对象的过程。有3种方式,spring采用的是运行时,为什么是运行时,后面解释。

  关键就是:切点定义了哪些连接点会得到通知

这篇博客是我在阅读 SpringBoot 英文文档过程中记录的对SpringBoot的新认知,或者说是之前比较模糊的概念。总而言之,算是我个人的随笔记录,方便以后查看,如果感觉对你帮助不大,可以跳过不看本篇博客。

1. Upgrading to a new feature release

When upgrading to a new feature release, some properties may have been renamed or removed. Spring Boot provides a way to analyze your application’s environment and print diagnostics at startup, but also temporarily migrate properties at runtime for you. To enable that feature, add the following dependency to your project:

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-properties-migrator</artifactId>
<scope>runtime</scope>
</dependency>

这段代码其实我之前并没有去深究他的含义,这也是我之前学习过程中的一个弊端,很多东西并没有去深究,后面慢慢发现这是一个很不好的习惯。

这几行代码是因为在SpringBoot版本升级过程中,一些 properties 可能会被改名或者移除,但是如果有这行代码,他会自动分析你的应用环境并在启动时打印出分析结果,也可以运行时临时迁移属性。

注意:一些较晚添加进环境的属性,比如 @PropertySource 这些,此配置不会对其生效。

另外,在完成迁移后,需要确认从项目依赖中移出这个依赖。

2. Using the “default” Package

When a class does not include a package declaration, it is considered to be in the “default package”. The use of the “default package” is generally discouraged and should be avoided. It can cause particular problems for Spring Boot applications that use the @ComponentScan, @ConfigurationPropertiesScan, @EntityScan, or @SpringBootApplication annotations, since every class from every jar is read.

We recommend that you follow Java’s recommended package naming conventions and use a reversed domain name (for example, com.example.project).

3. Locating the Main Application Class

We generally recommend that you locate your main application class in a root package above other classes. The @SpringBootApplication annotation is often placed on your main class, and it implicitly defines a base “search package” for certain items. For example, if you are writing a JPA application, the package of the @SpringBootApplication annotated class is used to search for @Entity items. Using a root package also allows component scan to apply only on your project.

If you do not want to use @SpringBootApplication, the @EnableAutoConfiguration and @ComponentScan annotations that it imports defines that behavior so you can also use those instead.

简而言之,上面的意思就是推荐使用 @SpringBootApplication 注解,它可以提供搜索架包的功能。如果不使用的话,也可以使用@EnableAutoConfiguration@ComponentScan 注解来代替相同的行为。

4. Auto-configuration

Spring Boot auto-configuration尝试根据添加的jar依赖项自动配置Spring应用程序。例如,如果HSQLDB在类路径上,并且您没有手动配置任何数据库连接bean,那么Spring Boot将自动配置内存中的数据库。

您需要通过在@Configuration类中添加@EnableAutoConfiguration@SpringBootApplication注释来选择自动配置。

您应该只添加一个@SpringBootApplication@EnableAutoConfiguration注释。我们通常建议您只将其中一个添加到主@Configuration类中。

5. Automatic Restart

5.1 Excluding Resources

某些资源在被更改时并不一定需要触发重新启动。例如,可以就地编辑Thymeleaf模板。默认情况下,在/META-INF/maven/META-INF/resources/resources/static/public/templates中更改资源不会触发重启,但会触发实时重新加载。如果你想定制这些排除,你可以使用spring.devtools.restart.exclude属性。例如,要只排除/static/public,你需要设置以下属性:

1
2
3
4
spring:
devtools:
restart:
exclude: "static/**,public/**"

如果你想保留这些默认值并添加额外的排除项,请改用spring.devtools.restart.additional-exclude属性。

5.2 Running the Remote Client Application

远程客户端应用程序被设计为从IDE中运行。您需要使用与您连接的远程项目相同的类路径运行org.springframework.boot.devtools.RemoteSpringApplication。应用程序的唯一必需参数是它所连接到的远程URL。

例如,如果你正在使用EclipseSpring Tools,并且你有一个名为my-app的项目,你已经部署到Cloud Foundry,你会这样做:

  • Select Run Configurations… from the Run menu.
  • Create a new Java Application “launch configuration”.
  • Browse for the my-app project.
  • Use org.springframework.boot.devtools.RemoteSpringApplication as the main class.
  • Add https://myapp.cfapps.io to the Program arguments (or whatever your remote URL is).

6. Application Events and Listeners

6.1 应用程序事件发送顺序

当你的应用程序运行时,应用程序事件按以下顺序发送:

  1. An ApplicationStartingEvent is sent at the start of a run but before any processing, except for the registration of listeners and initializers.
  2. An ApplicationEnvironmentPreparedEvent is sent when the Environment to be used in the context is known but before the context is created.
  3. An ApplicationContextInitializedEvent is sent when the ApplicationContext is prepared and ApplicationContextInitializers have been called but before any bean definitions are loaded.
  4. An ApplicationPreparedEvent is sent just before the refresh is started but after bean definitions have been loaded.
  5. An ApplicationStartedEvent is sent after the context has been refreshed but before any application and command-line runners have been called.
  6. An AvailabilityChangeEvent is sent right after with LivenessState.CORRECT to indicate that the application is considered as live.
  7. An ApplicationReadyEvent is sent after any application and command-line runners have been called.
  8. An AvailabilityChangeEvent is sent right after with ReadinessState.ACCEPTING_TRAFFIC to indicate that the application is ready to service requests.
  9. An ApplicationFailedEvent is sent if there is an exception on startup.

上面的列表只包括与 SpringApplication 绑定的 SpringApplicationEvents。除此之外,以下事件也会在 applicationprepareddevent applicationstarteddevent 之后发布:

  • A WebServerInitializedEvent is sent after the WebServer is ready. ServletWebServerInitializedEvent and ReactiveWebServerInitializedEvent are the servlet and reactive variants respectively.
  • A ContextRefreshedEvent is sent when an ApplicationContext is refreshed.

7. Web Environment

用来确定WebApplicationType的算法如下:

  • 如果存在Spring MVC,则会使用一个AnnotationConfigServletWebServerApplicationContext
  • 如果Spring MVC不存在而Spring WebFlux存在,那么会使用一个AnnotationConfigReactiveWebServerApplicationContext
  • 否则,使用AnnotationConfigApplicationContext

这意味着,如果你在同一个应用程序中使用Spring MVC和来自Spring WebFlux的新WebClient,那么默认情况下会使用Spring MVC。你可以通过调用setWebApplicationType(WebApplicationType)轻松覆盖它。也可以通过调用setApplicationContextClass(…)来完全控制ApplicationContext类型,具体可以参考源码去看。

7.1 Application Startup tracking(可用于Debug学习)

在应用程序启动期间,SpringApplicationApplicationContext执行许多与应用程序生命周期、bean生命周期甚至处理应用程序事件相关的任务。使用ApplicationStartup, Spring框架允许你使用StartupStep对象跟踪应用程序的启动顺序。收集这些数据可以用于分析目的,或者只是为了更好地理解应用程序的启动过程。

在设置SpringApplication实例时,您可以选择一个ApplicationStartup实现。例如,要使用BufferingApplicationStartup,你可以这样写:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(2048));
application.run(args);
}

}

第一个可用的实现FlightRecorderApplicationStartup是由Spring Framework提供的。它将Spring特定的启动事件添加到Java Flight Recorder会话中,用于分析应用程序,并将它们的Spring上下文生命周期与JVM事件(如分配、gc、类加载……)关联起来。配置完成后,你可以通过启用Flight Recorder运行应用程序来记录数据:

1
$ java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar demo.jar

Spring Boot自带BufferingApplicationStartup变量;这个实现是为了缓冲启动步骤,并将它们放入外部度量系统中。应用程序可以在任何组件中请求BufferingApplicationStartup类型的bean。

Spring Boot还可以配置为公开一个启动端点,该端点以JSON文档的形式提供该信息。

7.2 外部化配置

Spring Boot使用了一个非常特殊的PropertySource顺序,其目的是允许合理地重写值。属性按以下顺序考虑(较低项的值覆盖前面项的值):

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files).
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.

配置数据文件的顺序如下:

  1. Application properties packaged inside your jar (application.properties and YAML variants).
  2. Profile-specific application properties packaged inside your jar (application-{profile}.properties and YAML variants).
  3. Application properties outside of your packaged jar (application.properties and YAML variants).
  4. Profile-specific application properties outside of your packaged jar (application-{profile}.properties and YAML variants).

7.3 Configuring Random Values

RandomValuePropertySource用于注入随机值(例如,注入秘密或测试用例)。它可以生成integers, longs, uuids, 或string,如下例所示:

1
2
3
4
5
6
7
my:
secret: "${random.value}"
number: "${random.int}"
bignumber: "${random.long}"
uuid: "${random.uuid}"
number-less-than-ten: "${random.int(10)}"
number-in-range: "${random.int[1024,65536]}"

7.4 Type-safe Configuration Properties

7.5 JavaBean properties binding

可以绑定声明标准javabean属性的bean,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@ConfigurationProperties("my.service")
public class MyProperties {

private boolean enabled;

private InetAddress remoteAddress;

private final Security security = new Security();

// getters / setters...

public static class Security {

private String username;

private String password;

private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

// getters / setters...

}

}

第一条:用静态工厂方法代替构造器

静态工厂方法和构造器方法各有优势,在选择时应根据实际需求选择,而不是盲目采用构造器或者盲目采用静态工厂方法。

静态工厂方法相比于构造器,优势在于有名称

这个相对来说比较好理解。假如 BigInteger(int, int, Random) 返回素数,这时候它的返回类型只能定义为Int类型,调用者如果使用构造器,代码一般如下:

Int getNumber = new BigInteger(3, 9, ...)

这样再创建对象的时候,无法判断返回的是一个什么数。,但是使用静态工厂方法如下:

1
2
3
public static Int probablePrime(...){
/.../
}

则可以通过根据调用 BigInteger.probablePrime 来判断出这是一个素数。

静态工厂方法相比于构造器,不必每次调用时都创建一个新对象

这种方法类似于设计模式中的享元模式。比如下方代码:

1
2
3
4
5
6
7
public static final Boolean TRUE = new Boolean(true);

public static final Boolean FALSE = new Boolean(false);

public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

在调用 valueOf 方法时,每次不必返回新的对象,都是返回已有对象的地址引用。

静态工厂方法相比于构造器,可以返回原返回类型的任何子类型对象

这个在理解是可能会出现一些偏差。先看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class People {
//静态工厂方法
public static People createChildren() {
Children childer = new Children();
return childer;
}
}

//People的子类
class Children extends People {
public Children() {
}
}

如果使用构造器来创建对象,那么代码就是People people = new People();,这样返回的类型就只是 People类了,但是我们使用静态工厂方法的话,如上面代码所示,在静态工厂方法中可以直接返回Children的实例。

注:当时我的疑惑点主要在于People类中的createChildren()方法,当时认为不使用static也可以返回Children的实例。后面想明白,这使用静态工厂来创建对象,如果不使用static,那么该类的实例化会在createChildren()方法之前,那么就无法使用该方法来创建对象了。

静态工厂方法相比于构造器,所返回对象的类可以随着每次调用而发生变化,这取决于静态工厂方法的参数值

这个相对好理解一些。在静态工厂方法中,我们可以根据参数进行判断,根据参数不同返回不同的值,而构造方法则不行。其实从上一条也可以推出这条。

静态工厂方法相比于构造器,方法返回对象所属的类,在编写包含该静态工厂方法的类时可以不存在

这里引用一些别人的例子加上我自己的一些理解,代码如下:

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
//四大组成之一:服务接口
public interface LoginService {//这是一个登录服务
public void login();
}

//四大组成之二:服务提供者接口
public interface Provider {//登录服务的提供者。通俗点说就是:通过这个newLoginService()可以获得一个服务。
public LoginService newLoginService();
}

/**
* 这是一个服务管理器,里面包含了四大组成中的三和四
* 解释:通过注册将 服务提供者 加入map,然后通过一个静态工厂方法 getService(String name) 返回不同的服务。
*/
public class ServiceManager {
private static final Map<String, Provider> providers = new HashMap<String, Provider>();//map,保存了注册的服务

private ServiceManager() {
}

//四大组成之三:提供者注册API (其实很简单,就是注册一下服务提供者)
public static void registerProvider(String name, Provider provider) {
providers.put(name, provider);
}

//四大组成之四:服务访问API (客户端只需要传递一个name参数,系统会去匹配服务提供者,然后提供服务) (静态工厂方法)
public static LoginService getService(String name) {
Provider provider = providers.get(name);
if (provider == null) {
throw new IllegalArgumentException("No provider registered with name=" + name);

}
return provider.newLoginService();
}
}

上述代码的 ServiceManager类getService() 方法返回的是 LoginService(), 在编写的时候,LoginService方法可以没有被初始化等操作,待运行阶段调用前进行初始化等操作即可。

静态工厂方法相比于构造器的劣势

  1. 静态工厂方法的主要缺点在于,类如果不含公有的或者受保护的构造器,就不能被子类化。
  2. 静态工厂方法的第二个缺点在于,程序员很难发现他们。

消息队列的选择

如今用的相对较多的消息队列有ActiveMQ、RabbitMQ、RocketMQ和Kafka这几种,因此该项目消息队列的选择也将从这几种来挑选。

http://img.hznu.online/image-20211226110632594.png

对于ActiveMQ,由于其生态并不是很好,没经过大规模吞吐量场景的验证,社区也不是很活跃,因此不考虑采用

对于RocketMQ,由于目前该产品主要针对的是在校学生,万人左右,虽然是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高),基础架构研发实力较强,用 RocketMQ 是很好的选择。但是目前而言,我并没有足够的技术去研究并应用他的底层,而且目前来看也并不需要这么大的吞吐量支持,不考虑采用

对于KafKa,在大数据领域的实时计算、日志采集等场景使用较多,社区活跃,但是本系统并不涉及这些场景,目前来看也不需要这么大的吞吐量,不考虑采用

对于RabbitMQ,是一款开源产品,拥有比较稳定的支持,活跃度也高,最主要是之前略有涉及,因此我选择RabbitMQ作为该项目消息队列。

分布式还是单机

对于使用分布式还是单机系统,其实我也考虑了很久。

单机系统的优点自然不必多少,简单快速开发,而且之前也一直是采用单机系统研发,因此可以快速开始项目的编写。

在考虑是否采用分布式微服务架构前,当然需要先了解为什么要采用微服务以及传统的单机系统的弊端了。

最后决定先做一版单机的,目前在看《实现领域驱动设计》这本书,看完后再做一版微服务的。(2022.7.21注)

什么是分布式

在以往传统的企业系统架构中,我们针对一个复杂的业务需求通常使用对象或业务类型来构建一个单体项目。在项目中我们通常将需求分为三个主要部分:数据库、服务端处理、前端展现。

在业务发展初期,由于所有的业务逻辑在一个应用中,开发、测试、部署都还比较容易且方便。

但是,随着企业的发展,系统为了应对不同的业务需求会不断为该单体项目增加不同的业务模块;同时随着移动端设备的进步,前端展现模块已经不仅仅局限于Web的形式,这对于系统后端向前端的支持需要更多的接口模块。

单体应用由于面对的业务需求更为宽泛,不断扩大的需求会使得单体应用变得越来越臃肿。

单体应用的问题就逐渐凸显出来,由于单体系统部署在一个进程内,往往我们修改了一个很小的功能,为了部署上线会影响其他功能的运行。

并且,单体应用中的这些功能模块的使用场景、并发量、消耗的资源类型都各有不同,对于资源的利用又互相影响,这样使得我们对各个业务模块的系统容量很难给出较为准确的评估。

所以,单体系统在初期虽然可以非常方便地进行开发和使用,但是随着系统的发展,维护成本会变得越来越大,且难以控制。为了解决单体系统变得庞大臃肿之后产生的难以维护的问题,微服务架构诞生了并被大家所关注。

我们将系统中的不同功能模块拆分成多个不同的服务,这些服务都能够独立部署和扩展。由于每个服务都运行在自己的进程内,在部署上有稳固的边界,这样每个服务的更新都不会影响其他服务的运行。

同时,由于是独立部署的,我们可以更准确地为每个服务评估性能容量,通过配合服务间的协作流程也可以更容易地发现系统的瓶颈位置,以及给出较为准确的系统级性能容量评估。

但是,由于目前项目并不算太大,因此我将准备先做一版单机的,再在后期学习微服务架构,优化做一版微服务的。

总体来说,目前我对该项目的思路如下:

  1. 先构建用户端前端界面
  2. 采用单机系统构建后台,通信使用WebSocket,再搭配一些Redis场景,并逐步优化单机系统
  3. 构建管理后台前端界面,完善后端。初步做出一个可用的系统
  4. 采用分布式微服务框架构建后端,加入消息队列、netty等相关技术

当然,在开发的过程中也可能适时调整,并不一定完全按照这个过程走。

参考引用:

【1】 《Spring Cloud 微服务实战》翟永超

【2】 https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/why-mq.md

之前也写过关于前后端分离模式下动态权限管理相关的博客,但是由于之前对前后端知识了解尚且匮乏,因此在讲解时难免有讲的不够详细的地方,因此现在再以JeecgBoot项目(后面用 “本项目” 代替该词)中动态权限管理实现方式为例,重新详细讲解一遍。

本例基于Vue2 + SpringBoot来讲解,对于我认为比较基础的知识不再赘述,只讲解实现的大致脉络,并且大部分讲解在代码注释部分,请仔细阅读。

前端部分

本项目最重要的方法位于src目录下的permission.js(注意与/store/modules下的区分),精简代码如下:

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
router.beforeEach((to, from, next) => {
(...省略部分与该主题无关内容)
if (store.getters.permissionList.length === 0) { // 判断vuex的store是否存储有权限列表,以此判断是否获取到(可能缓存了)
store.dispatch('GetPermissionList').then(res => {
const menuData = res.result.menu;
if (menuData === null || menuData === "" || menuData === undefined) {
return;
}
let constRoutes = [];
constRoutes = generateIndexRouter(menuData); // 代码如下文所示
// 添加主界面路由
store.dispatch('UpdateAppRouter', { constRoutes }).then(() => { // 代码如下文所示
// 根据roles权限生成可访问的路由表
// 动态添加可访问路由表
router.addRoutes(store.getters.addRouters)
const redirect = decodeURIComponent(from.query.redirect || to.path)
if (to.path === redirect) {
// hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record
next({ ...to, replace: true })
} else {
// 跳转到目的路由
next({ path: redirect })
}
})
})
.catch(() => {
/* notification.error({
message: '系统提示',
description: '请求用户信息失败,请重试!'
})*/
store.dispatch('Logout').then(() => {
next({ path: '/user/login', query: { redirect: to.fullPath } })
})
})
}
}
})

我认为优秀的代码就是最好的注释,你能根据命名猜测出这段代码的大致意思,所以对于如上代码,我只写了大致的注释。但是有一段代码需要提醒大家。对于

1
store.dispatch('GetPermissionList').then(...)

有必要仔细阅读vuex参考文档

此外,对于上述中如下代码,需要进一步解释。

1
store.dispatch('GetPermissionList').then(...)
1
constRoutes = generateIndexRouter(menuData);
1
store.dispatch('UpdateAppRouter',  { constRoutes }).then(...)

store.dispatch(‘GetPermissionList’).then(…)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
GetPermissionList({ commit }) {
return new Promise((resolve, reject) => {
queryPermissionsByUser().then(response => { // 后台查询用户权限信息
const menuData = response.result.menu;
const authData = response.result.auth;
const allAuthData = response.result.allAuth;
sessionStorage.setItem(USER_AUTH,JSON.stringify(authData));
sessionStorage.setItem(SYS_BUTTON_AUTH,JSON.stringify(allAuthData));
if (menuData && menuData.length > 0) {
commit('SET_PERMISSIONLIST', menuData)
// 设置系统安全模式
commit('SET_SYS_SAFE_MODE', response.result.sysSafeMode)
} else {
reject('getPermissionList: permissions must be a non-null array !')
}
resolve(response)
}).catch(error => {
reject(error)
})
})
}

其中,上述返回的response结构如下所示(节选部分内容):

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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
{
"success":true,
"message":"",
"code":0,
"result":{
"allAuth":[
{
"action":"online:goGenerateCode",
"describe":"代码生成按钮",
"type":"1",
"status":"1"
},
{
"action":"user:add",
"describe":"添加按钮",
"type":"1",
"status":"1"
},
{
"action":"user:edit",
"describe":"用户编辑",
"type":"1",
"status":"1"
},
{
"action":"user:sex",
"describe":"表单性别可见",
"type":"1",
"status":"1"
},
{
"action":"user:form:birthday",
"describe":"禁用生日字段",
"type":"2",
"status":"1"
},
{
"action":"demo:add",
"describe":"demo添加功能",
"type":"1",
"status":"1"
},
{
"action":"user:form:phone",
"describe":"手机号禁用",
"type":"2",
"status":"1"
}
],
"auth":[
{
"action":"demo:add",
"describe":"demo添加功能",
"type":"1"
},
{
"action":"online:goGenerateCode",
"describe":"代码生成按钮",
"type":"1"
},
{
"action":"user:add",
"describe":"添加按钮",
"type":"1"
}
],
"menu":[
{
"redirect":null,
"path":"/dashboard/analysis",
"component":"dashboard/Analysis",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"icon":"home",
"componentName":"Analysis",
"title":"首页"
},
"name":"dashboard-analysis",
"id":"9502685863ab87f0ad1134142788a385"
},
{
"redirect":null,
"path":"/report",
"component":"layouts/RouteView",
"route":"1",
"children":[
{
"path":"/report/ArchivesStatisticst",
"component":"jeecg/report/ArchivesStatisticst",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"componentName":"ArchivesStatisticst",
"title":"布局统计报表"
},
"name":"report-ArchivesStatisticst",
"id":"2aeddae571695cd6380f6d6d334d6e7d"
},
{
"path":"/report/ViserChartDemo",
"component":"jeecg/report/ViserChartDemo",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"componentName":"ViserChartDemo",
"title":"ViserChartDemo"
},
"name":"report-ViserChartDemo",
"id":"020b06793e4de2eee0007f603000c769"
},
{
"path":"/online/cgreport/6c7f59741c814347905a938f06ee003c",
"component":"modules/online/cgreport/auto/OnlCgreportAutoList",
"route":"0",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"componentName":"OnlCgreportAutoList",
"title":"Online报表示例"
},
"name":"online-cgreport-6c7f59741c814347905a938f06ee003c",
"id":"1232123780958064642"
},
{
"path":"a93c0c3609dece99e85f4aa1caaac981",
"component":"layouts/IframePageView",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":true,
"componentName":"IframePageView",
"title":"Redis监控",
"url":"{{ window._CONFIG['domianURL'] }}/jmreport/view/1352160857479581696?token=${token}"
},
"name":"{{ window._CONFIG['domianURL'] }}-jmreport-view-1352160857479581696?token=${token}",
"id":"1352200630711652354"
}
],
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"icon":"bar-chart",
"componentName":"RouteView",
"title":"统计报表"
},
"name":"report",
"id":"f0675b52d89100ee88472b6800754a08"
},
{
"redirect":null,
"path":"/form",
"component":"layouts/PageView",
"route":"1",
"children":[
{
"path":"/form/step-form",
"component":"examples/form/stepForm/StepForm",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"componentName":"StepForm",
"title":"分步表单"
},
"name":"form-step-form",
"id":"6531cf3421b1265aeeeabaab5e176e6d"
},
{
"path":"/form/advanced-form",
"component":"examples/form/advancedForm/AdvancedForm",
"route":"1",
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"componentName":"AdvancedForm",
"title":"高级表单"
},
"name":"form-advanced-form",
"id":"e5973686ed495c379d829ea8b2881fc6"
}
],
"meta":{
"keepAlive":false,
"internalOrExternal":false,
"icon":"form",
"componentName":"PageView",
"title":"表单页"
},
"name":"form",
"id":"e3c13679c73a4f829bcff2aba8fd68b1"
}
],
"sysSafeMode":false
},
"timestamp":1639477587569
}

上述数据结构相关说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
`{ Route }` 对象

| 参数 | 说明 | 类型 | 默认值 |
| -------- | ----------------------------------------- -- ------- | ------ |
| hidden | 控制路由是否显示在 sidebar | boolean | falase |
| redirect | 重定向地址, 访问这个路由时,自定进行重定向 | string | - |
| name | 路由名称, 建议设置,且不能重名 | string | - |
| meta | 路由元信息(路由附带扩展信息) | object | {} |



`{ Meta }` 路由元信息对象

| 参数 | 说明 | 类型 | 默认值 |
| ------------------- |------------------------------------------------------------------------ ------- ------ |
| title | 路由标题, 用于显示面包屑, 页面标题 *推荐设置 | string | - |
| icon | 路由在 menu 上显示的图标 | string | - |
| keepAlive | 缓存该路由 | boolean | false |
| hiddenHeaderContent | *特殊 隐藏 [PageHeader]( 组件中的页面带的 面包屑和页面标题栏 ) | boolean | false |
| permission | 与项目提供的权限拦截匹配的权限,如果不匹配,则会被禁止访问该路由页面 | array | [] |

generateIndexRouter(menuData):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成首页路由
export function generateIndexRouter(data) { // 此处data参数即 menuData
let indexRouter = [{
path: '/',
name: 'dashboard',
component: resolve => require(['@/components/layouts/TabLayout'], resolve),
meta: { title: '首页' },
redirect: '/dashboard/analysis',
children: [
...generateChildRouters(data) // 代码如下文所示
]
},{
"path": "*", "redirect": "/404", "hidden": true
}]
return indexRouter;
}

而上述对应的**generateChildRouters(data)**方法如下:

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
// 生成嵌套路由(子路由)
function generateChildRouters (data) {
const routers = [];
for (let item of data) {
let component = "";
if(item.component.indexOf("layouts")>=0){ // 其实这里代码就做了一件事,将权限菜单的component进行拼接,得到文件夹的正确路径,不过为啥不一开始在路由表就存好正确的路径暂时没想通,项目较大,应该是迭代或者有其他我没注意到的地方的考虑
component = "components/"+item.component;
}else{
component = "views/"+item.component;
}

// eslint-disable-next-line
let URL = (item.meta.url|| '').replace(/{{([^}}]+)?}}/g, (s1, s2) => eval(s2)) // URL支持{{ window.xxx }}占位符变量
if (isURL(URL)) {
item.meta.url = URL;
}

let componentPath
if(item.component=="modules/online/cgform/OnlCgformHeadList"){
componentPath = onlineCommons.OnlCgformHeadList
}else if(item.component=="modules/online/cgform/OnlCgformCopyList"){
componentPath = onlineCommons.OnlCgformCopyList
} //(...省略部分与该主题无关的else if,上面的else if其实也无关,主要是最后else中的 `component` )
}else{
componentPath = resolve => require(['@/' + component+'.vue'], resolve)
}

let menu = {
path: item.path,
name: item.name,
redirect:item.redirect,
component: componentPath,
hidden:item.hidden,
meta: {
title:item.meta.title ,
icon: item.meta.icon,
url:item.meta.url ,
permissionList:item.meta.permissionList,
keepAlive:item.meta.keepAlive,
internalOrExternal:item.meta.internalOrExternal,
componentName:item.meta.componentName
}
}
if(item.alwaysShow){
menu.alwaysShow = true;
menu.redirect = menu.path;
}
if (item.children && item.children.length > 0) {
menu.children = [...generateChildRouters( item.children)];
}
//根据后台菜单配置,判断是否路由菜单字段,动态选择是否生成路由(为了支持参数URL菜单)
//判断是否生成路由
if(item.route && item.route === '0'){
//console.log(' 不生成路由 item.route: '+item.route);
//console.log(' 不生成路由 item.path: '+item.path);
}else{
routers.push(menu);
}
}
return routers
}

store.dispatch(‘UpdateAppRouter’, { constRoutes }).then()

1
2
3
4
5
6
7
8
// 动态添加主界面路由,需要缓存
UpdateAppRouter({ commit }, routes) { // 传入的routes为 generateIndexRouter(menuData) 的返回值,见上述该方法的代码
return new Promise(resolve => {
let routelist = routes.constRoutes;
commit('SET_ROUTERS', routelist)
resolve()
})
}

后端部分

后端部分我个人认为最重要的地方是数据库表的设计,而相关的业务代码其实没什么好讲的,但是还是贴一下

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
/**
* 查询用户拥有的菜单权限和按钮权限
*
* @return
*/
@RequestMapping(value = "/getUserPermissionByToken", method = RequestMethod.GET)
public Result<?> getUserPermissionByToken() {
Result<JSONObject> result = new Result<JSONObject>();
try {
//直接获取当前用户不适用前端token
LoginUser loginUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if (oConvertUtils.isEmpty(loginUser)) {
return Result.error("请登录系统!");
}
List<SysPermission> metaList = sysPermissionService.queryByUser(loginUser.getUsername());
//添加首页路由
//需要根据后台的路由配置来实现是否缓存
if(!PermissionDataUtil.hasIndexPage(metaList)){
SysPermission indexMenu = sysPermissionService.list(new LambdaQueryWrapper<SysPermission>().eq(SysPermission::getName,"首页")).get(0);
metaList.add(0,indexMenu);
}

List<String> roles = sysUserService.getRole(loginUser.getUsername());
String compUrl = RoleIndexConfigEnum.getIndexByRoles(roles);
if(StringUtils.isNotBlank(compUrl)){
List<SysPermission> menus = metaList.stream().filter(sysPermission -> "首页".equals(sysPermission.getName())).collect(Collectors.toList());
menus.get(0).setComponent(compUrl);
}
JSONObject json = new JSONObject();
JSONArray menujsonArray = new JSONArray();
this.getPermissionJsonArray(menujsonArray, metaList, null);
//一级菜单下的子菜单全部是隐藏路由,则一级菜单不显示
this.handleFirstLevelMenuHidden(menujsonArray);

JSONArray authjsonArray = new JSONArray();
this.getAuthJsonArray(authjsonArray, metaList);
//查询所有的权限
LambdaQueryWrapper<SysPermission> query = new LambdaQueryWrapper<SysPermission>();
query.eq(SysPermission::getDelFlag, CommonConstant.DEL_FLAG_0);
query.eq(SysPermission::getMenuType, CommonConstant.MENU_TYPE_2);
//query.eq(SysPermission::getStatus, "1");
List<SysPermission> allAuthList = sysPermissionService.list(query);
JSONArray allauthjsonArray = new JSONArray();
this.getAllAuthJsonArray(allauthjsonArray, allAuthList);
//路由菜单
json.put("menu", menujsonArray);
//按钮权限(用户拥有的权限集合)
json.put("auth", authjsonArray);
//全部权限配置集合(按钮权限,访问权限)
json.put("allAuth", allauthjsonArray);
json.put("sysSafeMode", jeeccgBaseConfig.getSafeMode());
result.setResult(json);
} catch (Exception e) {
result.error500("查询失败:" + e.getMessage());
log.error(e.getMessage(), e);
}
return result;
}

提取几个业务相关(其实就是数据库查询)相关代码如下:

1
2
3
4
5
List<SysPermission> metaList = sysPermissionService.queryByUser(loginUser.getUsername());

SysPermission indexMenu = sysPermissionService.list(new LambdaQueryWrapper<SysPermission>().eq(SysPermission::getName,"首页")).get(0);

List<String> roles = sysUserService.getRole(loginUser.getUsername());

相关数据库表设计如下

角色和权限相关,那么有权限表必然也有对应的角色表,因此我们关注的重点方向在于权限表、角色表 以及 权限角色关联表。

sys_role表

sys_role_permission表(节选)

image-20211214212110982

sys_permission表(如需看更多数据自行下载代码查看)

image-20211214212252390

image-20211214213445059

后端sql如下,总之,查询出来的数据就是上面介绍的json数据

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
<!-- 获取登录用户拥有的权限 -->
<select id="queryByUser" parameterType="Object" resultMap="SysPermission">
SELECT * FROM (
SELECT p.*
FROM sys_permission p
WHERE (exists(
select a.id from sys_role_permission a
join sys_role b on a.role_id = b.id
join sys_user_role c on c.role_id = b.id
join sys_user d on d.id = c.user_id
where p.id = a.permission_id AND d.username = #{username,jdbcType=VARCHAR}
)
or (p.url like '%:code' and p.url like '/online%' and p.hidden = 1)
or p.url = '/online')
and p.del_flag = 0
<!--update begin Author:lvdandan Date:20200213 for:加入部门权限 -->
UNION
SELECT p.*
FROM sys_permission p
WHERE exists(
select a.id from sys_depart_role_permission a
join sys_depart_role b on a.role_id = b.id
join sys_depart_role_user c on c.drole_id = b.id
join sys_user d on d.id = c.user_id
where p.id = a.permission_id AND d.username = #{username,jdbcType=VARCHAR}
)
and p.del_flag = 0
<!--update end Author:lvdandan Date:20200213 for:加入部门权限 -->
) h order by h.sort_no ASC
</select>

总结

基本思路为:数据库存储菜单结构、页面权限控制信息等,前端根据数据库中的菜单结构和权限信息来渲染一个菜单出来并只显示其有权限的菜单,并在路由守卫中进行权限控制防止手动输入path越权打开页面

1、前端路由(vue-router)中需要正常创建页面及路由。

2、数据库存储菜单结构和页面权限信息,需要时从数据库中查询出来

  • 所有角色都有的界面(比如dashboard)写死在前端
  • 由权限控制得到的视图路径存在数据库中
  • 菜单和页面组成上下级关系,一级可以是菜单也可以是内容页,内容页也可以放在菜单下,不可见的内容页也可以放在一个普通内容页下,这样理论(需要页面菜单样式支持)可以组成无限级菜单
  • 菜单和页面的基本属性见上文 json数据 下面的说明
  • 不需要控制权限且不需要显示到左侧菜单的路由这里可以不进行管理,比如404页面等

3、前端打开后获取数据库的所有菜单、页面及结构,根据是否登录、是否需要验证权限等进行控制,或无权限跳转至登录页

4、用户登录成功后,再获取用户对应的的页面权限列表,使用上一步获得的所有页面、结构和用户拥有权限的列表渲染出一个菜单,只包含此用户拥有权限的相应视图

5、路由守卫中根据上一步获得的权限列表判断每个跳转,无权限可返回404或无权限页面,防止用户手动输入path越权访问

刚刚从剧社排练完已经22:10了,由于今天下午刚刚把博客搭起来,还是回实验室写下了自己的第一篇博客。

写写删删,也不知道从哪开始写起了。说起来最近还是挺忙的,准确说应该是自己给自己制造的忙碌吧。

简单来说,搭建这篇博客的目的,简单来说,就是希望记录自己的学习过程吧。

其实在之前也经常在CSDN写过一些博客了,好吧,最近一年基本没写了,原因也是多种多样的。但是最近看了一些书还是深深感受到拥有一个个人博客并且坚持写下去的重要性,所以也在这里立下一个小小的flag,每周至少坚持总结输出一篇blog!

之前在CSDN写的文章可能和这个网站本身上面很多的文章一样,东抄一点,西抄一点,质量堪忧,并且并没有很大的价值。

现在回头来看,从前自己在上面写博客,是为了写而写,并不是真正遇到问题,并且做过详细的了解,而自发的想去总结并且记录它。

可能有人也会想,我立下的flag不也是一周输出一篇,同样是为了写去写啊。但是我觉得,在研究生期间自己还是有许多时间去学习很多东西,在这个过程中,必然会遇到许许多多的问题,所以一周至少一篇记录总结还是非常合理的!

在我写博客的过程中,也许会存在许多问题,比如写的不够清楚,或者是我本身技术方面存在不足而导致讲错的地方,这些都是很正常的,我也会坦然接受,毕竟,我现在真的很菜。但是,也希望你能友善探讨。

总而言之,尽管我写作的过程中会存在许多不足,我还是会坚持把能做到最好的样子呈现在大家面前。

“如果我认为我写的博客不比目前我能发现的大多数文章要好,那我就没有必要去写它!”

这会成为我写作的原则。至于为什么不说是能像很多作者口中的 “比其他所有文章要好”,那是因为我深知自己的水平远远达不到,也正是因为这样,我才决定开始搭建自己的私人博客了,否则,我就直接去出书啦!

希望我的博客,能对你有所帮助!