# spring-teaching-parent **Repository Path**: coder_chenjun/spring-teaching-parent ## Basic Information - **Project Name**: spring-teaching-parent - **Description**: spring-teaching-parent 完整版,另一个版本是侧重与code元数据编写的精简版 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 1 - **Created**: 2021-11-08 - **Last Updated**: 2024-10-31 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # spring容器 spring容器的作用是用来实例化对象,配置与装配被spring容器管理的bean对象,容器如何装配bean对象是靠配置元数据的信息来确定的 ![image-20210915092120982](images/image-20210915092120982.png) ## 配置元数据 配置元数据主要有3类 - xml - 注解 - 基于java的配置类 xml配置元数据如下: ```xml ``` ## spring 容器创建 ```java ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); ``` 除了ApplicationContext代表的容器以外,还有WebApplicationContext以及BeanFactory、ConfigurableApplicationContext等容器类型 ## 关闭容器 ```java ((ConfigurableApplicationContext)context).close(); ``` ## 从容器获取已配置对象 ```java UserServiceImpl user = (UserServiceImpl) context.getBean("user"); //或者 UserServiceImpl user2 = context.getBean("user",UserServiceImpl.class); ``` 被管理bean的构造函数是私有的,spring也可以管理并创建出对象出来。 ## 直接配置bean ### id属性 id唯一标识一个bean对象 ### name属性 给bean配置一些别名 #### spring分隔符 如果有多个,就用分隔符分开,spring的分隔符一般有以下几个 - 空格 - 逗号 - 分号 ### class属性 表明所管理类的全称 ## 作用域(scope) 作用域是决定被spring管理的bean的对象什么时候创建出来,存活多久的问题,作用域有4个 - singleton(默认),不配置就是这个作用域 - prototype(原型) - request:web环境下使用,当一个请求过来就创建一个对象,请求结束就销毁对象 - session:web环境下使用,是指当新会话创建时创建对象,会话结束就销毁对象 singleton作用域是在容器启动时对象就会创建出来,而原型是调用getBean的方式才会不断创建出来 > 思考: > > 如果有下面的配置,可以吗?报错否? > > ```xml > > > ``` > > 答案:不报错,相当于有2个配置元素,spring解析之后形成2条记录(BeanDefinition) > > 如果不报错,理由是什么,然后下面的代码,创建了多少个A类的对象 > > ```java > ApplicationContext context = ... > A a1 =context.getBean("a1"); > A a2= context.getBean("a1") > A a3= context.getBean("a2") > ``` > > 答案:2个A的对象,其中变量a1与a2来自于同一个BeanDefinition,a3来自于另一个BeanDefinition ## 工厂形式配置bean 目前为止只需要像下面这样配置就可以让你的pojo类被spring管理 ```xml ``` 等价于让spring `new A()`给我们使用,但有些对象不是简单的直接new出来,比如下面的SqlSessionFactory对象的创建 ```java InputStream inputStream = Resources.getResourceAsStream(resouce); SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream); ``` spring为了处理这种类似情况,提供了3种方法 - 工厂类+静态方法 - 工厂类+实例方法 - FactoryBean实现类 ### 工厂类+静态方法 假定你有一个类A代码如下: ```java class A {} ``` 然后有一个工厂类,代码如下: ```java class Factory { A create(){ return new A(); } } ``` 在配置元数据中像下面这样配置 ```xml ``` 那么获取A的对象代码如下: ```java ApplicationContext context =.... A a = context.getBean("a",A.class) ``` > 注意:xml文件中的class不是A而是Factory类型 > > getBean的第二个参数是A.class ### 工厂类+实例方法 比如你有下面的工厂类 ```java class Factory{ public A create2(){ return new A(); } } ``` xml中的配置如下: ```xml ``` 那么就像下面这样获取A的对象 ```java ApplicationContext context =.... A a = context.getBean("a",A.class) ``` 注意:上面的配置方法,是可以直接获取工厂对象本身的,比如下面的代码: ```java ApplicationContext context =.... Factory f = context.getBean("f",Factory.class) ``` ### 实现FactoryBean接口 首先写一个类,实现FactoryBean接口,比如下面的代码: ```java public class MyFactoryBean implements FactoryBean { @Override public UserServiceImplForFactory getObject() throws Exception { return new UserServiceImplForFactory(); } //返回要创建对象的类型 @Override public Class getObjectType() { return UserServiceImplForFactory.class; } @Override public boolean isSingleton() { //返回true表示是单利的 return true; } } ``` 接着你只需要在xml中配置如下内容就可以获得A对象 ```xml ``` 然后用下面的代码获取A类的对象 ```java ApplicationContext context =.... A a = context.getBean("a",A.class) ``` > 这种配置跟最开始的 > 1. 这里的class是一个实现了FactoryBean接口的类型 > 2. 返回的是FactoryBean实现类创建出来的对象,而不是FactoryBean本身 ## Aware接口 aware接口一般是让被spring管理的bean获取一些容器的信息,这些接口类的名字通常是以Aware结尾,比如`ApplicationContextAware`,`BeanNameAware` ```java public class UserServiceImplAware implements ApplicationContextAware, BeanNameAware { private ApplicationContext context; private String name; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.context = applicationContext; System.out.println("让我知道容器对象,因为我实现了ApplicationContextAware接口--"); } @Override public void setBeanName(String name) { this.name = name; } public void m() { System.out.println(context.hashCode()); System.out.println(this.name); } } ``` ## 生命周期 一个bean除了实例化(new一个类)也有一些初始化逻辑(比如调用init方法),没有spring之前,通常会像下面这样编写代码来完成 ```java public class SomeClass{ private int id; private String name; public SomeClass(){ this.name = "a"; this.id=100; init(); } public void init(){ //复杂的初始化逻辑 //.... } } ``` 有了spring之后就可以采用其它的方法来达成同样的效果,但不需要让构造函数直接调用init这样的方法,主要方法如下: - 直接配置的方式 - 实现接口的方式 ### init-method与destroy-method 假定有一个类,代码如下: ```java public class UserServiceImplLifecycle { public UserServiceImplLifecycle() { System.out.println("构造函数----"); } public void init(){ System.out.println("init in lifecycle--"); } public void destroy(){ System.out.println("destroy in lifecycle---"); } } ``` 在xml中进行如下配置: ```xml ``` 由于是单例的,所以容器启动时会自动实例化此bean,并调用配置的init方法,容器关闭的时候会自动调用destroy方法。 如果是原型bean,那么每次getBean获取对象时都会调用构造函数与init方法,容器销毁时并不会调用destroy方法,因为这个对象spring容器并没有进行跟踪管理,这些对象就交给jvm的垃圾回收机制处理 如果要进行全局配置,只需要在beans属性里面进行设置即可,这样会让此beans下的所有bean都会进行初始化与销毁方法设置,配置如下: ```xml ``` ### InitializingBean与DisposableBean ```java public class UserServiceImplLifecycle2 implements InitializingBean, DisposableBean { public UserServiceImplLifecycle2() { System.out.println("构造函数----"); } @Override public void destroy() throws Exception { System.out.println("disposalbe bean destroy**"); } @Override public void afterPropertiesSet() throws Exception { System.out.println("after prop set***"); } } ``` 在xml中的配置如下: ```xml ``` 这种写法等价于配置了init-method与destroy-method。优点是bean元素配置简单,缺点是让自己的类与spring深度耦合了。 > 思考:如果一个类既实现了接口,又配置了init与destroy方法,他们执行顺序会是怎么样的? # Spring IOC IOC:invert of Control,叫做控制反转,Martin Fowler这个大师认为IOC不好理解,就把DI等价于IOC,DI:Dependency Injection(依赖注入) 假定有下面的代码,A类型就需要B类型,所以可以认为A依赖B类型 ```java public class A{ private B b ; public A(B b){ this.b = b; } } ``` 安装没有spring类似的框架之前,我们会像下面这样使用这些类型。 ```java B b = new B(); A a = new A(b); ``` 像上面这样由我们自己来手动的处理依赖关系,如果有了框架之后,就会交给框架来处理这个依赖关系,这种处理逻辑的变化就称之为控制的反转 依赖关系主要有两种情况 - 构造函数依赖 - 属性依赖 在spring中除了上面两种依赖,还有依赖的形式,称之为接口依赖(基本不用) ## 构造函数依赖 就是上面的A,B代码的例子。 ## 属性依赖 ```java public class A{ private B b ; public void setB(B b){ this.b = b; } } B b = new B(); A a = new A(); a.setB(b); ``` ## Spring的构造函数注入 ```java public class DbConfig { private String username; private String password; private int id; private String url; public DbConfig() { this("root","123",1); } public DbConfig(String username, int id) { this(username, "123", id); } public DbConfig(String username, String password) { this(username, password, 2); } public DbConfig(String username, String password, int id) { this.username = username; this.password = password; this.id = id; } public void setUrl(String url) { this.url = url; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override public String toString() { return "DbConfig{" + "username='" + username + '\'' + ", password='" + password + '\'' + ", id=" + id + ", url='" + url + '\'' + '}'; } } ``` 配置文件的配置 ```xml ``` 上面的配置默认会找有2个参数,并且都为字符串类型的构造函数,如果像下面这样指定了具体类型,就会找第二个参数为int类型的构造函数用来实例化对象 ```xml ``` xml中进行配置的时候不需要与构造函数的顺序完全一致,想做到这一点可以用name与index指定 ```xml ``` index指定顺序 ```xml ``` ## spring的属性注入 ```xml ``` ## 注入其它bean ```java public class UserServiceImpl implements UserService { private UserDao dao ; public void setDao(UserDao dao) { this.dao = dao; } @Override public void insert(){ System.out.println("insert in com.service"); dao.insert(); } } public class UserDaoImpl implements UserDao { @Override public void insert(){ System.out.println("insert in dao"); } } ``` xml配置 ```xml ``` > IOC的功能是基于容器功能,比如上面的例子,dao与service必须首先被spring 管理,之后才能注入 ## 父子bean ```java public class A { private String p1; private String p2; //省略getter setter } public class B{ private String p1; private String p2; private String p3; //省略getter setter } ``` xml ```xml ``` ## 集合类型数据注入 ```java public class UserInfo { private Set stringSet; private List stringList; private Map stringIntegerMap; private Properties properties; //省略getter setter } ``` ```xml l1 l2 l2 11 11 aaa p1 p2 ``` ## Bean生命周期 > https://www.cnblogs.com/zrtqsk/p/3735273.html ![img](images/181453414212066.png) 基本的生命周期流程 1. 创建对象的事例(new XX) 2. 完成属性依赖的装配 3. 处理aware接口 4. BPP的前置初始化 5. init-method() 1. InitailizingBean 2. init-method 6. BPP的后置初始化 # 案例:整合dbutils-druid ## 读取外部属性文件 ```xml ``` ## 整合Druid ### 属性值是字面量值 直接在xml中写死属性值 ```xml ``` > 注意destroy-method的配置,spring容器关闭时会自动调用连接池的close方法以便释放资源 ### 属性值来自于外部属性文件 属性值来自于读取的外部的properties文件内容,***建议用这种方式*** ```xml ``` ### 静态工厂方法形式 利用DruidDataSourceFactory类的方法createDataSource完成整合,此方法的其中一个重载是需要一个Properties类型的参数,当这种工厂方法需要参数时,类似类的初始化一样,需要用constructor-arg来进行注入配置 ```xml ${jdbc.url} ${jdbc.username} ${jdbc.password} ${jdbc.driver} ``` ## 整合DbUtils 重点是管理`QueryRunner`对象 ```xml ``` ## dao的配置 BaseDao的写法: ```java //此类可以不用是abstract,这里的abstract与xml中的abstract没有关联性 public abstract class BaseDao { private QueryRunner queryRunner; public QueryRunner getQueryRunner() { return queryRunner; } public void setQueryRunner(QueryRunner queryRunner) { this.queryRunner = queryRunner; } } ``` 某一个dao的写法: ```java public interface EmpDao { String getNameById(Integer id); } //实现类 public class EmpDaoImpl extends BaseDao implements EmpDao { @Override public String getNameById(Integer id) { QueryRunner qr = getQueryRunner(); String sql = "select emp_name as empName from employee where id = ?"; BeanHandler handler = new BeanHandler<>(Employee.class); Employee employee = null; try { employee = qr.query(sql, handler, id); } catch (SQLException e) { e.printStackTrace(); } return employee.getEmpName(); } } ``` 配置文件的写法: ```xml ``` ## 整合业务类 ```java public interface EmpService { String getNameById(Integer id); } //实现类 public class EmpServiceImpl implements EmpService { private EmpDao dao; //构造函数注入 public EmpServiceImpl(EmpDao dao) { this.dao = dao; } @Override public String getNameById(Integer id) { return dao.getNameById(id); } } //---------------另一个业务类 public interface DeptService { String getNameById(Integer id); } //实现类 public class DeptServiceImpl implements DeptService { private DeptDao dao; public DeptDao getDao() { return dao; } //属性注入 public void setDao(DeptDao dao) { this.dao = dao; } @Override public String getNameById(Integer id) { return getDao().getNameById(id); } } ``` xml配置 ```xml ``` ## 多配置文件 项目中如果xml配置项过多会导致以后维护麻烦,你可以分成多个xml配置文件,最后用`import`元素导入到一个最终的配置文件中即可,比如分成`spring-dao.xml`来写dao方面的配置,`spring-services.xml` 来写业务类相关的配置,`application-context.xml`来写web层面的相关配置 spring-dao配置: ```xml ``` spring-services.xml相关配置 ```xml ``` > 需要注意的是,这样配置在IDEA可能报红,原因是不知道dao的配置 最终的applicationContext.xml相关配置 其中resource属性指定的就是相当于当前文件(applicationContext.xml)的相对位置,import相当于把其它文件的配置内容复制粘贴到这里 ```xml ``` ## 创建spring容器 因为spring容器管理着许多的bean,不应该创建许多spring容器出来,在web环境下可以利用servletContextlistener创建一个 ### 手写监听器创建spring容器 ```java @WebListener public class InitSpringListener implements ServletContextListener { public static final String SPRING_CONTAINER_NAME = "myspring"; private ApplicationContext context; @Override public void contextInitialized(ServletContextEvent sce) { //这里读取spring元数据配置文件(从配置参数) String config = sce.getServletContext().getInitParameter("config"); context = new ClassPathXmlApplicationContext(config); ServletContext servletContext = sce.getServletContext(); servletContext.setAttribute(SPRING_CONTAINER_NAME, context); } @Override public void contextDestroyed(ServletContextEvent sce) { if(context!=null){ ((ConfigurableApplicationContext)context).close(); } } ``` 为了不在源码中写死spring配置元数据的名字,可以利用监听器参数来灵活配置,比如下面代码: ```xml config applicationContext.xml ``` ### ContextLoaderListner创建spring容器 spring框架默认已经编写了一个监听器,专门用来创建spring容器(强烈建议看看源代码) ```xml org.springframework.web.context.ContextLoaderListener ``` > 想使用这个监听器需要添加`spring-web`的依赖 此监听器默认是到web工程目录下的WEB-INF下面找名字为`applicationContext.xml`配置元数据,如果你想指定到别的地方去找配置元数据,就需要进行下面的配置 ```xml contextConfigLocation classpath*:applicationContext.xml ``` ### 获取spring容器 ```java public class SpringUtil { public static ApplicationContext getSpringContext(HttpServletRequest req) { return (ApplicationContext) req.getServletContext().getAttribute(InitSpringListener.SPRING_CONTAINER_NAME); } } ``` ### WebApplicationContextUtils获取spring容器 ```java WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(req.getServletContext()); ``` ## spring编码过滤器 spring的编码过滤器需要添加对`spring-web`的依赖 ```xml encoding org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 forceEncoding true encoding /* ``` # 注解-容器 在需要被spring管理的bean上面添加以下4个注解之一即可 - @Component - Repository - Service - Controller 其中后3个注解的元注解都是Component,所以,本质上只要类上面有@Component就表示可以被spring扫描管理到 ## AnnotationConfigApplicationContext容器类 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); EmpService service = context.getBean(EmpService.class) ``` ## @Component 建议放在所有不适合放@Repository,@Service,@Controller注解的类上面,比如下面的类: ```java @Component public class DbConfig { private String url; private String username; private String pwd; } ``` ## @Repository 此注解建议放在持久层类上面 ## @Service 建议放在业务类上 ## @Controller 建议放在控制器类上面,主要应用在spring mvc框架中的控制器类上 ## 作用域 默认是singleton,通过@Scope("prototype")改成原型 ## 生命周期 类比xml中的init-method是@PostConstruct注解,类比xml中的destroy-method是@PreDestroy ## 导入其它配置元数据 当注解的能力不够或者需要与传统的xml元数据进行整合的时候,就可以使用@ImportResource注解来导入其它的xml元数据,比如下面的代码 ```java @Component @ImportResource("app.xml") public class Config { } ``` xml的配置如下 ```xml ``` 测试使用代码如下: ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); A a = context.getBean(A.class); System.out.println(a); ``` # 注解-IOC 注解提供的注入功能,都是自动的,有以下3个注解 - Autowire:主要用类型查找 - @Resource:来自于java标准,先以名字查找,再以类型 - @Inject:来自于jsr330标准,很少用 ## 注解应用的地方 日常主要用在三个地方 - 字段 - setter方法 - 构造函数上 - 方法参数上 ## 多候选bean处理 如果出现多个候选bean可以注入,就会报非唯一的错,解决办法有2个 - @Primary:放在被注入的类上面,放在谁上就用谁 - Qualifier:放在要注入的“地方”,指定要注入的bean的名字 ## 读取外部资源 ### @PropertySource与@PropertySources ```java //放在任意一个被spring管理的bean上面即可,不需要与@Value注解放在一起 //只放置一次即可,表示文件只读取一次 @PropertySource("db.properties") ``` 要读取多个外部文件时,添加多个PropertySource的注解即可,比如下面这样: ```java @PropertySource("db.properties") @PropertySource("db2.properties") ``` > 注意:@PropertySource注解可重复是由于jdk8对注解的增加,有一个对应的容器注解@PropertySources 如果项目中有多个外部文件要导入,可能会把PropertySource注解写到不同的类上,这样并不是一个好的做法,建议专门写一个类,在上面集中编写导入外部文件的注解,一是可以避免导入文件的注解分散在各处,二是可以避免重复编写导入同样文件的注解代码,比如下面 ```java @Component @PropertySource("db.properties") @PropertySource("db2.properties") @PropertySource("db3.properties") public class Config { } ``` ### @Value 读取属性文件数据会被放到Environment中,如果要获取这些数据,可以用@Value注解,比如下面 ```java @Value("${jdbc.url:default}") private String url; ``` ### Environment ## 特定类型bean注入 对于一些特定的类型,spring框架可以自动注入,不需要你额外在实现一些类似Aware的接口,比如下面的代码 ```java public class EmpDaoImpl { @Autowired private Environment env; } ``` > You can also use @Autowired for interfaces that are well-known resolvable dependencies: > BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher, and > MessageSource. These interfaces and their extended interfaces, such as > ConfigurableApplicationContext or ResourcePatternResolver, are automatically resolved, with no > special setup necessary. ## 集合注入 ```java @Autowired //会注入spring容器中所有类型是EmpDao及其实现类的所有bean过来 private Set daos; @Autowired //注入的是spring容器中所有的EmpDao的bean,键是bean的id,值就是bean private Map mapDaos; ``` # AOP Aspect-oriented Programming (AOP),从传统的oop中的类对象思考转而到切面思考,切面与目标对象是独立,是靠spring aop模块自动整合(切面切入到目标)形成一个完整的功能。 ## AOP术语 - 切面(Aspect):切面是一个类,一般是会被@Aspect注解修饰,里面会写一些通知方法 - 通知(Advice):也叫增强,一般是写在切面类中的一些方法,将会横切(crosscutting)到连接点去的,它分为以下几种通知类型,通知会形成一个执行链 - 前置通知(Before) - 后置通知(After):不管是否正常执行,此通知一定会得到执行 - 环绕通知(Around):最强的一个通知,其它4个通知全部可以被环绕通知所取代 - 返回通知(AfterReturning):连接点方法正常执行完毕之后执行的通知 - 异常通常(AfterThrowing):连接点方法抛出了异常会执行,连接点不抛异常就不会执行这个通知,它不能捕获并处理异常,只是接收异常信息 - 连接点(JoinPoint):就是一个方法,目标对象的方法 - 目标对象(Target Object):将会被一个或多个切面进行通知的对象,也称之为被通知对象(Advised Object),其实就是安利中的UserServiceImpl - 切点(PointCut):我个人更倾向于翻译为切点表达式,就是用来描述连接点的 - 织入(weaving):它是一个动作,用来把切面的通知与目标对象的连接点整合在一起的这个过程就叫织入。spring aop框架支持以下3种织入时机 - 编译时织入:需要特定的编译器 - 类加载时织入:利用特殊的类加载器(Classloader) - 运行时织入:就是利用cglib与jdk动态代理来实现 - AOP代理:spring aop框架其实是靠动态代理来实现aop,代理技术分为2种,一个是jdk代理,针对的是目标对象有实现接口这种情况,另外一种情况是利用第三方库cglib来创建动态代理的,它主要是针对目标对象没有实现接口的情况,主要是通过继承的手段来实现动态代理,当然,它也支持目标对象实现了接口的情况 spring aop可以理解为:把切面类的通知方法依据切点表达式找到的目标对象的连接点方法通过代理技术进行织入形成的一个bean。 ## 基本步骤 ### 添加aspectj的依赖 ```xml org.aspectj aspectjweaver ``` ### 启用aspectj的支持 ```java @Component @EnableAspectJAutoProxy // 就是它 public class Config { } ``` 强烈建议编写专门的类用来启用aspectj的能力,避免此注解到处都有添加,类似前面PropertySource的做法 ### 正常编写目标类 ```java @Service //目标对象 public class UserServieImpl { //连接点 public void delete() { System.out.println("delte in user service ---"); } } ``` ### 编写切面类 ```java @Component @Aspect//表明这个类是一个切面类 public class LogAspect { //通知方法 //execution(* com.service.impl.UserServieImpl.delete()) 切点表达式,描述所有符合条件的连接点 @Before("execution(* com.service.impl.UserServieImpl.delete())") public void log(){ System.out.println("log 开始****"); } } ``` 这里有几个重点 - @Aspect表明此类是一个切面类 - @Before注解表明被修饰的方法是一个前置通知,此方法准备切入到连接点方法“之前”执行 - 连接点由切点表达式确定,你可以理解为切点表达式是符合条件的连接点的集合 - 切点表达式:就是`execution(* com.service.impl.UserServieImpl.delete())`这样的东西,这只是切点表达式的一种写法 1. 获取业务对象,直接已经完成了切入 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); UserServieImpl userServie = context.getBean(UserServieImpl.class); userServie.delete(); userServie.update(); ``` 运行的结果如下: ```shell log 开始**** delte in user service --- update in user service --- ``` 可以看到delete方法被前置注入,update没有被注入,因为切点表达式只确定了UserServiceImpl的delete方法这一个连接点。 ### 测试运行 ```java ApplicationContext context=new AnnotationConfigApplicationContext("com"); UserServieImpl userServie=context.getBean(UserServieImpl.class); userServie.delete(); userServie.update(); ``` ## AOP代理问题 假定你有如下的类型: ```java @Service public class IntfAImpl implements IntfA { @Override public void doSth() { System.out.println("dosth intf impl---"); } } //接口 public interface IntfA { void doSth(); } ``` 当你采用下面的切面类配置 ```java @Component @Aspect @EnableAspectJAutoProxy() public class MyLog { @Before("execution(* com.aopproxy.IntfAImpl.doSth())") public void before() { System.out.println("before---"); } } ``` 那么当你运行下面的代码进行测试时,后一种获取bean的方式会报错,这是因为IntfAImpl有实现接口,spring默认采用jdk接口代理形式生成AOP代理对象,而这个代理对象只是实现了接口并把任务转给IntfAImpl对象完成,此代理对象与IntfAImpl没有父子关系,所以想考IntfAImpl获取对象时是没有这个对象。 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com.aopproxy"); IntfA bean = context.getBean(IntfA.class); System.out.println(bean.getClass()); //这里会报错,表示没有这样的bean IntfAImpl bean2 = context.getBean(IntfAImpl.class); System.out.println(bean2.getClass()); System.out.println(bean == bean2); ``` 如果想让上面的代码不报错,就可以配置强制代理目标类的方式,也就是代理类是IntfAImpl的子类的形式,配置内容如下 ```java @EnableAspectJAutoProxy(proxyTargetClass = true) ``` > 需要注意的是,spring容器中的BeanDefinition的class信息是没有改动的,你可以编写下面的代码进行验证 > > ```java > ApplicationContext context = new AnnotationConfigApplicationContext("com.aopproxy"); > > IntfA bean = context.getBean(IntfA.class); > System.out.println(bean.getClass()); > printBeanDefinition(context); > > private static void printBeanDefinition(ApplicationContext context){ > ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) context; > > ConfigurableListableBeanFactory factory = (ConfigurableListableBeanFactory) configurableApplicationContext.getBeanFactory(); > > String[] names = context.getBeanDefinitionNames(); > > for (String name : names) { > System.out.println("name---:" + name); > BeanDefinition definition = factory.getBeanDefinition(name); > System.out.println("definition class: ---" +definition.getBeanClassName()); > } > } > ``` > > 输出的部分结果是: > > ```shell > name---:intfAImpl > definition class: ---com.aopproxy.IntfAImpl > name---:myLog > definition class: ---com.aopproxy.MyLog > name---:org.springframework.aop.config.internalAutoProxyCreator > definition class: ---org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator > ``` > > 所以getBean大致逻辑如下: > > ```java > T getBean(String beanname,Class requireType){ > Object obj = 获取对象 类型仍然是IntfAImpl > Object aopObj = aopProxy(obj)//经过aop代理 > if(aopObj is requireType){ > (T) aopObj > }else { > throw new Exception > } > > } > ``` > > ## 通知 假定有如下的连接点代码 ```java public class UserServieImpl { public int add(int a, int b) { System.out.println("add *** "); return a + b; } } ``` ### Before ```java @Before("execution(* com.service.impl.UserServieImpl.add(int,int))") public void before() { System.out.println("before****"); } ``` 执行下面的代码就可以看到效果 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); UserServieImpl userServie = context.getBean(UserServieImpl.class); int result = userServie.add(5, 6); System.out.println("结果:" + result); ``` ### After 通知写法: ```java @After("execution(* com.service.impl.UserServieImpl.add(int,int))") public void after() { System.out.println("after****"); } ``` ### AfterReturning 通知写法: ```java @AfterReturning(value = "execution(* com.service.impl.UserServieImpl.add(int,int))", returning = "xx") public void afterReturning(int xx) { // System.out.println("xx就是用来接收方法返回(正常执行)之后的结果:" + xx); } ``` 方法参数名与@AfterReturning注解中的returing属性指定的值一样就可以用来接收连接点方法执行的结果。 如果你不想接收方法的返回值,你可以不用添加方法参数,也就可以不需要给@AfterReturning注解的returning属性赋值。 ### AfterThrowing 通知方法: ```java @AfterThrowing(value = "execution(* com.service.impl.UserServieImpl.add(int,int))", throwing = "ex") public void afterThrowing(RuntimeException ex) { System.out.println("ex-----advice --" + ex.getMessage() + "-----"); } ``` ### Around 通知方法 ```java @Around("execution(* com.service.impl.UserServieImpl.add(int,int))") public Object around(ProceedingJoinPoint joinPoint) { //相当于Before通知 System.out.println("around before----"); Object result = null; try { //下面的代码是让连接点方法得到执行 result = joinPoint.proceed(); //相当于AfterReturning通知 System.out.println("result:" + result); } catch (Throwable e) { //类似异常通知,环绕通知是可以捕获并处理异常, // 但是异常通知只是接收到异常,并不能捕获处理 e.printStackTrace(); } //相当于after通知 System.out.println("around after----"); return result;//一定要记得返回值 } ``` > 记得要返回值 环绕通知的参数ProceedingJoinPoint类型可以有很多有用得信息,比如获取参数值,签名信息,目标对象,https://www.cnblogs.com/muxi0407/p/11818999.html这篇文章有如何获取当前执行方法的示例代码,比如下面: ```java Signature sig = pjp.getSignature(); MethodSignature msig = null; if (!(sig instanceof MethodSignature)) { throw new IllegalArgumentException("该注解只能用于方法"); } msig = (MethodSignature) sig; Object target = pjp.getTarget(); Method currentMethod = target.getClass().getMethod(msig.getName(), msig.getParameterTypes()); ``` ### 切面内各个通知执行顺序 指的是一个切面内各个通知执行顺序,比如有下面的切面类 ```java @Component @Aspect public class AdviceOrderAspect { @Before("execution(* com.service.impl.UserServieImpl.add(int,int))") public void before() { System.out.println("before---"); } @AfterReturning("execution(* com.service.impl.UserServieImpl.add(int,int))") public void afterReturning() { System.out.println("after returning---"); } @After("execution(* com.service.impl.UserServieImpl.add(int,int))") public void after() { System.out.println("after---"); } @Around("execution(* com.service.impl.UserServieImpl.add(int,int))") public Object around(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println("around before---"); result = joinPoint.proceed(); System.out.println("around after---"); } catch (Throwable e) { e.printStackTrace(); } return result; } @AfterThrowing(value = "execution(* com.service.impl.UserServieImpl.add(int,int))", throwing = "ex") public void afterThrowing(RuntimeException ex) { System.out.println("throwing---"); } } ``` 执行下面的代码进行测试 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); UserServieImpl userServie = context.getBean(UserServieImpl.class); int result = userServie.add(5, 6); System.out.println("结果:" + result); ``` 最终的结果如下: ```shell around before--- before--- add *** after returning--- after--- around after--- 结果:11 ``` ### 通知链 有如下的2个切面类 ```java @Aspect @Order(1000)//默认切面执行的顺序是不确定,通过Order注解解决,数字越低越先执行,优先级越高 public class FirstAspect { @Around("execution(* com.service.impl.UserServieImpl.add(int,int))") public Object around(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println("111 around before---"); result = joinPoint.proceed(); System.out.println("111 around after---"); } catch (Throwable e) { e.printStackTrace(); } return result; } } //第二个 @Aspect @Order(200) public class SecondAspect { @Around("execution(* com.service.impl.UserServieImpl.add(int,int))") public Object around(ProceedingJoinPoint joinPoint) { Object result = null; try { System.out.println("222 around before---"); result = joinPoint.proceed(); System.out.println("222 around after---"); } catch (Throwable e) { e.printStackTrace(); } return result; } } ``` 最终执行结果如下: ```shell 222 around before--- 111 around before--- add *** 111 around after--- 222 around after--- 结果:11 ``` ## 切点表达式 > 在官网的5.4.3这一节有描述 ### 指示器种类(PCD) PointCut Designer(切点指示器)种类有如下几种 > spring aop中只对方法进行织入,也就是说所有的指示器最终是一定要找到***方法***的。 > > Intf===Interface(接口) - execution:匹配连接点方法,等价的意思就是切点中表达式要写到方法。com.A.doSth(匹配到doSth这一个方法) - within:在什么之内,限制匹配到特定内的所有方法com.A(匹配A类的所有方法) - this:表示当前代理对象是你指定的类型的实例this(Intf) - target:目标对象是指定类型的实例的所有方法 target(Intf) - args:指定方法的参数类型是指定实例的所有方法,args(int,int) - @target:目标对象上有指定的注解@target(MyAnnotation.class) - @args:方法的参数有指定的注解,@args(MyAnnotation.class) - @within:所在类上有指定的注解@within(MyAnnotation.class)的所有方法匹配 - @annotation:连接点方法上有指定的注解@annotation(MyAnnotation.class) - bean:指示是这个bean名字对象的所有方法 bean(“myService”) > • execution: For matching method execution join points. This is the primary pointcut designator > to use when working with Spring AOP. > • within: Limits matching to join points within certain types (the execution of a method declared > within a matching type when using Spring AOP). > • this: Limits matching to join points (the execution of methods when using Spring AOP) where > the bean reference (Spring AOP proxy) is an instance of the given type. > • target: Limits matching to join points (the execution of methods when using Spring AOP) where > the target object (application object being proxied) is an instance of the given type. > • args: Limits matching to join points (the execution of methods when using Spring AOP) where > the arguments are instances of the given types. > • @target: Limits matching to join points (the execution of methods when using Spring AOP) > 323 > where the class of the executing object has an annotation of the given type. > • @args: Limits matching to join points (the execution of methods when using Spring AOP) where > the runtime type of the actual arguments passed have annotations of the given types. > • @within: Limits matching to join points within types that have the given annotation (the > execution of methods declared in types with the given annotation when using Spring AOP). > • @annotation: Limits matching to join points where the subject of the join point (the method > being run in Spring AOP) has the given annotation > > - bean(spring 提供的) ### 指示器逻辑操作符 就是有3个逻辑操作符,分别是:`&&`,`||`,`!`,比如下面的写法 ```shell execution("com.A.*") && @annotaion(MyAnno.class) ``` 找到A类中有MyAnno注解修饰的方法 ### execution指示器结构 ```shell execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?namepattern(param-pattern) throws-pattern?) ``` 上面代码中的?表示是可有可无的,所以你就会发现有以下3个是必须写的 - ret-type-pattern - namepattern - param-pattern execution指示器的组成部分 - modifiers-pattern:方法修饰符 - ret-type-pattern:返回类型 - declaring-type-pattern:方法所在的类型 - namepattern:方法名字 - param-pattern:方法参数 - throws-pattern:方法抛出异常 还有需要注意的地方,是有以下几个特殊符号 - *:表示这段的任意字符 - ..:表示任意一段,`com.impl..A`:表示com.impl包以及任意子包下面的A类,比如com.impl.a满足,com.impl.a.b.c也满足 比如下面的例子 - 找出所有的public方法(任意类的任意方法,方法参数是任意个数,任意类型) ```java execution(public * *(..)) ``` - 找出set开头的任意参数,任意返回值的所有方法 ```java execution(* set*(..)) ``` - 找出AccountService下的所有方法,如果AccountService是个接口的话,意思就是找出此接口的所有实现类的所有方法 ```java execution(* com.xyz.service.AccountService.*(..)) ``` - 找出service包下面所有类的所有方法 ```java execution(* com.xyz.service.*.*(..)) ``` - 找出service包以及任意子包下的所有类的所有方法 ```java execution(* com.xyz.service..*.*(..)) ``` ### 切点表达式的编写 切点表达式有以下几种写法 - 在5个通知注解里写:只针对这一个通知方法有效 - 在切面类的专门方法里写:本切面类的所有通知方法有效,如果切点表达式方法是在外部可以访问的,那么所有的切面类的所有通知方法也可以使用此切点表达式方法 - 专门编写一个切面类,里面所有的方法都是用来写切点表达式的 ***专门的方法写法:*** ```java @Pointcut("execution(public * com.*.*(..))") private void xxx(){ //建议把切点表达式方法设置为私有的,以便只让本切面的所有通知方法使用 //如果想让所有切面类的通知方法可以使用,建议编写专门的切点表达式类,也就是第三种写法 } //引用切点表达式 @Before("xxx()") public void before() { System.out.println("before-----"); } ``` ***专门的切面类写表达式*** 这个类的所有方法都用来写切点表达式,每个方法没有实际的业务功能,也没有通知方法在里面。相当于一个切点表达式的容器 > 强烈推荐用这种方式来编写切点表达式,这样可以集中维护这些表达式 ```java @Component @Aspect public class CommonPointcuts { @Pointcut("execution(public * com.*.*(..))") public void xxx(){ } } ``` 像下面这样引用这些表达式 ```java @Before("CommonPointcuts.xxx()") public void before() { System.out.println("before-----"); } ``` ### execution 省略。。 ### within within指示器就是表明某个类的所有方法,比如下面的切点表达式 ```java //在com包及其子包下的所有类(*表示类)的所有方法 @Pointcut("within(com..*)") public void withinDemo() { } ``` 在通知上引用方法如下: ```java @Before("CommonPointcuts.withinDemo()") public void before() { System.out.println("before-----"); } ``` ### @annotation 一般这种情况常见的应用场景就是某个方法修饰了某个自定义注解之后,需要对这样的方法进行织入操作,实现步骤如下 1. 创建自定义注解 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MyAnno { //可以在此注解里编写属性用来记录一些数据 } ``` 自定义注解的驻留策略在spring 5.3.9这个版本中,可以是CLASS与RUNTIME 1. 在业务类上的某些方法上添加注解 ```java @Service public class UserServiceImpl { public void doSth() { System.out.println("doSth-----"); } //表示只有这个方法会被注入,doSth方法不会被注入,因为没有注解 @MyAnno public void doSth2() { System.out.println("doSth 222-----"); } } ``` 3. 编写切点表达式 ```java @Component @Aspect public class CommonPointcuts { @Pointcut("@annotation(MyAnno)") public void annotationDemo() { } } ``` 4. 编写通知 ```java @Before("CommonPointcuts.annotationDemo()") public void before() { System.out.println("before-----"); } ``` 5. 测试 ```java ApplicationContext context = new AnnotationConfigApplicationContext("com"); UserServiceImpl userService = (UserServiceImpl) context.getBean("userServiceImpl"); userService.doSth(); System.out.println("======================="); userService.doSth2(); ``` 执行结果如下: ```shell doSth----- ======================= before----- doSth 222----- ``` ### 常见表达式含义 官网5.4.3的examples一节 # 基于java的配置-容器 这种方式从spring 3.0就有了,是通过写java代码的形式来实现bean的注册,依赖注入等,一般会与注解结合使用。 > ***需要注意的是,3种spring的配置元数据写法是可以任意组合使用使用的。*** ## 基本实现 - 写一个配置类 ```java @Configuration public class HelloConfig { //bean方法类似于以前学过的factory-method方法 @Bean("yyy") public UserServiceImpl xxx(){ System.out.println("bean方法 注册UserServiceImpl中----"); //默认的bean的名字是方法的名字 UserServiceImpl service = new UserServiceImpl(); return service; } } ``` bean方法注册的bean,其名字默认是方法的名字,比如上面的xxx,所以getBean的时候用xxx就可以获取到bean,但也可以通过@Bean的名字属性指定别的名字,比如上面的yyy,这样就需要getBean("yyy") 而不是getBean("xxx")来获取bean了 > @Configuration注解有@Component元注解 - 创建支持java代码配置的容器对象 ```java AnnotationConfigApplicationContext context =new AnnotationConfigApplicationContext(HelloConfig.class); ``` - 可以从容器中获取管理的bean ```java UserServiceImpl userService = context.getBean("yyy", UserServiceImpl.class); System.out.println(userService); ``` 记得,配置类本身也被spring管理起来了。比如下面的代码 ```java HelloConfig config = context.getBean(HelloConfig.class); System.out.println(config); ``` 执行结果如下,说明这个配置类是被spring代理过的 ```shell config.HelloConfig$$EnhancerBySpringCGLIB$$4a374712@58134517 ``` ## 生命周期 Bean注解的initMethod属性与destroyMethod属性是设置初始化与销毁方法的,类似于以前xml中的init-method与destroy-method设置 ```java @Configuration public class HelloIocConfig { @Bean(initMethod="a",destroyMethod="b") public UserDao userDao() { return new UserDaoImpl(); } } //上面的initMethod与destroyMethod设置分别对应此类的两个方法 public class UserDaoImpl implments UserDao { public void a(){} public void b(){} } ``` ## 配置类的管理 主要讲述配置类放在什么包下面以及配置类的写法 ### 配置类包管理 配置类尽量与业务类分开,这是一个实践建议,不是技术限制的建议,比如下面这样,com包是业务类的顶级包,config是配置类所在的包 - com - 子包 - ... - 业务类 - config - 配置类 ### 配置类的写法 配置类一般有2种编写方法,一种是普通的写法,就是在“基本实现”一节种的写法 ```java @Configuration public class HelloConfig { @Bean public UserServiceImpl xxx(){ ... } } ``` 另外一种写法是在配置类里编写静态的内部类的形式,这种形式可以起到在一个文件种集中管理配置类的效果,比如下面 ```java @Configuration public class SomeBusinessConfig { // 第一个静态内部配置类 @Configuration static class DaoConfig{ @Bean() public UserDao userDao(){} } // 第二个静态内部配置类 @Configuration static class ServiceConfig{ @Bean() public UserService userService(){} } } ``` 使用时仍然通过静态内部类的父类来实例化spring容器 ```java AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SomeBusinessConfig.class); ``` ### Bean方法 Bean方法的返回类型可以是自己,也可以是自己的父类型或接口,某些情况下,返回自己是比较合适的,因为可以尽量消除类型模糊性 bean方法一般是实例方法,静态方法主要是用来注册BFPP(BeanFactoryPostProcessor),比如 ```java @Bean public static BeanFactoryPostProcessor reg(){ ... } ``` ### full模式与lite模式 full模式就是功能完整的bean方法配置模式,lite(精简)的模式意思就是功能不太完整。 full模式就是Bean方法写在Configuration注解修饰的类里,lite模式就是Bean方法修饰在普通的被spring管理的类里面,比如类修饰了Component注解 ***full模式:*** ```java @Configuration public class SomeConfig { @Bean public UserService userService() {...} } ``` ***lite模式:*** ```java @Component public class SomeClass{ @Bean public UserService userService(){...} } ``` full模式默认情况下配置类是被代理过的,所以调用Bean方法时,此方法会被代理过,因而产生了类似getBean调用的效果,full模式下Bean方法被代理后的伪代码如下: ```java //SomeConfig是被Configuration修饰的配置类 public class SomeConfigCGlib$$Enhancer extends SomeConfig{ @Autowired private ApplicationContext context; //被代理过的bean方法 @Bean public UserDao userDao(){ UserDao bean = context.getBean(UserDao.class); if(bean==null){ return super.userDao(); }else{ return bean; } } } ``` 由于这种情况,所以full模式的bean间调用是支持完整的生命周期的。而lite模式下调用bean方法只是一个普通的方法调用,并不支持完整的bean的生命周期,比如下面的代码: ```java @Configuration() public class FullAndLiteMode { @Bean(initMethod = "init") public UserDaoImpl userDao() { System.out.println("bean方法:注册UserDao"); return new UserDaoImpl(); } @Bean @Scope("prototype") public UserService userService() { System.out.println("bean方法:注册UserService"); UserServiceImpl userService = new UserServiceImpl(); UserDao dao = userDao(); userService.setDao(dao); return userService; } } //测试类 public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FullAndLiteMode.class); FullAndLiteMode config = context.getBean(FullAndLiteMode.class); System.out.println(config); System.out.println("1==========================="); UserService service = context.getBean(UserService.class); service.insert(); System.out.println("2==========================="); UserService service2 = context.getBean(UserService.class); service2.insert(); } } ``` 执行之后输出的日志,充分说明了调用userDao方法时等价于getBean从spring容器中获取对象 ```shell bean方法:注册UserDao userdao 构造函数---- userdao的init方法--- config.fullandlitemode.FullAndLiteMode$$EnhancerBySpringCGLIB$$ec03a740@448ff1a8 1=========================== bean方法:注册UserService userservice 构造函数---- insert in userservice impl--- insert into db--- 2=========================== bean方法:注册UserService userservice 构造函数---- insert in userservice impl--- insert into db--- ``` 基于full模式的特点,***配置类中的bean方法要求不能是final以及private修饰的***,因为这样的话代理子类没办法重写,也就没办法对bean方法进行代理 要想配置类不代理bean方法,可以配置Configuration注解的proxyBeanMethods属性值为false,比如下面 ```java @Configuration(proxyBeanMethods = false) public class FullAndLiteMode { //与上面一样,省略 ... } ``` 执行输出的日志如下,很明显userDao调用仅仅只是个方法调用,没有init方法的执行,也就是没有完整的getBean的生命周期处理 ```shell bean方法:注册UserDao userdao 构造函数---- userdao的init方法--- config.fullandlitemode.FullAndLiteMode@28ac3dc3 1=========================== bean方法:注册UserService userservice 构造函数---- bean方法:注册UserDao userdao 构造函数---- insert in userservice impl--- insert into db--- 2=========================== bean方法:注册UserService userservice 构造函数---- bean方法:注册UserDao userdao 构造函数---- insert in userservice impl--- insert into db--- ``` ## IOC 完成ioc能力有4种写法 ***第一种***:直接在业务类里用注解Autowired或Resource实现,这种方法不是用代码形式完成 ```java //@Autowired private UserDao dao; ``` ***第二种***:利用bean间方法调用,就是同一个配置类中的bean方法是可以互相调用的 ```java @Configuration public class HelloIocConfig { @Bean public UserDao userDao(){ return new UserDaoImpl(); } @Bean public UserService userService(){ UserServiceImpl userService = new UserServiceImpl(); //new UserDaoImpl(); //bean间调用来完成依赖注入 userService.setDao(userDao()); return userService; } } ``` ***第三种***:利用自动注入的能力以及配置类也是被spring管理的知识来实现依赖注入 ```java @Configuration public class HelloIocConfig3 { //3. 因为HelloIocConfig3是被spring管理的,所以也可以进行依赖的注入 @Autowired private UserDao dao; @Bean public UserService userService(){ UserServiceImpl userService = new UserServiceImpl(); //这里用上spring注入的UserDao对象 userService.setDao(dao); return userService; } @Bean public UserDao userDao(){ return new UserDaoImpl(); } } ``` ***第四种***:由于项目中经常会出现很多配置类,bean间调用的方案就不现实,再加上配置类是被spring管理的,所以可以把某一些配置类自动注入到其它配置类,注入完成后就可以再调用其bean方法来完成依赖注入 ```java //第一个配置类 @Configuration public class HelloIocConfig4 { @Bean public UserDao userDao(){ return new UserDaoImpl(); } } //第二个配置类 @Configuration public class HelloIocConfig42 { //这里注入的是其它的配置类 @Autowired private HelloIocConfig4 config4; @Bean public UserService userService(){ UserServiceImpl userService = new UserServiceImpl(); //调用注入的配置类的bean方法来完成依赖注入 userService.setDao(config4.userDao()); return userService; } } ``` 别忘了,当有多个配置类时,实例化spring容器对象的方法如下 ```java AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( HelloIocConfig4.class, HelloIocConfig42.class); ``` ## 导入其它配置元数据 有两种情况,一种是导入其它的配置类,一种是导入其它的xml元数据。 ### Import 导入其它配置类 ```java @Import(BConfig.class) @Import({CConfig.class,DConfig.class}) @Configuration public class AConfig{ } ``` 实例化容器时,只需要这样 ```java AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( AConfig.class) ``` 而不需要像下面这样 ```java AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( AConfig.class, BConfig.class, CConfig.class, DConfig.class) ``` ### ImportResource 此注解用来导入外部的xml配置元数据 ```java @ImportResource("classpath:b.xml") @ImportResource({"classpath:b.xml","classpath:c.xml"}) @Configuration public class AConfig{ } ``` ## 注解支持 一般不会纯粹用Bean方法来项目中类的注册,都是注解加bean方法结合的方式,第三方类由于不能在上面加注解,所以采用bean方法的方式来注册,自己编写的类采用注解的方式 ### 容器(ComponentScan) 一般是在配置类上添加@ComponentScan注解来指定要扫描的包,可以指定一个顶级包,也可以指定多个要扫描的顶级包,比如下面的代码 ```java @Configuration @ComponentScan({"dao", "service", "aspect"}) public class AppConfig { @Autowired private DbConfig dbConfig; //bean方法注册第三方类 @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(dbConfig.getUrl()); dataSource.setUsername(dbConfig.getUsername()); dataSource.setPassword(dbConfig.getPassword()); dataSource .setDriverClassName(dbConfig.getDriverClassName()); return dataSource; } @Bean public QueryRunner queryRunner(DataSource dataSource) { QueryRunner queryRunner = new QueryRunner(dataSource, true); return queryRunner; } } ``` 然后在自己编写的类上添加4个注解之中的任意一个即可 ```java //注册自己编写的类,与@ComponentScan注解结合使用 @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptDao dao; } ``` ### IOC 如果类需要自动装配功能,仍然可以用@Autowired,@Resource等注解,如果出现候选类模糊的情况,仍然可以使用@Primary或@Qualifier注解解决 ### AOP相关注解 一般做法是: - 在配置类上添加@EnableAspectJAutoProxy注解 - 编写专门的切点表达式类 - 编写切面类以及通知 ***配置类*** ```java @Configuration @EnableAspectJAutoProxy public class AppConfig {} ``` ***切点表达式类*** ```java public class Pointcuts { @Pointcut("execution(public * service..*.*(..))") public void allServices() {} } ``` ***切面类及通知*** ```java @Component @Aspect public class PerformanceAspect { @Around("Pointcuts.allServices()") public Object performance(ProceedingJoinPoint joinPoint) { Object result = null; try { Long start = System.currentTimeMillis(); result = joinPoint.proceed(); Long end = System.currentTimeMillis(); long elapsed = end - start; System.out.println("业务方法执行消耗时长:" + elapsed); } catch (Throwable e) { e.printStackTrace(); } return result; } } ``` ### PropertySource 当要读取外部属性文件时,可以在配置类上添加@PropertySource注解实现,之后再利用@Value注解注入到需要的地方 配置类 ```java @Configuration @PropertySource("classpath:db.properties") public class AppConfig {} ``` 读取外部属性文件的值 ```java @Component @Data public class DbConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.driver}") private String driverClassName; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; } ``` 有一点需要特别注意,如果你的代码是像下面编写,那么注册DataSource是会失败的,因为在进行DruidDataSource注册时还没有把从外部属性读取到的值注入到本配置类的字段中去,有一个先后顺序的问题,所以建议单独写一个类来读取外部属性文件中的值,然后再把这个已经完全装配完毕的类注入到配置类中去就可以,也就是案例中的DbConfig类的写法 ```java @Configuration @PropertySource("classpath:db.properties") public class AppConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.driver}") private String driverClassName; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource dataSource = new DruidDataSource(); //此时这4个字段还没有注入值,都是null, //所以DruidDataSource对象的创建就会无效 dataSource.setUrl(this.url); dataSource.setUsername(this.username); dataSource.setPassword(this.password); dataSource.setDriverClassName(this.driverClassName); return dataSource; } } ``` ### 小结 所以一个配置如果提供了扫描自己编写类的能力,也提供了aop的能力以及读取外部配置文件类的能力,那么它通常会像下面这样 ```java @Configuration @ComponentScan({"dao", "service", "aspect"}) @EnableAspectJAutoProxy @PropertySource("classpath:db.properties") public class AppConfig { @Autowired private DbConfig dbConfig; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(dbConfig.getUrl()); dataSource.setUsername(dbConfig.getUsername()); dataSource.setPassword(dbConfig.getPassword()); dataSource .setDriverClassName(dbConfig.getDriverClassName()); return dataSource; } @Bean public QueryRunner queryRunner(DataSource dataSource) { QueryRunner queryRunner = new QueryRunner(dataSource, true); return queryRunner; } } ``` # Spring-jdbc spring jdbc模块是spring为了简化jdbc操作弄出的模块,操作数据库时主要是靠JdbcTemplate完成的 ## 依赖 ```xml org.springframework spring-jdbc ``` ## 注册JdbcTemplate JdbcTemplate主要依靠DataSource来工作的,所以注册JdbcTemplate时需要先注册DataSource ```java @Configuration @PropertySource("classpath:db.properties") @ComponentScan("com") public class AppConfig { @Autowired private DbConfig dbConfig; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(dbConfig.getUrl()); dataSource.setUsername(dbConfig.getUsername()); dataSource.setPassword(dbConfig.getPassword()); dataSource.setDriverClassName(dbConfig.getDriverClassName()); return dataSource; } @Bean public JdbcTemplate jdbcTemplate() { JdbcTemplate template = new JdbcTemplate(dataSource()); return template; } } ``` ## 使用JdbcTemplate ```java @Repository public class DeptDaoImpl implements DeptDao { //注入JdbcTemplate @Autowired private JdbcTemplate jdbcTemplate; @Override public void insert() { String sql = "insert into dept(deptname) values('jdbctem') "; jdbcTemplate.update(sql); } } ``` # Spring事务 ## 实操 ### 依赖 ```xml org.springframework spring-jdbc org.springframework spring-tx ``` ### 启用事务管理 在配置类上添加`@EnableTransactionManagement`注解即可,如果是在xml中是别的方式 ### 注册事务管理器 ```java @Configuration @ComponentScan("com") @PropertySource("classpath:db.properties") @EnableTransactionManagement // 事务(transaction):第一步 public class AppConfig { @Autowired private DbConfig dbConfig; @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(dbConfig.getUrl()); dataSource.setUsername(dbConfig.getUsername()); dataSource.setPassword(dbConfig.getPassword()); dataSource.setDriverClassName(dbConfig .getDriverClassName()); return dataSource; } @Bean public JdbcTemplate jdbcTemplate() { JdbcTemplate template = new JdbcTemplate(dataSource()); return template; } //事务第二步:注册事务管理器,基本就一个DataSourceTransactionManager @Bean public PlatformTransactionManager transactionManager() { //这里用的dataSource,必须跟你操作数据库用的dataSource是同一个 DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource()); return txManager; } } ``` ### 业务方法添加事务注解 ```java @Service public class DeptServiceImpl { //可以注入许多个其它的dao和其它的业务类 @Autowired private DeptDao deptDao; @Autowired private EmpDao empDao; // 事务注解是添加在业务类上的,而不是dao类上 // 事务第三步:在需要事务管理的方法或类上添加这个注解 @Transactional public void doBusiness() { empDao.delete(); deptDao.insert(); } } ``` ## 事务概念 ### 全局事务与本地事务 牵涉到多个事务资源的情况,就称之为全局(Global)事务,只关联到一个事务资源就称之为本地事务(local) 实物资源可能就是一个数据库或者是消息队列。全局事务建议用alibaba的seata,本地事务的资源技术的说法就是一个Jdbc的连接,而不是DataSource ### 声明式事务与编程式事务 用spring编写事务代码的时候,有两种方法,一个是编程的方式,一个是声明的方式。 声明的方式就是案例的方式,编程的方式不推荐在项目中使用,下面就是编程式事务的写法 ```java //1.创建事务管理器对象 PlatformTransactionManager txManager = new DataSourceTransactionManager() //2.创建事务的定义(事务超时时间的设置和隔离级别) DefaultTransactionDefinition def = new DefaultTransactionDefinition(); //3. 创建出事务 TransactionStatus status = txManager.getTransaction(def); try { // 业务逻辑代码 。。。dao1.xxx(); dao2.yyy(); service.mm(); // 提交事务 txManager.commit(); } catch (MyException ex) { // 回滚事务 txManager.rollback(status); throw ex; } ``` ### 事务原理 基于上面的特性,完全可以利用AOP的环绕通知方式来把事务性质的代码放到一个单独的通知(拦截器中),这个拦截器在spring源码中是`TransactionInterceptor` Spring的事务是基于Jdbc连接的,下面的代码是jdbc事务的写法 ```java public static void main(String[] args) { Connection connection = null; PreparedStatement pstat1 = null; PreparedStatement pstat2 = null; try { connection = JdbcUnits.getConnection(); //创建事务 开启手动提交 connection.setAutoCommit(false); pstat1 = connection.prepareStatement( "UPDATE change_money SET money=money-? WHERE id=?"); pstat2 = connection.prepareStatement( "UPDATE change_money SET money=money+? WHERE id=?"); pstat1.setInt(1,500); pstat1.setInt(2,1); pstat2.setInt(1,500); pstat2.setInt(2,2); pstat1.executeUpdate(); // 在两次提交中出现错误导致第二次无法提交 可以回滚到之前状态 // int i= 2/0; // 由于已经设置了连接对象为非自动提交 // 所以下面的代码,不真正完成数据库的操作 pstat2.executeUpdate(); // 提交事务(真正影响数据库) connection.commit(); } catch (Exception e) { // 出现错误 回滚 try { connection.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } e.printStackTrace(); }finally { JdbcUnits.getClose(connection,pstat1); JdbcUnits.getClose(null,pstat2); } } ``` jdbc事务写法的本质要求 - 多个PreparedStatement要求是同一个Jdbc的Connection - Jdbc Connection的自动提交属性设置为false - 然后再由connection,commit或rollback完成数据库操作 spring要求我们提供的是DataSource,而不是Jdbc Connection,这其实是有问题的,这也就是DbUtils操作事务会失败的原因。原因是 - PlatformTransactionManager会依据DataSource得到一个Connection - 得到Connection之后,它会把Connection放置到ThreadLocal(可以这样理解:key是dataSource,值是jdbc连接) - JdbcTempldate对象,会依据你传递DataSource(key),因为JdbcTemplate与PlatformTransactionManager是同一个Datasource,就是相同的key,这样就可以得到刚刚PlatformTransactionManager保存的connection - JdbcTemplate就利用这个Connection操作数据库 - PlatformTransactionManager进行commit或rollback 为什么dbtuils事务会失败 - PlatformTransactionManager会依据DataSource得到一个Connection - 得到Connection之后,它会把Connection放置到ThreadLocal(可以这样理解:key是dataSource,值是jdbc连接) - QueryRunner利用传递给它的DataSource,然后利用这个DataSource.getConnection()获取连接 - 接着QueryRunner利用上一步创建的连接操作数据库 - PlatformTransactionManager的commit与rollback,但是此PlatformTransactionManager用的连接(第二步)与第三步的Connection不是同一个。 ***所以:我们用的PlatformTransactionManager管理器是DataSourceTransactionManager,你不要认为只要我们用的是同一个DataSource,事务就一定生效*** spring源码TransactionInterceptor类的父类TransactionAspectSupport的invokeWithinTransaction方法中看到 克隆https://e.coding.net/david_cj/transaction.git 此项目看看简单的模仿实现以便更好的理解事务的原理 ## EnableTransactionManagement详解 添加了这个注解就意味着可以识别项目中的@Transactional,类似于xml中`tx:annotation-driven` spring中基本上以Enable开头的注解主要是添加了一些bean对象到spring中,这些bean对象可能是 - 一个配置类,里面有bean方法 - 一个普通bean对象 - 一个BPP对象 - 一个BFPP对象 这些bean对象,比如BPP,就可以额外提供一些事务AOP的能力等等 ### proxyTargetClass 设置为true的话就利用cglib子类的形式来代理,默认false就尽可能的用jdk的动态代理方式 ### AdviceMode 假定你有一个业务类像下面这样 ```java public class SomeServiceImpl { @Transactional public void doSth(){ doSth2(); } @Transactional public void doSth2(){ } } ``` 那么它经过事务拦截之后,执行doSth方法时会类似下面 ```java public class SomeServiceImplProxy extends SomeServiceImpl { @Transactional public void doSth(){ try{ PlatformTransaction txManager= ... //super是父类对象,是干净没有被代理过的 super.doSth(); txManager.commit(); }catch(Throwable e){ txManager.rollback(); } } } ``` 由于doSth与doSth2都在同一个目标类里面,所以doSth方法调用doSth2的时候,虽然doSth2有@Transactional注解,但doSth2执行的时候不是被代理过的执行逻辑,只是简单的非代理逻辑的执行 正是基于同一个目标对象的方法间互相调用,不会重复代理,所以spring提供了一种机制,利用Aspectj可以做到即使是同一个对象的方法间的互相调用也都有代理 它的类型是个枚举,它的默认值是Proxy,也就是说创建事务代理的时候用的是传统的jdk或cglib的动态代理 aspectj的相关类在spring-aspects依赖中 ```xml org.springframework spring-aspects 5.3.9 ``` > https://my.oschina.net/yqz/blog/1603556 ## PlatformTransactionManager详解 spring就是靠它完成事务功能 ### bean名 它的名字,如果是以代码+ EnableTransactionManagement这种方式来配置的话,名字不重要 ```java @Bean public PlatformTransactionManager txManager() { //这里用的dataSource,必须跟你操作数据库用的dataSource是同一个 DataSourceTransactionManager txManager = new DataSourceTransactionManager(dataSource()); return txManager; } ``` 但是建议用以下3个名字 - txManager - transactionManager - tm 原因是因为传统的xml事务配置中,是识别这2个名字 ### 使用的DataSource dataSource至少要与操作数据库jdbcTemplate使用的DataSource是同一个 ## @Transactional注解详解 此注解可以修饰在类上,也可以修饰在方法上面,也可以修饰在接口上,修饰在接口上,意味着此接口的所有实现类都有@Transactional注解的功能,但spring官方不建议把它放在接口上 如果此注解类与方法都用,策略就是合并+覆盖 ### readOnly readOnly属性的详细理解: 1)readonly并不是所有数据库都支持的,不同的数据库下会有不同的结果。 2)设置了readonly后,connection都会被赋予readonly,效果取决于数据库的实现。 a. 在oracle下测试,发现不支持readOnly,也就是不论Connection里的readOnly属性是true还是false均不影响SQL的增删改查; b. 在mysql下测试,发现支持readOnly,设置为true时,只能查询,若增删改会发生如下异常: > Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed > at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:910) > at com.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:792) 3)在ORM中,设置了readonly会赋予一些额外的优化,例如在Hibernate中,会被禁止flush等。 ### rollbackFor与noRollbackFor spring默认是当发生RuntimeException及其子类的异常时才会回滚,所以如果你想调整这个情况,你就需要设置rollbackFor属性,比如下面的方法只有在发生了ArithmeticException及其子类的异常时才会回滚 ```java @Transactional(rollbackFor = ArithmeticException.class)//事务第三步:在需要事务管理的方法或类上添加这个注解 public void doBusiness() { empDao.delete(); deptDao.insert(); } ``` noRollbackFor属性设置的含义是,发生了这个异常但不回滚 ### 事务传播propagation 假定有一个业务类A的事务方法m1,调用了业务类B的事务方法m2(),那么会产生多少个事务?spring是靠传播级别设置来处理的,比如2个事务方法变成同一个事务,或者变成保存点的方式 1. REQUIRED(需要事务): 业务方法需要在一个事务中运行,如果方法运行时,已处在一个事务中,那么就加入该事务,否则自己创建一个新的事务.这是spring默认的传播行为; 2. NOT_SUPPORTED(不支持事务): 声明方法需要事务,如果方法没有关联到一个事务,容器不会为它开启事务.如果方法在一个事务中被调用,该事务会被挂起,在方法调用结束后,原先的事务便会恢复执行; 3. REQUIREDS_NEW(需要新事务):业务方法总是会为自己发起一个新的事务,如果方法已运行在一个事务中,则原有事务被挂起,新的事务被创建,直到方法结束,新事务才结束,原先的事务才会恢复执行; 备注:新建的事务如果没有进行异常捕获,发生异常那么原事务方法也会发生回滚。(该结论经过自测验证) 4. MANDATORY(强制性事务):只能在一个已存在事务中执行。业务方法不能发起自己的事务,如果业务方法在没有事务的环境下调用,就抛异常 5. NEVER(不能存在事务):声明方法绝对不能在事务范围内执行,如果方法在某个事务范围内执行,容器就抛异常.只有没关联到事务,才正常执行. 6. SUPPORTS(支持事务):如果业务方法在某个事务范围内被调用,则方法成为该事务的一部分,如果业务方法在事务范围外被调用,则方法在没有事务的环境下执行. 7. NESTED(嵌套事务):如果一个活动的事务存在,则运行在一个嵌套的事务中.如果没有活动的事务,则按REQUIRED属性执行.它使用了一个单独的事务,这个事务拥有多个可以回滚的保证点.内部事务回滚不会对外部事务造成影响, 它只对DataSourceTransactionManager 事务管理器起效. 思考:Nested和RequiresNew的区别: a. RequiresNew每次都创建新的独立的物理事务,而Nested只有一个物理事务; b. Nested嵌套事务回滚或提交不会导致外部事务回滚或提交,但外部事务回滚将导致嵌套事务回滚,而 RequiresNew由于都是全新的事务,所以之间是无关联的; c. Nested使用JDBC 3的保存点实现,即如果使用低版本驱动将导致不支持嵌套事务。 ### 事务隔离级别 SQL规范于1992年提出了数据库事务隔离级别,以此用来保证并发操作数据的正确性及一致性。Mysql的事务隔离级别由低往高可分为以下几类: 1. READ UNCOMMITTED(读取未提交的数据) 这是最不安全的一种级别,查询语句在无锁的情况下运行,就读取到别的未提交的数据,造成脏读,如果未提交的那个事务数据全部回滚了,而之前读取了这个事务的数据即是脏数据,这种数据不一致性读造成的危害是可想而知的。 1. READ COMMITTED(读取已提交的数据) 一个事务只能读取数据库中已经提交过的数据,解决了脏读问题,但不能重复读,即一个事务内的两次查询返回的数据是不一样的。如第一次查询金额是100,第二次去查询可能就是50了,这就是不可重复读取。 1. REPEATABLE READ(可重复读取数据,这也是Mysql默认的隔离级别) 一个事务内的两次无锁查询返回的数据都是一样的,但别的事务的新增数据也能读取到。比如另一个事务插入了一条数据并提交,这个事务第二次去读取的时候发现多了一条之前查询数据列表里面不存在的数据,这时候就是传说的中幻读了。这个级别避免了不可重复读取,但不能避免幻读的问题。 1. SERIALIZABLE(可串行化读) 这是效率最低最耗费资源的一个事务级别,和可重复读类似,但在自动提交模式关闭情况下可串行化读会给每个查询加上共享锁和排他锁,意味着所有的读操作之间不阻塞,但读操作会阻塞别的事务的写操作,写操作也阻塞读操作。 spring 的 5种事务隔离级别 1. ISOLATION_DEFAULT (使用后端数据库默认的隔离级别) 以下四个与JDBC的隔离级别相对应: 1. ISOLATION_READ_UNCOMMITTED (允许读取尚未提交的更改,可能导致脏读、幻影读或不可重复读) 2. ISOLATION_READ_COMMITTED (允许从已经提交的并发事务读取,可防止脏读,但幻影读和不可重复读仍可能会发生) 3. ISOLATION_REPEATABLE_READ (对相同字段的多次读取的结果是一致的,除非数据被当前事务本身改变。可防止脏读和不可重复读,但幻影读仍可能发生) 4. ISOLATION_SERIALIZABLE (完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的) ## 常见面试题:事务失效 https://blog.csdn.net/weixin_44236420/article/details/103937015 - bean 不被spring管理不会有事务 > AopUtils.isAopProxy(Object object) > > AopUtils.isCglibProxy(Object object) //cglib > > AopUtils.isJdkDynamicProxy(Object object) //jdk动态代理; - 如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB; - @Transactional 注解只能应用到 public 可见度的方法上。 如果你在 protected、private 或者 package-visible 的方法上使用 @Transactional注解,它也不会报错,事务也会失效。这一点由Spring的AOP特性决定的 - 如果你使用了父子容器,可能是context:component-scan重复扫描引起的 父子容器 ```java ConfigurableApplicationContext child=new.....; ConfigurableApplicationContext parent=new....; child.setParent(parent); ``` 父子容器的特点:getBean的时候,先从子容器中找,找到就结束,找不到就到父容器去找 如果传统的xml配置,想识别@Transactional注解,需要在xml中配置tx:annotation-driven ```xml ``` 假定有父子容器,子容器对a.xml,父对应b.xml a.xml ```xml ``` b.xml ```xml ``` 这样配置产生子容器会扫描到com包下面的业务类,父容器也会扫描到com包下业务类。所以找业务类是会直接在子容器中找到,但子容器没有配置事务驱动这一条目,所以找到的这个业务类是没有事务的,虽然父容器的业务类是有事务的,但永远用不着 - 异常类型错误,默认是runtimeException才会回滚的 - 异常被catch住,忘记抛出,记住必须抛异常才会回滚的 - 如果一个业务类的方法没有修饰@Transactional,此方法调用了同一个类的另外一个修饰了@Transational注解的方法,那么不会有事务.但是如果你直接调用的doSth2是有事务的 ```java public class SomeServiceImpl { public void doSth() { doSth2(); } @Transactional public void doSth2() { ... } } ``` # Mvc > 中文翻译官方文档:https://www.w3cschool.cn/spring_mvc_documentation_linesh_translation/ ## Hello ### 添加依赖 ```xml javax.servlet javax.servlet-api org.springframework spring-webmvc ``` ### 配置DispatcherServlet 在web.xml中配置spring mvc框架提供的servlet,并且最好配置url-pattern模式为`/` ,这样你就可以让所有的请求都交给mvc框架处理了,你也可以不配置这个url模式,只让部分请求进入到mvc框架,甚至你也可以配置多个DispatcherServlet,相当于让项目有多个spring mvc的运行环境 ```xml asdf org.springframework.web.servlet.DispatcherServlet contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation config.AppConfig ``` 默认情况下,DispatcherServlet关联一个读取xml元数据的web容器,你想改变容器类型就需要配置contextClass,比如 ```xml contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext ``` 如果你想给容器指定元数据就需要配置contextConfigLocation ```xml contextConfigLocation config.AppConfig ``` ### 编写配置类 配置类要配置ComponentScan,以便扫描控制器,控制器是有@Controller注解修饰的类 第二个要配置的地方就是注册一个spring提供的bean,此bean是用来解析逻辑视图名为物理视图的,比如abc->/WEB-INF/abc.jsp,以便控制器可以正确转发到这个jsp文件 ```java @Configuration @ComponentScan("com") public class AppConfig { @Bean public InternalResourceViewResolver resourceViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/"); resolver.setSuffix(".jsp"); return resolver; } } ``` > spring支持其它视图引擎,比如freemarker,thymeleaf等,这里只讲jsp ### 编写控制器 编写的要点有以下4个 ```java @Controller // 要点1:表明是一个控制器 public class HelloController { @RequestMapping("/hello") //要点2:表明此方法处理的请求地址 public String hello() { System.out.println("处理地址为/hello的请求---"); //abc是一个逻辑视图的名字,靠视图解析器解析成物理的jsp文件 return "abc"; //要点3:表明返回的逻辑视图名 } @RequestMapping("/world") public ModelAndView world() { System.out.println("world---"); ModelAndView mav = new ModelAndView(); mav.setViewName("world"); // req.setAttribute("msg","helloWorld"); mav.addObject("msg", "hello world!"); //要点4:返回的ModelAndView可以设置模型数据,页面可以访问到 return mav; } } ``` ### 视图 jsp文件就是视图,按照视图解析器(见配置类)的配置,jsp文件在WEB-INF目录下 ```jsp <%@ page contentType="text/html;charset=UTF-8" language="java" %> abc

msg:${msg}

``` > WEB-INF目录下的资源是不能直接访问的 ## mapping ### url 写法 ```java //1.多个url值 @RequestMapping(value={"/index2","index3"}) //2. fallback 备胎 @RequestMapping(value="*") //3. ant地址格式 (三个通配符? * **) @RequestMapping(value = "/product/**/*.jpg") // 4. 精确匹配 @RequestMapping("/index") ``` 当多个方法都可以处理请求时,地址最精确地去处理 ### 类与方法上都写 ```java //类上的地址与方法上的地址合并,/dept/insert @Controller @RequestMapping("/dept") public class HomeController2 { @RequestMapping("/insert") public String insert() { return "list"; } @RequestMapping("/update") public String update() { return "list"; } } ``` ### 组合注解 ```java // 几个组合注解等于RequestMapping加一个限制条件 // GetMapping只能处理get请求。===@RequestMapping(method = RequestMethod.GET) @Controller public class HomeController3 { @GetMapping("/get") //@RequestMapping(method = RequestMethod.GET) public String getMapping() { System.out.println(" 只处理get请求--"); return "index"; } } ``` ### PathVariable ```java //可选的,在变量后面用冒号分隔,可以写正则表达式对其约束 @RequestMapping("/product2/page/{pageno:\\d}/{pagesize}") public String productPage(@PathVariable("pageno") int pageno, @PathVariable("pagesize") int asdf) { System.out.println(pageno + "---" + asdf); return "index"; } ``` ### 限缩 RequestMapping注解以及组合注解的属性通常是用来限制被其修饰的请求处理方法能处理的请求,常见的有 - consumes:表示请求头中的`Content-Type` 有某个值时可以处理请求 - produces:表示请求头中的`Accept`有某个值时可以处理请求 - params:表示请求参数有某个参数及值时处理请求 - headers:表示请求中有某些头时处理请求 ```java //只有请求头部中的content-type=text/plain时才处理你这个请求 @RequestMapping(value = "/xz", consumes = {"text/plain"}) public String xianzhi() { return "index"; } //发送的请求头部中accept的值是text/plain时才处理这个请求 // 如果请求中没有设置Accept的值,也可以处理这个请求 @RequestMapping(value = "/xz2", produces = {"text/plain"}) public String xianzhi2() { return "index"; } ``` ## 接收数据 ### 简单类型 简单类型指的是java的基础类型,它不能赋值为null,所以方法参数是简单类型必须就有值 ```java /simple?a=100 @RequestMapping("/simple") public String simple(int a) {} ``` > 默认情况下,反射是获取不到方法的参数名,是因为spring解决了这个问题,它可以获取到参数名,如果你用老版本的spring就不一定可行 ### 包装类型 ```java @RequestMapping("/wrapper") public String wrapper(Integer b) { } ``` ### 复杂类型 ```java //complex?id=100&name=abc //其中id与name这2个url参数是Emp类型的属性名 @RequestMapping("/complex") public String complex(Emp emp) { System.out.println("complex---" + emp); return "index"; } ``` ### 日期类型做法 有两种,一种是在参数上添加@DateTimeFormat(pattern="yyyy-MM-dd"),缺点是只在被此注解修饰的参数上有效,不是整个项目全局有效 ```java public String d(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date) {} ``` 第二种情况是可以全局有效,并且不再需要用到@DateTimeFormat注解了,其实现步骤 - 加注解EnableWebMvc - 实现接口WebMvcConfigurer - 实现一个方法用来注册转换器(formatter) ```java @Override public void addFormatters(FormatterRegistry registry) { DateFormatter formatter = new DateFormatter(); formatter.setPattern("yyyy-MM-dd"); registry.addFormatter(formatter); } ``` 接下来只要有日期,什么都不需要额外处理就可以 ```java //d2?date=1999-9-28 @RequestMapping("/d2") public String d2( Date date) { System.out.println("date2---" + date); return "index"; } ``` ### 接收数组 ```java // http://localhost:8080/array?hobby=a,b,c // http://localhost:8080/array?hobby=aaa&hobby=bb @RequestMapping("/array") public String array( String[] hobby) { System.out.println("length:" + hobby.length); System.out.println("hobby---" + Arrays.toString(hobby)); return "index"; } ``` ### 接收list数据 ```java // 不能象下面这样写来接收list数据,spring mvc不支持 /* @RequestMapping("/list") public String list( List hobby) { System.out.println("size:" + hobby.size()); hobby.forEach(System.out::println); return "index"; }*/ //地址类似于数组案例的格式 @RequestMapping("/list") public String list( ListVO vo) { System.out.println("size:" + vo.getHobby().size()); vo.getHobby().forEach(System.out::println); return "index"; } //http://localhost:8080/list2?emps[0].id=1&emps[0].name=a&emps[1].id=2&emps[1].name=b @RequestMapping("/list2") public String list2(ListVO vo) { System.out.println("size:" + vo.getEmps().size()); vo.getEmps().forEach(System.out::println); return "index"; } // 包装list数据的类型 @Data public class ListVO { private List emps; //见上面的list2的方法 private List hobby;//见上面的list的方法 } ``` ### @RequestParam ```java //rp?b=200 //rp 如果这样请求,没有给参数赋值就用默认值100 @RequestMapping("/rp") public String requestParam(@RequestParam(value = "b",required = false,defaultValue = "100") int a) { System.out.println("a:" + a); return "index"; } ``` ### @PathPariable ```java @RequestMapping("/page{pageno}") public String pathPariable(int pageno) { System.out.println("pageno:" + pageno); return "index"; } ``` ### web特有的类型 ```java //象HttpServletRequest,HttpServletResponse,HttpSession等特殊类型, // springmvc会自动注入 @RequestMapping("/teshu") public String teShu( HttpServletRequest request) { //request.getParameter() System.out.println(request); return "index"; } ``` 详细列表 | Controller method argument | Description | | :----------------------------------------------------------- | :----------------------------------------------------------- | | `WebRequest`, `NativeWebRequest` | Generic access to request parameters and request and session attributes, without direct use of the Servlet API. | | `javax.servlet.ServletRequest`, `javax.servlet.ServletResponse` | Choose any specific request or response type — for example, `ServletRequest`, `HttpServletRequest`, or Spring’s `MultipartRequest`, `MultipartHttpServletRequest`. | | `javax.servlet.http.HttpSession` | Enforces the presence of a session. As a consequence, such an argument is never `null`. Note that session access is not thread-safe. Consider setting the `RequestMappingHandlerAdapter` instance’s `synchronizeOnSession` flag to `true` if multiple requests are allowed to concurrently access a session. | | `javax.servlet.http.PushBuilder` | Servlet 4.0 push builder API for programmatic HTTP/2 resource pushes. Note that, per the Servlet specification, the injected `PushBuilder` instance can be null if the client does not support that HTTP/2 feature. | | `java.security.Principal` | Currently authenticated user — possibly a specific `Principal` implementation class if known.Note that this argument is not resolved eagerly, if it is annotated in order to allow a custom resolver to resolve it before falling back on default resolution via `HttpServletRequest#getUserPrincipal`. For example, the Spring Security `Authentication` implements `Principal` and would be injected as such via `HttpServletRequest#getUserPrincipal`, unless it is also annotated with `@AuthenticationPrincipal` in which case it is resolved by a custom Spring Security resolver through `Authentication#getPrincipal`. | | `HttpMethod` | The HTTP method of the request. | | `java.util.Locale` | The current request locale, determined by the most specific `LocaleResolver` available (in effect, the configured `LocaleResolver` or `LocaleContextResolver`). | | `java.util.TimeZone` + `java.time.ZoneId` | The time zone associated with the current request, as determined by a `LocaleContextResolver`. | | `java.io.InputStream`, `java.io.Reader` | For access to the raw request body as exposed by the Servlet API. | | `java.io.OutputStream`, `java.io.Writer` | For access to the raw response body as exposed by the Servlet API. | | `@PathVariable` | For access to URI template variables. See [URI patterns](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-uri-templates). | | `@MatrixVariable` | For access to name-value pairs in URI path segments. See [Matrix Variables](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-matrix-variables). | | `@RequestParam` | For access to the Servlet request parameters, including multipart files. Parameter values are converted to the declared method argument type. See [`@RequestParam`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestparam) as well as [Multipart](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart-forms).Note that use of `@RequestParam` is optional for simple parameter values. See “Any other argument”, at the end of this table. | | `@RequestHeader` | For access to request headers. Header values are converted to the declared method argument type. See [`@RequestHeader`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestheader). | | `@CookieValue` | For access to cookies. Cookies values are converted to the declared method argument type. See [`@CookieValue`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-cookievalue). | | `@RequestBody` | For access to the HTTP request body. Body content is converted to the declared method argument type by using `HttpMessageConverter` implementations. See [`@RequestBody`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestbody). | | `HttpEntity` | For access to request headers and body. The body is converted with an `HttpMessageConverter`. See [HttpEntity](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-httpentity). | | `@RequestPart` | For access to a part in a `multipart/form-data` request, converting the part’s body with an `HttpMessageConverter`. See [Multipart](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart-forms). | | `java.util.Map`, `org.springframework.ui.Model`, `org.springframework.ui.ModelMap` | For access to the model that is used in HTML controllers and exposed to templates as part of view rendering. | | `RedirectAttributes` | Specify attributes to use in case of a redirect (that is, to be appended to the query string) and flash attributes to be stored temporarily until the request after redirect. See [Redirect Attributes](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-redirecting-passing-data) and [Flash Attributes](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-flash-attributes). | | `@ModelAttribute` | For access to an existing attribute in the model (instantiated if not present) with data binding and validation applied. See [`@ModelAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-modelattrib-method-args) as well as [Model](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-modelattrib-methods) and [`DataBinder`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder).Note that use of `@ModelAttribute` is optional (for example, to set its attributes). See “Any other argument” at the end of this table. | | `Errors`, `BindingResult` | For access to errors from validation and data binding for a command object (that is, a `@ModelAttribute` argument) or errors from the validation of a `@RequestBody` or `@RequestPart` arguments. You must declare an `Errors`, or `BindingResult` argument immediately after the validated method argument. | | `SessionStatus` + class-level `@SessionAttributes` | For marking form processing complete, which triggers cleanup of session attributes declared through a class-level `@SessionAttributes` annotation. See [`@SessionAttributes`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-sessionattributes) for more details. | | `UriComponentsBuilder` | For preparing a URL relative to the current request’s host, port, scheme, context path, and the literal part of the servlet mapping. See [URI Links](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-uri-building). | | `@SessionAttribute` | For access to any session attribute, in contrast to model attributes stored in the session as a result of a class-level `@SessionAttributes` declaration. See [`@SessionAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-sessionattribute) for more details. | | `@RequestAttribute` | For access to request attributes. See [`@RequestAttribute`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestattrib) for more details. | | Any other argument | If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by [BeanUtils#isSimpleProperty](https://docs.spring.io/spring-framework/docs/5.3.11/javadoc-api/org/springframework/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-), it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`. | ## 校验 校验分为前端校验和后端校验,前端校验是通过js完成,它有个最大的问题,js是可以被屏蔽的,之所以坚持还要写校验逻辑,是为了用户友好。 重点是后端校验一定要写,非常重要,宁肯没有前端校验也不能没有后端,否则乱七八糟的数据就会进入后端并保存到数据库 ### 依赖 ```xml org.hibernate.validator hibernate-validator ``` ### 注解 校验功能一定要添加@EnableWebMvc注解 ```java @Configuration @EnableWebMvc @ComponentScan("com") public class AppConfig implements WebMvcConfigurer {} ``` ### 设定校验注解 ```java @Data public class Emp { private Integer id; //如果没有值是不管的,有值之后才是长度在3到10 【3,10】 @Size(min = 3, max = 10, message = "长度在3和10之间") //表示不能是null或空白字符 @NotBlank private String name; } ``` 常用校验注解都在javax.validation.constraints包下面 ### 确定要校验 在控制器方法参数中,给要校验的类型添加@Valid注解,紧跟着添加BindingResult参数,此参数可以获取校验结果,中间不能添加别的内容 ```java @RequestMapping("/index") public String index(@Valid Emp emp, BindingResult result) { List fieldErrors = result.getFieldErrors(); for (FieldError fieldError : fieldErrors) { System.out.println("field:" + fieldError + " msg:" + fieldError.getDefaultMessage()); } if (result.hasErrors()) { // } else { //调用业务逻辑。 } return "index"; } ``` 如果有多个类型要校验,可以像下面这样写 ```java @RequestMapping("/index") public String index(@Valid Emp emp, BindingResult result, @Valid Dept dept,BindingResult result2) { ``` > @Valid注解属于javax.validation 包,别添加错了 ## 数据传递 ### 视图渲染流程 ![viewRender](images/viewrender.png) ### 渲染视图(render) 两种方法 ```java @RequestMapping("/index") public String index() { return "index"; } @RequestMapping("/index2") public ModelAndView index2() { ModelAndView mav = new ModelAndView(); mav.setViewName("index"); return mav; } ``` ### 跳转 重定向(redirect):先请求a3地址,可以看到浏览器地址栏变成了a4地址,并且index视图被渲染出来 ```java @RequestMapping("/a3") public String a3() { return "redirect:a4"; } @RequestMapping("/a4") public String a4() { return "index"; } ``` 转发(Forward):先请求a1,看到index视图被渲染出来,浏览器地址栏还是a1 ```java @RequestMapping("/a1") public String a1() { return "forward:a2"; // return "forward:/jump/a2"; //绝对路径 } @RequestMapping("/a2") public String a2() { return "index"; } ``` ### 数据传递 分为两类来理解,转发一类的控制器方法传递数据给视图和发生重定向时数据怎么在请求中传递 ***传统对象*** ```java // 1.第一种方法往视图传递数据,传统的对象 @RequestMapping("/a1") public String a1(HttpServletRequest request, HttpSession session) { request.setAttribute("msg", "req msg"); session.setAttribute("msg", "session msg"); request.getServletContext().setAttribute("msg", "context msg"); //渲染 (Render) return "index"; } ``` ***ModalAndView*** ```java // 2.第二种方法往视图传递数据,ModelAndView @RequestMapping("/a2") public ModelAndView a2() { //下面的数据还是放在请求作用域内 ModelAndView mav = new ModelAndView(); mav.setViewName("index"); mav.addObject("msg", "modelandview"); return mav; } ``` ***Model*** ```java // 3.第三种方法往视图传递数据,Model @RequestMapping("/a3") public String a3(Model model) { model.addAttribute("msg", "model"); return "index"; } // 4.第四种方法往视图传递数据,其实与第三种类似 @RequestMapping("/a4") public String a4(Model model, ModelMap modelMap, Map map) { model.addAttribute("msg", "model"); modelMap.addAttribute("msg1", "model map"); map.put("msg2", "map"); return "index"; } ``` 上面的例子都是请求方法渲染视图时怎么把数据交给视图,如果发生转发(forward)上面的方法仍然是有效的,因为是同一个请求 重定向传数据由于是多个请求,所以与转发优点不一样,大体有如下一些方法 - url重写方式:比如:通过url a4?a=2&b=3 优点:简单,缺点:只能传简单的数据以及数量有限 - 共同可以访问的对象,比如session, 优点:复杂数据,缺点:占内存 ***url重写*** ```java @RequestMapping("/a5") public String a5() { return "redirect:a6?msg=urlredire"; } @RequestMapping("/a6") public String a6() { return "index"; } ``` ***HttpSession*** ```java @RequestMapping("/a7") public String a7(HttpSession session) { session.setAttribute("msg", "s redirect"); return "redirect:a8"; } @RequestMapping("/a8") public String a8() { return "index"; } ``` ***RedirectAttributes*** ```java @RequestMapping("/a9") public String a9(RedirectAttributes attributes) { //这个里面放的数据会自动拼接到url上 attributes.addAttribute("msg1", "add attri"); // flash属性,是添加到session中,当你读取之后,就立即从session中删除 //(在重定向发生前放在会话中,重定向发生后,立即从会话中删除,并放到了请求作用域中 // 注意:不是直接把msg作为key放在session中) attributes.addFlashAttribute("msg", "flash attr"); return "redirect:a10"; } @RequestMapping("/a10") public String a10(HttpServletRequest request) { // 从a9重定向到这里 想获取flashAttribute里的值,用下面这种方法 Map inputFlashMap = RequestContextUtils.getInputFlashMap(request); Object msg = inputFlashMap.get("msg"); System.out.println("msg消息 = " + msg); // 想直接从请求作用域取值是不行的,返回null, // 因为此时mvc框架还没有把其用msg作为key放到请求作用域中 // 等这个控制器方法执行完毕之后才会把msg作为key放到请求作用域中 Object msgAttr = request.getAttribute("msg"); System.out.println("msgAttr = " + msgAttr); //如果现在就想从请求作用域中获取值,可以采用下面的方法, // 注意键不是msg Map attributeInputFlash = (Map) request.getAttribute("org.springframework.web.servlet.DispatcherServlet.INPUT_FLASH_MAP"); System.out.println("attributeInputFlash = " + attributeInputFlash.get("msg")); return "index"; } ``` InputFlashMap与OutputFlashMap RedirectAttributes内部使用了FlashMap去处理,上面的写法类似于下面的写法 ```java // 下面的方法了解即可,RedirectAttributes内部用的就是FlashMap来做得 @RequestMapping("/a11") public String a11(HttpServletRequest request) { FlashMap outputFlashMap = RequestContextUtils.getOutputFlashMap(request); outputFlashMap.put("msg", "flashmap 222"); return "redirect:a12"; } @RequestMapping("/a12") public String a12(HttpServletRequest request) { //下面的方式可以获取到RedirectAttributes设定的FlashAttribute值 Map inputFlashMap = RequestContextUtils.getInputFlashMap(request); Object msg = inputFlashMap.get("msg"); System.out.println("msg消息 = " + msg); return "index"; } ``` ### RequestContextUtils ```java WebApplicationContext context = RequestContextUtils.findWebApplicationContext(request); context.getBeansWithAnnotation(Controller.class) ``` ## 静态资源处理 ### servlet容器默认servlet 在tomcat的conf文件夹下有一个web.xml里面对于default的配置如下,默认servlet通常用来处理静态资源 由于其url-pattern值为`/`,所以所有没有对应映射的请求就会由此默认servlet处理,包括对静态资源的请求 ```xml default org.apache.catalina.servlets.DefaultServlet debug 0 listings false 1 default / ``` ### mvc添加默认servlet ```java @Configuration @EnableWebMvc @ComponentScan("com") public class AppConfig implements WebMvcConfigurer { /** * 启用默认servlet的功能 * 这样就会注册一个处理/**地址的处理器,他的优先级最低 * @param configurer */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // 默认去找servlet容器中名字为default的servlet去处理请求 configurer.enable(); //如果servlet容器中没有名字为default的servlet,该怎么办? // configurer.enable("cj"); } } ``` ### mvc资源处理 ```java @Configuration @EnableWebMvc @ComponentScan("com") public class AppConfig implements WebMvcConfigurer { /** * 下面的方法与上面的方法可以同时存在,但建议只用下面的方法 * * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry .addResourceHandler("/static/**") .addResourceLocations("classpath:/public/"); } } ``` ## 全局异常处理 ### 基本做法 实现步骤 - 创建类 - 类上加@ControllerAdvice - 类里添加方法 - 方法上添加@ExceptionHandler注解,注解里写上它能处理的异常类型 - 方法的参数写上一个异常类型作为参数 - 方法的返回值与@RequestMapping修饰的方法能支持的返回类型一样,可以是String,ModelAndView等等 ```java @ControllerAdvice public class GlobalExceptionHandler { /** * 1.异常处理方法的返回值与@RequestMapping修饰的方法的返回值类型是一样 * 2.方法的参数是可以没有的,如果有,基本上就是能处理异常的类型作为参数类型 * * @return */ @ExceptionHandler(Throwable.class) public String handleException(Throwable throwable) { System.out.println("throwable-------"); return "error"; } /** * 这个方法是处理RuntimeException及其子类的异常 * 如果发生的异常有多个处理方法都可以处理, * 会选最靠近自己的方法来处理异常 * * @param throwable * @return */ @ExceptionHandler(RuntimeException.class) public String handleRuntimeException(RuntimeException throwable) { System.out.println("runtime exception-----"); return "error"; } } ``` ### 视图找不到 下面代码由于index2.jsp这个文件,仍然会报404错误,这不属于全局异常能处理的范畴 ```java /** * 这个方法返回index2,但没有对应的视图,会出现404 * 这个不归属于全局异常处理的范畴 *

* 全局异常处理的只是控制器方法 * * @return */ @RequestMapping("/index2") public String index2() { System.out.println("演示找不到视图的情况---"); return "index2"; } ``` ## json ### 添加依赖 ```xml com.fasterxml.jackson.core jackson-databind ``` ### 配置消息转换器 这一步是可选的,主要是处理日期格式的问题,建议在`extendMessageConverters`方法里配置 ```java /** * 此方法是完全由自己配置各种转换器,不会采用任何mvc框架默认已有的转换器 * * @param converters */ @Override public void configureMessageConverters(List> converters) { } /** * extend开头的方法,是在已有的转换器基础之上,额外在处理转换器 * * @param converters */ @Override public void extendMessageConverters(List> converters) { // jackson的序列化主要是靠ObjectMapper // 所以设置日期格式就需要像下面这样 ObjectMapper objectMapper = new ObjectMapper(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); objectMapper.setDateFormat(dateFormat); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper); converters.add(0, converter); //建议用Builder的方式来配置 // Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); // ObjectMapper objectMapper1 = builder.dateFormat(new SimpleDateFormat("yyyy-MM-dd")).build(); } ``` ### 编写控制器方法 ResponseBody注解可以写在方法上,也可以写在类上,写在类上等于此类中所有的方法上都加了这个注解 ResponseBody注解写在控制器类上的话,就可以用RestController注解取代ResponseBody注解与Controller注解 ```java @RequestMapping("/emp") @ResponseBody public EmpVO emp() { EmpVO e = new EmpVO(11, "cj", new Date()); return e; } ``` ### 获取ajax数据 编写下面的控制器方法,在要把请求json数据转换为对象的方法参数添加@RequestBody注解即可 ```java @RequestMapping("/insert") public ResponseVO insert(@RequestBody EmpVO empVO) {} ``` 客户端传递如下的数据即可 ```json { "id":100, "username":"cj" } ``` ### 统一的数据响应 #### 统一的数据响应对象 ```java @Data @AllArgsConstructor @NoArgsConstructor public class ResponseVO { private int code; private String msg; private Object result; } ``` ##### 全局异常处理器 ```java @RestControllerAdvice //===ControllerAdvice+ ResponseBody public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseVO handleRuntime(RuntimeException re) { ResponseVO responseVO = new ResponseVO(500, re.getMessage(), null); return responseVO; } } ``` #### 某个正常响应方法 ```java @RequestMapping("/xxx") @ResponseBody public ResponseVO xxx() { ResponseVO responseVO = new ResponseVO(200, "ok", true); return responseVO; } ``` ### ajax案例 #### 查询所有的案例 ```html Title

index

{{msg}}

id username
{{e.id}} {{e.username}}
``` ***后端控制方法*** ```java @RequestMapping("/getAll") @ResponseBody public ResponseVO getAll() { EmpVO e = new EmpVO(11, "cj", new Date()); EmpVO e2 = new EmpVO(12, "cj2", new Date()); EmpVO e3 = new EmpVO(13, "cj3", new Date()); ArrayList al = new ArrayList<>(); al.add(e); al.add(e2); al.add(e3); Random random = new Random(); int j = random.nextInt(100); if (j > 20) { throw new RuntimeException("cucuo"); } ResponseVO responseVO = new ResponseVO(200, "ok", al); return responseVO; } ``` ***异常处理代码*** ```java @RestControllerAdvice //===ControllerAdvice+ ResponseBody public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseVO handleRuntime(RuntimeException re) { ResponseVO responseVO = new ResponseVO(500, re.getMessage(), null); return responseVO; } } ``` #### 增加案例 ```html Title

index

{{msg}}

``` ***后端控制器方法*** ```java @RequestMapping("/insert") @ResponseBody public ResponseVO insert(@RequestBody EmpVO empVO) { System.out.println(empVO); ResponseVO responseVO = new ResponseVO(200, "ok", true); return responseVO; } ``` ## 拦截器 ### 编写拦截器 几个要点 - 实现HandlerInterceptor接口 - 三个方法含义见下面代码中的注释 ```java public class FirstInterceptor implements HandlerInterceptor { /** * 在控制器方法执行之前执行 *

* 此方法的返回值为true的时候表示放行 * * @param request * @param response * @param handler * @return * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("-----1 pre------"); return true; } /** * 在控制器方法执行之后执行 * * @param request * @param response * @param handler * @param modelAndView * @throws Exception */ @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("-----1 post------"); } /** * 在控制器方法执行完毕,试图解析完毕,试图渲染(render)完毕之后才执行 *

* 3个接口方法,这一个是最后执行 * * @param request * @param response * @param handler * @param ex * @throws Exception */ @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("------1 after------"); } } ``` ### 注册拦截器 几个要点 - addInterceptor方法就完成了拦截器的注册 - order方法控制拦截器之间的顺序,数字越小优先级越高 - excludePathPatterns用来添加排除路径模式,意思是符合这些模式的地址不经过拦截器处理 ```java @Override public void addInterceptors(InterceptorRegistry registry) { // 顺序:值越小优先级越高 // registry.addInterceptor(new FirstInterceptor()).order(50); // registry.addInterceptor(new SecondInterceptor()).order(100); registry .addInterceptor(new AuthenticationInterceptor()) .order(Ordered.HIGHEST_PRECEDENCE) .excludePathPatterns("/login") .excludePathPatterns("/static/**"); } ``` ### 拦截器与过滤器 ![interceptor and filter](images/interceptor_filter.png) ### 验证案例 ***完成登录逻辑*** ```java @Controller public class LoginController { @GetMapping("/login") public String loginView() { return "login"; } @PostMapping("/login") public String doLogin(String username, String password, HttpSession session) { if ("admin".equalsIgnoreCase(username) && "123".equalsIgnoreCase(password)) { session.setAttribute("username", username); return "index"; } else { return "redirect:login"; } } } ``` ***编写Auth注解*** ```java @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Auth { } ``` ***使用注解*** 添加了Auth注解的表示需要验证之后才能访问 ```java @Auth @RequestMapping("/a") public String a() { return "a"; } @RequestMapping("/b") public String b() { return "b"; } ``` ***验证拦截器*** ```java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //第三个参数类型是HandlerMethod。里面有个成员method代表方法 HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); boolean needAuth = method.isAnnotationPresent(Auth.class); if (needAuth) { HttpSession session = request.getSession(); Object username = session.getAttribute("username"); if (ObjectUtils.isEmpty(username)) { response.sendRedirect("/login"); return false; } return true; } else { return true; } } ``` ## 文件上传 ### 配置DispatcherServlet ```xml mvc org.springframework.web.servlet.DispatcherServlet c:/tmp 102400 102400000 1024000 ``` ### 表单 要素如下 - method必须是post - enctype必须是multipart/form-data - 有type为file的控件 ```html

``` ### 控制器方法 要素如下 - 参数类型是MultipartFile - 参数名必须是表单file域的名字 - 建议用Path参数给transferTo,用File作为参数在jetty里会报异常 ```java @RequestMapping("/upload") public String upload(MultipartFile myFile) { //要用Path(java.nio),不能用File,因为jetty下会报文件找不到的异常 Path path = Paths.get("D:\\temp", myFile.getOriginalFilename()); try { myFile.transferTo(path); } catch (IOException e) { e.printStackTrace(); } return "index"; } ``` ### MultipartResolver注册 如果像上面那样在控制器方法参数中直接编写MultipartFile参数的参数,那么是可以不用注册一个MultipartResolver bean对象的,但如果是把此类型的数据作为一个类的属性,并把类作为控制器方法的参数,那么就必须注册MultipartResolver了,否则就会报不能转换的异常 ```java @Data public class FileVO { private MultipartFile myFile; private int id; private String name; } @RequestMapping("/uploadForAjax2") @ResponseBody public String uploadForAjax2(FileVO fileVO) {} ``` 配置类注册MultipartResolver,名字必须是multipartResolver ```java @Configuration @EnableWebMvc @ComponentScan("com") public class AppConfig implements WebMvcConfigurer { @Bean public MultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); return multipartResolver; } } ``` 注册了MultipartResolver解析器之后对MultipartFile作为参数也没有坏的影响,所以,强烈建议注册此解析器 > MultipartFile作为参数是靠RequestParamMethodArgumentResolver去处理的,它可以不需要有MultipartResolver解析器 > > 解析器的名字在DispatcherServlet中有定义名字 > > ```java > public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; > ``` ### 上传多个文件 表单里多写几个file控件,名字必须是一样的 ```html
``` 在后台控制器方法里参数名与file控件名一样,类型是数组或者List都可以 ```java @RequestMapping("/upload2") public String upload2(MultipartFile[] myFiles) {} @RequestMapping("/upload3") public String upload3(List myFiles) {} ``` ### ajax文件上传 ```html
``` > 前台用axios传递FormData类型的数据时,可以不用设置http 头部的content-type=multipart/form-data,因为axios会自动处理,也就是说上面的post请求可以变为 > > ```js > axios.post('http://localhost:8080/uploadForAjax', data) > .then(res => { > console.log(res); > alert(res.data); > }); > ``` > > 后台写法 ```java @RequestMapping("/uploadForAjax") @ResponseBody public String uploadForAjax(MultipartFile myFile,String name) { System.out.println("额外的表单数据:" + name); Path path = Paths.get("D:\\temp", myFile.getOriginalFilename()); try { myFile.transferTo(path); } catch (IOException e) { e.printStackTrace(); } return "upload ok"; } ``` 如果前端传递的数据非常多,那么后台可以用一个类封装一下,比如下面这样 ```java @Data public class FileVO { private MultipartFile myFile; private int id; private String name; } ``` 这样控制器方法就使用此类型作为参数即可 ```java @RequestMapping("/uploadForAjax2") @ResponseBody public String uploadForAjax2(FileVO fileVO) { System.out.println("额外的表单数据:" + fileVO.getName()); MultipartFile myFile = fileVO.getMyFile(); Path path = Paths.get("D:\\temp", myFile.getOriginalFilename()); try { myFile.transferTo(path); } catch (IOException e) { e.printStackTrace(); } return "upload ok"; } ``` 前端的代码保持不变,只是地址变成http://localhost:8080/uploadForAjax2 即可 ## 文件下载 ### 直接作为静态资源下载 ```html
直接下载 ``` 对应静态资源配置(在配置类中) ```java @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/public/"); } ``` ### 代码实现下载 ```java @RequestMapping("/download") public ResponseEntity download(String filename) { String path = "E:\\Image\\" + filename; InputStreamSource source = new FileSystemResource(path); HttpHeaders headers = new HttpHeaders(); headers.setContentType(getMediaType(filename)); try { //让浏览器以另存为的方式来下载文件,而不是直接打开 headers.setContentDispositionFormData("attachment", URLEncoder.encode(filename, "UTF-8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } ResponseEntity responseEntity = new ResponseEntity<>(source, headers, HttpStatus.OK); return responseEntity; } private MediaType getMediaType(String filename) { //guessContentTypeFromName是从文件名猜测其内容类型,如果为null就猜测失败 String midiaType = URLConnection.guessContentTypeFromName(filename); if (midiaType == null) { midiaType = MediaType.APPLICATION_OCTET_STREAM_VALUE; } // 不要直接实例化MediaType,而要用parseMediaType方法 // 因为直接实例化对/符号没有进行转意操作 return MediaType.parseMediaType(midiaType); } ``` ## cors 参考https://www.ruanyifeng.com/blog/2016/04/cors.html理解什么是跨域请求 ### 前端内容 ```html Document

{{emp.id}}

{{emp.empname}}

``` > 前端内容我放在案例中的resources下面了,文件夹名字叫corsdemo > > 用vscode打开此文件夹并以open with liver server的形式运行index.html文件就可以测试跨域情况了 ### 后端内容 ```java @RestController public class CorsDemoController { @CrossOrigin @RequestMapping("/data1") public ResponseVO getData(){ EmpVO empVO = new EmpVO(100, "cj"); ResponseVO responseVO = new ResponseVO(200, "ok", empVO); return responseVO; } @RequestMapping("/data2") public ResponseVO getData2(){ EmpVO empVO = new EmpVO(200, "cj2"); ResponseVO responseVO = new ResponseVO(200, "ok", empVO); return responseVO; } } ``` 配置类 ```java @Configuration @EnableWebMvc @ComponentScan("com") public class AppConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedMethods("GET","POST","OPTIONS","PUT","DELETE") .allowedOrigins("http://localhost:5500","http://127.0.0.1:5500"); } } ``` ### CrossOrigin注解 它可以修饰在方法上,也可以修饰在控制器类上。修饰在类上时,等价于此控制器中的所有方法都添加了CrossOrigin注解 修饰在方法上,对于此方法来说是本地跨域配置,而修饰在类上的注解,对于此类里的方法来说是全局配置,对于配置项可以允许多个值得,原则上是会合并的,而对于配置项只能有一个值得,那么本地的配置会覆盖全局的配置 如果想让许多的控制器都支持跨域,可以给每个要跨域的控制器添加CrossOrigin注解,也可以在配置类里面进行全局配置,这样就不需要在类上加注解了,只在配置类配置就行了 不管是注解还是配置类里面进行配置,跨域的设置是有如下的一些默认值情况的 * allowOrigins=“*” * allowMethods=“GET,POST,HEAD”,这3个方法就是简单请求支持的3个方法 * allowHeaders="所有的http header“ * maxAge=1800秒 == 30分钟 ### 全局配置 ```java public void addCorsMappings(CorsRegistry registry) { // registry.addMapping("/list"); //相当于在某个控制器方法上的requestMapping值为/list上面添加 //地址是/**,表示所有的请求都配置了跨域 registry.addMapping("/**") .allowedOrigins("http://127.0.0.1:5500") .allowedMethods("GET","POST","OPTIONS"); } ``` ### 实现原理 上面讲的跨域设置内部其实都是靠`CorsInterceptor`拦截器实现的,除了上面的配置方式实现跨域处理以外,还可以通过配置过滤器`CorsFilter`的方式实现,需要知道的是过滤器的执行是在所有拦截器之前执行的 当你开启了跨域并且也编写了自己的拦截器,而且这个自定义的拦截器注册时配置的优先级高于跨域拦截器,那么如果你的拦截器的preHandle方法返回false会导致跨域拦截器得不到执行,这样就会导致跨域失败。 跨域拦截器在处理复杂请求时,由于会发起预请求,也就是Options请求,跨域拦截器会使用一个PreFlightHandler这个Handler来处理此预请求,此Handler不是HandlerMethod类型。这样当你自己编写的高优先级的拦截器的代码是下面这样编写时,会导致类型转换失败,因而会导致跨域拦截器失效 ```java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HandlerMethod handlerMethod = (HandlerMethod) handler; return true; } ``` 自己编写的拦截器一般都是针对HandlerMethod这种Handler来处理的,所以最好是编写下面的逻辑以免导致跨域拦截器执行不了 ```java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if(!(handler instanceOf HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; return true; } ``` 还有,如果你的高优先级的拦截器像下面这样写,通常也会导致跨域拦截器失效,由于可能进入到else里面返回false导致跨域拦截器不执行让跨域失败 ```java @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String someValue = request.getHeader("key"); if("some value".equals(someValue)){ //做一些事情 return true; }else{ //直接输出响应 return false; } } ``` 上面说的高优先级的自定义拦截器导致跨域拦截器失效的情况,如果跨域处理不是通过拦截器的方式,而是通过CorsFilter实现跨域就不会出现上述失效的情况,因为过滤器总是在拦截器之前执行 > 关于PreFlightHandler可以在AbstractHandlerMapping类的getCorsHandlerExecutionChain方法的源代码中看到使用情况 ## restful ### 什么是restful http://www.ruanyifeng.com/blog/2011/09/restful.html https://www.runoob.com/w3cnote/restful-architecture.html ### 地址格式 - 地址都是名字描述 - 地址如果单词过长就用连字符分开 ,比如restful-architecture - 地址都是全小写 - 使用查询字符串(?)作为额外的参数 下面是一些比较好的地址设计 - https://github.com/git - https://github.com/git/git - https://github.com/git/git/blob/master/block-sha1/sha1.h - https://github.com/git/git/commit/e3af72cdafab5993d18fae056f87e1d675913d08 - https://github.com/git/git/pulls - https://github.com/git/git/pulls?state=closed - http://www.ruanyifeng.com/blog/2011/09/restful.html ### Swagger https://www.cnblogs.com/liusuixing/p/14427568.html ***添加依赖*** ```xml io.springfox springfox-swagger-ui 2.9.2 io.springfox springfox-swagger2 2.9.2 ``` ***编写swagger配置类*** ```java @Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .enable(true) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.any()) .paths(PathSelectors.any()) .build(); } /** * ApiInfo:主要返回接口和接口创建者的信息 */ private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("电影系统") .description("模仿优酷") .contact(new Contact("联系方式", "https://www.nfit.com/cj/", "123456@qq.com")) .version("v1.0") .build(); } } ``` ***导入swagger配置类并处理静态资源*** swagger是一个网页用来显示api信息,这些页面都是静态资源,所以需要配置这些静态资源的处理者,以便可以正确找到 ```java @Configuration @EnableWebMvc @ComponentScan("com") @Import(SwaggerConfig.class) //这里导入 public class AppConfig implements WebMvcConfigurer { //这里处理静态资源 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/public/"); registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); } } ``` ***访问*** 启动项目后,访问地址:http://localhost:8080/swagger-ui.html 即可 ### 后端代码 ```java @RestController @RequestMapping("/emps") public class EmpController { @ApiOperation(value = "获取所有员工", httpMethod = "GET") @GetMapping("") public ResponseVO> getAll() { ResponseVO> responseVO = new ResponseVO<>(200, "ok", list); return responseVO; } @PostMapping("") public ResponseVO insert(@RequestBody EmpVO empVO) { return new ResponseVO<>(200, "ok", true); } } ``` 前端就是常规的ajax请求处理,记得最好是传递json数据就可以了 ### 异常处理 ```java @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseVO handleRuntimeException(RuntimeException re){ return new ResponseVO(500, re.getMessage(), null); } } ``` ## 父子容器 web.xml中像下面这样配置就有父子spring容器的效果 ```xml org.springframework.web.context.ContextLoaderListener contextConfigLocation config.ParentConfig contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext mvc org.springframework.web.servlet.DispatcherServlet contextClass org.springframework.web.context.support.AnnotationConfigWebApplicationContext contextConfigLocation config.AppConfig mvc / ``` ## 动态注册 动态注册指的是servlet、listener、filter三类组件的动态注册,这是servlet 3推出的新技术,servlet 3之前提供了2种注册技术 - web.xml中进行配置 - 注解的形式 > 不能在一个servlet里面注册另一个servlet,会抛出IllegalStateException > > > > 下面是addServlet方法上的注释,表明什么时候会抛出IllegalStateException ,也就是说只能在ServletContext还没有初始化时才能动态注册 > > IllegalStateException if this ServletContext has already been initialized ### 基本用法一 编写ServletContainerInitializer接口的实现类 ```java public class MyServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> c, ServletContext ctx) throws ServletException { ServletRegistration.Dynamic registration = ctx.addServlet("xxxx", "com.controller.SecondServlet"); registration.addMapping("/second"); //这里可以给其配置multipart相关内容 //registration.setMultipartConfig(new MultipartConfigElement()); } } ``` 在类路径下创建META-INF文件夹,在此文件夹下再建services文件夹,最后在services文件夹下建立一个文件,名字是ServletContainerInitializer接口的全称 ![dynamic_register](images/dynamic_register.png) 在此文件里写上上面实现类的全称,如果有多个实现类,就一行一个 ``` com.MyServletContainerInitializer ``` ### 基本用法二 编写下面的实现ServletContainerInitializer实现类,并在其上面添加@HandlesTypes直接,此注解一般指定一个接口类型 ```java @HandlesTypes(MyAppInitializer.class) public class MyServletContainerInitializer implements ServletContainerInitializer {} ``` 接口是可以任意设计的,可以设计用来注册servlet也可以不 ```java public interface MyAppInitializer { void onBegin(ServletContext sc); } ``` 这样作为MyAppInitializer接口的实现类就只需要实现这个接口,并利用ServletContext参数来完成servlet的注册,而不要在文件里面进行配置了,常见的实现类如下 ```java public class MyAppInit1 implements MyAppInitializer { @Override public void onBegin(ServletContext sc) { System.out.println("------my app init 1"); sc.addServlet("two", "com.controller.SecondServlet").addMapping("/second"); } } ``` spring mvc框架中的SpringServletContainerInitializer实现了ServletContainerInitializer接口,其在HandlesTypes注解中指定的接口是WebApplicationInitializer ## 无webxml配置 无webxml配置指的是不在web.xml中进行DispatcherServlet的配置,这是利用servlet 3.0退出的动态注册能力进行的,spring mvc已经实现了这个,基本只需要继承`AbstractAnnotationConfigDispatcherServletInitializer`来实现配置 ### SystemConfig ```java public class SystemConfig extends AbstractAnnotationConfigDispatcherServletInitializer { //如果你不想做父子容器,就只需要在下面的方法返回一个配置类即可 // 然后让getServletConfigClasses方法返回null @Override protected Class[] getRootConfigClasses() { return new Class[]{AppConfig.class}; } @Override protected Class[] getServletConfigClasses() { return new Class[]{MvcConfig.class}; //return null; } @Override protected String[] getServletMappings() { return new String[]{"/"}; } @Override protected void customizeRegistration(ServletRegistration.Dynamic registration) { registration.setMultipartConfig(new MultipartConfigElement("c:/tmp", 10240000, 102400000, 1024000)); } } ``` ### AppConfig ```java @Configuration @PropertySource("classpath:db.properties") @ComponentScan(value = {"com"}, excludeFilters = { @ComponentScan.Filter(classes = {Controller.class}) }) @EnableTransactionManagement @MapperScan("com.dao") public class AppConfig { //1.DataSource,2.SqlSessionFactory(日志,插件),3事务管理器 @Autowired private DbConfig dbConfig; // region dataSource @Bean public DataSource dataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setUrl(dbConfig.getUrl()); dataSource.setUsername(dbConfig.getUsername()); dataSource.setPassword(dbConfig.getPassword()); dataSource.setDriverClassName(dbConfig.getDriverClassName()); return dataSource; } // endregion //region SqlSessionFactory @Bean public SqlSessionFactory sqlSessionFactory() throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource()); PathMatchingResourcePatternResolver loader = new PathMatchingResourcePatternResolver(); Resource[] resource = loader.getResources("classpath*:mappers/**/*.xml"); factoryBean.setMapperLocations(resource); PageInterceptor pageInterceptor = pageInterceptor(); factoryBean.setPlugins(pageInterceptor); factoryBean.setTypeAliasesPackage("com.entity"); factoryBean.setConfiguration(configuration()); return factoryBean.getObject(); } private org.apache.ibatis.session.Configuration configuration() { org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); configuration.setLogImpl(StdOutImpl.class); return configuration; } private PageInterceptor pageInterceptor() { PageInterceptor pageInterceptor = new PageInterceptor(); Properties properties = new Properties(); properties.setProperty("supportMethodsArguments", "true"); properties.setProperty("reasonable", "true"); properties.setProperty("helperDialect", "mysql"); pageInterceptor.setProperties(properties); return pageInterceptor; } // endregion //region 事务管理器 @Bean public PlatformTransactionManager transactionManager() { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(dataSource()); return transactionManager; } //endregion } ``` ### MvcConfig ```java @Configuration @EnableWebMvc @ComponentScan("com.controller") public class MvcConfig implements WebMvcConfigurer { // 视图解析器其实可以不用配置 @Override public void configureViewResolvers(ViewResolverRegistry registry) { registry.jsp("/WEB-INF/", ".jsp"); } // 前后端分析的情况下,下面的配置也可以不需要 @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations("classpath:/public/"); } @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT", "OPTIONS", "HEAD") .allowedOrigins("http://127.0.0.1:5500"); } @Override public void addFormatters(FormatterRegistry registry) { registry.addFormatter(new DateFormatter("yyyy-MM-dd")); } @Override public void extendMessageConverters(List> converters) { ObjectMapper objectMapper = Jackson2ObjectMapperBuilder .json() .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) .build(); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper); converters.add(0, converter); } } ``` # 附录 ## UriComponentsBuilder ## 获取所有的请求地址 https://blog.csdn.net/weixin_42290901/article/details/115864024 https://blog.csdn.net/kkgbn/article/details/74455702