狂神说Spring &《Spring in Action 第四版》笔记

Spring(狂神说笔记)

初始spring

1.简介

  • Spring是一个开源的免费的框架(容器)
  • Spring是一个轻量级的,非入侵的框架
  • 控制反转(IOC),面向切面编程(AOP)
  • 支持事务处理,对框架整合的支持

Spring就是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架

2.组成

img

3.扩展

  • SpringBoot
    • 一个快速开发的脚手架
    • 基于Springboot可以快速开发单个微服务
    • 约定大于配置
  • SpringCloud
    • SpringCloud是基于SpringBoot实现的

IOC的理解

可以参考前面的springboot第七节课笔记对IOC的理解

下面简单看一下spring中对IOC代码实现

1.简单例子

pojo类的编写

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Hello {
    private String str;
}

bean的注册

id相当于变量名,class是new的对象,property代表属性,name是具体的属性,value是属性的值,ref是已经创建好的bean的id

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

    <bean id="hello" class="com.hznu.ch.pojo.Hello">
        <property name="str" value="hello world"/>
    </bean>
</beans>

测试

public class MyTest {
    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        Hello bean = (Hello) ctx.getBean("hello");
        System.out.println(bean);
    }
}

以上这个过程就叫控制反转

控制:谁来控制对象的创建,传统应用程序的对象是由程序本身控制创建,使用spring后,对象是由spring来创建的

反转:程序本身不创建对象,而变成被动的接收对象

IOC更是一种编程思想,由主动的编程变成被动的接收

为了更加深刻的了解IOC,我们接下来来一个更加复杂的例子

2.复杂例子

image-20201125165420463

dao层

public interface UserDao {
    void test();
}

public class UserDaoMysqlImpl implements UserDao{
    public void test() {
        System.out.println("mysql");
    }
}

public class UserDaoOracleImpl implements UserDao {
    public void test() {
        System.out.println("oracle");
    }
}

service层

public interface UserService {
    void testService();
}

public class UserServiceImpl implements UserService {

    private UserDao userDao;

    public void testService() {
        userDao.test();
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

配置文件

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

    <bean id="userDaoMysql" class="com.hznu.ch.dao.UserDaoMysqlImpl"/>
    <bean id="userDaoOracle" class="com.hznu.ch.dao.UserDaoOracleImpl"/>


    <bean id="userServiceImpl" class="com.hznu.ch.service.UserServiceImpl">
        <property name="userDao" ref="userDaoOracle"/>
    </bean>
</beans>

测试

public class MyTest {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");
        UserService userServiceImpl = (UserService) ctx.getBean("userServiceImpl");
        userServiceImpl.testService();
    }
}

我们主要看一下配置文件,我们发现我们如果想把数据库改成mysql只需要把ref的值改成userDaoMysql即可,不在像以前一样去修改代码里的东西了

到了现在,我们彻底不用在程序中去改动了,要实现不同的操作,只需要在xml配置文件中进行修改,所谓的IOC,一句话搞定:对象由spring创建,管理,装配!

3.IOC创建对象的方式

1.构造器注入

  1. 使用无参构造创建
  2. 通过调用有参构造函数
<bean id="hello" class="com.hznu.ch.pojo.Hello">
    <constructor-arg index="0" value="chenheng"/>
    <constructor-arg index="1" value="123"/>
</bean>
  1. 通过调用有参构造函数(另一种方法)
<bean id="hello" class="com.hznu.ch.pojo.Hello">
    <constructor-arg type="java.lang.String" value="chendan"/>
    <constructor-arg type="int" value="321"/>
</bean>

2.set注入

  1. 这种方法是通过无参构造初始化的,再通过set来完成参数的注入的
<bean id="hello" class="com.hznu.ch.pojo.Hello">
    <property name="str" value="ch"/>
    <property name="age" value="116"/>
</bean>

小结:在配置文件加载的时候,容器中管理的对象就已经被初始化了

  1. 扩展IOC的使用
public class Address {

}
@Data
public class Student {
    private String name;
    private Address address;
    private String[] books;
    private List<String> hobbies;
    private Map<String, String> card;
    private Set<String> games;
    private String wife;
    private Properties info;
}
<bean id="student" class="com.hznu.ch.pojo.Student">
    <!--        字符串类型-->
    <property name="name" value="ch"/>

    <!--        引用类型-->
    <property name="address" ref="address"/>

    <!--        数组类型-->
    <property name="books">
        <array>
          <value>红楼梦</value>
          <value>三国演义</value>
          <value>水浒传</value>
          <value>西游记</value>
        </array>
    </property>

    <!--        list类型-->
    <property name="hobbies">
        <list>
          <value>打代码</value>
          <value>吃零食</value>
          <value>玩三国杀</value>
        </list>
    </property>

    <!--        map类型-->
    <property name="card">
        <map>
          <entry key="46" value="dd"/>
          <entry key="30" value="cd"/>
        </map>
    </property>


    <!--        set类型-->
    <property name="games">
        <set>
          <value>三国杀</value>
        </set>
    </property>

    <!--        null类型-->
    <property name="wife">
      	<null/>
    </property>

    <!--        properties-->
    <property name="info">
        <props>
          <prop key="性格">很傻很天真</prop>
        </props>
    </property>
</bean>

3.其他注入

命名空间的注入,需添加对应的约束

xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
  1. p命名空间注入(通过set注入)
<!--两则等价-->
<bean id="teacher" class="com.hznu.ch.pojo.Teacher">
    <property name="name" value="ch"/>
    <property name="age" value="21"/>
</bean>

<bean id="teacher" class="com.hznu.ch.pojo.Teacher" p:age="22" p:name="ch"/>
  1. c命名空间注入(通过构造器注入)
<!--两则等价-->
<bean id="teacher" class="com.hznu.ch.pojo.Teacher">
    <constructor-arg name="age" value="21"/>
    <constructor-arg name="name" value="陈恒"/>
</bean>
<bean id="teacher" class="com.hznu.ch.pojo.Teacher" c:age="21" c:name="陈恒"/>

Bean的作用域

ScopeDescription
singleton(Default) Scopes a single bean definition to a single object instance for each Spring IoC container.
prototypeScopes a single bean definition to any number of object instances.
requestScopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring ApplicationContext.
sessionScopes a single bean definition to the lifecycle of an HTTP Session. Only valid in the context of a web-aware Spring ApplicationContext.
applicationScopes a single bean definition to the lifecycle of a ServletContext. Only valid in the context of a web-aware Spring ApplicationContext.
websocketScopes a single bean definition to the lifecycle of a WebSocket. Only valid in the context of a web-aware Spring ApplicationContext.
  1. 默认是单例模式

singleton

  1. 原型模式

prototype

  1. 其余的request,session,application都是在web开发中使用到的

session和application的区别:

  1. 所有用户共享一个Application对象,session和用户则是一一对应关系
  2. application它类似于系统的全局变量,用于保存所有程序中的公有数据。它在服务器启动时自动创建,在服务器停止时销毁。当application对象没有被销毁的时候,所有用户都可以享用该application对象。它的生命周期可以说是最长的
  3. session是会话变量,只要同一个浏览器没有被关闭,session对象就会存在。因此在同一个浏览器窗口中,无论向服务器发送多少请求,session对象只有一个。但是如果在一个会话中,客户端长时间不向服务器发出请求,session对象就会自动消失。这个时间取决于服务器,但是我们可以通过编写程序进行修改这个session的生命周期的时间

Bean自动装配

《spring实战》中给装配下了一个定义:创建应用对象之间协作关系的行为称为装配。也就是说当一个对象的属性是另一个对象时,实例化时,需要为这个对象属性进行实例化。这就是装配

依赖注入的本质就是装配,装配是依赖注入的具体行为

依赖注入有两种形式:构造器注入和setter注入。也就是我们在xml中写的一堆<bean></bean>,如果bean太多我们还这样写基本是要成为码农了,更何况我们还有把有关联的bean装配起来,一旦bean很多,就不好维护了。

为此Spring使用自动装配解决这个问题,开发人员不用关心具体装配哪个bean的引用,识别工作由Spring来完成,因此一般配有自动监测来和自动装配配合完成。自动装配其实就是将依赖注入“自动化”的一个简化配置的操作

  • 自动装配是Spring满足依赖的一种方式
  • Spring会在上下文自动寻找,并自动给bean装配属性

Spring三种实现自动装配的方法

  1. xml中显式配置(上面一直在使用)
  2. java中显式配置
  3. 隐式的自动装配bean

测试环境

@Data
public class People {
    private String name;
    private Dog dog;
    private Cat cat;
}

public class Cat {

    public void shout(){
        System.out.println("miao");
    }
}

public class Dog {
    public void shout(){
        System.out.println("wang");
    }
}

1.自动装配的形式

1.1.byName自动装配

<bean id="dog" class="com.hznu.ch.pojo.Dog"/>
<bean id="cat" class="com.hznu.ch.pojo.Cat"/>

<!--    byName:会自动在容器上下文查找,和自己对象set方法后面的值对应的 beanid-->
<!--    byType:会自动在容器上下文查找,和自己对象属性类型相同的值对应的 beanid-->
<bean id="people" class="com.hznu.ch.pojo.People" autowire="byName">
  	<property name="name" value="ch"/>
</bean>

1.2.byType自动装配

<bean class="com.hznu.ch.pojo.Dog"/>
<bean class="com.hznu.ch.pojo.Cat"/>

<!--    byName:会自动在容器上下文查找,和自己对象set方法后面的值对应的 beanid-->
<!--    byType:会自动在容器上下文查找,和自己对象属性类型相同的值对应的 beanid-->
<bean id="people" class="com.hznu.ch.pojo.People" autowire="byType">
    <property name="name" value="ch"/>
</bean>

总结:byName需要bean的id唯一,byType需要bean的class唯一

2.注解实现自动装配

2.1.导入约束

<?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 id="cat" class="com.hznu.ch.pojo.Cat"/>
    <bean id="cat1" class="com.hznu.ch.pojo.Cat"/>
    <bean id="dog" class="com.hznu.ch.pojo.Dog"/>
    <bean id="people" class="com.hznu.ch.pojo.People"/>
</beans>

2.2.使用@Autowired

可以作用在属性,方法,构造,方法参数和注解上

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {
    boolean required() default true;
}

使用@Autowired可以实现自动装配,@Autowired先采用byType的寻找bean,再通过byName寻找

如下这段代码,id和class不相符,但测试还是可以通过;class同一种的多个,但id唯一,测试通过;所以想法得到验证

@Autowired
private Dog dog;

@Autowired
private Cat cat;
<bean id="dog" class="com.hznu.ch.pojo.Cat"/>
<bean id="cat" class="com.hznu.ch.pojo.Dog"/>

@Autowired
private Cat cat;
<bean id="cat" class="com.hznu.ch.pojo.Cat"/>
<bean id="cat1" class="com.hznu.ch.pojo.Cat"/>

使用@Qualifier可以指定注入cat对象是哪个bean

如下这段代码指定的就是cat1这个bean

@Autowired
@Qualifier("cat1")
private Cat cat;

小技巧:@Autowired可以指定参数,false|true,以此来允许指定注入的bean是否可以为null,true——不能;false——能

@Autowired(false)

2.3.使用@Resource

作用在方法,属性,(类,接口,注解和集合)上

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {
  	......
}

@Resouce是javaee的注解,是一个组合注解,可以达到@Autowired和@Qualifier一起的效果

@Resource(name = "cat1")
private Cat cat;

@Resource先采用byName,再通过byType

如下这段代码,如果byType先的话,那么测试应该通过,但是测试报错:cat对应的是Dog类,所以可以知道@Resource先通过byName再通过byType

<bean id="cat1" class="com.hznu.ch.pojo.Cat"/>
<bean id="cat" class="com.hznu.ch.pojo.Dog"/>
@Resource
private Cat cat;

image-20201127211112463

使用注解开发

在Spring3之后,要使用注解开发,必须要保证aop的包导入

image-20201126223303929

1.添加约束

<?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/>
    <!--    指定要扫描的包,这个包下的注解就会生效-->
  	<context:component-scan base-package="com.hznu.ch"/>

</beans>

2.类注入

Bean的id为类名,首字母小写

@Component
public class User {

    private String name;

    @Value("ch")
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class MyTest {
    @Test
    public void test() {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");
        User user = (User) ctx.getBean("user");
        System.out.println(user);
    }
}

3.属性如何注入

@Value("ch")
public void setName(String name) {
  	this.name = name;
}	

@Value("ch")
private String name;

4.衍生的注解

@Component有几个衍生的注解,我们在web开发中,会按照MVC三层架构分层

  • dao(@Repository)
  • service(@Service)
  • controller(@Controller)

这四个注解功能一致,都是代表将某个类注册到spring中,装配Bean

5.作用域

单例与原型注解配置

@Scope("singleton")
@Scope("prototype")

使用javaConfig实现自动装配

1.基本的使用

使用@Configuration@Bean 配置,通过AnnotationConfigApplicationContext实例化spring容器

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String name;
    private int age;

    public void program() {
        System.out.println("敲代码");
    }
}
@Configuration
public class MyConfig {

    @Bean
    public User myUser() {
        return new User("ch", 20);
    }
  	/* @Bean相当于如下的xml配置
      <bean id="myUser" class="com.hznu.ch.pojo.User">
          <property name="name" value="ch"/>
          <property name="age" value="20"/>
      </bean>
     */
}
public class MyTest {
    @Test
    public void test() {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfig.class);
        User user = (User) ctx.getBean("myUser");
        user.program();
    }
}

2.@ComponentScan的使用

注意加了一个@Component注解

@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class User {
    private String name;
    private int age;

    public void program() {
        System.out.println("敲代码");
    }
}

这个会显式的扫描com.hznu.ch.pojo下的@Component ,所以无需在Config中显式注册

相当于<context:component-scan base-package="com.hznu.ch.pojo"/>

@Configuration
@ComponentScan(basePackages = "com.hznu.ch.pojo")
public class MyConfig {

//    @Bean
//    public User myUser() {
//        return new User("ch", 20);
//    }
}

注册的bean的id是类名的,首字母小写

public class MyTest {
    @Test
    public void test() {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(MyConfig.class);
        User user = (User) ctx.getBean("user");
        user.program();
    }
}

3.@Import的使用

导入合并另一个配置类

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

ConfigB导入ConfigA后,在ConfigB中也就可以使用ConfigA中的Bean了!

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

IOC小结

1.@Bean@Autowired 的区别:

  • @Bean 告诉Spring,“这是此类的一个实例,请保留该类,并在我询问时将其还给”。
  • @Autowired说,“请给我一个该类的实例,例如,我之前用@Bean注释创建的一个实例”。

2.复习

Spring三种实现自动装配的方法

  1. xml中显式配置(上面一直在使用)
<bean id="cat" class="com.hznu.ch.pojo.Cat"/>
  1. java中显式配置

要通过@Configuration与@Bean搭配来完成

@Configuration
public class MyConfig {

    @Bean
    public User myUser() {
        return new User("ch", 20);
    }
}
  1. 隐式的自动装配bean
    1. 组件扫描
    2. 自动装配

1.组件扫描可以通过xml或javaConfig两种方法启动

xml

<context:component-scan base-package="com.hznu.ch.pojo"/>

小彩蛋:< context:component-scan/>包含了< context:annotation-config/> 的功能,两则同时存在,后则将被覆盖,所以当两个同时出现时,也不会出现重复注入的情况

javaConfig

@ComponentScan(basePackages = "com.hznu.ch.pojo")

2.自动装配

通过 @Autowired 自动装配

@Data
@Component
public class Boy {
    @Autowired
    private User user;
}

AOP

1.AOP的理解及其作用

可以看SpringBoot第七节课的笔记,这里不再赘述

2.使用Spring实现AOP

2.1.使用Spring的API实现

2.1.1.导入依赖
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.6</version>
</dependency>
2.1.2.服务类和日志类的编写
public interface UserService {
    void query();

    void add();

    void remove();

    void update();
}
public class UserServiceImpl implements UserService {
    public void query() {
        System.out.println("query");
    }

    public void add() {
        System.out.println("add");
    }

    public void remove() {
        System.out.println("remove");
    }

    public void update() {
        System.out.println("update");
    }
}
public class BeforeLog implements MethodBeforeAdvice {
    /**
     * Callback before a given method is invoked.
     *
     * @param method the method being invoked
     * @param args   the arguments to the method
     * @param target the target of the method invocation. May be {@code null}.
     * @throws Throwable if this object wishes to abort the call.
     *                   Any exception thrown will be returned to the caller if it's
     *                   allowed by the method signature. Otherwise the exception
     *                   will be wrapped as a runtime exception.
     */
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了!");
    }
}
public class AfterLog implements AfterReturningAdvice {
    /**
     * Callback after a given method successfully returned.
     *
     * @param returnValue the value returned by the method, if any
     * @param method      the method being invoked
     * @param args        the arguments to the method
     * @param target      the target of the method invocation. May be {@code null}.
     * @throws Throwable if this object wishes to abort the call.
     *                   Any exception thrown will be returned to the caller if it's
     *                   allowed by the method signature. Otherwise the exception
     *                   will be wrapped as a runtime exception.
     */
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println(target.getClass().getName() + "的" + method.getName() + "被执行了!" + "返回结果为" + returnValue);
    }
}
2.1.3.配置文件

千万别导错约束,不要问我怎么知道的

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


    <!--    注册bean-->
    <bean id="userService" class="com.hznu.ch.service.UserServiceImpl"/>
    <bean id="beforeLog" class="com.hznu.ch.aspect.BeforeLog"/>
    <bean id="afterLog" class="com.hznu.ch.aspect.AfterLog"/>

    <!--    配置aop:需要导入aop的约束-->
    <aop:config>
        <!--切入点:expression:表达式,execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?),?表示可选,详见官方文档-->
        <!--        execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)-->
        <aop:pointcut id="pointcut" expression="execution(* com.hznu.ch.service.UserService.*())"/>

        <!--        执行环绕增加-->
        <aop:advisor advice-ref="beforeLog" pointcut-ref="pointcut"/>
        <aop:advisor advice-ref="afterLog" pointcut-ref="pointcut"/>
    </aop:config>
</beans>
2.1.4.测试
public class MyTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("bean.xml");
        UserService userService = ctx.getBean("userService", UserService.class);
        userService.add();
    }
}

image-20201128145059675

2.2.自定义来实现AOP

2.2.1.编写自定义切面
public class DiyLog {

    public void m_before(){
        System.out.println("调用前");
    }

    public void m_after(){
        System.out.println("调用后");
    }
}
2.2.2.配置文件

省略了一部分

<bean id="diy" class="com.hznu.ch.aspect.DiyLog"/>

<aop:config>
    <!--        切入面-->
    <aop:aspect ref="diy">
        <!--            切入点-->
        <aop:pointcut id="point" expression="execution(* com.hznu.ch.service.UserServiceImpl.*(..))"/>
        <!--            通知-->
        <aop:before method="m_before" pointcut-ref="point"/>
        <aop:after method="m_after" pointcut-ref="point"/>
    </aop:aspect>
</aop:config>
2.2.3.测试

image-20201128152916826

2.3.注解实现AOP

2.3.1.编写切面
@Aspect
public class AnnotationPointCut {
    @Before("execution(* com.hznu.ch.service.UserServiceImpl.*(..))")
    public void before() {
        System.out.println("前!");
    }

    @After("execution(* com.hznu.ch.service.UserServiceImpl.*(..))")
    public void after() {
        System.out.println("后!");
    }

    @Around("execution(* com.hznu.ch.service.UserServiceImpl.*(..))")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        Object o = jp.proceed();
        System.out.println("环绕后");
    }
}
2.3.2.配置文件
<bean id="annotationPointCut" class="com.hznu.ch.aspect.AnnotationPointCut"/>
<aop:aspectj-autoproxy/>
2.3.3.测试

顺便还可以看出来这个切面的执行周期

Around前—— 》Before——》方法执行——》After——》 Around后

image-20201128160040500

Spring整合Mybatis

需要导入的依赖

<dependencies>
    <dependency>
        <!--            WebMVC框架-->
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>

    <!--        单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
    </dependency>

    <!--        mybatis-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.4</version>
    </dependency>

    <!--        mysql-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.16</version>
    </dependency>

    <!--        jdbc,和DriverManagerDataSource有关-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>

    <!--        aop-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>

    <!--        mybatis-spring-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.2</version>
    </dependency>
</dependencies>

1.方法一

1.1.编写数据源配置

1.2.sqlSessionFactory

1.3.sqlSessionTemplate

文件名:spring-dao.xml

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


    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://127.0.0.1:3306/m_test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false"/>
        <property name="username" value="root"/>
        <property name="password" value="XXXXX"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="mybatis-config.xml"/>
        <property name="mapperLocations" value="mapper/*.xml"/>
    </bean>

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>
</beans>

1.4.需要给实现类加接口

public class UserMapperImpl implements UserMapper {

    private SqlSessionTemplate sqlSession;

    public void setSqlSession(SqlSessionTemplate sqlSession) {
        this.sqlSession = sqlSession;
    }

    public User queryUser(int id) {
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
        return userMapper.queryUser(id);
    }
}

1.5.实现类注入到Spring中

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

    <import resource="spring-dao.xml"/>

    <bean id="userMapper" class="com.hznu.ch.mapper.UserMapperImpl">
        <property name="sqlSession" ref="sqlSession"/>
    </bean>
</beans>

1.6.测试

public class MyTest {
    @Test
    public void test() {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        UserMapper userMapper = ctx.getBean("userMapper", UserMapper.class);
        User user = userMapper.queryUser(1);
        System.out.println(user);
    }
}

2.方法二(了解即可)

SqlSessionDaoSupport 是一个抽象的支持类,用来为你提供 SqlSession。调用 getSqlSession() 方法你会得到一个 SqlSessionTemplate

public class UserMapperImpl2 extends SqlSessionDaoSupport implements UserMapper {
    public User queryUser(int id) {
        return getSqlSession().getMapper(UserMapper.class).queryUser(id);
    }
}
<bean id="userMapper2" class="com.hznu.ch.mapper.UserMapperImpl2">
    <property name="sqlSessionFactory" ref="sqlSessionFactory"/>
</bean>

相对比于第一种方法,我们可以看到,它少了一个SqlSessionTemplate的注册

声明式事务

其实就下面这一段内容,重点在结合AOP实现事务的织入和配置事务切入这两段,如需修改execution后面的参数即可

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/tx
        https://www.springframework.org/schema/tx/spring-tx.xsd">


    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url"
                  value="jdbc:mysql://127.0.0.1:3306/m_test?useUnicode=true&amp;characterEncoding=utf-8&amp;useSSL=false"/>
        <property name="username" value="root"/>
        <property name="password" value="11480357chen"/>
    </bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="configLocation" value="mybatis-config.xml"/>
        <property name="mapperLocations" value="mapper/*.xml"/>
    </bean>

    <bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
        <constructor-arg index="0" ref="sqlSessionFactory"/>
    </bean>

    <!--   配置声明式事务-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <constructor-arg ref="dataSource"/>
    </bean>


    <!--    结合AOP实现事务的织入-->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="*" propagation="REQUIRED"/>
        </tx:attributes>
    </tx:advice>


    <!--    配置事务切入-->
    <aop:config>
        <aop:pointcut id="txPointCut" expression="execution(* com.hznu.ch.mapper.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointCut"/>
    </aop:config>


</beans>

Spring MVC(狂神说笔记)

构建Hello Spring MVC

1.导入Spring MVC的依赖

在pom.xml中导入依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.13</version>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>servlet-api</artifactId>
        <version>2.5</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.2</version>
    </dependency>

    <dependency>
        <groupId>javax.servlet.jsp.jstl</groupId>
        <artifactId>jstl-api</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

确保依赖导入,注意这是一个很坑的点,就你可能代码写好了,但是说没找到DispatcherServlet这个类,那很大一部分原因就是你发布的web项目中没有添加进这些依赖,详见第二幅图,手动添加lib目录,并导入刚才导入的那些的依赖

image-20201205171305099

image-20201205171324590

2.Controller控制器的编写

解释一下:根据HTTP的请求再处理业务,设置视图名,然后返回

public class HelloController implements Controller {
    public ModelAndView handleRequest(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
        ModelAndView mv = new ModelAndView();
        mv.addObject("msg", "这个一个SpringMVC Hello");
        mv.setViewName("hello");
        
        //返回给视图解析器
        return mv;
    }
}

3.配置SpringMVC文件

springmvc-config.xml

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

    <!--处理器映射器,根据bean的id确定url-->
    <bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

    <!--处理器适配器-->
    <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

    <!--视图解析器:模板引擎-->
    <bean id="InternalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <!--前缀-->
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <!--后缀-->
        <property name="suffix" value=".jsp"/>
    </bean>

    <!--bean的class关联controller,id关联url-->
    <bean id="/hello" class="com.hznu.controller.HelloController"/>
</beans>

4.配置web.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--配置Servlet分发器-->
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--配置spring的配置文件存放地-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

</web-app>

5.编写视图层

hello.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
    ${msg}
</body>
</html>

6.测试结果展示

image-20201205173827389

7.Spring MVC结构图

image-20201205193741679

可以对应上面的代码比对执行过程,有助于理解原理

注解构建Hello Spring MVC

1.导入依赖,并检查web项目是否导入

不重复写,就会上面的那一段

2.Controller的编写

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("msg", "这又是一个spring mvc测试");
        return "hello";
    }
}

3.SpringMVC配置文件的编写

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

    <!--注解扫描-->
    <context:component-scan base-package="com.hznu.controller"/>
    <!--SpringMVC不处理静态资源,如.css .html .js .mp3-->
    <mvc:default-servlet-handler/>
    <!--配置完成映射关系,包括了原来的映射器和适配器-->
    <mvc:annotation-driven/>

    <bean id="InternalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

</beans>

4.配置web.xml文件&视图层的编写

还是和之前的一样,没差,就不放出来了

5.测试结果展示

image-20201205202646158

注解开发真的能省很多事情哈哈哈

Spring MVC扩展

@Controller
public class HelloController {
    @RequestMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("msg", "这又是一个spring mvc测试");
        //转发
        //return "hello";
      
        //重定向
        return "redirect:/test.jsp";//需要注意这个是在web的目录下的,但无法访问WEB-INF下面的
    }
}

Spring实战(第四版)

1.Spring之旅

1.2.容纳你的bean

使用应用上下文

  • AnnotationConfigApplicationContext:从一个或多个基于java的配置类中加载spring应用上下文
  • AnnotationConfigWebApplicationContext:从一个或多个基于java的配置类中加载spring web应用上下文
  • ClassPathXmlApplicationContext:从类路径下加载一个或多个xml配置文件
  • FileSystemXmlApplicationContext:从文件系统下的。。。。
  • XmlWebApplicationContext:从Web应用下。。。

bean的生命周期==(留坑)==

深究Spring中Bean的生命周期

1.3.俯瞰Spring风景线

1.3.1.Spring模块

在这里插入图片描述

3.高级装配

3.1.环境与profile

场景

应用程序经常性的变迁,如数据库环境,开发阶段,测试阶段,上线阶段可能都不一样,所以也就产生了不同的策略生成数据源Bean,但本质还是数据源Bean。

需求

针对上面的场景,我们试想如果有一个方法可以让spring自动识别环境是不是就可以解决问题了,程序猿编写三个阶段的代码,但只有在对应的阶段中它们才会被实例化出来,即有条件性的过滤掉Bean

解决措施

配置profile bean,通过运行spring程序时传入的命令行参数进行识别是哪种环境,从而配置对应环境的profile bean

这里采用javaConfig的配置

@Configuration
public class ProfileConfig {

    @Bean
    @Profile("test")
    public DataSource m_mysql() {
        return new MysqlDAO();
    }

    @Bean
    @Profile("dev")
    public DataSource m_sqlserver() {
        return new SqlServerDAO();
    }

    @Bean
    @Profile("prod")
    public DataSource m_oracle() {
        return new OracleDAO();
    }
}
public class MyTest {
    @Test
    public void test() {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
        ctx.getEnvironment().setActiveProfiles("test");
        ctx.register(ProfileConfig.class);
        ctx.refresh();
        DataSource dataSource = ctx.getBean("m_mysql", DataSource.class);
        dataSource.getDataSourceName();
    }
}

上面的环境我们定为test,所以m_mysql才能被实例化出来,但如果我们定义成dev,那么这一段实例化m_mysql的代码将会报错

image-20201202101007206

再介绍一种方法,这种方法是通过隐式的自动装配来完成的,通过@ActiveProfiles("dev")注解来确定当前的运行环境

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProfileConfig.class)
@ActiveProfiles("dev")
public class MyTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void test() {
        dataSource.getDataSourceName();
    }
}

image-20201202101808008

以上都是在测试环境下,如果是在web环境下,我们可以添加如下的代码到web.xml中,指定当前的环境为production环境

<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>production</param-value>
</context-param>

参考文献

Spring @Profile 注解介绍

3.2.条件化的bean

场景

希望某个bean只有当另外有某个特定的bean也声明了之后才会创建,或者也可以采用上面的那个例子,我们希望在某个特定的环境变量下才会去创建某个bean,总的来说就是希望bean的创建有前提条件

解决措施

@Conditional

@Bean
@Profile("test")
@Conditional(MysqlExistsCondition.class)
public DataSource m_mysql() {
    return new MysqlDAO();
}
public class MysqlExistsCondition implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return false;
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProfileConfig.class)
@ActiveProfiles("test")
public class MyTest {
    @Autowired
    private DataSource dataSource;

    @Test
    public void test() {
        dataSource.getDataSourceName();
    }
}

image-20201202103800248

分析一下上面这3段代码,在第一段中我们添加了@Conditional注解,用于Bean的条件化创建,注解中存在一个MysqlExistsCondition.class,第二段就是MysqlExistsCondition.class的代码,实现了接口的matches方法,直接返回false,表示不符合条件,这时MysqlDAOBean的创建因条件不满足也就失败了,因为没有一个符合条件的bean(把matches中返回值改成true,这段代码就可以了)

探究@Profile

我们采用递归的形式一层一层下去

@Conditional(MysqlExistsCondition.class)

再看看MysqlExistsCondition

public class MysqlExistsCondition implements Condition {
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        return true;
    }
}

接着来看看Condition

@FunctionalInterface
public interface Condition {

   /**
    * Determine if the condition matches.
    * @param context the condition context
    * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
    * or {@link org.springframework.core.type.MethodMetadata method} being checked
    * @return {@code true} if the condition matches and the component can be registered,
    * or {@code false} to veto the annotated component's registration
    */
   boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

接下来我们看一下这两个参数

ConditionContext context——获取当前条件的上下文,具体查看源码

AnnotatedTypeMetadata metadata——检查带有@Bean注解的方法上还有什么其他的注解

介绍完这些,我们回头来看一下@Profile ,它里面也有@Conditional(ProfileCondition.class)

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

   /**
    * The set of profiles for which the annotated component should be registered.
    */
   String[] value();

}

我们发现@Profile的原理是@Conditional,引用ProfileConditional作为@Conditional 的实现

class ProfileCondition implements Condition {

   @Override
   public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
      MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
      if (attrs != null) {
         for (Object value : attrs.get("value")) {
            if (context.getEnvironment().acceptsProfiles(Profiles.of((String[]) value))) {
               return true;
            }
         }
         return false;
      }
      return true;
   }

}

首先获取@Profile的全部属性,检查value属性,根据ConditionContext得到的Environment的acceptsProfiles方法判断该profile是否被激活从而返回

3.3.处理自动装配的歧义性

场景

自动装配存在歧义性,属性类型为一个接口时,Spring不知道该装配哪一个Bean

解决措施

标识首选Bean
@Bean
@Primary
//@Profile("dev")
public DataSource m_sqlserver() {
    return new SqlServerDAO();
}

image-20201202132652509

限定自动装配的bean
@Bean
@Qualifier("ch")
//@Profile("prod")
public DataSource m_oracle() {
    return new OracleDAO();
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProfileConfig.class)
//@ActiveProfiles("test")
public class MyTest {
    @Autowired
    @Qualifier("ch")
    private DataSource dataSource;

    @Test
    public void test() {
        dataSource.getDataSourceName();
    }
}

但如果一个bean存在多个别名,能使用多个@Qualifier吗?换一个问题,两个bean拥有相同的别名,如何进一步缩小范围

源码告诉我们是不行的,因为value并不是数组

同时java不允许一个条目上重复出现相同类型的多个注解

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {
    String value() default "";
}

自定义限定符注解【重要】

使用自定义限定符注解可以解决上诉的问题

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface CMysql {
}
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface HMysql {
}
@Configuration
public class ProfileConfig {
    //@Profile("test")
    //@Conditional(MysqlExistsCondition.class)
    @Bean
    @CMysql
    @HMysql
    public DataSource m_mysql() {
        return new MysqlDAO();
    }

    @Bean
    @CMysql
    //@Profile("dev")
    public DataSource m_sqlserver() {
        return new SqlServerDAO();
    }

    @Bean
    //@Profile("prod")
    public DataSource m_oracle() {
        return new OracleDAO();
    }
}
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ProfileConfig.class)
//@ActiveProfiles("test")
public class MyTest {
    @Autowired
    @CMysql
    @HMysql
    private DataSource dataSource;

    @Test
    public void test() {
        dataSource.getDataSourceName();
    }
}

image-20201202135421151

限定符注解解决的问题:一个条目上重复出现相同类型的多个注解

3.4.bean的作用域

场景

两个作用域不同的bean如何装配在一起,比如仓库服务的作用域是Application级别的,但用户的购物车的作用域是Session级别的,这两个如何装配在一起?

Spring的策略

Spring并不会将实际的购物车bean注入到仓库服务bean中,Spring会注入一个购物车bean的代理,这个代理会暴露与购物车bean相同的方法,所以仓库服务bean会认为它就是一个购物车bean,当仓库服务bean调用它的方法时,代理会进行懒加载并将调用委托给Session作用域内真正的购物车bean

image-20201202141642644

这边先理解一下,具体的代码会在SpringMVC中给出,不急

最后说一下,接口和类的代理参数不同,即proxyMode的值不同

ScopedProxyMode.INTERFACES		//接口
ScopedProxyMode.TARGET_CLASS	//类

3.5.运行时值注入(了解即可)

为了避免硬编码值,实现java代码和配置属性的解耦合,也就出现了配置文件和运行时值注入的方法

这样理解可能跳跃,我们慢慢来

我们可能经常在代码中判断一个值与一个定值的关系,我们的想法都是把那个定值固定成一个常量,但这还是有耦合,如果我们的常量有变化,我们还是得到java代码中修改那个常量的值,那有没有什么办法可以把那个常量的值提取出来,配置文件出现了!我们把属性值提取到配置文件中,等到运行时把配置文件中的属性值填进那些个原先需要这些值的地方就可以了

这样带来的好处是什么呢?我们原先更改属性值需要到java代码中改,现在呢,java代码中是占位符,对于它的修改我们只需要修改配置文件中的值即可,完全不去碰java代码,不就实现了解耦,实现了软编码了嘛!

==这部分内容主要以看书为主,下面的笔记质量很差==

3.5.1.属性占位符(Property placeholder)

通过Environment获得
@Component
@PropertySource("app.properties")
public class ExpressiveConfig {
    @Autowired
    Environment env;

    public void showData() {
        System.out.println(env.getProperty("ch.username"));
        System.out.println(env.getProperty("ch.password"));
    }
}
@Test
public void test1() {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ExpressiveConfig.class);
    ExpressiveConfig config = ctx.getBean("expressiveConfig", ExpressiveConfig.class);
    config.showData();
}
通过@Value获得
@Data
@NoArgsConstructor
@Component
@PropertySource("app.properties")
public class ExpressiveConfig {
    private String username;
    private String password;

    @Autowired
    public ExpressiveConfig(@Value("${ch.username}") String username, @Value("${ch.password}") String password) {
        this.username = username;
        this.password = password;
    }
}

3.5.2.Spring表达式语言(SpEL)

@Data
@Component
@PropertySource("app.properties")
public class User {
    private Environment env;
    private String username;
    private String password;

    @Autowired
    public User(@Value("${ch.username}") String username, @Value("${ch.password}") String password) {
        this.username = username;
        this.password = password;
    }

    public String get1() {
        return "1";
    }
}
@Configuration
@ComponentScan(basePackages = "com.hznu.ch.running")
public class ExpressiveConfig {

    private String username;
    private String password;

    @Autowired
    public void setData(@Value("#{user.get1()}") String username, @Value("#{user.password}") String password) {
        this.username = username;
        this.password = password;
    }

    public void getData() {
        System.out.println(username);
        System.out.println(password);
    }
}

通过#{user.get1()}使用对象的方法,#{user.password}使用对象的属性

#{T(java.lang.Math).PI}访问类的静态方法和变量

?. ——!null才执行后面

?: ——null取:后面的值

.?[] ——对集合过滤

.^[] ——查询第一个匹配项

.$[] ——查询最后一个匹配项

.![] ——投影

4.面向切面的Spring

4.1.什么是面向切面编程

4.1.1.定义AOP的术语

  • 通知(Advice)
    • 前置通知(Before):目标方法调用之前调用通知功能
    • 后置通知(After):通知方法会在目标方法返回或抛出异常后调用
    • 返回通知(After-Returning):通知方法会在目标方法返回后调用
    • 异常通知(After-Throwing):通知方法会在目标方法抛出异常后调用
    • 环绕通知(Around):通知方法会把目标方法封装起来
  • 连接点(Join point)
    • 连接点是在应用执行过程中能够插入切面的一个点
    • 这个点可以是调用方法时,抛出异常时,甚至修改一个字段时
    • 切面代码可以利用这些点插入到应用的正常流程中,并添加新的行为
  • 切点(Pointcut)
    • 如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处“
    • 切点的定义会匹配通知所要织入的一个或多个连接点
    • 通常使用明确的类和方法名或是利用正则表达式定义所匹配的类和方法名来指定这些切点

(切点和连接点的关系——通过切点来选择连接点)

image-20201203122850381

  • 切面(Aspect)
    • 切面是切点和通知的结合
  • 引入(Introduction)
    • 引入允许我们向现有的类添加新方法或属性,让它们具有新的行为和状态,同时无需修改这些现有的类
  • 织入(Weaving)
    • 织入是把切面应用到目标对象并创建新的代理对象的过程
    • 在目标对象的生命周期里有多个点可以进行织入
      • 编译期
      • 类加载期
      • 运行期(Spring AOP就是以这种方式织入切面)

ps:Spring只支持方法级别的连接点

4.3.使用注解创建切面

4.3.1.定义切面

和之前的笔记的区别之处在于多了个可复用切点和使用javaConfig配置(原先采用xml配置)

@Aspect
public class AnnotationPointCut {
    // 可复用切点
    @Pointcut("execution(* com.hznu.ch.service.UserServiceImpl.*(..))")
    public void doIt() {

    }

    @Before("doIt()")
    public void before() {
        System.out.println("前!");
    }

    @After("doIt()")
    public void after() {
        System.out.println("后!");
    }

    @Around("doIt()")
    public void around(ProceedingJoinPoint jp) throws Throwable {
        System.out.println("环绕前");
        Object o = jp.proceed();
        System.out.println("环绕后");
    }
}
@Configuration
@EnableAspectJAutoProxy     //主要是这个注解,开启AspectJ的自动代理
@ComponentScan(basePackages = "com.hznu.ch.aspect")
public class ConcertConfig {
    @Bean
    public AnnotationPointCut annotationPointCut() {
        return new AnnotationPointCut();
    }

    @Bean
    public UserService userServiceImpl() {
        return new UserServiceImpl();
    }
}

4.3.3.处理通知参数

UserService中添加showArgs(int x)方法,并在UserServiceimpl中实现

public void showArgs(int x) {
    System.out.println(x);
}

AnnotationPointCut中添加切点并修改before方法

流程分析:调用showArgs(int x)方法,x会被args捕获,然后传进before的参数x中,并在方法中使用

@Pointcut("execution(* com.hznu.ch.service.UserServiceImpl.showArgs(int)) && args(x)")
public void show(int x) {

}

@Before("show(x)")
public void before(int x) {
  	System.out.println("前!" + x);
}

==4.3.4.通过注解引入新功能==(留坑)

image-20201204120903029

场景

想为某个类扩展一个功能,但又不能在目标类上修改或添加

解决方法
  1. 使用代理模式,为目标类添加功能
  2. 使用注解注入新功能,使用@DeclareParents

我们这里主要使用注解注入,这个注解注入使用到的也是AOP,AOP的核心也是代理模式,所以本质还是第一种方法,不过是在@Aspect上使用AOP规定的方法进行代理罢了(个人理解哈哈哈)

这里有个男人类,我们想为这个类加一个吃东西的方法

public interface Person {
    void likePerson();
}
@Component
public class Man implements Person {
    public void likePerson() {
        System.out.println("喜欢一个人,就像风走了八万里,不问归期");
    }
}

添加方法的具体实现

public interface Animal {
    void eat();
}
public class MaleAnimal implements Animal {
    public void eat() {
        System.out.println("我想吃螺蛳粉!");
    }
}

核心

@Aspect
@Component
public class AspectConfig {
    @DeclareParents(value = "com.hznu.ch.concert.Person+", defaultImpl = MaleAnimal.class)//所有实现了Person的子类型bean要引入MaleAnimal接口
    public static Animal animal;
}

javaConfig类我们省略掉

测试方法

public void test1() {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConcertConfig.class);
    Person man = ctx.getBean("man", Person.class);
    man.likePerson();
    Animal animal = (Animal) man;
    animal.eat();
}

测试结果,成功为Man类引入了eat方法,主要是通过@DeclareParents注解实现

image-20201204201323909

@DeclareParents解析

@DeclareParents(value = "com.hznu.ch.concert.Person+", defaultImpl = MaleAnimal.class)

所有实现了Person的子类型bean要引入MaleAnimal接口

  • value指定哪种类型的bean要引入该接口,标识符号后面的+表示该类的所有子类型,而不是Person本身
  • defaultImpl属性指定了为引入功能提供实现的类

4.4.在XML中声明切面

详见Spring(狂神说笔记的AOP部分)

4.4.1.通过切面引入新的功能

这一段等同于上面的javaConfig类和AspectConfig类

<aop:aspectj-autoproxy/>
<context:component-scan base-package="com.hznu.ch.*"/>
<aop:config>
    <aop:aspect>
        <aop:declare-parents types-matching="com.hznu.ch.concert.Person+"
                             implement-interface="com.hznu.ch.concert.Animal"
                             default-impl="com.hznu.ch.concert.MaleAnimal"/>
    </aop:aspect>
</aop:config>

测试类变换不大,就是把注解上下文改成xml上下文,就不贴出来了

image-20201204214128475

4.5.为Aspect切面注入依赖

场景

==@Aspect中无法@Autowired,Autowired的结果是null==

但我的却可以正常注入。。。。暂时留个坑吧~

public class Perform {
    public void dance() {
        System.out.println("跳舞");
    }
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CriticismEngine {
    private String msg;

    public void doIt() {
        System.out.println("他说:" + msg);
    }

}
@Aspect
public class AspectConfig {
    @Autowired
    private CriticismEngine criticismEngine;


    public void deed() {
        criticismEngine.doIt();
    }


    @Before("execution(* com.hznu.ch.autowired.Perform.*(..))")
    public void before() {
        criticismEngine.doIt();
    }
}
@EnableAspectJAutoProxy
@Configuration
public class PerformConfig {


    @Bean
    public Perform thePerform() {
        return new Perform();
    }

    @Bean
    public CriticismEngine theCriticismEngine() {
        return new CriticismEngine("加油!");
    }

    @Bean
    public AspectConfig theAspectConfig() {
        return new AspectConfig();
    }
}
@Test
public void test3() {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(PerformConfig.class);
    Perform thePerform = ctx.getBean("thePerform", Perform.class);
    thePerform.dance();
}

image-20201205142941742

就完全可以注入,和书上的不太一样,就很离谱

但还是需要注意一下,如果按照书上说的,AspectJ提供了aspectOf()方法,该方法返回切面的一个单例,只有返回切面的单例之后才能完成Spring bean的依赖注入

img

5.构建Spring Web应用程序

5.1.Spring MVC起步

AbstractAnnotationConfigDispatcherServletInitializer 的任意类都会自动配置DispatcherServlet和Spring应用上下文,Spring应用上下文位于Servlet上下文中,当DispatcherServlet启动时,会创建Spring应用上下文

AbstractAnnotationConfigDispatcherServletInitializer同时创建DispatcherServlet和ContextLoaderListener

getServletConfigClasses方法会返回带有@Configuration注解的类用来定义DispatcherServlet应用上下文的bean

getRootConfigClasses方法会返回带有@Configuration注解的类用来配置ContextLoaderListener创建的应用上下文中的bean

5.2.web.xml的替代方案

5.2.1.依赖

可能用到的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.2.9.RELEASE</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.16</version>
    </dependency>
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <version>2.23.4</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp.jstl</groupId>
        <artifactId>jstl-api</artifactId>
        <version>1.2</version>
    </dependency>
    <dependency>
        <groupId>taglibs</groupId>
        <artifactId>standard</artifactId>
        <version>1.1.2</version>
    </dependency>
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.1.5.Final</version>
    </dependency>
</dependencies>

5.2.2.配置类

解析见上面

public class SpittrWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{RootConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class};
    }
}
@Configuration
@EnableWebMvc
@ComponentScan("com.hznu.controller")
public class WebConfig extends WebMvcConfigurationSupport {
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setExposeContextBeansAsAttributes(true);//????
        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();//要求DispatcherServlet将对静态资源对的请求转发到Servlet容器中默认的Servlet
    }
}
@Configuration
@ComponentScan(basePackages = {"com.hznu"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
        })
public class RootConfig {
}

5.2.3.实体类

public class Spittle {
    private final Long id;
    private final String message;
    private final Date time;
    private Double latitude;
    private Double longitude;

    public Spittle(String message, Date time) {
        this(message, time, null, null);
    }

    public Spittle(String message, Date time, Double latitude, Double longitude) {
        this.id = null;
        this.message = message;
        this.time = time;
        this.latitude = latitude;
        this.longitude = longitude;
    }

    public Long getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }

    public Date getTime() {
        return time;
    }

    public Double getLatitude() {
        return latitude;
    }

    public Double getLongitude() {
        return longitude;
    }

    @Override
    public int hashCode() {
        //apache common lang包的工具类,此处是根据"id","time"确定hash值
        return HashCodeBuilder.reflectionHashCode(this, new String[]{"id", "time"});
    }

    @Override
    public boolean equals(Object obj) {
        //apache common lang包的工具类,此处是根据"id","time"确定两个对象是否相等
        return EqualsBuilder.reflectionEquals(this, obj, new String[]{"id", "time"});
    }
}

5.2.4.控制层

@Controller
@RequestMapping({"/", "/homePage"})//web项目中,如果/后面没有对应路径,则/会被省略,此时/ == /*
public class HomeController {
    @GetMapping("/home")
    public String home() {
        return "home";
    }

    @GetMapping("/test")
    public String test(@Valid Spitter spitter, Errors errors) {//合法校验好像没用???
        System.out.println(spitter);
        if (errors.hasErrors())
            return "home";
        return "spittles";
    }
}
@Controller
@RequestMapping("/spittles")
public class SpittleController {
    private SpittleRepository spittleRepository;

    @Autowired
    public SpittleController(SpittleRepository spittleRepository) {
        this.spittleRepository = spittleRepository;
    }
		//查询参数
    @RequestMapping(value = "list", method = RequestMethod.GET)
    public String spittles(Model model) {
        model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, 20));
        return "spittles";
    }
		//路径变量
    @RequestMapping(value = "query/{spittleId}", method = RequestMethod.GET)
    public String spittlesById(@PathVariable("spittleId") int spittleId, Model model) {
        model.addAttribute("spittleList", spittleRepository.findSpittles(Long.MAX_VALUE, spittleId));
        return "spittles";
    }
}

5.2.5.数据访问层

因为还没有连接数据库,所以直接自己模拟数据类。。。。。。

@Repository
public class SpittleRepository {
    private List<Spittle> createSpittleList(int i) {
        List<Spittle> list = new ArrayList<Spittle>();
        for (int j = 0; j < i; j++)
            list.add(new Spittle("Spittle " + j, new Date()));
        return list;
    }

    public List<Spittle> findSpittles(long max, int count) {
        return createSpittleList(count);
    }
}

5.2.6.视图

home.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>SpringMVC javaConfig测试</h1>
<a href="#">Hello</a>
<a href="#">Bridge</a>
</body>
</html>

spittles.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--这里用注意,添加的两个依赖,jstl-api和standard--%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>Hello Bridge</h1>
<c:forEach items="${spittleList}" var="spittle">
    <li id="spittle_<c:out value="${spittle.id}"/>">
        <div class="spittleMessage">
            <c:out value="${spittle.message}"/>
        </div>
        <div>
            <span class="spittleTime">
                <c:out value="${spittle.time}"/>
            </span>
            <span class="spittleLocation">
                (<c:out value="${spittle.latitude}"/>,<c:out value="${spittle.longitude}"/>)
            </span>
        </div>
    </li>
</c:forEach>
</body>
</html>

5.2.7.测试

可以使用postman去替代。

5.3.待解决的问题

  • @Vaild不起作用

包版本问题(6.1.5的@Size不起作用,更换成6.0.7即可)

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.7.Final</version>
</dependency>

对于低本版的hibernate-validator包含了validation-api

高版本的hibernate-validatorvalidation-api独立

放到下面备用

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>7.0.0.Final</version>
</dependency>
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>2.0.1.Final</version>
</dependency>

6.渲染Web视图

如果使用了JSTL标签处理格式化和信息,我们会希望InternalResourceView将视图解析成JstlView

resolver.setViewClass(JstlView.class);//希望InternalResourceView将视图解析成JstlView实例

image-20210111204821344

6.1.创建JSP视图

知识点

  • 表单绑定对象:modelAttributepath绑定对象属性
  • 展现错误:cssErrorClass, <sf:errors/>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib uri="http://www.springframework.org/tags/form" prefix="sf" %>
<%@taglib uri="http://www.springframework.org/tags" prefix="s" %>
<html>
<head>
    <title>My SpringMVC Test</title>
    <style>
        div.errors {
            background-color: #ffcccc;
            border: 2px solid red;
        }

        label.error {
            color: red;
        }

        input.error {
            background-color: #ffcccc;
        }
    </style>
</head>
<body>
<%--modelAttribute绑定对象,请求的路径会被定义为当前的url--%>
<sf:form method="post" modelAttribute="spitter">
    <sf:errors path="*" element="div" cssClass="errors"/>

    <%--path绑定对象的属性,cssErrorClass代表发生错误之后的css--%>
    <sf:label path="username" cssErrorClass="error">
        <%--code用于绑定配置文件中键值对,具体国际化的语言取决于ValidationConfig类中的声明--%>
        <s:message code="spitter.username"/>
    </sf:label>
    <sf:input path="username" cssErrorClass="error"/>
    <br>

    <sf:label path="password" cssErrorClass="error"><s:message code="spitter.password"/></sf:label>
    <sf:password path="password" cssErrorClass="error"/>
    <br>

    <input type="submit" value="<s:message code='spitter.register'/>"/> <br>
</sf:form>
</body>
</html>
  • 配置文件配置注解错误提示信息
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Spitter {
    private Long id;

    @NotBlank(message = "用户名不为空")
    @Size(min = 5, max = 20, message = "{username.size}")//这里引入配置文件的内容
    private String username;

    @NotBlank(message = "密码不能为空")
    @Size(min = 5, max = 20, message = "{password.size}")
    private String password;
}

配置文件ValidationMessages.properties一定要放置在resources文件夹下

==Web页面展示配置信息会出现乱码,这边建议修改文件本身的编码格式为UTF-8==

username.size=用户名长度必须在{min}和{max}字符之间
password.size=密码长度必须在{min}和{max}字符之间
  • 国际化:通过<s:message code="spitter.username"/>展示国际化
@Configuration
public class ValidationConfig {
    //国际化的文件命名有规范,不可随意命名,切记!!!
    @Bean
    public MessageSource messageSource() {
      	//ResourceBundleMessageSource区别于ReloadableResourceBundleMessageSource,前者应用内部查找,后者应用外部查找
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages_zh_CN");
        return messageSource;
    }	
}
spitter.username=用户名
spitter.password=密码
spitter.register=注册
  • 创建URL:通过<s:url></s:url>
  • 转义内容:htmlEscape | javaScriptEscape | <s:escapeBody></s:escapeBody>设置,默认为false,不转义
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="s" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
    <title>Title</title>
</head>
<body>

<%--通用转义标签,作用在响应体,响应体,响应体!!!可以查看浏览器的Sources--%>
<s:escapeBody htmlEscape="true">
    <h1>SpringMVC javaConfig测试</h1>
</s:escapeBody>


<s:url value="/spittles/query/{spittleId}" var="showSpittlesUrl">
    <s:param name="spittleId" value="20"/>
</s:url>
<a href="${showSpittlesUrl}">Show</a>

<%--htmlEscape,javaScriptEscape分别对HTML和JS转义--%>
<s:url value="/spitter/register" htmlEscape="false" var="registerUrl" javaScriptEscape="false">
    <s:param name="max" value="60"/>
    <s:param name="count" value="20"/>
</s:url>
<a href="${registerUrl}">Register</a>


<%
    class NewBean {
        private String text = "在这里输入文本";//定义一个字符串

        public void setText(String text) {  //生成get和set方法
            this.text = text;
        }

        public String getText() {
            return text;
        }
    }
    NewBean fanBean = new NewBean();
    request.setAttribute("formBean", fanBean);
%>
<%--默认的绑定对象是command--%>
<form:form modelAttribute="formBean">
    请输入你对本站的看法:<br>
    <form:textarea path="text" cols="20" rows="8" htmlEscape="true"/>
</form:form>
</body>

<script>
    console.log("${registerUrl}");
</script>
</html>

6.2.使用Thymeleaf视图

声明配置类,启动3个Thymeleaf和Spring集成的bean

ThymeleafViewResolver,SpringTemplateEngine,SpringResourceTemplateResolver(下面注释做了解释)

==三者关系的个人理解:视图解析器解析逻辑视图成Thymeleaf模版,但模版并不是HTML,所以仍需要解析,模版引擎中的模版解析器就可以把Thymeleaf模版解析成HTML==

//注意修改全局配置类中的getServletConfigClasses方法
@Override
protected Class<?>[] getServletConfigClasses() {
  return new Class<?>[]{ThymeleafWebConfig.class};
}

//ThymeleafWebConfig类
@Configuration
@EnableWebMvc
@ComponentScan("com.hznu.controller")
public class ThymeleafWebConfig extends WebMvcConfigurationSupport {
    //视图解析器
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setTemplateEngine(templateEngine);
        //防止中文乱码
        viewResolver.setCharacterEncoding("UTF-8");
        return viewResolver;
    }

    //模版引擎
    @Bean
    public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    //模版解析器
    @Bean
    public SpringResourceTemplateResolver templateResolver() {
        SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setTemplateMode("HTML5");
        return templateResolver;
    }
}

Thymeleaf的视图实现:

  • ${}和*{}的区别
    • ${}是变量表达式,基于SpEL上下文计算,本例中绑定的对象是spitter
    • *{}是选择表达式,基于某一个选中对象计算的,本例中对应的是spitter的username属性
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Thymeleaf Spitter</title>
    <style>
        div.errors {
            background-color: #ffcccc;
            border: 2px solid red;
        }
    </style>
</head>
<body>
<!--th:object绑定spitter对象-->
<form method="post" th:object="${spitter}">
    <!--th:if用于逻辑判断,后面的条件满足,就会渲染子元素,否则不会渲染-->
    <div class="errors" th:if="${#fields.hasErrors('*')}">
        <ul>
            <!--用于err : 声明变量;${err}使用变量-->
            <li th:each="err : ${#fields.errors('*')}" th:text="${err}">
                Input is incorrect
            </li>
        </ul>
    </div>
    <!--如果字段username有错误,class将被赋值为error-->
    <label th:class="${#fields.hasErrors('username')}?'error'">
        Username:
    </label>
    <!--使用th:field将输入域绑定到后端对象的username属性上,同时还可以将HTML input标签中value和name也都设置成"username"-->
    <input type="text" th:field="*{username}" th:class="${#fields.hasErrors('username')}?'error'"/>
    <br>

    <label th:class="${#fields.hasErrors('password')}?'error'">
        Password:
    </label>
    <input type="text" th:field="*{password}" th:class="${#fields.hasErrors('password')}?'error'"/>
    <br>

    <input type="submit" value="Register">
</form>
</body>
</html>

7.Spring MVC的高级技术

  • servlet和controller的区别?

    • servlet的本质其实也是一个java bean,controller是对servlet的封装,底层依旧是servlet。
  • 为什么采用multipart/form-data的表单形式传输文件?==(原理还是不怎么明白,留坑)==

    • 如果要发送大量的二进制数据(non-ASCII),application/x-www-form-urlencoded 显然是低效的,因为它需要用 3 个字节来表示一个 non-ASCII 的字符。因此,这种情况下,应该使用 “multipart/form-data” 格式。
    • application / x-www-form-urlencoded对于发送大量二进制数据或包含非ASCII字符的文本效率低下。multipart / form-data应该用于提交包含文件,非ASCII数据和二进制数据的表单。

深入解析 multipart/form-data

7.1.处理multipart形式的数据

7.1.1.配置multipart解析器

  • StandardServletMultipartResolver(默认的)
//自定义DispatcherServlet配置
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
    //临时文件目录(绝对路径) 上传文件最大容量 最大请求容量 上传过程中,如果文件大小达到一个指定最大容量,将会写入到临时文件中(个人理解:缓冲区,方便集中处理,最后还是写入磁盘中),默认值为0,也就是所有上传文件都会写入到磁盘上
    //找不到解析器,会自动声明一个StandardServletMultipartResolver(标准)解析器
    registration.setMultipartConfig(new MultipartConfigElement("/Users/chennianzuisui/Desktop/Java/SpringBoot/workspace/spring_mvc_ex/SpringMVC-03-javaConfig/web/WEB-INF/tmp/spitter/", 7 * 1024 * 1024, 10 * 1024 * 1024, 1024 * 1024));
}
  • CommonsMultipartResolver

声明CommonsMultipartResolver之后,会覆盖前面的customizeRegistration,所以二选一就可以了

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.3</version>
</dependency>
@Bean
public MultipartResolver multipartResolver() {
    //return new StandardServletMultipartResolver();//这里可以没有StandardServletMultipartResolver这个解析器,因为会自动生成

    //CommonsMultipartResolver配置解析器,如果使用CommonsMultipartResolver替代StandardServletMultipartResolver,可以把前面customizeRegistration那一块删掉,即使不删除,也还是以这里的配置为主
    CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
    multipartResolver.setMaxUploadSize(2 * 1024 * 1024);
    multipartResolver.setMaxInMemorySize(0);
    return multipartResolver;
}

7.1.2.处理multipart请求

<!--multipart/form-data是告诉浏览器以multipart数据的形式提交表单-->
<form method="post" th:object="${spitter}" enctype="multipart/form-data">
  ....
  <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif"/>
</form>

如何接收表单的文件

  • MultipartFile
  • ==Part(跟书上的描述有出入,有待考证)==
@PostMapping("/register")
public String showRegistrationForm(@RequestPart("profilePicture") MultipartFile profilePicture, @Valid Spitter spitter, Errors errors, RedirectAttributes model) throws IOException {
    //使用MultipartFile
    profilePicture.transferTo(new File("/Users/chennianzuisui/Desktop/Java/SpringBoot/workspace/spring_mvc_ex/SpringMVC-03-javaConfig/web/WEB-INF/tmp/spitter/uploads/" + profilePicture.getOriginalFilename()));


    //使用Part,无需配置MultipartResolver(Part会与CommonsMultipartResolver冲突),但还是需要customizeRegistration(但这玩意会默认配置MultipartResolver)。。。
    //profilePicture.write("/Users/chennianzuisui/Desktop/Java/SpringBoot/workspace/spring_mvc_ex/SpringMVC-03-javaConfig/web/WEB-INF/tmp/spitter/uploads/" + profilePicture.getSubmittedFileName());
    if (errors.hasErrors()) return "registerForm";

    model.addAttribute("username", spitter.getUsername());
    //1)通过URL模版进行重定向
    //如果username的属性值为chenheng,password=123456,那么URL为/spitter/chenheng?password=123456
    //model.addAttribute("password", spitter.getPassword())

    //2)通过flash属性
    model.addFlashAttribute("spitter", spitter);
    return "redirect:/spitter/{username}";//重定向携带参数
}

7.2.处理异常&为控制器添加通知

7.2.1.异常映射为HTTP状态码

SpittleNotFoundException映射为404

@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Spittle Not Found")
public class SpittleNotFoundException extends RuntimeException {
}

7.2.2.控制器添加异常

全局异常

//@ExceptionHandler
//@InitBinder
//@ModelAttribute
//控制器通知,在带有@ControllerAdvice注解的类中,以上所述的这些方法会运用到整个应用程序所有控制器中带有@RequestMapping注解的方法上
//@ControllerAdvice本身已经使用了@Component,所以会被组件扫描扫描到
@ControllerAdvice
public class AppWideExceptionHandler {

    //将所有@ExceptionHandler收集到一个类中,对控制器的所有异常做统一处理
    @ExceptionHandler(SpittleNotFoundException.class)
    public String handleException() {
        return "home";
    }
}

7.3.跨重定向请求传递数据

部分代码

  • URL模版重定向
  • 使用flash属性
    • 将重定向之前的对象保留到会话中,重定向之后,从会话中取出
    • 使用flash属性需要使用RedirectAttributes类替代掉Model类
model.addAttribute("username", spitter.getUsername());
//1)通过URL模版进行重定向
//如果username的属性值为chenheng,password=123456,那么URL为/spitter/chenheng?password=123456
//model.addAttribute("password", spitter.getPassword())

//2)通过flash属性
model.addFlashAttribute("spitter", spitter);
return "redirect:/spitter/{username}";//重定向携带参数
@GetMapping("/{username}")
public String userInfo(@PathVariable("username") String username, Model model) {
    if (model.containsAttribute("spitter"))
        return "user";

    Spitter spitter = spittleRepository.findOne(username);
    if (spitter == null)
        throw new SpittleNotFoundException();
    model.addAttribute("spitter", spitter);
    return "user";
}

image-20210119141724787

9.保护Web应用

9.1.Spring Security入门

(本节与书本无关,主要是书上对入门有点不友好,我看的是B站黑马程序员的网课,所以下面的内容也是网课对应的个人笔记)

9.1.1.基本概念

9.1.1.1.什么是认证

用户认证就是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。

9.1.1.2.什么是授权

授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有权限则拒绝访问。(微信登录后只有绑定了银行卡才可以发红包)

9.1.1.3.授权的数据模型

image-20210123160619140

9.1.1.4.RBAC

RBAC基于角色的访问控制(Role-Based Access Control)是按角色进行授权,不具扩展性

RBAC基于资源的访问控制(Resource-Based Access Control)是按资源(或权限)进行授权,扩展性强

9.1.2.Spring Security应用详解

9.1.2.1.工作原理

通过过滤链配合完成认证和授权

image-20210123162340733

9.1.2.2.认证流程

image-20210125141543919

9.1.2.3.授权流程

image-20210126115238550

9.1.3.会话

用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取用户身份。

9.1.3.1.会话控制
机制描述
always如果没有session存在就创建一个
ifRequired如果需要就创建一个Session(默认)登录时
neverSpringSecurity 将不会创建Session,但是如果应用中其他地方创建了Session,那么Spring Security将会使用它
statelessSpringSecurity将绝对不会创建Session,也不使用Session

9.1.4.授权

Spring Security原生注解实现授权,必须有ABC权限才能访问注解下的方法

@PreAuthorize("hasAnyRole('ABC')")//注解实现授权

9.1.5.代码

9.1.5.1.依赖导入
<dependencies>
    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-web</artifactId>
        <version>5.1.4.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-config</artifactId>
        <version>5.1.4.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>5.1.4.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.2.9.RELEASE</version>
    </dependency>

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.22</version>
    </dependency>

    <!--Caused by: java.lang.NoClassDefFoundError: org/springframework/dao/DataAccessException的解决方案-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.3.2</version>
    </dependency>
</dependencies>
9.1.5.2.启动类配置
9.1.5.2.1.SpringMVC配置

SpringMvcDispatcherServletInitializer

public class SpringMvcDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    protected Class<?>[] getRootConfigClasses() {
        return new Class<?>[]{RootConfig.class, SecurityConfig.class};
    }

    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[]{WebConfig.class};
    }
}

RootConfig

@Configuration
@ComponentScan(basePackages = {"com.hznu"},
        excludeFilters = {
                @ComponentScan.Filter(type = FilterType.ANNOTATION, value = EnableWebMvc.class)
        })
public class RootConfig {

}

WebConfig

@Configuration
@EnableWebMvc//springboot中不需要,因为springboot已经自动装配了WebMvcConfigurationSupport类,不需要声明@EnableWebMvc再次装配,否则会覆盖
@ComponentScan("com.hznu")
//使用WebMvcConfigurationSupport无法启用addViewControllers的解决措施:
//先讲原因:@EnableWebMvc=WebMvcConfigurationSupport,使用了@EnableWebMvc注解等于扩展了WebMvcConfigurationSupport但是没有重写任何方法,所以两个一起用的时候,注解直接覆盖了类里面写的方法导致addViewControllers配置失效
//1.保留@EnableWebMvc,实现WebMvcConfigurer接口(我选择这种)
//2.去掉@EnableWebMvc,继承WebMvcConfigurationSupport
//参考文献:https://www.cnblogs.com/JonaLin/p/11633820.html
public class WebConfig implements WebMvcConfigurer {
    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        resolver.setViewClass(JstlView.class);//希望InternalResourceView将视图解析成JstlView实例
        resolver.setExposeContextBeansAsAttributes(true);//可以在jsp页面中通过${}访问beans
        return resolver;
    }

    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();//要求DispatcherServlet将对静态资源对的请求转发到Servlet容器中默认的Servlet
    }

    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/login-view");//注意这里也遵循url匹配原则,如果存在/(/index),则不会重定向
        registry.addViewController("/login-view").setViewName("/login");
    }
}
9.1.5.2.2.SpringSecurity配置

SpringSecurityApplicationInitializer

//配置SpringSecurity的自启动
public class SpringSecurityApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
    //如果不是spring或springmvc项目,就需要把SecurityConfig配置到这里,反之配置到spring上下文即可
    public SpringSecurityApplicationInitializer() {
        //super(SecurityConfig.class);
    }
}

SecurityConfig

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)//启动Spring Security的安全注解配置
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    //@Override
    //@Bean
    //配置用户存储认证
    protected UserDetailsService userDetailsService() {
        //roles和authorities的区别在于前者自带ROLE_前缀,后者无
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("user").password("123").roles("USER").build());
        manager.createUser(User.withUsername("admin").password("456").roles("ADMIN").build());
        return manager;
    }

    @Bean
    //密码编码器
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    //保护HTTP请求
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
                .invalidSessionUrl("/login-view?error=INVALID_SESSION")         //传入的session_id无效的跳转路径
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)   //如果需要就创建一个Session(默认)登录时
                .and()
                .csrf().disable()                                               //屏蔽CSRF控制,即spring security不再限制CSRF
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyRole("USER")//  /r/r1需要ROLE_USER权限才可以访问
                .antMatchers("/r/r2").hasAnyRole("ADMIN")
                .antMatchers("/r/**").authenticated()           //访问/r/**需要权限
                .anyRequest().permitAll()                                    //其他url无需权限
                .and()
                .formLogin()
                .loginPage("/login-view")                                   //指定自己的登录页,Spring Security以重定向方式跳转到/login-view
                .loginProcessingUrl("/login")                               //指定登录处理的url,也就是用户名和密码表单提交的目的路径
                .successForwardUrl("/login-success")                        //登录成功转发到login-success路径
                .permitAll();
    }
}
9.1.5.2.3.DataSource配置

关于配置数据源的具体会在Spring实战(第四版)中提及

@Component
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8");
        ds.setUsername("root");
        ds.setPassword("XXXXXX");
        return ds;
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
9.1.5.3.数据库建立

数据库名为spring

CREATE TABLE `t_user` ( 
`id` bigint(20) NOT NULL COMMENT '用户id', 
`username` varchar(64) NOT NULL, 
`password` varchar(64) NOT NULL, 
`fullname` varchar(255) NOT NULL COMMENT '用户姓名', 
`mobile` varchar(11) DEFAULT NULL COMMENT '手机号', 
PRIMARY KEY (`id`) USING BTREE 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC 

CREATE TABLE `t_role` ( 
`id` varchar(32) NOT NULL, 
`role_name` varchar(255) DEFAULT NULL, 
`description` varchar(255) DEFAULT NULL, 
`create_time` datetime DEFAULT NULL, 
`update_time` datetime DEFAULT NULL, 
`status` char(1) NOT NULL, 
PRIMARY KEY (`id`), 
UNIQUE KEY `unique_role_name` (`role_name`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_role`(`id`,`role_name`,`description`,`create_time`,`update_time`,`status`) values ('1','管理员',NULL,NULL,NULL,''); 

CREATE TABLE `t_user_role` ( 
`user_id` varchar(32) NOT NULL, 
`role_id` varchar(32) NOT NULL, 
`create_time` datetime DEFAULT NULL, 
`creator` varchar(255) DEFAULT NULL, 
PRIMARY KEY (`user_id`,`role_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_user_role`(`user_id`,`role_id`,`create_time`,`creator`) values ('1','1',NULL,NULL); 

CREATE TABLE `t_permission` ( 
`id` varchar(32) NOT NULL, 
`code` varchar(32) NOT NULL COMMENT '权限标识符', 
`description` varchar(64) DEFAULT NULL COMMENT '描述', 
`url` varchar(128) DEFAULT NULL COMMENT '请求地址', 
PRIMARY KEY (`id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_permission`(`id`,`code`,`description`,`url`) values ('1','USER','测试资源1','/r/r1'),('2','ADMIN','测试资源2','/r/r2');

CREATE TABLE `t_role_permission` ( 
`role_id` varchar(32) NOT NULL, 
`permission_id` varchar(32) NOT NULL, 
PRIMARY KEY (`role_id`,`permission_id`) 
) ENGINE=InnoDB DEFAULT CHARSET=utf8 
insert into `t_role_permission`(`role_id`,`permission_id`) values ('1','1'),('1','2'); 
9.1.5.4.实体类

UserDto

@Data
public class UserDto {
    private String id;
    private String username;
    private String password;
    private String fullname;
    private String mobile;
}

Permission

@Data
public class Permission {
    private String id;
    private String code;
    private String description;
    private String url;
}
9.1.5.5.DAO获取数据库数据

UserDao

@Repository
public class UserDao {
    @Autowired
    JdbcTemplate jdbcTemplate;

    public UserDto getUserByUsername(String username) {
        String sql = "select * from t_user where username=?";
        List<UserDto> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(UserDto.class), username);
        if (list == null || list.size() <= 0)
            return null;
        return list.get(0);
    }

    public List<String> getPermissionByUserId(String userId) {
        String sql = "select *\n" +
                "from t_permission\n" +
                "where id in (select permission_id\n" +
                "             from t_role_permission\n" +
                "             where role_id in\n" +
                "                   (select role_id from t_user_role where user_id = ?))";
        List<Permission> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Permission.class), userId);
        List<String> permissions = new ArrayList<>();
        list.forEach(x -> permissions.add(x.getCode()));
        return permissions;
    }
}
9.1.5.6.自定义用户信息

SpringDataUserDetailsService

在上面有提到过认证的流程,会使用loadUserByUsername认证,这里采用自定义用户信息,另一种方法可查看上面SecurityConfig类中的userDetailsService()方法,也实现了自定义用户信息。

@Service
public class SpringDataUserDetailsService implements UserDetailsService {
    @Autowired
    private UserDao userDao;

    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        UserDto user = userDao.getUserByUsername(username);
        System.out.println(user);
        if (user == null)
            return null;
        List<String> permissions = userDao.getPermissionByUserId(user.getId());
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        System.out.println(Arrays.toString(permissionArray));
        UserDetails userDetails = User.withUsername(user.getUsername()).password(user.getPassword()).roles(permissionArray).build();
        return userDetails;
    }
}
9.1.5.7.Controller实现路由

HomeController

  • @PreAuthorize注解实现授权
  • 通过会话获取用户信息
@Controller
public class HomeController {
    @PostMapping(value = "/login-success")//成功登录之后的页面
    public String loginSuccess(Model model) {
        model.addAttribute("username", getUsername());
        return "home";
    }

    @GetMapping("/r/r1")//资源1
    //@PreAuthorize("hasAnyRole('ABC')")//注解实现授权
    public String resource_1() {
        return "resource_1";
    }

    @GetMapping("/r/r2")//资源2
    public String resource_2() {
        return "resource_2";
    }

    //@GetMapping("/{customUrl}")
    public String customUrl(@PathVariable("customUrl") String url) {
        return url;
    }


    //通过会话得到用户的信息,SecurityContext与当前线程进行绑定
    private String getUsername() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (!authentication.isAuthenticated())
            return null;
        Object principal = authentication.getPrincipal();
        String username = null;
        if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
            username = ((org.springframework.security.core.userdetails.UserDetails) principal).getUsername();
        } else {
            username = principal.toString();
        }
        return username;
    }
}
9.1.5.8.JSP页面

具体页面跳转逻辑可以查看HomeController类和WebConfig类的addViewControllers方法(关于这个方法还有一个知识点,具体可以查看WebConfig的注释)

9.1.5.8.1.自定义登录页面

已在SecurityConfig类中指定,具体可查看SecurityConfig

login.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>MyLoginPage</title>
</head>
<body>
<form action="/login" method="post">
    用户名:<input name="username" id="username"/>
    <br>
    密码:<input name="password" id="password"/>
    <br>
    <button type="submit" value="login">登录</button>
    <br>
</form>
</body>
</html>
9.1.5.8.2.资源页面

resource_1.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>资源1</h1>
</body>
</html>

resource_2.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>资源2</h1>
</body>
</html>
9.1.5.8.3.主页
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Home</title>
</head>
<body>
<h1>Welcome to Spring Security</h1>
${username}
</body>
</html>

==这里补充一下,《Spring 实战(第四版)》中提及了LDAP认证,个人感觉了解一下即可,所以下面放出了了解的链接,有兴趣可以参阅一下==

LDAP入门

9.3.拦截请求

很重要的一点就是将最为具体的请求路径放在前面,而最不具体的路径(如anyRequest())放在最后。如果不这样做的话,那不具体的路径配置将会覆盖掉更为具体的路径配置

protected void configure(HttpSecurity http) throws Exception {
      http
    .authorizeRequests()
    .antMatchers("/r/r1").hasAnyRole("USER")				//  /r/r1需要ROLE_USER权限才可以访问
    .antMatchers("/r/r2").hasAnyRole("ADMIN")
    .antMatchers("/r/**").authenticated()           //访问/r/**需要权限
    .anyRequest().permitAll()                      	//其他url无需权限
}

9.3.1.使用Spring表达式进行安全保护

Spring Security安全保护大多数方法都是一维的,也就是说我们可以使用hasRole()限制某个特定的角色,但是我们不能在相同的路径上同时通过hasIpAddress()限制特定IP地址,所以就可以通过使用access来扩展。

使用access(String)方法更加灵活,对于给定的SpEL表达式计算结果为true,就允许访问。

.antMatchers("/r/r1").access("hasRole('USER') and hasIpAddress('192.168.1.1')")

9.3.2.强制通道的安全性

选定的url强制使用https安全连接

.requiresChannel().antMatchers("/r/r2").requiresSecure()

选定的url使用http连接

.requiresChannel().antMatchers("/r/r2").requiresInsecure()

9.3.3.防止跨站请求伪造

跨站请求伪造(CSRF),简单来讲,如果一个站点欺骗用户提交请求到其他服务器的话,就会发生CSRF攻击,Spring Security默认开启CSRF防护,它会通过同步token的方式来实现CSRF的防护功能。它将会拦截状态变化的请求并检查CSRF token。如果请求中不包含CSRF token的话,或者token不能与服务端的token相匹配,请求将会失败,并抛出CsrfException异常

image-20210127183243388

Thymeleaf的<form>标签的action属性添加了Thymeleaf的命名空间前缀,那么就会自动生成一个_csrf隐藏域

JSP页面需要手动添加到表单中

<%--防止CSRF--%>
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}">

Spring表单绑定标签,<sf:form>会自动为我们添加隐藏的CSRF token标签

<%--sf:form标签自动为我们添加了隐藏的CSRF token标签--%>
<sf:form action="/login" method="post">...</sf:form>

9.4.认证用户

9.4.1.添加自定义的登录页

自定义登录页实现已在Spring Security中写明,这里不再赘述

9.4.2.启用HTTP Basic认证

protected void configure(HttpSecurity http) throws Exception {
    http.httpBasic()//开启httpBasic认证
            .and()
            .authorizeRequests()
            .anyRequest()
            .authenticated();//所有请求都需要登录认证才能访问
}

image-20210127193958587

应用场景:HttpBasic登录验证模式是Spring Security实现登录验证最简单的一种方式,也可以说是最简陋的一种方式。它的目的并不是保障登录验证的绝对安全,而是提供一种==“防君子不防小人”==的登录验证。

就好像是我小时候写日记,都买一个带小锁头的日记本,实际上这个小锁头有什么用呢?如果真正想看的人用一根钉子都能撬开。它的作用就是:某天你的父母想偷看你的日记,拿出来一看还带把锁,那就算了吧,怪麻烦的。

举一个我使用HttpBasic模式的进行登录验证的例子:我曾经在一个公司担任部门经理期间,开发了一套用于统计效率、分享知识、生成代码、导出报表的Http接口。纯粹是为了工作中提高效率,同时我又有一点点小私心,毕竟各部之间是有竞争的,所以我给这套接口加上了HttpBasic验证。公司里随便一个技术人员,最多只要给上一两个小时,就可以把这个验证破解了。说白了,这个工具的数据不那么重要,加一道锁的目的就是不让它成为公开数据。如果有心人破解了,真想看看这里面的数据,其实也无妨。这就是HttpBasic模式的典型应用场景。来源于SpringSecurity HttpBasic模式登录认证

9.4.3.启用Remember-me功能

9.4.3.1.代码实现
@Autowired
private UserDetailsService userDetailsService;//前面已经声明为@Bean

protected void configure(HttpSecurity http) throws Exception {
    http
      .rememberMe()
      .tokenValiditySeconds(24 * 3600)
      .key("heng")
      .userDetailsService(userDetailsService);//需要这个,否则会抛出用户名不存在的异常
}

登录之后会生成存储在cookie中的token,包含用户名,密码,过期时间和一个私钥——在写入cookie前都进行了MD5哈希

默认情况下,私钥名为SpringSecured,为了实现这一点,登录请求必须包含一个名为remember-me的参数,在登录表单中增加一个简单的复选框就可以完成这件事

<input id="remember_me" name="remember-me" type="checkbox"/>
<label for="remember_me" class="inline">Remember me</label>

登录之后浏览器中存储的参数如下图所示:

image-20210127202937368

9.4.3.2.如何测试remember-me的效果

没有启动remember-me前登录再退出浏览器,就无法访问资源;启动remember-me后登录再退出浏览器,依然可以访问资源,这就是remember-me启动的效果。

9.4.3.3.原理

在这里要注意一下,为什么会多了一个DB,这是因为token的比对肯定是得有两个token的,具体可以查看PersistentTokenRepository类或者下面这篇参考博客,里面有怎么把token持久化到DB中的代码(个人理解:如果没有显式的持久化,那么肯定也会存在内存中的)

Spring Security实现RememberMe功能以及原理探究

这里写图片描述

9.4.4.自定义退出页面

用户退出应用,所有的Remember-me token都会被清除掉

@GetMapping("/logout-view")
public String logout() {
  	return "logout";
}
//---------------------------------
protected void configure(HttpSecurity http) throws Exception {
    http
        .logout()//退出
        .logoutUrl("/logout")//指定退出处理的url
        .logoutSuccessUrl("/")//退出成功重定向到/
}

logout.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="sf" uri="http://www.springframework.org/tags/form" %>
<html>
<head>
    <title>Logout</title>
</head>
<body>
<sf:form action="/logout" method="post">
    <input type="submit" value="退出">
</sf:form>
</body>
</html>

9.5.保护视图

9.5.1.使用Spring Security的JSP标签库

9.5.1.1.前置知识

这里写图片描述

这里写图片描述

9.5.1.2.命名空间

需要导入如下的依赖才可以使用JSP的安全标签

<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-taglibs</artifactId>
    <version>5.1.4.RELEASE</version>
</dependency>
<%--Spring Security的JSP标签库的命名空间--%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
9.5.1.3.视图

只要通过认证并有相应的授权才可以查看到JSP安全标签标注的HTML标签

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--Spring Security的JSP标签库的命名空间--%>
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<html>
<head>
    <title>Home</title>
</head>
<body>
<h1>Welcome to Spring Security</h1>
<%--property表示用户认证对象的一个属性,var表示为属性指明变量,scope表示请求作用域--%>
Hello <security:authentication property="principal.username" var="loginId" scope="request"/>

<%--security:authorize的url属性对给定的url模式会间接引用其安全性约束,所以这里的/r/r1会启用安全配置中的授权校验--%>
<%--只有认证用户满足了权限要求才会出现如下的标签--%>
<security:authorize url="/r/r1">
    <spring:url value="/r/r1" var="user_url"/>
    <br>
    <a href="${user_url}">User</a>
</security:authorize>

<security:authorize url="/r/r2">
    <spring:url value="/r/r2" var="admin_url"/>
    <br>
    <a href="${admin_url}">Admin</a>
</security:authorize>
</body>
</html>

9.5.2.使用Thymeleaf的Spring Security方言

这里写图片描述

9.5.2.1.导入依赖
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
9.5.2.2.注册安全方言
//模版引擎
@Bean
public SpringTemplateEngine templateEngine(SpringResourceTemplateResolver templateResolver) {
    SpringTemplateEngine templateEngine = new SpringTemplateEngine();
    templateEngine.setTemplateResolver(templateResolver);
    templateEngine.addDialect(new SpringSecurityDialect());//注册安全方言
    return templateEngine;
}
9.5.2.3.视图命名空间
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
</html>

注意啊!!!Thymeleaf的安全方言是利用属性完成的,JSP则是通过标签完成的!!!

10.通过Spring和JDBC征服数据库

10.1.Spring的数据访问哲学

10.1.1.数据访问模版化

Spring的数据访问模版类负责通用的数据访问功能。对于应用程序特定的任务,则会调用自定义的回调对象。

image-20210201142443100

10.1.2.参考文献

一个经典例子让你彻彻底底理解java回调机制

10.2.配置数据源

个人理解:这里的数据源指代数据库交互操作的封装,包括池化的操作,简单来说,这里的数据源不是代表数据库,而是对数据库连接的管理

建议使用从连接池获取连接的数据源

数据库配置嘛,那势必导入之前Spring Security的3个依赖,==tx,jdbc,mysql==

10.2.1.使用JNDI数据源

JNDI:Java Naming and Directory Interface Java命名和目录接口

Tomcat这样的Web容器运行你配置通过JNDI获取数据源,而且他们通常以池的方式管理数据库连接,支持热切换

我这里使用的Tomcat是9.0.391的

10.2.1.1.修改context.xml文件

位置:/Tomcat/conf/context.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--
  Licensed to the Apache Software Foundation (ASF) under one or more
  contributor license agreements.  See the NOTICE file distributed with
  this work for additional information regarding copyright ownership.
  The ASF licenses this file to You under the Apache License, Version 2.0
  (the "License"); you may not use this file except in compliance with
  the License.  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->
<!-- The contents of this file will be loaded for each web application -->
<Context>

    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
  
   <!-- 数据库连接的配置 -->
    <Resource name="jdbc/SpitterDS"
    auth="Container"
    type="javax.sql.DataSource"
    factory="org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory"
    driverClassName="com.mysql.jdbc.Driver"
    username="root"
    password="XXXXXXX"
    maxIdle="40"
    maxWait="4000"
    maxActive="250"
    url="jdbc:mysql://localhost:3306/spring?characterEncoding=utf8&amp;generateSimpleParameterMetadata=true"/>
   
</Context>

10.2.1.2.java配置获取JNDI

lookup的参数=java:/comp/env/ + (context.xml Resource的name)

@Bean
@Profile("production")
public DataSource dataSource() throws NamingException {
    System.out.println("production");
    JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
    JndiTemplate jndiTemplate = jndiObjectFactoryBean.getJndiTemplate();
    DataSource dataSource = jndiTemplate.lookup("java:/comp/env/jdbc/SpitterDS", DataSource.class);
    return dataSource;
}

10.2.2.使用DBCP数据源

10.2.2.1.导入依赖
<!--使用DBCP(DataBase connection pool)-->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-dbcp2</artifactId>
    <version>2.7.0</version>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.9.0</version>
</dependency>
10.2.2.2.java配置DBCP
@Bean
    @Profile("qa")
    public DataSource data() {
        //使用JDBC驱动的数据源(最简单,无池化)
//        DriverManagerDataSource ds = new DriverManagerDataSource();
//        ds.setDriverClassName("com.mysql.jdbc.Driver");
//        ds.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8");
//        ds.setUsername("root");
//        ds.setPassword("XXXXXX");
//        return ds;

        System.out.println("qa");
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8");
        ds.setUsername("root");
        ds.setPassword("XXXXXX");
        ds.setInitialSize(5);
        ds.setMaxWaitMillis(7000);
        ds.setMaxTotal(1000);
        return ds;
    }

10.2.3.使用H2数据源

H2是嵌入式数据库

10.2.3.1.导入依赖
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <version>1.4.200</version>
    <scope>test</scope>
</dependency>
10.2.3.2.java配置H2
@Bean
@Profile("dev")
public DataSource embeddedDataSource() {
    return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.H2)
            .addScript("classpath:spring.sql")
            .build();
}
10.2.3.3.编写SQL文件

位置在resource文件夹下,文件名spring.sql

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL COMMENT '用户id',
  `username` varchar(64) NOT NULL,
  `password` varchar(64) NOT NULL,
  `fullname` varchar(255) NOT NULL COMMENT '用户姓名',
  `mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
  PRIMARY KEY (`id`)
);

-- ----------------------------
-- Records of t_user
-- ----------------------------
BEGIN;
INSERT INTO `t_user` VALUES (1, 'bbb', '$2a$10$rkUJD6s03sKOI93MoKieRO18DxnjlobIrTh9Vti4.hnFGC9X85KCu', 'ch', '1111111');
INSERT INTO `t_user` VALUES (2, 'ch', '$2a$10$rkUJD6s03sKOI93MoKieRO18DxnjlobIrTh9Vti4.hnFGC9X85KCu', 'cd', '2222222');
COMMIT;

H2不支持以下配置

ENGINE=InnoDB DEFAULT CHARSET=utf8;
USING BTREE

10.2.4.使用profile选择数据源

10.2.4.1.配置web环境

在web.xml中添加如下的配置,指定web环境为dev环境

<context-param>
    <param-name>spring.profiles.default</param-name>
    <param-value>dev</param-value>
</context-param>
10.2.4.2.完整代码
package com.XXXX.config;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.jndi.JndiObjectFactoryBean;
import org.springframework.jndi.JndiTemplate;
import org.springframework.stereotype.Component;

import javax.naming.NamingException;
import javax.sql.DataSource;
@Component
public class DataSourceConfig {
    @Bean
    @Profile("production")
    public DataSource dataSource() throws NamingException {
        System.out.println("production");
        JndiObjectFactoryBean jndiObjectFactoryBean = new JndiObjectFactoryBean();
        JndiTemplate jndiTemplate = jndiObjectFactoryBean.getJndiTemplate();
        DataSource dataSource = jndiTemplate.lookup("java:/comp/env/jdbc/SpitterDS", DataSource.class);
        return dataSource;
    }

    @Bean
    @Profile("qa")
    public DataSource data() {
        //使用JDBC驱动的数据源(最简单,无池化)
//        DriverManagerDataSource ds = new DriverManagerDataSource();
//        ds.setDriverClassName("com.mysql.jdbc.Driver");
//        ds.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8");
//        ds.setUsername("root");
//        ds.setPassword("XXXXXXX");
//        return ds;


        System.out.println("qa");
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/spring?useUnicode=true&characterEncoding=utf-8");
        ds.setUsername("root");
        ds.setPassword("XXXXXXX");
        ds.setInitialSize(5);
        ds.setMaxWaitMillis(7000);
        ds.setMaxTotal(1000);
        return ds;
    }

    @Bean
    @Profile("dev")
    public DataSource embeddedDataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("classpath:spring.sql")
                .build();
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

10.2.5.总结

个人理解:

JNDI和JDBC的区别:

  • 两者都可以配置数据源,不过JNDI是池化管理,JDBC驱动配置无池化
  • JDBC封装数据源,使用java操作数据库,简化数据库操作
  • 一般的流程:JNDI配置池化数据源——>JDBC封装数据源——>java代码操作数据库

10.2.6.参考文献

JNDI 和 JDBC 的区别-个人理解

Spring结合Tomcat和Jndi实现数据源外部化配置

10.3.在Spring中使用JDBC

NamedParameterJdbcTemplate接收命名参数

JdbcTemplate和参数数据的顺序紧耦合

@Bean
public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
    return new NamedParameterJdbcTemplate(dataSource);
}

@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    return new JdbcTemplate(dataSource);
}

NamedParameterJdbcTemplate的使用

@Override
public void addSpitter(Spitter spitter) {
    String sql = "insert into t_user(username,password,fullname,mobile) values (:username,:password,:fullname,:mobile);";
    Map<String, Object> map = new HashMap<>();
    map.put("username", spitter.getUsername());
    map.put("password", BCrypt.hashpw(spitter.getPassword(), BCrypt.gensalt()));
    map.put("fullname", spitter.getFullname());
    map.put("mobile", spitter.getMobile());
    namedParameterJdbcTemplate.update(sql, map);
}

JdbcTemplate的使用

@Override
public Spittle findOne(Long id) {
    String sql = "select * from t_spittle where id=?";
    Spittle spittle = jdbcOperations.queryForObject(sql, new SpittleRowMapper(), id);
    return spittle;
}

private static final class SpittleRowMapper implements RowMapper<Spittle> {
    @Override
    public Spittle mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Spittle(rs.getLong("id"),
                rs.getString("message"),
                rs.getDate("time"),
                rs.getDouble("latitude"),
                rs.getDouble("longitude"));
    }
}

11.使用对象-关系映射持久化数据

11.1.Spring集成Hibernate

这里会采用xml的形式集成Hibernate,问为什么不用java配置,问就是不会哈哈哈

下面的配置中展示的是单向一对多(去掉hbm.xml中的<many-to-one>即可),单向多对一(去掉hbm.xml中的<one-to-many>即可)和双向一对多(两边注释都放开就好了)

11.1.1.导入依赖

Hibernate需要的包,一般导一个hibernate-core里面就有这些了,对了还有数据库的一些包,mysql,tx,jdbc

image-20210202223512415

Spring整合Hibernate(一)

11.1.2.配置文件

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--配置spring的配置文件存放地-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

springmvc-config.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:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans.xsd
   http://www.springframework.org/schema/context
   http://www.springframework.org/schema/context/spring-context.xsd
   http://www.springframework.org/schema/aop
   http://www.springframework.org/schema/aop/spring-aop.xsd
   http://www.springframework.org/schema/tx
   http://www.springframework.org/schema/tx/spring-tx.xsd
    http://www.springframework.org/schema/mvc
    https://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <!--注解扫描-->
    <context:component-scan base-package="com.hznu"/>
    <!--SpringMVC不处理静态资源,如.css .html .js .mp3-->
    <mvc:default-servlet-handler/>
    <!--配置完成映射关系,包括了原来的映射器和适配器-->
    <mvc:annotation-driven/>

    <bean id="InternalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="username" value="root"/>
        <property name="password" value="XXXXXX"/>
        <property name="url" value="jdbc:mysql://localhost:3306/hibernate?useUnicode=true&amp;characterEncoding=utf-8"/>
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!-- hibernateProperties属性:配置与hibernate相关的内容,如显示sql语句,开启正向工程 -->
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQL57Dialect</prop>
                <!-- 显示当前执行的sql语句 -->
                <prop key="hibernate.show_sql">true</prop>
                <!-- 开启正向工程 -->
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
      	<!-- Hibernate类配置文件的位置 -->
        <property name="mappingResources">
            <list>
                <value>com/hznu/pojo/User.hbm.xml</value>
                <value>com/hznu/pojo/Email.hbm.xml</value>
            </list>
        </property>
        <!-- 扫描实体所在的包 -->
        <property name="packagesToScan">
            <list>
                <value>com.hznu.pojo</value>
            </list>
        </property>
    </bean>

    <!-- 配置HiberanteTemplate对象 -->
    <bean id="hibernateTemplate" class="org.springframework.orm.hibernate5.HibernateTemplate">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>
    <!-- 配置Hibernate的事务管理器 -->
    <bean id="transactionManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
        <property name="sessionFactory" ref="sessionFactory"/>
    </bean>

    <!-- 配置开启注解事务处理 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

11.1.3.实体类和类配置

11.1.3.1.User类

User.java

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
//不要重写toString方法,因为会递归调用自己对象属性的toString,从而导致栈溢出
public class User implements Serializable {
    private Integer userId;
    private String userName;
    private String userPassword;
    private Set<Email> emails = new HashSet<>();
}

User.hbm.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.hznu.pojo">
    <class name="User" table="user">
        <id name="userId" column="user_id">
            <generator class="native"/>
        </id>
        <property name="userName" column="user_name"/>
        <property name="userPassword" column="user_password"/>
        <!--
        单向一对多关系
        set:集合
        name:集合属性名
        key:每个set的分类依据
        column:外键名(重要,不然外键会乱加的。。。。)
        one-to-many:所属关系类型
        -->
        <set name="emails" table="email">
            <key column="user_id"/>
            <one-to-many class="Email"/>
        </set>
    </class>
</hibernate-mapping>
11.1.3.2.Email类

Email.java

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class Email implements Serializable {
    private Integer emailId;
    private String emailInfo;
    private User user;
}

Email.hbm.xml

<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-mapping PUBLIC
        "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
        "http://hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.hznu.pojo">
    <class name="Email" table="email">
        <id name="emailId" column="email_id">
            <generator class="native"/>
        </id>
        <property name="emailInfo" column="email_info"/>
        <!--
        单向多对一
        name对象属性名称
        class对应的类型
        not-null不可以为空
        column外键列名
        -->
        <many-to-one name="user" class="User" column="user_id"/>
    </class>
</hibernate-mapping>

11.1.4.测试

@Test
@Transactional
@Rollback(false)//这里只测试一对多,多对一就是把顺序反一下,先放User再放Email
public void testOneToMany() {
    User user = new User();
    user.setUserName("1");
    user.setUserPassword("1");

    Email email1 = new Email();
    email1.setEmailId(1);
    email1.setEmailInfo("hello world1");
    Email email2 = new Email();
    email2.setEmailId(2);
    email2.setEmailInfo("hello world2");
    Email email3 = new Email();
    email3.setEmailId(3);
    email3.setEmailInfo("hello world3");

    email1.setUser(user);
    email2.setUser(user);
    email3.setUser(user);
    user.getEmails().add(email1);
    user.getEmails().add(email2);
    user.getEmails().add(email3);

    hibernateTemplate.save(email1);
    hibernateTemplate.save(email2);
    hibernateTemplate.save(email3);

    hibernateTemplate.save(user);
}

@Test
@Transactional
@Rollback(false)
public void testGetMany() {
    User user = hibernateTemplate.get(User.class, 1);
    for (Email email : user.getEmails()) {
        System.out.println(email.getEmailInfo());
    }
}

image-20210204142121590

11.1.5.注意点

  • 不要重写toString方法
  • 如果target包中不存在*.hbm.xml文件,添加下面一段代码到pom.xml中
<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
        </resource>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>false</filtering>
        </resource>
    </resources>
</build>
  • 不管是一对多还是多对一,那个column一定要注意是外键啊啊啊啊

11.2.不依赖于Spring的Hibernate

11.2.1.hibernate.cfg.xml

这个文件要放在resource目录下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>

    <session-factory>

        <!-- Hibernate 连接数据库的基本信息 -->
        <property name="connection.username">root</property>
        <property name="connection.password">123456</property>
        <property name="connection.driver_class">com.mysql.jdbc.Driver</property>
        <property name="connection.url">jdbc:mysql://localhost:3306/hibernate?useUnicode=true&amp;characterEncoding=utf-8</property>

        <!-- Hibernate 的基本配置 -->
        <!-- Hibernate 使用的数据库方言 -->
        <property name="dialect">org.hibernate.dialect.MySQLInnoDBDialect</property>

        <!-- 运行时是否打印 SQL -->
        <property name="show_sql">true</property>

        <!-- 运行时是否格式化 SQL -->
        <property name="format_sql">true</property>

        <!-- 生成数据表的策略 -->
        <property name="hbm2ddl.auto">update</property>

        <!-- 设置 Hibernate 的事务隔离级别 -->
        <property name="connection.isolation">2</property>

        <!-- 删除对象后, 使其 OID 置为 null -->
        <property name="use_identifier_rollback">true</property>

        <!-- 配置 C3P0 数据源 -->
        <property name="hibernate.c3p0.max_size">10</property>
        <property name="hibernate.c3p0.min_size">5</property>
        <property name="c3p0.acquire_increment">2</property>

        <property name="c3p0.idle_test_period">2000</property>
        <property name="c3p0.timeout">2000</property>

        <property name="c3p0.max_statements">10</property>

        <!-- 设定 JDBC 的 Statement 读取数据的时候每次从数据库中取出的记录条数 -->
        <property name="hibernate.jdbc.fetch_size">100</property>

        <!-- 设定对数据库进行批量删除,批量更新和批量插入的时候的批次大小 -->
        <property name="jdbc.batch_size">30</property>

        <!-- 需要关联的 hibernate 映射文件 .hbm.xml -->
        <mapping resource="com/hznu/pojo/User.hbm.xml"/>
        <mapping resource="com/hznu/pojo/Email.hbm.xml"/>


    </session-factory>

</hibernate-configuration>

11.2.2.测试用例

@Test
public void test() {
    //初始化注册服务对象
    final StandardServiceRegistry registry = new StandardServiceRegistryBuilder()
            .configure() // 默认加载hibernate.cfg.xml文件
            .build();

    //从元信息获取Session工厂
    SessionFactory sessionFactory = new MetadataSources(registry).buildMetadata().buildSessionFactory();

    //从工厂创建Session连接
    Session session = sessionFactory.openSession();

    //开启事务
    Transaction tx = session.beginTransaction();

    //实例化对象
    User user = new User();
    user.setUserName("zhangsan");
    user.setUserPassword("123");
    session.save(user);

    //提交事务
    tx.commit();

    //关闭Session
    session.close();
}

11.3.Hibernate扩展

等用到再慢慢啃好了,现在没用到也只是看个大概,先结束了哈哈哈

  • 级联操作,注意这里的级联操作针对的是后面的对象,比如one-to-many,添加one以后会自动添加many
cascade="save-update"
  • 取消外键关联关系,true指定对方维护关系,默认为false
inverse="true"
  • 外键一对一
unique="true"

==从这里开始,我打算使用SpringBoot去啃书本接下去的内容了。。。。==

11.4.SpringBoot与Java持久化API(JPA)

11.4.1.JPA&Hibernate&Spring Data JPA&Mybatis的关系

Hibernate:一个希望不用写sql语句来操作数据库的懒到愿意为此开发一个框架的创始人

JPA是Hibernate的一个子集,同时Hibernate又是JPA的实现,Spring Data JPA是对JPA规范的再次封装

image-20210205203223576

11.4.2.使用JPA

img

根据上图来配置

11.4.2.1.dbcp2配置数据源

上面使用Spring配置过,这次使用SpringBoot

@Configuration
public class Dbcp2DataSource {
    @Bean("Dbcp2DataSource")
    @ConfigurationProperties(prefix = "customize.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(BasicDataSource.class).build();
    }
}
#dbcp数据源配置
customize.datasource.url=jdbc:mysql://localhost:3306/jpa
customize.datasource.username=root
customize.datasource.password=XXXXXXX
customize.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
11.4.2.2.实体管理器工厂配置

如果不是SpringBoot项目中该类是需要手动配置,这里主要是了解一下如何配置,其实这整个类也可以省略的,不妨碍我们后面使用Spring Data Jpa(所以说SpringBoot真方便哈哈哈)

JPA有两种管理工厂,一个是应用程序管理类型,一个是容器管理类型,这里选用后者

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        entityManagerFactoryRef = "entityManageFactoryPrimary",
        transactionManagerRef = "transactionManagerPrimary",
        basePackages = {"com.hznu.ch.springinaction.dao"}
)
public class EntityManagerConfig {

    @Autowired
    @Qualifier("Dbcp2DataSource")
    private DataSource dataSource;

    @Bean("entityManageFactoryPrimary")
    //SpringBoot自动装配了EntityManagerFactoryBuilder
    //实体管理工厂
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(EntityManagerFactoryBuilder builder) {
        LocalContainerEntityManagerFactoryBean entityManagerFactoryBean = builder
                .dataSource(dataSource)
                .packages("com.hznu.ch.springinaction.pojo")
                .build();
        //指定JPA的实现细节
        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
        jpaProperties.put("hibernate.physical_naming_strategy", "org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy");
        jpaProperties.put("hibernate.connection.charSet", "utf-8");
        jpaProperties.put("hibernate.show_sql", "false");
        entityManagerFactoryBean.setJpaProperties(jpaProperties);
        return entityManagerFactoryBean;
    }

    @Bean("entityManagerPrimary")
    //实体管理器
    public EntityManager entityManager(EntityManagerFactoryBuilder builder) {
        return entityManagerFactoryBean(builder).getObject().createEntityManager();
    }

    @Bean("transactionManagerPrimary")
    //事务管理器
    public PlatformTransactionManager transactionManager(EntityManagerFactoryBuilder builder) {
        return new JpaTransactionManager(entityManagerFactoryBean(builder).getObject());
    }

}
11.4.2.3.配置实体类&Repository
@Data
@Entity
@Table(name = "user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Integer userId;

    @Column(name = "user_name")
    private String userName;

    @Column(name = "user_password")
    private String userPassword;

    @Column(name = "user_full_name")
    private String userFullName;
}

//会被EnableJpaRepositories自动扫描到,具体的使用方法请看下节SpringBoot集成JPA
public interface UserRepository extends JpaRepository<User, Integer> {
  	.......
}
11.4.2.4.测试

这里可以使用@PersistenceUnit指定EntityManagerFactory,但会带来重复代码,所以我们这里采用EntityManager,并使用@PersistenceContext指定实体管理器的上下文,即实体管理器工厂。但EntityManager是线程不安全的,所以对于下面这段代码@PersistenceContext给出的是EntityManager的代理,我们还是以线程安全的方式使用实体管理器==(是不是很晕,不知道在讲啥,我也不知道,所以先留个坑吧)==

因为使用的是SpringBoot,它会自动装配,所以把@PersistenceContext和@Transactional里面的参数去掉也可以运行。。。

@PersistenceContext(unitName = "entityManageFactoryPrimary")//指定谁创建了EntityManager
private EntityManager entityManager;

@Test
@Transactional("transactionManagerPrimary")
@Rollback(false)
void test1() {
    User user = new User();
    user.setUserName("qh");
    user.setUserPassword("123");
    user.setUserFullName("qiuhan");
    entityManager.merge(user);//saveOrUpdate
  	entityManager.flush();//提交操作
}

SpringBoot 自动配置:Spring Data JPA

11.5.借助Spring Data实现自动化的JPA Repository

这里的学习主要是通过官方文档

Spring Data JPA官方文档

springboot集成jpa

11.5.1.SpringBoot集成JPA

11.5.1.1.导入依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
11.5.1.2.实体类和数据源配置

和上面的一样

11.5.1.3.测试
//@NoRepositoryBean //告诉JPA不要创建对应接口的bean对象,这里会自动注册Bean
public interface UserRepository extends JpaRepository<User, Integer> {
    User findByUserId(Integer userId);

    @Query(value = "select * from user where user_name=:userName", nativeQuery = true)
    List<User> findQiuhan(@Param("userName") String userName);//@Param("userName")放这里是说明还有这种方法,如果sql语句中和方法参数同名可以省略

    @Modifying
    @Transactional
    @Query(value = "update user set user_name=?2 where user_id=?1", nativeQuery = true)
    int setUserNameByUserId(Integer userId, String userName);


    //这里的sql有点蠢,完全可以简化,主要是为了检验sql子查询的可用性
    @Query(value = "select * from user where user_name=:userName and user_id in (select u.user_id from user u where u.user_age > :userAge)", nativeQuery = true)
    User findUserNameByUserAgeGreater(String userName, int userAge);

    //条件分页
    Page<User> findAllByUserAge(int userAge, Pageable pageable);

    //自定义排序
    List<User> findAllByUserName(String userName, Sort sort);

  	//limit
    List<User> findTop3ByUserName(String userName);

  	//Stream流
    Stream<User> readAllByUserName(String userName);

    //异步查询
    @Async
    Future<List<User>> findAllByUserName(String userName);
}
@Resource
private UserRepository userRepository;

@Test
@Rollback(false)
void test2() {
    User user = userRepository.findById(1).get();//JPA基础
    System.out.println(user);

    User user1 = userRepository.findByUserId(1);//JPA自带的sql体系
    System.out.println(user);

    //使用JPA自带的insert和update操作,存在就update,不存在就insert
    User user2 = userRepository.saveAndFlush(new User(8, "zsj", "321", "zhusijie", 22));
    System.out.println(user2);

    List<User> users = userRepository.findQiuhan("qh");
    users.forEach(System.out::println);

    int yyt = userRepository.setUserNameByUserId(1, "yyt");
    System.out.println(yyt);

    User ch = userRepository.findUserNameByUserAgeGreater("qh", 1000);
    System.out.println(ch);
}

@Test
void test3() {
    //分页
    Page<User> users = userRepository.findAll(PageRequest.of(2, 2));//page从0开始
    users.forEach(System.out::println);

    Page<User> users1 = userRepository.findAllByUserAge(20, PageRequest.of(0, 2));
    users1.forEach(System.out::println);
}

@Test
void test4() {
    //Sort排序使用的是类的属性名
    List<User> users = userRepository.findAllByUserName("qh", Sort.by("userAge").descending());
    users.forEach(System.out::println);

    //类型安全的排序API
    List<User> users1 = userRepository.findAllByUserName("qh", Sort.sort(User.class).by(User::getUserAge).descending());
    users1.forEach(System.out::println);
}

@Test
void test5() {
  	//limit
    List<User> users = userRepository.findTop3ByUserName("qh");
    users.forEach(System.out::println);
}


//使用Stream需要开启事务,不是在JPA方法上开启事务,而是在使用Stream的方法上开启事务
@Test
@Transactional
void test6() {
    System.out.println("-----------分割线-------------");
    Stream<User> qh = userRepository.readAllByUserName("qh");
    qh.forEach(System.out::println);
}

@Test
void test7() throws ExecutionException, InterruptedException {
    //异步
    Future<List<User>> qh = userRepository.findAllByUserName("qh");
    qh.get().forEach(System.out::println);
}

11.5.2.JPQL关键词

KeywordSampleJPQL snippet
DistinctfindDistinctByLastnameAndFirstnameselect distinct … where x.lastname = ?1 and x.firstname = ?2
AndfindByLastnameAndFirstname… where x.lastname = ?1 and x.firstname = ?2
OrfindByLastnameOrFirstname… where x.lastname = ?1 or x.firstname = ?2
Is, EqualsfindByFirstname,findByFirstnameIs,findByFirstnameEquals… where x.firstname = ?1
BetweenfindByStartDateBetween… where x.startDate between ?1 and ?2
LessThanfindByAgeLessThan… where x.age < ?1
LessThanEqualfindByAgeLessThanEqual… where x.age <= ?1
GreaterThanfindByAgeGreaterThan… where x.age > ?1
GreaterThanEqualfindByAgeGreaterThanEqual… where x.age >= ?1
AfterfindByStartDateAfter… where x.startDate > ?1
BeforefindByStartDateBefore… where x.startDate < ?1
IsNull, NullfindByAge(Is)Null… where x.age is null
IsNotNull, NotNullfindByAge(Is)NotNull… where x.age not null
LikefindByFirstnameLike… where x.firstname like ?1
NotLikefindByFirstnameNotLike… where x.firstname not like ?1
StartingWithfindByFirstnameStartingWith… where x.firstname like ?1 (parameter bound with appended %)
EndingWithfindByFirstnameEndingWith… where x.firstname like ?1 (parameter bound with prepended %)
ContainingfindByFirstnameContaining… where x.firstname like ?1 (parameter bound wrapped in %)
OrderByfindByAgeOrderByLastnameDesc… where x.age = ?1 order by x.lastname desc
NotfindByLastnameNot… where x.lastname <> ?1
InfindByAgeIn(Collection<Age> ages)… where x.age in ?1
NotInfindByAgeNotIn(Collection<Age> ages)… where x.age not in ?1
TruefindByActiveTrue()… where x.active = true
FalsefindByActiveFalse()… where x.active = false
IgnoreCasefindByFirstnameIgnoreCase… where UPPER(x.firstname) = UPPER(?1)

11.5.3.规律总结

  • 简单的业务,涉及的表较少:saveAndFlush和自定义的基础方法

  • 涉及多条件又想简化方法名;sql语句更为灵活的情况:@Query

  • 多表查询:hibernate注解实现(spring data jpa的内核就是hibernate)或entityManager,前者不支持分页,后者较为麻烦

  • 特殊需求:比如我看到一个使用saveAll和使用entityManager两个性能上会有差异,所以有特殊需求可以考虑使用entityManager这种原生JPA的方法进行持久化操作

对于现阶段的我来说,我还是喜欢用Mybatis哈哈哈

11.5.4.混合自定义查询

11.5.4.1.添加自定义接口
public interface JpaCustomRepository {
    void doSomething();
}

public interface UserRepository extends JpaRepository<User, Integer>, JpaCustomRepository{
  	......
}
11.5.4.2.实现接口
public class UserRepositoryImpl implements JpaCustomRepository {

    @PersistenceContext
    private EntityManager entityManager;

    @Override
    public void doSomething() {
        User user = new User();
        user.setUserName("cd");
        user.setUserPassword("123");
        user.setUserFullName("chendan");
        entityManager.merge(user);//saveOrUpdate
        entityManager.flush();//提交操作
    }
}
11.5.4.3.测试
@Test
@Transactional
@Rollback(false)
void test8() {
    userRepository.doSomething();
}
11.5.4.4.说明

UserRepositoryImpl并没有实现UserRepository接口,Spring Data JPA负责实现这个接口,但UserRepository可以使用UserRepositoryImpl的实现是因为名字的关联,默认后缀是Impl,但需要在UserRepository的扩展接口中添加UserRepositoryImpl的接口JpaCustomRepository,以保证UserRepository中存在相应的方法。

12.使用NoSql数据库

12.1.使用MongoDB持久化文档数据

12.2.使用Neo4j操作图数据

其实以上这两种NoSql被Spring集成的操作和==Spring集成JPA==类似,这里总结一下,但就跳过基本的配置,等用到再深入学习好了

  1. 导入依赖
  2. 配置对应NoSql启动类,扫描对应的Repository
  3. 扩展Spring对应NoSql的Repository,编写方法
  4. 注入Repository,使用编写好的方法
  5. 混合自定义查询
    1. 添加自定义接口,实现接口,以类名关联Repository
      1. JPA使用entityManager
      2. MongoDB使用MongoTemplate
      3. Neo4j使用Neo4jTemplate
    2. 原先的Repository的接口扩展自定义接口
    3. 完成混合查询

12.3.NoSql基本类型

参考文献:NoSQL数据库的四大类型

一般将NoSQL数据库分为四大类:键值(Key-Value)存储数据库:Redis、列存储数据库、文档型数据库:MongoDB和图形(Graph)数据库:Neo4j。它们的数据模型、优缺点、典型应用场景如下表所示。

分类数据模型优点缺点典型应用场景
键值(Key-Value)存储数据库Key指向Value的键值对,通常用hash表来实现查找速度快数据无结构化(通常只被当作字符串或者二进制数据)内容缓存,主要用于处理大量数据的高访问负载,也用于一些日志系统等
列存储数据库以列簇式存储,将同一列数据存在一起查找速度快,可扩展性强,更容易进行分布式扩展功能相对局限分布式的文件系统
文档型数据库Key-Value对应的键值对,Value为结构化数据数据结构要求不严格,表结构可变(不需要像关系型数据库一样需预先定义表结构)查询性能不高,而且缺乏统一的查询语法Web应用
图形(Graph)数据库图结构利用图结构相关算法(如最短路径寻址,N度关系查找等)很多时候需要对整个图做计算才能得出需要的信息,而且这种结构不太好做分布式的集群方案社交网络,推荐系统等

12.4.SpringBoot集成Redis

Spring Boot 2.X(六):Spring Boot 集成 Redis

# Spring  框架 

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×