spring 核心概念
目标:充分解耦
使用IoC容器管理bean(IoC)
在IoC容器内将有依赖关系的bean进行关系绑定(DI)
最终效果
使用对象时不仅可以直接从IoC容器中获取,并且获取到的bean已经绑定了所有的依赖关系
IoC入门案例思路分析
管理什么?(Service与Dao)
如何将被管理的对象告知IoC容器?(配置)
被管理的对象交给IoC容器,如何获取到IoC容器?(接口)
IoC容器得到后,如何从容器中获取bean?(接口方法)
使用Spring导入哪些坐标?(pom.xm1)
先导入spring依赖,然后新建
这个就是spring的配置文件,取名applicationContext.xml
1 2 3 4 5 6 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" /> <bean id ="bookService" class ="com.itheima.service.impl.BookServiceImpl" />
1 2 3 4 5 6 7 public class App2 { public static void main (string[]args) { Applicationcontext ctx = new classPathXmlApplicationcontext ( configLocation: "applicationcontext.xml" ), BqokDao bookDao = (BookDao)ctx.getBean( s:"bookDao" ); bookDao.save();
DI入门案例思路分析
基于IoC管理bean
Service中使用new形式创建的Dao对象是否保留?(否)
Service中需要的Dao对象如何进入到Service中?(提供方法)
Service与Dao间的关系如何描述?(配置)
1 2 3 4 5 6 7 8 9 10 11 12 13 public class BookServiceImpl implements Bookservice { private BookDao bookDao public void save () { System.out.println("book service save ..." ); bookDao.save(); } public void setBookDao (BookDao bookDao) { this .bookDao = bookDao; } }
1 2 3 4 5 6 7 <bean id ="bookService" class ="com,itheima.service.impl.BookServiceImpl" > <property name ="bookDao" ref ="bookDao" /> </bean >
bean bean基础配置
以下是新的bean配置文件,可以通过name增加别名,用逗号、分号和空格分隔,但还是推荐id起名
1 2 3 4 <bean id ="bookservice" name ="service service2 bookEbi" class ="com.itheima.service.impl.BookseryiceImpl" > <property name ="bookDao" ref ="bookDao" /> </bean > <bean id ="bookDao" name ="dao" class ="com.itheima.dao.impl.BookDaoImpl" />
如果命名出现问题,就会出现NoSuchBeanDefinitionException Create breakpoint : No bean named ‘service4’ available的报错
bean作用范围:默认创建出来的是同一个对象
为什么bean默认为单例 ?如果不是单例就会执行一次造一次对象,但其实可以用同一个对象(也就是可复用)
适合交给容器进行管理的bean
不适合交给容器进行管理的bean
bean实例化 查看异常的方法是从下往上看,越往上越详细
方式有三种
一:构造方法(常用)
1 2 3 4 5 6 7 8 public class BookDaoImpl implements BookDao { public BookDaoImpl () { System.out.printIn("book constructor is running..." ); } public void save () { System.out.println("book dao save ..." ); } }
1 2 3 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" />
无参构造方法如果不存在,将抛出异常BeancreationException
二:使用静态工厂实例化bean(以前常用)
这里class造出来的是工厂对象,但我们需要的不是这个工厂,所以用后面那个配置工厂里面真正用来造对象的方法名
1 <bean id ="orderDao" class ="com.itheima.factory.OrderDaoFactory" Factory-method ="getOrderDao" />
1 2 3 4 public class OrderDaoFactory { public static orderDao getOrderDao () { return new OrderDaoImpl ();
三:使用实例工厂实例化bean(了解)
1 2 3 <bean id ="userFactory" class ="com.itheima.factory.UserDaoFactory" /> <bean id ="userDao" factory-method ="getUserDao" factory-bean ="userFactory" />
1 2 3 4 5 public class AppForInstanceUser { public static void main (string[] args) { Applicationcontext ctx = new classPathXmlApplicationContext ("applicationcontext.xml" ); UserDao userDao=(UserDao)ctx.getBean("userDao" ); userDao.save();
四:使用FactoryBean实例化bean(重点)
1 <bean id ="userDao" class ="com.itheima.factory.UserDaoFactoryBean" />
1 2 3 4 5 6 7 8 9 10 11 12 public class UserDaoFactoryBean implements FactoryBean <UserDao> { public UserDao getobject () throws Exception { return new UserDaoImpl (); public class<?>getobjectType(){ return UserDao.class; } public boolean issingleton () { return false ; }
bean生命周期
生命周期:从创建到消亡的完整过程
bean生命周期:bean从创建到销毁的整体过程
bean生命周期控制:在bean创建后到销毁前做一些事情
管理生命周期通过java代码有以下两种,但其实可以交给Tomcat管理,所以是白学
1 <bean id ="bookDao" class ="com.itheima.dao.imp1.BookDaoImp1" init-method ="init" dastroy-method ="destory" />
1 2 3 4 5 6 7 8 9 10 11 public class AppForLifecycle { public static void main (string[]args ) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext ( configLocation: "applicationcontext.xm1" ) BookDao bookDao=(BookDao)ctx.getBean(s:"bookDao" ); bookDao.save(); ctx.close(); ctx.registerShutdownHook();
也可以通过实现接口的方式管理
1 2 3 4 5 6 7 8 9 10 public class BookServiceImpl implements BookService , InitializingBean ,DisposableBean { public void destroy () throws Exception { System.out.println("destroy" ); } public void afterPropertiesSet () throws Exception { System.out.println("afterPropertiesset" ); }
bean在整个初始化过程中经历的阶段:
创建对象(内存分配)
执行构造方法
执行属性注入(set操作 )
执行bean初始化方法
执行业务操作
执行bean销毁方法
bean销毁时机
容器关闭前触发bean的销毁
关闭容器方式
手工关闭容器:
ConfigurableApplicationcontext接close()操作
注册关闭钩子,在虚拟机退出前先关闭容器再退出虚拟机:
ConfigurableApplicationcontext接registerShutdownHook()操作
依赖注入方式
思考:向一个类中传递数据的方式有几种 ?
思考:依赖注入描述了在容器中建立bean与bean之间依赖关系的过程,如果bean运行需要的是数字或字符串呢 ?
依赖注入方式
setter引用类型 在bean中定义引用类型属性并提供可访问的set方法
1 2 3 4 5 public class BookServiceImpl implements BookService { private BookDao bookDao; public void setBookDao (BookDao bookDao) { this .bookDao = bookDao; }
配置中使用property标签ref属性注入引用类型对象
1 2 3 4 5 <bean id ="bookService" class ="com.itheima.service.impl.BookServiceImpl" > <property name ="bookDao" ref ="bookDao" /> </bean > <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" />
setter简单类型 在bean中定义引用类型(基本数据类型也可以)属性并提供可访问的set方法
1 2 3 4 5 6 7 8 9 public class BookDaoImpl implements BookDao private int connectionNum; private String databaseName; public void setConnectionNum (int connectionNum) { this .connectionNum = connectionNum; } public void setDatabaseName (string databaseName) this .databaseName =databaseName; }
配置中使用property标签value属性注入简单类型数据
1 2 3 4 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" . > <property name ="databaseName" value ="mysql" /> <property name ="connectionNum" value ="10" /> </bean >
构造器注入-简单类型(了解) 1 2 3 4 5 6 7 public class BookDaoImpl implements BookDao { private string databaseName; private int connectionNum; public BookDaoImpl (string database,int connectionNum) { this .databaseName =database; this .connectionNum = connectionNum; }
这里的property位置换成了constructor-arg,一般了解这个就行
1 2 3 4 5 6 7 8 9 10 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl”> <constructor-arg name=" connectionNum ” value =”10 "/> <constructor-arg name ="databaseName" value ="mysqL" /> </bean > <bean id ="userDao" class ="com.itheima.dao.impl.UserDaoImpl" /> <bean id ="bookService" class ="com.itheima.service.impl.BookServiceImpl" > <constructor-arg name ="userDao" ref ="userDao" /> <constructor-arg name ="bookDao" ref ="bookDao" /> </bean >
把构造方法名改成了直接写类型,但是如果有重复类型就会出问题
1 2 3 4 5 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" > <constructor-arg type ="int” value=" 10 "/> <constructor-arg type ="java.lang.String" value ="mysql" /> </bean >
解决参数类型重复问题,使用下标位置解决参数匹配
1 2 3 4 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" > <constructor-arg index ="0" value ="10" /> <constructor-arg index ="1" value ="mysql" /> </bean >
依赖注入方式选择:
强制依赖(bean运行必须要的东西)使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
可选依赖使用setter注入进行,灵活性强
Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
自己开发的模块推荐使用setter注入
自动装配
IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配
自动装配方式
按类型(常用)
按名称
按构造方法
不启用自动装配
这里用按类型配置
1 2 3 4 5 public class BookserviceImpl implements BookService { private BookDao bookDao; public void setBookDao (BookDao bookDao) { this .bookDao = bookDao;
1 2 3 4 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" /> <bean id ="bookService" class ="com.itheima.service.impl.BookServiceImpl" autowire ="byType" />
依赖自动装配特征:
自动装配用于引用类型依赖注入,不能对简单类型进行操作(简单类型比如数字这种,就不需要注入bean了)
使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效
集合注入 就学个格式
1 2 3 4 5 6 7 8 public class BookDaoImpl implements BookDao { private int [] array; private List<string> list; private Set<string> set; private Map<string,string> map; private Properties properties; public void setArray (int []array) {this .array = array; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" > <property name ="array" > <array > <value > xxx</value > </array > </property > <property name ="list" > <list > <value > xxx</value > </list > </property > <property name ="set" > <set > <value > xxx</value > </set > </property > <property name ="map" > <map > <entry key ="x" value ="x" /> </map > </property > <property name ="properties" > <props > <prop key ="x" > x</prop > </props > </property >
案例:数据源对象管理 管理第三方的bean
思考用构造方法还是set方法注入,这个的话得看这个类是怎么写的,这里是找到了set相关的方法
1 2 3 4 5 6 7 <bean id ="dataSource" class ="com.alibaba.druid.pool.DruidDataSource" > <property name ="driverclassName" value ="com.mysql.jdbc.Driver" /> <property name ="url" value ="jdbc:mysql://1ocalhost:3306/spring_db" /> <property name ="username" value ="root" /> <property name ="password" value ="root" /> </bean >
随便输出一下
1 2 3 4 5 6 public class App { public static void main (string[l args) { Applicationcontext ctx = new classPathXmlApplicationcontext ( configLocation: "applicationcontext.xml" ); DataSource dataSource = (DataSource)ctx.getBean( s:"dataSource" ); System.out.println(dataSource); }
加载properties文件 第一步开启context命名空间,下面的context就是新加的命名空间
第二步使用context空问加载properties文件
第三步使用属性占位待${}读取properties 文件中的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <beans xmIns ="http://www.springframework.org/schema/beans' xmlns:xsi=" http: //www.w3.org /2001 /XMLSchema-instance " 下面这个xmlns:就是开了一个新的namespace ,命名空间叫做context xmlns:context ="http://www.springframework.org/schema/context" xsi:schemaLocation =" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd " > 这个叫属性占位符,location是指定要加载的文件 <context:property-placeholder location ="jdbc.properties" /> <bean class ="com.alibaba.druid.pool.DruidDataSpurce" > <property name ="driverclassName" value ="${jdbc.driver}" /> <property name ="url" value ="${jdbc.url}" /> <property name ="username" value ="${jdbc.username}" /> <property name ="password" value ="${jdbc.password}" /> </bean > </beans >
环境变量问题:
有个需要注意的是如果value里的占位符改成username会出问题,这是因为系统中有一个环境变量叫username,会和这里要读取的username冲突,系统的环境变量比这里的优先级高
1 2 3 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" > <property name ="name" value ="${username}" /> </bean >
解决方法是在上面的context里加个参数就不会加载环境变量了
1 2 <context:property-placeholder location ="jdbc.properties" system-properties-mode ="NEVER" />
如何加载多个配置文件:
方式一(不理想):在location的里面用逗号分隔添加多个
1 2 <context:property-placeholder location ="jdbc.properties,jdbc2.properties" system-properties-mode ="NEVER" />
方式二(规范格式):名称用*代替,加载所有配置文件,在前面加上类路径,但是只能读取当前工程里的配置文件
1 2 <context:property-placeholder location ="calsspath:*.properties" system-properties-mode ="NEVER" />
方式三(标准):calsspath后面再加一个星号,这样可以从依赖的jar包读,也可以从自己的工程读
1 2 <context:property-placeholder location ="calsspath*:*.properties" system-properties-mode ="NEVER" />
容器 加载配置文件有两种方法:
1.加载类路径下的配置文件(用这个)
1 Applicationcontext ctx = new classPathXmlApplicationcortext ("applicationcontext.xml" );
2.从文件系统下加载配置文件,用的绝对路径
1 Applicationcontext ctx = new classPathXmlApplicationcortext ("D:\\xxxx\\xxx.xml" );
3.加载多个配置文件
1 Applicationcontext ctx = new classPathXmlApplicationcontext ("bean1.xml" , "bean2.xml" );
获取bean:
1.利用bean名称和强制转换
1 BookDao bookDao=(BookDao)ctx.getBean( "bookDao" );
2.前面获取bean的名称后面获取bean的类型,以上两种没有什么区别
1 BookDao bookDao = ctx.getBean("bookDao" ,BookDao.class);
3.按照类型获取bean,但是这种有个问题就是容器中对应的bean只能有一个,如果有多个bean就会不唯一
1 BookDao bookDao = ctx.getBean(BookDao.class);
这个是最早期的容器初始化方案(已过时)
1 2 3 4 5 6 7 public class AppForBeanFactory { public static void main (string[] args) { Resource resources = new classPathResource ("applicationcontext.xml" ), BeanFactory bf=new XmlBeanFactory (resources); BookDao bookDao=bf.getBean(BookDao.class); bookDao.save();
BeanFactory和applicationcontext的区别:BeanFactory创建完毕后,所有的bean是延迟加载,applicationcontext是立即加载,如果applicationcontext也想延迟加载可以在配置文件里添加lazy-init
1 <bean id ="bookDao" class ="com.itheima.dao.impl.BookDaoImpl" lazy-init ='true"/>
容器相关
BeanFactory是IoC容器的顶层接口,初始化BeanFactory对象时,加载的bean延迟加载
Applicationcontext接口是Spring容器的核心接口,初始化时bean立即加载
Applicationcontext接口提供基础的bean操作相关方法,通过其他接口扩展其功能
Applicationcontext接口常用初始化类
ClassPathXmlApplicationContext
FileSystemXmlApplicationContext(几乎不用)
注解开发 示例:这里的**@Component**是组件的意思,代表的是xml文件中的,后面就是bean的名字,这个名字可以默认不写,默认的话就是空的。加上这个注解,在xml中的标签就可以不用写了
1 2 3 4 5 6 7 @Component("bookDao") public class BookDaoImpl implements BookDao { publis void save () { System.out.println("book dao save ..." ); 有bean名用BookDao)ctx.getBean(s:"bookDao" ); 无bean名用ctx.getBean(BookService.class);
但使用了这个注解也需要spring知道这个,就需要在xml里配置,context:component-scan用于扫描,base-package是指定的位置
1 2 <context:component-scan base-package ="com.itheima.dao.impl" /> com.itheima也可以,这样扫描的就是itheima下所有的
注解开发bean
Spring提供@Component注解的三个衍生注解
@Controller:用于表现层bean定义
@service:用于业务层bean定义
@Repository:用于数据层bean定义
纯注解开发
Spring3.0升级了纯注解开发模式,使用Java类替代配置文件,开启了Spring快速开发赛道
也就是不写配置文件了,换个形式,新建一个config包下的SpringConfig类来代替配置文件,只需要用**@Configuration**就可以代表这个是配置类了,代表的是下图这些
然后用@ComponentScan代替,但是要带上包的路径
1 <context:component-scan base-package ="com.itheima" />
替换完是这样的
1 2 3 @Configuration @ComponentScan("com.itheima") public class springconfig {}
但用注解的话启动程序就也要跟着改,因为原本是需要配置文件来启动的,新建一个AppForAnnotation,new的接口不一样了,剩下的都一样
1 2 3 4 5 6 7 8 public class AppForAnnotation { public static void main (string[] args) { ApplicationContext ctx = new AnnotationConfigApplicationcontext (springconfig.class) BookDao bookDao = (BookDao)ctx.getBean("bookDao" ); System.out.println(bookDao); BookService bookService = ctx.getBean(BookService.class); System.out.println(bookService);
读取Spring核心配置文件初始化容器对象切换为读取Java配置类初始化容器对象
1 2 3 4 ApplicationContext ctx= new ClassPathXmlApplicationContext ("applicationcontext.xml" ); ApplicationContext ctx= new AnnotationConfigApplicationContext (SpringConfig.class);
@Configuration注解用于设定当前类为配置类
@Componentscan注解用于设定扫描路径,此注解只能添加一次,多个数据请用数组格式
1 @Componentscan({"com.itheima.service","com.itheima.dao"})
bean管理和生命周期 注解@Scope(“singleton”)就可以让bean变成单例,prototype是非单例
1 2 3 4 5 6 7 8 9 10 11 12 13 @Repository @scope("singleton") public class BookDaoImpl implements BookDao { public void save () { System.out.println("book dao save ..." ); } @PostConstruct public void init () { System.out.println("init ..." ); } @PreDestroy public void destroy () { System.out.println("destroy ..." );
上面的两个注解弹幕说java8之后就不用了,如果执行后没有销毁,就是需要在运行方法那里使用关闭容器的方式来做,ctx.close();
依赖注入 比如说下面这个程序是存在dao没有注入成功的问题,需要@Autowired这个注解来注入,这个是按类型装配
1 2 3 4 5 6 7 8 9 10 11 @Service public class BookserviceImpl implements Bookservice { @Autowired private BookDao bookDao; public void setBookDao (BookDao bookDao) { this .bookDao = bookDao; } public void save () { System.out.println("book service save ..." ); bookDao.save();
然后发现这个@Autowired注解放在哪里都不影响结果,所以这个set方法不要了也没问题,这个是使用反射里边的暴力反射给它直接加值了
如果是多个dao那就用按名称装配,之前的dao类里的@Repository注解后面加上名字比如(“bookDao”),这样用@Autowired注入bookDao时就按照上面的名字去寻找了。但这种有多个相同的bean推荐使用@Qualifier来指定名称,想注入谁就写谁,并且@Autowired不能删除。
1 2 3 @Autowired @Qualifier("bookDao2") private BookDao bookDao;
使用@Autowired注解开启自动装配模式(按类型)
注意:自动装配基于反射设计创建对象并暴力反射对应属性为私有属性初始化数据,因此无需提供setter方法
注意:自动装配建议使用无参构造方法创建对象(默认),如果不提供对应构造方法,请提供唯一的构造方法
使用@Qualifier注解开启指定名称装配bean
注意:@Qualifier注解无法单独使用,必须配合@Autowired注解使用
简单类型怎么用?@Value
1 2 3 4 @Repository("bookDao") public class BookDaoImpl implements BookDao { @Value("itheima666") private string name;
如果这个value值是从外部的properties文件获取,应该怎么获取
1 2 3 4 5 6 7 @Configuration @ComponentScan("com.itheima") @PropertySource("jdbc.properties") public class springconfig {}上面的value后面改成("${name}" )就行了
注意配置文件的名字不支持通配符*
第三方bean管理
这个springconfig里就不放那个扫描注解了,这里不能直接把配置写在人家的源代码里,所以要自己写,但这些三方配置都写这一个文件里就太多了,所以要拆出去
1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class SpringConfig { @Bean public Datasource datasource () { DruidDataSource ds=new DruidDatasource (); ds.setDriverclassName("com.mysql.jdbc.Driver" ); ds.setUrl("jdbc:mysq1://localhost:3306/spring_db" ); ds.setUsername("root" ); ds.setPassword("root" ); return ds;
拆出去一般就是专门的类管理,使用独立的配置类管理第三方bean,比如jdbcConfig,然后把上面的代码拿到jdbcConfig里面,但是这样会导致springconfig识别不到,解决方法有两种
扫描式(不推荐使用),把这个类也加上@Configuration,然后在SpringConfig里加上扫包注解@ComponentScan,不推荐的原因是下面这个类也写了@Configuration,而且SpringConfig看不出到底加载了哪些配置类
1 2 3 4 5 6 7 8 @Configuration public class JdbcConfig { @Bean public Datasource datasource () { DruidDataSource ds=new DruidDatasource (); ds.setDriverclassName("com.mysql.jdbc.Driver" ); ~~~
导入式,使用@Import注解手动加入配置类到核心配置,此注解只能添加一次,多个数据请用数组格式
1 2 3 @Configuration @Import(JdbcConfig.java) 多个就是({xxx.java,xxx.java})public class SpringConfig {
为第三方bean注入资源 对于简单类型的方式:使用@Value
1 2 3 4 5 public class Jdbcconfig { @Value("com.mysql.jdbc.Driver") private string driver; @Value("jdbc:mysql:/localhost:3306/spring_db") private string url;
@Bean
public Datasource dataSource(){
DruidDatasource ds=new DruidDatasource();
ds.setDriverclassName(driver);
ds.setUr1(ur1);
1 2 3 4 5 6 7 8 9 10 对于引用类型:引用类型注入只需要为bean定义方法设置形参即可,容器会根据类型自动装配对象 假设运行需要dao,dao类写上@Repository(不写会出现NoSuchBeanDefinitionException异常),然后SpringConfig类是下面的这样,JdbcConfig里只加上个形参就可以,这是因为自动装配 ~~~java public class JdbcConfig{ @Value("com.mysql.jdbc.Driver") private string driver; @Value("jdbc:mysql:/localhost:3306/spring_db") private string url;
@Bean
public Datasource dataSource(BookDao bookDao){//这里多了个形参
1 2 3 4 @Configuration @ComponentScan("com.itheima.dao" ) @Import(JdbcConfig.class) public class SpringConfig {
XML配置与注解配置比较
Spring整合MyBatis
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class App { public static void main (string[]args) throws IOException { SqlSessionfactoryBuilder sqlSessionFactoryBuilder = new sqlsessionfactoryBuilder (), InputStream inputStream = Resources.getResourceAsStream("SqlMapCconfig.xml" ); SqlSessionfactory sqlsessionFactory = sqlsessionfactoryBuilder.build(inputstream); SqlSession sqlSession=sqlsessionFactory.opensession(); AccountDao accountDao = sqlsession.getMapper(AccountDao.class); Account ac = accountDao.findById(2 ); System.out.println(ac); sqlsession.close();
依赖要添加两个新的
第一个包是spring里操作数据库专用的包
第二个包时spring整合mybatis用的包,spring提供接口规范,让被整合的按照标准开发
1 2 3 4 5 6 7 8 9 10 11 <dependency > <groupId > org.springframework</groupId > <artifactId > spring-jdbc</artifactId > <version > 5.2.10.RELEASE</version > </dependency > <dependency > <groupId > org.mybatis</groupId > <artifactId > mybatis-spring</artifactId > <version > 1.3.0</version > </dependency >
创建JdbcConfig(三方bean的管理格式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class Jdbcconfig { @Value("${jdbc.driver}") private string driver; @Value("${jdbc.url}") private string url; @Value("${jdbc.username}") private string userName; @Value("${jdbc.password}") private string password; @Bean public Datasource datasource () { DruidDatasource ds = new DruidDatasource (); ds.setDriverClassName(driver); ds.setUr1(url); ds.setUsername(userName); ds.setPassword(password); return ds; } }
创建SpringConfig
1 2 3 4 5 6 7 @Configuration @Componentscan("com.itheima") @ropertySource("classpath:jdbc.properties") @Import({JdbcConfig.class,MybatisConfig.class}) public class SpringConfig { }
写个专门做mybatis的配置类,里面写上sqlsessionfactorybean,然后在springconfig里导入。通过创建这个类来获取factory,xml配置文件里的东西都不能少,如果不在配置文件里配置,也可以通过set方法配置,只需要配置最需要的东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class MybatisConfig { @Bean public SqlsessionFactoryBean sqlsessionFactory (DataSource dataSource) { SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean (); ssfb.setTypeAliasesPackage("com.itheima.domain" ); ssfb.setDataSource(dataSource); return ssfb; } @Bean public MapperScannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer msc = new MapperScannerConfigurer (); msc.setBasePackage("com.itheima.dao" ) return msc; }
以上的只需要改包路径就可以,之前的springConfig.xml就不要了,但之前的运行方式就不能要了,新建一个app2
1 2 3 4 5 6 7 8 public class App2 { public static void main (string[] args) { ApplicationContext ctx = new AnnotationConfigApplicationContext (SpringConfig.class); AccountService accountService = ctx.getBean(AccountService.class); Account ac = accountService.findById(1 ); System.out.println(ac); }
spring整合JUnit 使用spring整合结构的这种方式来运行程序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testFindById () { System.out.println(accountservice.findById(1 )); @Test public void testFindA1l () { System.out.println(accountService.findAll()); }
AOP 简介
AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构
00P(0bject Oriented Programming)面向对象编程
作用:在不惊动原始设计的基础上为其进行功能增强
Spring理念:无入侵式/无侵入式
想让蓝色部分在别的方法也拥有,就把蓝色部分抽取出来做一个单独的方法,然后原始save方法起名叫连接点 ;对追加功能的方法叫做切入点 ,这个切入点就说明了哪些方法要追加功能;让大家都有的方法是一组共性的功能,起名叫通知 ,但怎么就知道要在update和delete上执行这个通知呢,看来在通知和切入点之间还得有个东西把他俩绑定到一块,这样的话一个通知就对应一个切入点,这个东西叫做切面 ,切面描述的是这个通知的共性功能与对应的切入点的关系。然后在java中的方法需要依托通知类 ,不能独立存在
了解完后整体的流程就是:首先找到程序中间的共性功能,写一个通知类,在通知类中定义一个方法,这个方法叫通知,这个方法里放的是共性功能 ,把需要执行对应通知的方法找出定义成切入点,把切入点和通知绑定就得到了切面,切面描述的是在哪个切入点执行哪些通知
连接点(JoinPoint):程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等
切入点(Pointcut):匹配连接点的式子
在SpringAoP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
一个具体方法:com.itheima.dao包下的BookDao接口中的无形参无返回值的save方法
匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
通知(Advice):在切入点处执行的操作,也就是共性功能
通知类:定义通知的类
切面(Aspect):描述通知与切入点的对应关系
切入点范围小,连接点范围大,切入点一定在连接点中
AOP入门案例 设定:在接口执行前输出当前系统时间 开发模式:XML or 注解
思路分析:
导入坐标(pom.xml)
制作连接点方法(原始操作,Dao接口与实现类)
制作共性功能(通知类与通知)
定义切入点
绑定切入点与通知关系(切面 )
导入坐标的话导入spring-context,aop也会被自动导入,再导入一个aspectj
第二步是制作连接点方法
1 2 3 4 5 6 7 @Repository public class BookDaoImpl implements BookDao { public void save () { System.out.println(system.currentTimeMillis()); System.out.println("book dao save ... public void update(){ System.out.println(" book dao update ..."
第三步是把共性功能抽出做新的类,里面写个方法叫method,这就是共性功能
第四步定义切入点,写个私有方法,加上@Pointcut,里面的execution是执行的意思,括号里是返回值void,类全限定名的update方法有没有参数。说明:切入点定义依托一个不具有实际意义的方法进行,即无参数,无返回值,方法体无实际逻辑
第五步,让method方法在切入点的什么位置执行呢,如果是前面就用@before,里面是方法名
第六步加上@Component让它受springbean控制,加上@Aspect让spring知道这里是aop
1 2 3 4 5 6 7 public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt () {} @Before("pt()") public void method () { System.out.println(System.currentTimeMillis());
但现在spring不知道是用注解开发的aop,所以要在配置类加个@EnableAspectJAutoProxy告诉spring这里面有用注解开发的aop,也就是启动了@Aspect去识别里面的东西
1 2 3 4 5 6 @Configuration @Componentscan("com.itheima") @EnableAspectJAutoProxy public class springcinfig { }
AOP工作流程
Spring容器启动
读取所有切面配置中的切入点(配置在@Before的才会生效)
初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
如果匹配失败,就创建对象
匹配成功,创建原始对象(目标对象)的 代理 对象,代理就表示可以用代理对象调用对应方法走增强的那些操作
获取bean执行方法
获取bean,拿上面匹配失败创建的对象去调用方法并执行,完成操作
获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作,这个也就是aop用的是代理模式
核心概念:
目标对象(Target):原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的
代理(Proxy):目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现
没有开启aop的时候打印对象和class类:
com.itheima.dao.impl.BookDaoImpl@279fedbd class com.itheima.dao.impl.BookDaoImpl
开启aop的时候打印对象和class类:
com.itheima.dao.impl.BookDaoImpl@4e50c791 class com.sun.proxy.$Proxy19
因为aop会对最终的对象的toString方法进行重写,直接打印对象比如System.out.println(bookDao);就会带来一些误导
AOP切入点表达式
切入点:要进行增强的方法
切入点表达式:要进行增强的方法的描述方式
比如现在有两个方法
1 2 3 4 5 6 7 8 9 public interface BookDao { public void update () ; } public class BookDaoImpl implements BookDao { public void update () { System.out.println("book dao update ..." ); } }
描述方式一:执行com.itheima.dao包下的BookDao接口中的无参数update方法
execution(void com.itheima.dao.BookDao.update())
描述方式二:执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
execution(void com.itheima.dao.impl.BookDaoImpl.update())
上面两种方式都可以,描述接口或者描述实现类
切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名 )
1 execution (public User com.itheima.service.UserService.findById (int ))
动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点,这个词几乎不动
访问修饰符:public,private等,可以省略
返回值
包名
类/接口名
方法名
参数
异常名:方法定义中抛出指定异常,可以省略,忽略就是public
可以使用通配符描述切入点,快速描述
单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现
1 2 execution(public * com.itheima.*.UserService.find* (*))
匹配com.itheima包下的任意包中的UserService类或接口中所有find开头的(必须)带有一个参数的方法
..:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写
1 execution(public User com..UserService.findById(..))
意思是匹配com包下的任意包中的UserService类或接口中所有名称为findByld的任意方法
+:专用于匹配子类类型
1 execution(* *..*Service+.*(..))
任意返回值,任意包下面以Service结尾的子类的任意方法任意参数,括号里的是*就必须有参数才行
书写技巧:
所有代码按照标准规范开发,否则以下技巧全部失效
描述切入点通常描述接口 ,而不描述实现类,因为描述实现类就紧耦合了
访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
返回值类型对于增删改类使用精准 类型加速匹配,对于查询类使用*通配 快速描述
包名 书写尽量不使用..匹配 ,效率过低,常用*做单个包描述匹配,或精准匹配
接口名 /类名书写名称与模块相关的采用*匹配 ,例如UserService书写成*Service,绑定业务层接口名
方法名 书写以动词 进行精准匹配 ,名词采用*匹配,例如getByld书写成getBy*,selectAl书写成selectAll
参数规则较为复杂,根据业务方法灵活调整
通常不使用异常 作为匹配规则
AOP通知类型
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置
AOP通知共分为5种类型
前置通知
后置通知
环绕通知(重点)
返回后通知(了解 )
抛出异常后通知(了解)
演示:准备以下代码
1 2 3 4 public interface BookDao { public void update () ; puplic iht select () ; }
1 2 3 4 5 6 7 8 9 10 11 @Component @Aspect public class MyAdvice { @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void pt () {} @Before("pt()") public void before () { system.out.println("before advice ..." );} @After("pt()") public void after () { system.out.println("after advice ..." );}
环绕通知这个直接输出的话会输出 around before advice … around after advice …,原方法没有执行,所以必须要加个东西表示对原始操作的调用,方法是在around的括号里加上ProceedingJoinPoint参数,取名pjp,然后用这个调用proceed,这个就是代表对原始操作的调用,这里有红线因为原始操作无法预期是否有异常,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Component @Aspect public class MyAdvice { @Pointcut("execution(int com.itheima.dao.BookDao.update())") private void pt () {} @Pointcut("execution(int com.itheima.dao.BookDao.select())") private void pt2 () {} @Around("pt()") public void around (ProceedingJoinPoint pjp) throws Throwable{ System.out.println("around before advice ..." ); pjp.proceed(); System.out.println("around after advice ..." ); }
但如果新弄一个select的,运行时会抛出异常AopInvocationException,后面的英文意思是空的返回值从这个advance中出来了,它不匹配原始操作调用的返回值类型,原始操作返回的是int,所以要让最后能有个返回值给抛出去,返回类型用object。原始操作其实调用的是impl方法,里面return的是100,那么这100哪去了? pjp.proceed();其实有个返回值,这个返回值就是原本的100
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Around("pt2()") public Object aroundSelect (ProceedingJoinPoint pjp) throws Throwable{ System.out.println("around before advice ..." ); Object proceed = pjp.proceed(); System.out.println("around after advice ..." ); return ret; 以上是标准写法 }
下面代表原始方法执行成功后执行这个动作,了解即可
1 2 3 4 @AfterReturning("pt2()") public void afterReturning () { system.out.println("afterReturning advice ..." ); }
下面是抛出异常后才运行
1 2 3 @AfterThorwing("pt2()") public void afterThrowing () { system.out.println("afterThrowing advice ..." );}
@Around注意事项
环绕通知必须依赖形参ProceedingJoinPoint才能实现对原始方法的调用,进而实现原始方法调用前后同时添加通知
通知中如果未使用ProceedingjoinPoint对原始方法进行调用将跳过原始方法的执行,会产生一种对原始操作进行隔离的效果,比如做权限校验
对原始方法的调用可以不接收返回值,通知方法设置成void即可,如果接收返回值,必须设定为Obiect类型
原始方法的返回值如果是void类型,通知方法的返回值类型可以设置成void,也可以设置成Object
由于无法预知原始方法运行后是否会抛出异常,因此环绕通知方法必须抛出Throwable对象
案例:测量业务层接口万次执行效率 需求:任意业务层接口执行均可显示其执行效率(执行时长) 分析:
业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
通知类型选择前后均可以增强的类型——环绕通知
准备以上数据
业务层
1 2 3 4 5 6 public interface Accountservice { void save (Account account) ; void delete (Integer id) ; void update (Account account) ; List<Account> findA11 () ; Account findById (Integer id) ;
第一步,先在springConfig里把aop注解开启
1 2 3 4 5 6 @Configuration @Componentscan("com.itheima") @PropertySource("classpath:jdbc.properties") @Import({Jdbcconfig.class,MybatisConfig.class}) @EnableAspectJAutoProxy public class springconfig {}
然后创建aop包,新建ProjectAdvice类,我们是对业务层接口做监控所以起名也是这个,返回值不好说是什么,所以用通配,方法是任意方法,参数是什么都行。然后用循环去执行原始方法,最后打印结果,但这个并不能用,因为并不知道执行时间是哪个具体的方法,而ProceedingJoinPoint描述的是原来那个方法的执行对象,通过这个拿到执行的签名信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Component @Aspect public class ProjectAdvice { @Pointcut("execution(* com.itheima.service.*Service.*(..))") private void servicePt () {} @Around("ProjectAdvice.servicePt()") public void runspeed (ProceedingJoinPoint pjp) throws Throwable{ Signature signature = pjp.getsignature(); String className = signature.getDeclaringTypeName(); String methodName = signature.getName(); long start=system.currentTimeMillis(); for (int i = 0 ;i < 10000 ; i++){ pjp.proceed(); } long end = System.currentTimeMillis(); System.out.println("万次执行:" + className +"." + methodName +"---->" +(end-start)+ "ms" ); } }
补充说明:当前测试的接口执行效率仅仅是一个理论值,并不是一次完整的执行过程
AOP通知获取数据
获取切入点方法的参数
JoinPoint:适用于前置、后置、返回后、抛出异常后通知(每个都有)
ProceedJointPoint:适用于around环绕通知
获取切入点方法返回值
返回后通知(afterReturning)
环绕通知(around)
获取切入点方法运行异常信息
这个参数是pjp的父
1 2 3 4 5 6 @Before("pt()") public void before (JoinPoint jp) { object[] args = jp.getArgs(); System.out.println(Arrays.tostring(args)); System.out.println("before advice ..." ); }
然后在bookdao添加参数
1 2 3 public interface BookDao { public String findName (int id,String password) ; }
1 2 3 4 5 6 @Repository public class BookDaoImpl implements BookDao { public string findName (int id,string password) { System.out.println(“id:"+id); return " itcast"; }
启动程序
1 2 3 4 5 6 7 8 9 10 11 12 public class App { public static void main (string[] args) { ApplicationContext ctx= new AnnotationConfigApplicationcontext (Springconfig.class); BookDao bookDao = ctx.getBean(BookDao.class); String name = bookDao.findName(id:100 ,password: "itheima" ); System.out.println(name): } } 然后输出:[100 , itheima] before advice ... id:100 itcast
参数的调用:
其他的都一样这样用,对于around,父接口能调用的方法子接口也可以用,所以直接pjp.getArgs调用对应参数
在调用原始方法时,对于proceed这个操作除了空参外还可以传个object数组,也就是可以把上面的args传进去
1 2 3 4 5 6 7 8 9 10 11 @Around(“pt()") public object around (ProceedingJoinPoint pjp) throws Throwable { object[] args = pjp.getArgs(); System.out.println(Arrays.tostring(args)); object ret = pjp.proceed(args); return ret; } 输出[100 ,itheima] id:100 itcast
但如果在获取之后调用之前把args的值更改了,比如args[0] = 666;最后输出的结果也会被更改,所以如果传过来的参数有问题,我们就可以先处理一下,这里可以保证程序的健壮性,不需要每个程序都写,一个aop就可以搞定
返回值的调用 :
一个是around一个是afterReturning,around的返回值就是ret
而afterReturning要拿返回值,可以先定义一个用来接受返回值的形参,但如果定义了后就要告诉@AfterReturning用ret这个变量准备接受返回值。点进@AfterReturning可以看到提供了很多东西,其中一个是returning和value,returning的值要和形参名对应
1 2 3 4 @AfterReturning(value = "pt()", returning = "ret") public void afterReturning (Object ret) { System.out.println("afterReturning advice ..." +ret); }
当JoinPoint和Object同时存在,JoinPoint必须在第一个位置,其他的也是一样,如果不是这样就会报IllegalArgumentException异常
1 public void afterReturning (JoinPoint jp,Object ret) {
异常对象的调用:
两种写法,第一种是
1 2 3 4 5 6 7 8 9 10 11 public object around (ProceedingJoinPoint pjp) { object[] args = pjp.getArgs(); System.out.println(Arrays.tostring(args)); object ret = null ; try { ret = pjp.proceed(args); } catch (Throwable throwable){ throwable.printStackTrace(); } return ret; }
第二种
1 2 3 4 @AfterThrowing(value = "pt()",throwing ="t") public void afterThrowing (Throwable t) { System.out.println("afteiThrowing advice ..." +t) }
案例:百度网盘密码数据兼容处理
分析: ①:在业务方法执行之前对所有的输入参数进行格式处理——trim(0) ②:使用处理后的参数调用原始方法–环绕通知中存在对原始方法的调用
准备代码
1 2 3 public interface ResourcesService { public boolean openURL (string url ,string password) ; }
1 2 3 4 5 6 7 8 @Service public class ResourcesServiceImpl implements ResourcesService { @Autowired private ResourcesDao resourcesDao; public boolean openURL (string url, string password) { return resourcesDao.readResources(url,password); }
1 2 public interface ResourcesDao { boolean readResources (String url,String password) ;
1 2 3 4 5 6 @Repository public class ResourcesDaoImpl implements ResourcesDao { public boolean readResources (String url,String password) { return password.equals("root" ); }
1 2 3 4 5 6 7 public class App { public static void main (string[l args) { ApplicationContext ctx = new AnnotationConfigApplicationcontext (SpringConfig.class); ResourcesService resourcesService = ctx.getBean(ResourcesService.class): boolean flag = resourcesService.openURL( url: "http://pan.baidu.com/haha" , password: "root" ), System.out.println(flag); }
现在简单运行一下,返回的是true,如果把root后面加个空格就会返回false,我们想要的效果是加上空格也能返回true,也就是帮我把这个空格去掉
首先开启aop注解
1 2 3 4 @Configuration @Componentscan("com.itheima") @EnableAspectJAutoProxy public class springconfig {
新建aop处理类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class DataAdvice { @Pointcut("execution(boolean com.itheima.service.*service.*(*,*))") private void servicePt () {} @Around("DataAdvice.servicePt()") public object trimstr (ProceedingJoinPoint pjp) throws Throwable { object[l args = pjp.getArgs(); for (int i = 0 ; i < args.length; i++){ if (args[i].getClass().equals(String.class)){ args[i] = args[i].toString().trim(); } } Obiect ret = pip.proceed(args), return ret; }
如果想测试做没做对就可以在输出程序app里添加
1 System.out.println(password.length());
然后把root改成1234空格空格,打印出来的长度为4就是对的
AOP总结
概念:AOP(Aspect 0riented Programming)面向切面编程,一种编程范式
作用:在不惊动原始设计的基础上为方法进行功能增强
核心概念
代理(Proxy):SpringAoP的核心本质是采用代理模式实现的
连接点(JoinPoint):在SpringA0P中,理解为任意方法的执行
切入点(Pointcut):匹配连接点的式子,也是具有共性功能的方法描述
通知(Advice):若干个方法的共性功能,在切入点处执行,最终体现为一个方法
切面(Aspect):描述通知与切入点的对应关系
目标对象(Target):被代理的原始对象成为目标对象
切入点表达式书写技巧
按标准规范 开发
查询操作的返回值建议使用*匹配
减少使用..的形式描述包
对接口进行描述 ,使用*表示模块名,例如userservice的匹配描述为*Service
方法名书写保留动词,例如get,使用表示名词,例如getById匹配描述为getBy
参数根据实际情况灵活调整
环绕通知可以模拟出其他四种通知,比如只在调用原始操作前做事情,这就是前置通知,用try catch写完后在finally写的东西就是后置通知,而返回后通知是在你的try catch finally中try大括号结束之前,原始调用方法之后的那些可以模拟返回后通知,catch模拟抛出异常后通知
环绕通知(重点 )
环绕通知依赖形参ProceedingJoinPoint才能实现对原始方法的调用
环绕通知可以隔离原始方法的调用执行
环绕通知返回值设置为0bject类型
环绕通知中可以对原始方法调用过程中出现的异常进行处理
spring事务简介 案例:银行账户转账
事务作用:在数据层保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
spring提供了一个接口,叫做平台事务管理器,大概意思是提交的时候都提交,回滚的时候都回滚
1 2 3 4 public interface PlatformTransactionManager { void commit (TransactionStatus status) throws TransactionException; void rollback (TransactionStatus status) throws TransactionException; }
实现类
1 2 public class DatasourceTransactionManager {}
但是这个内部使用的是jdbc的事务,如果用的是jdbc的技术就可以用这个,而mybatis内部就是用的jdbc
案例:模拟银行账户间转账业务 需求:实现任意两个账户间转账操作 需求微缩:A账户减钱,B账户加钱
分析 ①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney) ②:业务层提供转账操作(transfer),调用减钱与加钱的操作 ③:提供2个账号和操作金额执行转账操作 ④:基于Sprinq整合MyBatis环境搭建上述操作
转账接口:
1 2 3 4 5 6 7 8 9 public interface AccountService { @Transactional public void transfer (String out,String in ,Double money) ;
数据层接口:
1 2 3 4 5 6 public interface AccountDao @Update("update tbl_account set money = money + #{money} where name = #{name}") void inMoney (@Param("name") String name, @Param("money") Double money) ; @Update("update tbl_account set money = money - #{money} where name = #{name}") void outMoney (@Param("name") String name, @Param("money") Double money) ;
业务层实现类:
1 2 3 4 5 6 7 8 9 @Service public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; public void transfer (String out,String in ,Double money) { accountDao.outMoney(out,money); accountDao.inMoney(in,money); }
测试
1 2 3 4 5 6 7 8 9 10 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer () throws IOException { accountService.transfer("Tom" ,"Jerry" ,100D ); }
结果分析 ①:程序正常执行时,账户金额A减B加,没有问题 ②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败
第一步:要在接口上添加事务注解
第二部,到jdbc配置中配一个事务管理器
1 2 3 4 5 @Bean public PlatformTransactionManager transactionManager (DataSource dataSource) { DataSourceTransactionManager transactionManager = new DataSourceTransactionManager (); transactionManager.setDataSource(dataSource); return transactionManager;
第三步要让spring知道你是用注解形式开启事务管理,到spring的配置文件中配置
1 2 3 4 5 6 @Configuration @Componentscan("com.itheima") @PropertySource("classpath:jdbc.properties") @Import({Jdbcconfig.class,Mybatisconfig.class}) @EnableTransactionManagement public class springconfig {
注意事项:Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务
spring事务角色 上一节的案例里业务层方法中使用了这个操作
1 2 3 4 5 @Transactional public void transfer (string out,string in ,Double money) { accountDao.outMoney(out,money); accountDao.inMoney(in,money); }
而这两个操作分别对应独立的数据层操作
1 2 3 4 public interface AccountDao { @Update("update tbl_account set money = money -#{money} where name = #{name}" void outMoney(@Param("name") String name, @Param("money") Double money); }
1 2 3 public interface AccountDao { @Update("update tbl_account set money = money + #{money} where name = #{name}" void inMoney(@Param("name")String name, @Param("money")Double money);
上面两个属于增删改操作所以都要开启事务,分别叫t1t2,这两个是不同的事务,假如第二个出现了异常,第一个不会去回滚。当业务层加上开启事务注解后就会有事务T,目前是三个事务,谁也不挨谁,于是spring在这里做了一件事,既然数据层都在业务层的控制范围内,这两个事务都改成和他一样的事务。讲spring开启的这个事务叫做事务管理员,将加入事务管理员的这些成员叫做事务的协调员
事务角色
事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法
事务协调员:加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法
还有个需要注意的是在写程序的时候jdbc和mybatis里都注入了一个datasource,正因为使用了同一个datasource,所以才能够进行统一管理
事务相关配置
属性
作用
示例
readOnly
设置是否为只读事务
readonly=true 只读事务
timeout
设置事务超时时间
timeout =-1(永不超时)
rollbackFor
设置事务回滚异常(class)
rollbackFor ={NullPointException.class}
rollbackForClassName
设置事务回滚异常(string)
同上格式为字符串
noRollbackFor
设置事务不回滚异常(class)
noRollbackFor ={NullPointException.class}
noRollbackForClassName
设置事务不回滚异常(string)
同上格式为字符串
propagation
设置事务传播行为
…..
点进事务按ctrl+f12可以查看类的方法
1 2 3 public interface AccountService { @Transactinnal(readOnly = true,timeout = -1) public void transfer (String out,String in ,Double money) throws IoException;
重点是rollbackFor遇到某一类异常回滚事务
一般遇到这两种会进行回滚,第一种是error系列的错误,比如内存溢出,第二种是运行时异常,而IOException这种属于编译时异常。而你希望在IOException异常发生后进行回滚就可以这样写
1 @Transactional(rollbackFor ={I0Exception.class)
案例:转账业务追加日志 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕 需求微缩:A账户减钱,B账户加钱,数据库记录日志
分析:
基于转账操作案例添加日志模块,实现数据库中记录日志
业务层转账操作(transfer),调用减钱、加钱与记录日志功能
实现效果预期 :
无论转账操作是否成功,均进行转账操作的日志留痕
log业务层
1 2 3 4 public interface LogService { @Transactional void log (string out, string in, Double money) ; }
实现类
1 2 3 4 5 6 7 8 @Service public class LogserviceImpl implements LogService { @Autowired private LogDao logDao; public void log (String out,String in,Double money ) { logDao.log( info:"转账操作由" +out+"到" +in+",金额:" +money); }
数据层
1 2 3 public interface LogDao { @Insert("insert into tbl log(info,createDate)values(#{info},now())") void log (String info) ;
实现类,回过头看要求是无论转账操作是否成功,均进行转账操作的日志留痕,怎样保证代码一定会执行,可以用try finally方法,把有可能出问题的代码放上面,finally放一定执行的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Service public class AccountServiceImpl implements Accountservice { @Autowired private AccountDao accountDao; @Autowired private LogService logService; public void transfer (String out,String in ,Double money) { try { accountDao.outMoney(out,money); int i = 1 /0 ; accountDao.inMoney(in,money); }finally { logService.log(out,in,money); }
然后在accountService和LogService都开启事务,去数据库查看运行效果,发现没有问题,然后在程序里模拟异常,然后再查看数据库,这时候金额没有发生变化,但日志也没有被记录
存在的问题 : 日志的记录与转账操作隶属同一个事务,同成功同失败
实现效果预期改进 : 无论转账操作是否成功,日志必须保留
这里学习到一个事务传播行为 :事务协调员对事务管理员所携带事务的处理态度,协调员是加入呢还是不加入还是搞一个新的。
这里是搞一个新的事务,点开LogService的事务注解,里面有个方法是propagation,里面有一系列的默认值,用法是如下
1 @Transactinnal(propagation = Propagation.REQUIRES_NEW)
SpringMVC SpringMVC简介 SpringMVC概述
SpringMVc技术与Servlet技术功能等同,均属于web层开发技术
优点
使用简单,开发便捷(相比于Servlet)
灵活性强
以下是用servlet的保存代码(仅演示,除此之外还有其他三个不同功能的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Webservlet("/user/save") public class UserSaveServlet extends HttpServlet { @Override protected void doGet (HttpservletRequest reg, HttpservletResponse resp) throws ServletException, IoException { String name = reg.getParameter( name:"name" ); System.out.println("servlet save name ==>" + name); resp.setcontentType("text/json;charset=utf-8" ); PrintWriter pw=resp.getWriter(); pw.write( s:"{'module':'servlet save'}" ); } @override protected void doPost (HttpservletRequest reg, HttpservletResponse resp) throws ServletException, IOException { this .doGet(reg,resp); }
用mvc做就变成只有一个类,里面集成了四个方法
1 2 3 4 5 6 7 @Controller public class Usercontroller { @RequestMapping("/save") @ResponseBody public String save (String name) { System.out.println("springmvc save name ==>" + name); return "{'module':'springmvc save'}" ;
入门案例 ①:使用SpringMVc技术需要先导入SpringMVc坐标与Servlet坐标,也就是导入maven
②:创建SpringMVC控制器类(等同于servlet功能),用@Component是定义bean,但在mvc中开发表现层的bean要用@Controller这个注解
③:初始化SpringMVC环境(同Spring环境),设定SpringMVC加载对应的bean
④:初始化Servlet容器,加载SpringMVC环境,并设置SpringMVC技术处理的请求。在mvc中提供了一个专用的开发web容器的配置类,只需要自己定义一个类然后继承就行了
然后新建项目,在pom.xml配置,导入servlet包说会与tomcat有冲突,之前的web里学的是启动项参数(大概),这里是加了个标签provided,意思是在编译时可用运行时不可用,test相反
1 2 3 4 5 6 <dependency > <groupId > javax.servlet</groupId > <artifactId > javax.servlet-api</artifactId > <version > 3.1.0</version > <scope > provided</scope > </dependency >
然后新建controller包UserController类,返回json就是String类型
1 2 3 4 5 6 7 8 9 10 11 12 @Controller public class UserController { @RequestMapping("/save") @ResponseBody public String save () { System.out.println("user save...”); return " {'module' :'springmvc' }"; }
再新建一个spring配置类
1 2 3 4 @Configuration @ComponentScan("com.itheima.controller") public class SpringMvcConfig {}
再做一个tomcat容器启动的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class ServletContainersInitConfig extends AbstractDispatcherServletInitializer { @Override protected WebApplicationContext createServletApplicationContext () { AnnotationConfigWebApplicationcontext ctx = new AnnotationConfigWebApplicationContext (); ctx.register(SpringMvcConfig.class); return ctx; } protected String[] getServletMappings(){ return new string []{"/" }; } protected WebApplicationContext createRootApplicationContext () { AnnotationConfigWebApplicationcontext ctx = new AnnotationConfigWebApplicationContext (); ctx.register(SpringConfig.class); return ctx; }
名称:@Controller
类型:类注解
位置:SpringMvc控制器类定义上方
作用:设定SpringMVc的核心控制器bean
名称:@RequestMapping
类型:方法注解
位置:SpringMVc控制器方法定义上方
作用:设置当前控制器方法请求访问路径
相关属性:value:请求访问路径
名称:@ResponseBody
类型:方法注解
位置:SpringMVc控制器方法定义上方
作用:设置当前控制器方法响应内容为当前返回值,无需解析
AbstractDispatcherServletInitializer类是SpringMVC提供的快速初始化Web3.8容器的抽象类
AbstractDispatcherServletInitializer提供三个接口方法供用户实现
createServletApplicationContext()方法,创建Servlet容器时,加载SpringMVC对应的bean并放入 WebApplicationContext对象范围中,而webApplicationContext的作用范围为ServletContext范围,即整个web容器范围
getservletMappings()方法,设定SpringMVC对应的请求映射路径,设置为/表示拦截所有请求,任意请求都将转入到SpringMVc进行处理
createRootApplicationcontext()方法,如果创建Servlet容器时需要加载非SpringMVC对应的bean,使用当前方法进行,使用方式同createservletApplicationcontext()
入门案例工作流程分析 启动服务器初始化过程:
服务器启动,执行ServletContainersInitConfig类,初始化web容器
执行createServletApplicationContext方法,创建了WebApplicationcontext对象,这个对象就是mvc最终体现的对象,有了这个对象就被加载到了web容器中,但被加载到什么位置呢,就是ServletContext,最大范围的这个
加载SpringMvcConfig
执行@Componentscan加载对应的bean
加载Usercontroller,每个@RequestMapping的名称对应一个具体的方法
执行getservletMappings方法,定义所有的请求都通过SpringMVC
[]getRootconfigclasses(){ return new class[]{SpringConfig.class}; } protected class<?>[]getServletconfigclasses(){ return new Class[]{SpringMvcConfig.class}; } protected string[l getservletMappings(){ return new string[]{“/“}; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 ## 网页调试与发送网页HTTP请求的插件 老师教的是postman,但我用的是apifox,所以看个原理就行 主要作用是模拟浏览器发送请求,比如发送get请求在地址栏写一下就行,但post请求就得写个表单,ajax就得写个js文件,但使用了这个工具,上面的工作就不用做了 先新建工作空间,这里面的操作可以备份和记录,然后新建请求,选择请求的类型比如get,然后输入地址点击发送就可以 ## 请求与响应 ### 请求的映射路径 准备数据 ~~~java @Controller @RequestMapping("/user") public class Usercontroller { @RequestMapping("/save") //@RequestMapping("/user/save") @ResponseBody public String save(){ System.out.println("user save.."); return "{'module':'user save'}"; } @RequestMapping("/delete") //@RequestMapping("/user/delete") @ResponseBody public String delete(){ System.out.println("user delete..."); return "{'module':'user delete'}"; } }
1 2 3 4 5 6 7 8 9 @Controller public class Bookcontroller { @RequestMapping("/book/save") @ResponseBody public String save () { System.out.println("book save ..." ); return "{'module':'book save'}" ; }
这里面有两个save,然后启动一下发现报错
com.itheima.controller.UserController#save() to{/save}: There is already “bookController’ bean method
意思是/save已经被bookController的bean方法用过了
1.团队多人开发,每人设置不同的请求路径,冲突问题如何解决——设置模块名作为请求路径前缀,比如@RequestMapping(“/user/save”)
也可以把@RequestMapping(“/user”)放在整个类上面定义整个模块的请求路径前缀
请求方式 get方式:
准备数据ServletContainersInitConfig简单配置,SpringMvcConfig
1 2 3 @Configuration @Componentscan("com.itheima.controller") public class springMvcconfig {
控制器类,如果请求的路径是http://localhost/commonParam?name=itcast,这个name参数怎么接收呢,在括号里直接添加String来接收就行,如果是多个参数就在后面接着添加就行
1 2 3 4 5 6 7 8 9 @Controller public class UserController { @RequestMapping("/commonParam") @ResponseBody public String commonParam (String name) { System.out.println("普通参数传递 name ==>" + name); return "{'module':'common param'}" ; }
Post方式:
在这里不管是post还是get请求,对于后台代码是没有变化的不区分的,和之前的Servlet是不一样的。请求地址:http://localhost/commonParam,然后在老师演示的工具里不能再点params了,因为post请求的参数属于请求体内,所以要点body这项
如果是发表单数据应该用这个选项,和左边的区别在于这个不仅能发表单还能发文件,代码还是使用上面的,设置以下参数
如果把英文值改成中文,就会出现乱码现象,之前的处理方式是在web服务器设置一个过滤器,这里也是一样,只不过要在ServletContainersInitConfig里配置,用override覆盖方法。可以看到这里是要一个Filter数组,那么就可以直接new一个filter数组然后把过滤器对象放进去。但是过滤器在哪,springmvc有现成的字符过滤器,设定好字符集后直接把filter放进去就行
1 2 3 4 5 6 7 8 9 10 11 12 13 public class ServletContainersInitConfig extends AbstractAnnotationConfigpispatcherservletInitializer { protected Class<?>[] getRootConfigClasses(){ return new Class [0 ];} protected Class<?>[] getServletConfigClasses() { return new Class []{SpringMvcConfig.class}; } protected String[] getServletMappings(){ return new String []{"/" }; } @Override protected Filter[] getservletFilters(){ CharacterEncodingFilter filter = new characterEncodingFilter (); filter.setEncoding("UTF-8" ); return new Filter []{filter); }
这个characterEncodingFilter在哪里?图下坐标包含了这个
请求参数 **普通参数:**url地址传参,地址参数名与形参变量名相同,定义形参即可接收参数
之前提到过一个问题就是get请求里的name与代码里的名字不一样,还能接收吗,答案是不能,解决方式是添加@RequestParam,也就是把请求参数中的name给到username
1 2 3 @RequestMapping("/commonParamDifferentName") @ResponseBody public string commonParamDifferentName (@RequestParam("name") String userName , int age) {
**pojo参数:**传参的目的是通过大量的数据组成一个model,下面这个user里面有两个属性name和age,然后发送http://localhost/pojoParam?name=itcast&age=15,还是能被接收到,也就是前面的属性名和后面实体类的属性名一样就可以自动把属性塞进去
1 2 3 4 5 @RequestMapping("/pojoParam") @ResponseBody public String pojoParam (User user) {
1 2 3 4 5 public class User { private string name; private int age; private Address address;
然后还有个实体类里面定义了省市,然后封装成一个对象,然后加到上面
1 2 3 public class Address { private String province; private String city;
然后要给实体类里某个对象传属性值应该怎么做,以下这样做就可以
**数组参数:**比如你的个人爱好之类的,是很多个数据,比如复选框,可以用以下接收
1 2 3 4 @RequestMapping("/arrayParam") @ResponseBody public String arrayParam (String[] likes) {
但是java编程一般用集合比较多
集合参数:
还是用上面的数据发送看看有什么结果,发现报错了,提示NoSuchMethodException没有这个方法,后面还有java.util.List.(),只要出现代表的就是构造方法,list是个接口本身就不会有构造方法。他现在是在尝试造一个list类型的对象,为什么要造这个对象呢,因为这是引用类型,上面的那些也都是引用类型,所以都是准备造个对象然后setlike属性,但我们要的是作为集合的参数放进去。
用@RequestParam告诉他吧那些东西作为参数扔进去,只要是集合就不按照造对象的方法来了
1 2 3 4 5 @RequestMapping("/listParam") @ResponseBody /public String listParam (@RequestParam List<String> likes) {
名称:@RequestParam
类型:形参注解
位置:SpringMVc控制器方法形参定义前面
作用:绑定请求参数与处理器方法形参间的关系
参数:
required:是否为必传参数
defaultValue:参数默认值
json数据 先在pom导入json,然后发送数据,在body里选择raw然后选择json
接收代码,但目前mvc不知道外面过来的json要转化成List,需要开启一个功能键,让它帮忙做这种转化
1 2 3 4 @RequestMapping("/listParamForJson") @ResponseBody public String listParamForJson (@RequestBody List<String> likes)
在mvc配置中添加
1 2 3 4 @Configuration @ComponentScan("com.itheima.controller") @EnableWebMvc public class springMvcconfig {
然后运行还是报错无法造对象,然后这次信息是在请求体里,也就是requestbody中,@RequestParam就不好用了,用@RequestBody就可以了
如果是传给pojo的,在body里变成这样,然后加上@RequestBody就行
集合也是一样
@RequestBody与@RequestParam区别
区别
@RequestParam用于接收url地址传参,表单传参【application/x-www-form-urlencoded】
@RequestBody用于接收json数据【application/json】,在postman软件里发送的请求头中找到content-type,这里指定的是application/json,老师说以前手写发ajax强求时这项要自己手写,现在自动加上了,如果是post的话这一项就变成了application/x-www-form-urlencoded
应用
后期开发中,发送json格式数据为主,@RequestBody应用较广
如果发送非ison格式数据,选用@RequestParam接收请求参数
日期类型参数传递 如果是标准日期格式比如2012/02/02是可以直接由字符串转换成日期格式,但如果不是标准格式就需要用@DateTimeFormat后面带上格式就行
1 2 3 4 5 @RequestMapping(Ov"/dataParam" @ResponseBody public string dataParam(Date date, @DateTimeFormat(pattern="yyyy-MM-dd")Date date1, @DateTimeFormat(pattern="yyyy/MM/dd HH:mm:ss")Date date2){
这个注解内部是怎么工作的呢?里面用到了类型转换器converter,它是个接口,打开时注意包的位置在springframework里,这个接口就定义了一个方法T convert(S source);,这个接口的作用是将你传递的参数与我们要收集的参数进行转换,有非常多的实现类来帮助转换
响应 相应页面,注意这里没有加@RequsetBody
1 2 3 4 5 6 7 8 @Controller public class UserController { @RequestMapping("/toJumpPage") public String toJumpPage () { System.out.println("跳转页面" ); return "page.jsp" ; }
1 2 3 4 5 <html> <body> <h2>Hejlo Spring Mvc!</h2> </body> </html>
然后访问路径就可以看到page.jsp的内容被输出了,在mvc中想要响应页面只需要把页面名称写在return里然后返回字符串就可以了
如果只想返回文本呢,如果没加@ResponseBody会报错No mapping for GET/response text,它在找一个叫response text的页面,而response text只是个文本,所以要加上@ResponseBody就行了
1 2 3 4 5 6 @RequestMapping("/toText") @ResponseBody public String toText () { System.out.println("返回纯文本数据" ); return "response text" ;
然后是响应json数据,如果想响应pojo对象,只需要把方法的返回值设成pojo对象然后再return这个对象就行了
1 2 3 4 5 6 7 8 9 @RequestMapping("/toJsonP0J0") public User to]sonPo]0 (){ System.out.println("返回json对象数据" ); User user = new User (); user.setName("itcast" ); user.setAge(15 ); return user; }
但这样运行会报错,因为mvc对User返回值的要求默认是要个字符串,会把return user认成字符串”user”,所以还是要加@ResponseBody,而转换的这个操作是pom里导入的jackson帮我们做的,集合的获取也是这样
名称:@ResponseBody
类型:方法注解
位置:SpringMVc控制器方法定义上方
作用:设置当前控制器方法响应内容为当前返回值,无需解析
作用:设置当前控制器返回值作为响应体
实现这个转换的过程是一个叫HttpMessageConverter的接口,专门转换http消息的,打开类型层次图找到MappingJackson2HttpMessageConverter就是最终的实现类了
REST风格
REST(Representational state Transfer),表现形式状态转换,在开发中就是叫做访问网络资源的格式
这两个链接和上面的作用完全一样
上述行为是约定方式,约定不是规范,可以打破,所以称REST风格,而不是REST规范 描述模块的名称通常使用复数,也就是加s的格式描述,表示此类资源,而非单个资源,例如:users、books、accounts.
根据REST风格对资源进行访问称为RESTful,开发使用这个风格开发就叫RESTful
案例 根据之前的代码进行更改
1 2 3 4 5 6 7 @Controller public class UserController {@RequestMapping("/save") @ResponseBody public String save (@RequestBody User user) { System.out.println("user save..." + user); return "{'module':'user save'}" ;
然后更改,先不传参,第一链接那里要写users,第二步指定请求行为,点进RequestMapping可以看到有个叫method的,这个可以设定对应的请求方法
1 2 3 4 5 6 7 @Controller public class UserController {@RequestMapping(Value ="/users",method = RequestMethod.POST) @ResponseBody public String save () { System.out.println("user save..." ); return "{'module':'user save'}" ;
再改个delete,在请求路径上包含了一个值1,要想让id参数对应请求路径就要用到@PathVariable,也就是路径变量的意思,只有这个注解不够,还要告诉来自路径中的哪里,所以要加/{id}进行匹配
1 2 3 4 5 6 @RequestMapping(value = "/users/{id},method = RequestMethod.DELETE) @ResponseBody public string delete (@PathVariable Integer id) {”+ id); System.out.println("user delete... return " {'module' :'user delete' }";
@RequestBody、@RequestParam、@PathVariable
区别
@RequestParam用于接收url地址传参或表单传参
@RequestBody用于接收json数据
@PathVariable用于接收路径参数,使用{参数名称}描述路径参数
应用
后期开发中,发送请求参数超过1个时,以json格式为主,@RequestBody应用较广
如果发送非json格式数据,选用@RequestParam接收请求参数
采用RESTfu1进行开发,当参数数量较少时,例如1个,可以采用@Pathvariable接收请求路径变量,通常用于传递id值
restful快速开发 value=”/books”和@ResponseBody这种重复代码写起来很繁琐能不能简化呢。可以用@ReqstMapping(“/books”)代替前面重复的value那些,但是有参数的还是要留着{id}这种。
然后@ResponseBody可以拿到外面了,而springmvc给合成了一个@RestController,里面包含了这两个注解。
@RequestMapping(value = “/{id}”,method = RequestMethod.DELETE)这个就简化成了DeleteMapping(“/{id}”)
1 2 3 4 5 6 7 8 9 10 @RestController @ReqstMapping("/books”) public class BookController { public string delete (@PathVariable Integer id) { System.out.println("book delete..." + id); return "{'module':'book delete'}" ;
案例:基于RESTful页面数据交互 能正常展示数据,并且点击新建按钮输入信息后能提交到后台
先把后台的controller做出来然后再使用postman测通,然后让页面能运行,然后发送ajax提交能访问后台表现层接口
准备数据
1 2 3 4 @Configuration @Componentscan("com.itheima.controller") @EnableWebMvc public class SpringMvcConfig {
1 2 3 4 5 6 7 8 9 10 11 12 public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer { protected Class<?>[] getRootconfigclasses(){ return new class [0 ]; } protected Class<?>[] getServletConfigclasses(){ return new class []{springMvcConfig.class}; } protected String[] getservletMappings(){ return new String []{"/" }; } @Override protected Filter[] getservletFilters(){ CharacterEncodingFilter filter = new CharacterEncodingFilter (); filter.setEncoding("UTF-8" ); return new Filter []{filter}; }
domain包下Book类
1 2 3 4 5 6 public class Book { private Integer id; private String type; private String name; private String description; 下面是tostring和setget方法
创建BookController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping("/books") public class BookController { @PostMapping public String save (@RequestBody Book book) { System.out.println("book save ==>" + book); return "{'module':'book save success'}" ; } @GetMapping public List<Book> getAll () { List<Book>bookList = new ArrayList <Book>(); Book book1 = new Book (); book1.setType("计算机" ); book1.setName("springMvc入门教程" ); book1.setDescription("小试牛刀" ); bookList.add(book1); Book book2 = new Book (); book2.setType("计算机" ); book2.setName("springMVc实战教程" ); book2.setDescription("一代宗师" ); bookList.add(book2); return bookList:
然后postman发送和接收一下测试测试
然后把html的那些东西复制到webapp文件夹里,运行一下服务器,然后访问html网址,结果404,后台输出提示[WARNING] No mapping for GET /pages/books.html,是被mvc拦截了,它认为应该有个配置叫books.html,所以应该把这个放行交给tomcat处理,问题出在下面,”/“是把所有请求都经过mvc,应该把静态资源不过mvc
1 2 public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer { protected String[] getservletMappings(){ return new String []{"/" }; }
方案一:做一个功能类,在config里新建一个SpringmvcSupport
1 2 3 4 5 6 @Configuration public class SpringMvcSupport extends WebMvcconfigurationSupport { @override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**" ).addResourceLocations("/pages/" );
写完上面的代码记得必须要有个动作就是让mvc启动时加载这里,加上@Configuration然后在Springmvc配置类里改成
1 @ComponentScan({"com.itheima.controller","com.itheima.config")
以后要放行什么就在SpringmvcSupport继续添加就行
这里是vue代码
1 2 3 4 5 6 7 8 9 10 getA11 ( ){ axios.get ("/books" ).then ((res )=> { this .dataList = res,data; }); }, saveBook ( ){ axios.post ("/books" ,this .formData ).then ((res )=> { });
SSM整合 SSM整合流程
创建工程
SSM整合
Spring
MyBatis
MybatisConfig
Jdbcconfig
jdbc.properties
SpringMVC
ServletConfig
SpringMvcConfig
功能模块
表与实体类
dao(接口+自动代理)
service(接口+实现类
controller
先导入pom,里面有Spring、mybatis、mybatisSpring整合、mysql、junit、Servlet、jackson
再创建包config、controller、dao、domain、Service、impl
1 2 3 4 5 @Configuration @ComponentScan({"com.itheima.service"}) @PropertySource("jdbc.properties") @Import({JdbcConfig.class,MyBatisConfig.class}) public class springconfig {
1 2 3 4 jdbc.driver =com.mysql.jdbc.Driver jdbc.url =jdbc:mysql://localhost:3306/ssm db jdbc.username =root jdbc.password =root
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class Jdbcconfig { @Value("${jdbc.driver}") private String driver;, @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @bean public DataSource dataSource () { DruidDataSource dataSource = new DruidDataSource (); dataSource.setDriverclassName(driver); datasource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); return dataSource;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class MyBatisconfig { @Bean public SqlSessionFactoryBean sqlSessionFactory (DataSource dataSource) { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean (); factoryBean.setDataSource(dataSource); factoryBean.setTypeAliasesPackage("com.itheima.domain" ); return factoryBean; } @Bean public MapperscannerConfigurer mapperScannerConfigurer () { MapperScannerConfigurer msc = new MapperScannerConfigurer (); msc.setBasePackage("com.itheima.dao" ); return msc;
1 2 3 4 5 6 7 8 9 10 11 12 public class servletcontainersInitconfig extends AbstractAnnotationConfigDispatcherservletInitializer { protected Class<?>[] getRootconfigclasses(){ return new class [0 ]; } protected Class<?>[] getServletConfigclasses(){ return new class []{springMvcConfig.class}; } protected String[] getservletMappings(){ return new String []{"/" }; } @Override protected Filter[] getservletFilters(){ CharacterEncodingFilter filter = new CharacterEncodingFilter (); filter.setEncoding("UTF-8" ); return new Filter []{filter}; }
1 2 3 4 @Configuration @Componentscan("com.itheima.controller") @EnableWebMvc public class SpringMvcConfig {
这里老师提到mvc容器能访问Spring容器,反过来不行,这里是父子容器概念
功能模块 创建pojo对象
1 2 3 4 5 6 public class Book {private Integer id;private String type;private String name;private String description;
Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public interface BookDao { @Insert("insert into tbl_book (type,name,description)values(#{type},#{name},#{description})"//这种写法前面的type是数据库中的字段,后面的是book中的属性,没有了id的null public void save(Book book); @Update("update tbl_book set type = #{type}, name = #{name}, description = #{idescription} where id = #{id}") public void update(Book book); @Delete("delete from tbl_book where id = #{id}") public void delete(Integer id); @Select("select * from tbl_book where id = #{id}") public Book getById(Integer id);//查单个 @Select("select * from tbl_book") public List<Book> getA11();//查所有
这样的接口层一般是不行的,最好是见名知意,返回值用boolean用来判断返回成功或失败
1 2 3 4 5 6 7 8 9 10 11 public interface Bookservice { public boolean save (Book book) ; public boolean update (Book book) ; public boolean delete (Integer id) ; public Book getById (Integer id) ; public List<Book> getA11 () ;
然后实现类,要用到dao接口了,然后注入dao
1 2 3 4 5 6 7 8 9 10 @Service public class BookServiceImpl implements BookService { @Autowired private BookDao bookDao; public boolean save (Book book) { bookDao.save(book); return true ; }
public Book getById(Integer id){
return bookDao.getById(id);
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 但做ssm整合的时候如果要注入这个的东西,在整个系统中不存在,也就是Spring中没有配bookdao的bean,我们用的自动代理,所以没有对应的bean给它自动装配,所以idea在这里把bookDao做个检查然后报错,但这个报错不影响运行,在弹出的下列选项里进入到idea做语法检查的设置,有一个Autowiring for bean class是自动装配的时候对bean的类型做检测,把它勾掉或者更改提示类型为警告   然后是控制层 ~~~java @RestController @RequestMapping("/books") public class Bookcontroller{ @Autowired private BookService bookService; @PostMapping public boolean save(@RequestBody Book book){//从前端提交的json数据获取所以要加RequestBody return bookService.save(book); }
@DeleteMapping("/{id}")
public boolean delete(@PathVariable Integer id){
return bookService.delete(id);
}
@GetMapping
public List<Book> getA11(){
return bookService.getA11();
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ### 接口测试 ~~~java @RunWith(SpringJUnit4ClassRunner.class)//Spring整合测试类 @ContextConfiguration(classes = SpringConfig.class)//加载配置类 public class BookServiceTest{ @Autowired private Bookservice bookservice; @Test public void testGetById(){ Book book = bookService.getById(1); System.out.println(book); }//这里老师说正常测试要做断言测试,不知道是什么
然后是开启事务,先在SpringConfig里加上@EnableTransactionManagement,然后写事务管理器,要用到datasource,在jdbcconfig里添加代码
1 2 3 4 5 @Bean public PlatformTransactionManager transactionManager (DataSource dataSource) { DataSourceTransactionManager ds= new DataSourceTransactionManager (); ds.setDataSource(dataSource): return ds;
然后在业务层加上@Transactional
表现层数据封装 前端接收数据格式太多,增删改返回true,查单条一个json,查全部一个集合json,所以现在要整合到一个里面
前端接收数据格式-创建结果模型类,封装数据到data属性中
但现在分不出返回的true是增还是删,所以再加一个code用来区分功能
又有个问题,如果查询不存在的数据,后台会返回data:null,code:20041,但这个代表成功还是失败?所以我们做个规定,结尾是1就是成功,0就是失败。所以就变成了这样,如果是20041就从data取数据
1 2 "code" : 20040 , "data" : null
然后又有问题,20040是没取到,应该给用户看什么呢,于是封装特殊消息到message(msg)属性中,变成
1 2 3 "code" : 20040 , "data" : null "msg" : "数据查询失败,请重试!"
设置统一数据返回结果类
1 2 3 4 public class Result { private Object data; private Integer code; private String msg;
注意:Result类中的字段并不是固定的,可以根据需要自行增减 提供若干个构造方法,方便操作
实现 其他数据继承上面的ssm整合,然后因为这是前后端沟通,要展示给前端的所以是表现层用,放到controller包里。因为是前后端沟通的所以不需要写tostring,只需要写setget,然后要提供构造方法不然就是new一个对象然后不停往里面set。构造方法分别是三个都要,data、code和无参
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class Result { private Object data; private Integer code; private String msg; public Result () {} public Result (Integer code,Object data) { this .data = data; this .code = code; } public Result (Integer code, Object data, String msg) { this .data = data; this .code = code; this .msg = msg; }
然后再创建一个Code类
1 2 3 4 5 6 7 8 9 public class code {public static final Integer SAVE_OK = 20011 ;public static final Integer DELETE_OK = 20021 ;public static final Integer UPDATE OK = 2031 ;public static final Integer GET OK = 20041 ;public static final Integer SAVE ERR = 20010 ;public static final Integer DELETE ERR = 20020 ;public static final Integer UPDATE_ERR = 20030 ;public static final Integer GET ERR = 20040 ;
然后BookController里return的结果就变了,全都返回Result
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping public Result save (@RequestBody Book book) { boolean flag = bookservice.save(book); return new.Result(flag ? Code.SAVE_OK:Code.SAVE_ERR,flag); } @GetMapping("/{id}") public Result getById (@PathVariable Integer id) { Book book = bookService.getById(id); Integer code = book != null ? Code.GET_OK : Code.GET_ERR; String msg = book != null ? "" :"数据查询失败,请重试!" ; return new Result (code,book,msg);
异常处理器
出现异常现象的常见位置与常见诱因如下
框架内部抛出的异常:因使用不合规导致
数据层抛出的异常:因外部服务器故障导致(例如:服务器访问超时)
业务层抛出的异常:因业务逻辑书写错误导致(例如:遍历业务书写操作,导致索引异常等)
表现层抛出的异常:因数据收集、校验等规则导致(例如:不匹配的数据类型间导致异常)
工具类抛出的异常:因工具类书写不严谨不够健壮导致(例如:必要释放的连接长期未释放等)
思考:
各个层级均出现异常,异常处理代码书写在哪一层
-所有的异常均往上抛出到表现层进行处理
表现层处理异常,每个方法中单独书写,代码书写量巨大且意义不强,如何解决
-AOP思想,但不需要自己写,mvc提供了快捷的处理方案
首先在表现层新建一个ProjectExceptionAdvice类,然后加上注解用来声明这个类是做异常处理的,注意要在mvc配置类里加上扫描包路径
1 2 3 4 5 6 7 8 @RestControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler(Exception.class) public Result doException (Exception ex) { System.out.println("嘿嘿,异常你哪里跑!" ); return new Result (666 , null , "嘿嘿,异常你哪里跑!" ); }
项目异常处理方案 项目异常分类
业务异常(BusinessException)
不规范的用户行为操作产生的异常,比如访问路径被更改
规范的用户行为产生的异常,比如年龄输入嘿嘿
系统异常(SystemException)
其他异常(Exception)
项目异常处理方案
业务异常(BusinessException)
系统异常(SystemException)
发送固定消息传递给用户,安抚用户
发送特定消息给运维人员,提醒维护
记录日志
其他异常(Exception)
发送固定消息传递给用户,安抚用户
发送特定消息给编程人员,提醒维护(纳入预期范围内)
记录日志
创建一个exception包,然后创建一个系统异常类,然后加一个code用以区分错误,然后实现构造方法,可以用一个写一个也可以全写上,这里是只用了两个,然后再加上code的getset方法就行
1 2 3 4 5 6 7 8 9 10 11 public class SystemException extends RuntimeException { private Integer code; public SystemException (Integer code,string message) { super (message); this .code = code; } public SystemException (Integer code, String message, Throwable cause) { super (message,cause); this .code = code; }
然后在可能出现异常的地方去进行一下处理,土办法是try catch,再自己new一个异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public Book getById (Integer id) { if (id == 1 ){ throw new BusinessException (Code.BUSINESS_ERR,"请不要xxxx" ); } try { int i = 1 /0 ; }catch (Exception e){ throw new SystemException (Code.SYSTEM TIMEOUT_ERR.,"服务器访问超时,请重试!" ,e); } return bookDao.getById(id); }
然后就是异常处理器,code和消息都可以从ex里获得,其他异常就按照上面的异常处理器里写的就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @RestControllerAdvice public class ProjectExceptionAdvice { @ExceptionHandler(SystemException.class) public Result doSystemException (SystemException ex) { return new Result (ex.getCode(),data:null ,ex.getMessage()); } @ExceptionHandler(BusinessException.class) public Result doBusinessException (BusinessException ex) { return new Result (ex.getCode(),data:null ,ex.getMessage());
前后端协议联调 添加功能 复制前端文件过来,然后现在没有配置的话页面的请求一定会被mvc拦截,所以要建个配置类给他放行,将任意请求都指向到哪个location
1 2 3 4 5 6 7 8 9 @Configuration public class SpringMvcSupport extends WebMvcconfigurationsupport @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/pages/**" ).addResourceLocations("/pages/" ); registry.addResourceHandler("/css/**" ).addResourceLocations("/css/" ); registry.addResourceHandler("/js/**" ).addResourceLocations("/js/" ); registry.addResourceHandler("/plugins/**" ).addResourceLocations("/plugins/" );
然后要在mvc中添加下面代码确保能扫描到
1 @Componentscan({"com.itheima.controller","com.itheima.config"})
前端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 methods :{ getA11 ( ){ axios.get ("/books" ).then ((res )=> { this .dataList =res.data .data ; }); handleAdd ( ){ axios.post ("/books" ,this .formData ).then ((res )=> { if (res.data .code == 20011 ){ this .dialogFormVisible = false , this .$message .success ("添加成功" ); }else if (res.data .code == 20010 ){ this .$message .error ("添加失败" ); }else { } this .getAll ();
然后看代码,要让代码失败就要让code为20010,去bookController看到post的返回代码里如果想返回失败就得让flag值为false,看到BookServiceImpl的代码为以下,我们要让save这个功能必须把结果反馈给我们,所以要改dao
1 2 3 4 5 public boolean save (Book book) { return bookDao.save(book) > 0 ; }
更改返回值为int,这个int就是行计数,用来判断到底是成功或失败
1 2 3 4 5 public interface BookDao { @Insert("xxx") pubilc int save (Book book) ; }
这里就把成功返回写好了,但怎么构建失败,在老师的数据库设计里如果输入的东西超过20个然后提交就会报错,然后提交的时候应该弹个框提示错误的但没有反应,在前端post那里添加代码console.log(res.data);查看打印数据,发现走的是sql异常,被异常拦截器拦截了,code是5999,就直接让它这个异常输出到前端就行。然后this.getAll();这句不管成功还是失败都要输出,一般用finally来写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 handleAdd ( ){ axios.post ("/books" ,this .formData ).then ((res )=> { console .log (res.data ); if (res.data .code == 20011 ){ this .dialogFormVisible = false , this .$message .success ("添加成功" ); }else if (res.data .code == 20010 ){ this .$message .error ("添加失败" ); }else { this .this .$message .error (res.data .msg ); } }).finally (()=> { this .getAll (); });
然后还有个问题是弹窗输入完信息后再次打开信息没有被清空,要让每次打开弹窗都是空的就加入以下代码
1 2 3 4 5 6 7 8 handlecreate(){ this .dialogFormVisible =true ; this .resetForm(); resetForm(){ this .formData ={}; }。
修改功能 修改功能弹出的框里应该是有数据的,所以要先查询数据再展示,打印row看看里面的数据,选择id为查询条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 handleUpdate (row ){ axios.get ("/books/" +row.id ).then ((res )=> { if (res.data .code == 20041 ){ this .formData = res.data .data ; this .dialogFormVisible4Edit = true ; }else { this .$message .error (res.data .msg ); } });
显示组件的时候发现报错,打印数据看看,发现输出了个null,结果是之前的Serviceimpl里有模拟的异常导致的,注释掉就行了
编辑功能 和添加功能一模一样,把里面的代码匹配一下,然后axios.post改为put
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 handleEdit ( ){ axios.put ("/books" ,this .formData ).then ((res )=> { if (res.data .code == 20031 ){ this .dialogFormVisible4Edit =false ; this .$message .success ("修改成功" ); }else if (res.data .code == 20030 ){ this .$message .error ("修改失败" ); }else { this .$message .error (res.data .msg ); }).finally (()=> { this .getA11 (); }); },
删除功能 删除要先提示是否删除,then是确定后干什么,catch是取消后干什么。然后根据id删除,删除完在finally那里记得刷新页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 handleDelete (row ){ this .$confirm *("此操作永久删除当前数据,是否继续?" ."提示" ,{ type :'info' }).then (()=> { axios.delete ("/books/" +row.id ).then ((res )=> { if (res.data .code == 20021 ){ this .$message .success ("删除成功" ); }else { this .$message .error ("删除失败" ); } }).catch (()=> { this .$message .info ("取消删除操作" ); }).finally (()=> { this .getA11 (); }); }
拦截器 拦截器概念 浏览器发送请求会发送到tomcat服务器,拦截这个请求后会划分成静态和动态资源,静态的一般直接返回,动态的就是经过过滤器,过完过滤器后就会交给处理器工作,也就是mvc,然后先看访问的是什么请求,这个地方就是中央控制器,中央控制器根据你的访问将它分发到具体的控制器方法中,让它执行操作,操作完会返回一个数据,比如json。
然后我们想要在每个Controller访问之前做一件事,比如说权限控制,有的人觉得在controller后也需要一些事,所以就这样产生了拦截器
拦截器(Interceptor)是一种动态拦截方法调用的机制,在SpringMvc中动态拦截控制器方法的执行
作用
在指定的方法调用前后执行预先设定后的的代码
阻止原始方法的执行
拦截器与过滤器区别:
归属不同:Filter属于Servlet技术,Interceptor属于SpringMVc技术
拦截内容不同:Filter对所有访问进行增强,Interceptor仅针对SpringMvVc的访问进行增强
弹幕:springmvc核心就是一个servlet做请求分发,filter是在进入servlet之前,interceptor是在进入servlet之后
入门案例 准备的类和之前的差不多,过滤器放在表现层里,在controller新建interceptor包,这个单词要记住,然后新建ProjectInterceptor。继承的这个方法后发现不需要实现方法,这是因为接口里面都有默认方法,然后把这三个方法都覆盖掉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Component public class ProjectInterceptor implements HandlerInterceptor { @override public boolean preHandle (HttpservletRequest request, HttpservletResponse response, object handler) throws Exception{ System.out.println("preHandle..." ); return true ; } @0verride public void postHandle (HttpservletRequest reguest, HttpservletResponse response, obiect handler, ModelAndview modelAndView) throws Exception{ System.out.println("postHandle..." ); } @0verride public void afterCompletion (HttoservletReguest reguest, HttoservletResponse response, obiect handler, Exception ex) throws Exception{ System.out.println("aftercompletion..." ); }
记得让mvc扫这个包
然后在config新建一个SpringMvcSupport,这个是过滤访问静态资源的,之前有写过。拦截器和过滤的写法区别差不多,只不过注册的东西不一样。下面括号里的拦截器可以通过自动注入来加入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Configuration public class SpringMvcSupport extends WebMvcConfigurationsupport @Autowired private ProjectInterceptor projectIntereptor; @Override protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler( "/pages/**" ).addResourceLocations("/pages/" ); } @override protected void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(projectIntereptor).addPathPatterns("/books" ,"/books/*" ); }
记得也要让mvc加载这个包
然后前端发送请求去调试,返回了以下信息
1 2 3 4 preHandle...这是在什么什么之前 book save...Book{书名='springmvc实战',价格=66.8} postHandle..这是在什么什么之后 afterCompletion...在完成之后
不过SpringMvcSupport可以简化开发,在mvcConfig里可以继承这个方法,里面可以实现的方法就有之前SpringMvcSupport的两个addResourceHandlers和addInterceptors,开头的protected换成了public
1 2 3 4 5 6 7 8 9 @Configuration @ComponentScan({"com.itheima.controller"}) @EnablewebMvc public class springMvcconfig implements WebMvcConfigurer { @Autowired private ProjectInterceptor projectInterceptor; @override public void addInterceptors (InterceptorRegistry registry) { registry.addInterceptor(projectInterceptor).addPathPatterns("/books" ,"/books/*" );
但是这种简化开发的侵入式较强
拦截参数 上面的参数都是用来干什么的呢,就是现在的程序执行以后可以通过这些参数取数据。对于HandlerMethod这个类里面封装了现在执行的方法,用的是反射,然后通过强转然后调用getMethod来获取方法之类的
1 2 3 4 5 6 7 8 9 10 11 @Component public class ProjectInterceptor implements HandlerInterceptor { @override public boolean preHandle (HttpservletRequest request, HttpservletResponse response, object handler) throws Exception{ String contentType = request.getHeader("Content-Type" ); HandlerMethod hm = (HandlerMethod)handler; hm.getMethod().~~~调用想获取的反射 System.out.println("preHandle..." + contentType); return true ; }
modelAndView封装了mvc进行页面跳转的相关数据。下面那个ex可以拿到原始程序执行过程中的异常,但我们有异常统一处理机制所以也没有这个需求了
1 2 3 4 5 6 7 8 9 @0verride public void postHandle (HttpservletRequest reguest, HttpservletResponse response, obiect handler, ModelAndview modelAndView) throws Exception{ System.out.println("postHandle..." ); } @0verride public void afterCompletion (HttoservletReguest reguest, HttoservletResponse response, obiect handler, Exception ex) throws Exception{ System.out.println("aftercompletion..." ); }
preHandle
参数
request:请求对象
response:响应对象
handler:被调用的处理器对象,本质上是一个方法对象,对反射技术中的Method对象进行了再包装
返回值
postHandle
modelAndview:如果处理器执行完成具有返回结果,可以读取到对应数据与页面信息,并进行调整
afterCompletion
ex:如果处理器执行过程中出现异常对象,可以针对异常情况进行单独处理
多个拦截器执行顺序
当配置多个拦截器时,形成拦截器链
拦截器链的运行顺序参照拦截器添加顺序为准
当拦截器中出现对原始处理器的拦截,后面的拦截器均终止运行
当拦截器运行中断,仅运行配置在前面的拦截器的afterCompletion操作
全成功就是运行到post1后,再把所有的after执行完,如果3是false,就会中止拦截器运行,直接跳到after2和1运行完,以此类推,如果1就是false,那么后面的都不运行
拦截器链的运行顺序 preHandle:与配置顺序相同,必定运行 postHandle:与配置顺序相反,可能不运行 aftercompletion:与配置顺序相反,可能不运行
Maven进阶 分模块开发与设计 白学警告
将原始模块按照功能拆分成若干个子模块,方便模块间的相互调用,接口共享
右击新建个maven模块,取名叫maven_03_pojo,在这个里面有pom.xml,然后再java里建个con.itheima.domain,把maven_02_ssm里的domain里的Book剪切到03的里面,这里就要思考如果我们要在02里调用03的book应该怎么弄
可以打开02的pom.xml添加以下这行,这行是03的pom.xml里复制过来的,然后在右侧的maven管理里也能看到多了个这个的依赖
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > com.itheima</groupId > <artifactId > maven 03 pojo</artifactId > <version > 1.0-SNAPSHOT</version > </dependency >
maven想要真正运行最终要执行的是生命周期里的操作,点击compile,发现提示了error:Could not resolve dependencies for project com.itheima:maven 02 ssm:war:1.8-SNAPsHoT: Could not find artifact com.itheima:maven 03_pojo:jar:1.0-SNAPSHOT ->[Help 1],引用03的时候找不到
我们去本地仓库找,maven-repository-com-itheima这个路径找不到对应的文件夹,但是其他的仓库是能找到的,也就是本地仓库没有这个资源坐标,要保证可移植性肯定是需要仓库也有的,所以需要打包进去,点击maven指令里的install,将当前的模块安装到本地仓库中,安装的时候注意是选择安装03
依赖管理 依赖指当前项目运行所需的jar,一个项目可以设置多个依赖
打开右侧的maven管理,前面有箭头的代表当前的依赖又依赖了别的东西,比如02里面有03和04,而04又依赖了03,那么把02里面的03注释掉,依然可以正常使用,这说明依赖具有传递性
直接依赖:在当前项目中通过依赖配置建立的依赖关系
间接依赖:被资源的资源如果依赖其他资源,当前项目间接依赖其他资源
依赖传递冲突问题
路径优先:当依赖中出现相同的资源时,层级越深,优先级越低,层级越浅,优先级越高
声明优先:当资源在相同层级被依赖时,配置顺序靠前的覆盖配置顺序靠后的
特殊优先:当同级配置了相同资源的不同版本,后配置的覆盖先配置的,以最后配置的为准
这个按钮显示了依赖的关系和深度
可选依赖和排除依赖 可选依赖指对外隐藏当前所依赖的资源–不透明
如果不想让别人知道我的依赖里用过什么东西,想给隐藏起来,就可以加一个标签
1 2 3 4 5 6 7 <dependency > <groupId > com.itheima</groupId > <artifactId > maven 03 pojo</artifactId)<version > 1.0-SNAPSHOT</version > <optional > true</optional > 加这个</dependency >
排除依赖是指在当前引用的坐标中,将它的依赖从你的依赖关系中去除,是主动断开依赖的资源
1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > com.itheima</groupId > <artifactId > maven 04 dao</artifactId > <version > 1.0-SNAPSHOT</version > <exclusions > <exclusion > <groupId > log4j</groupId > <artifactId > log4j</artifactId > </exclusion > </exclusions > </dependency >
继承与聚合 聚合 :
聚合:将多个模块组织成一个整体,同时进行项目构建的过程称为聚合
聚合工程:通常是一个不具有业务功能的“空”工程(有且仅有一个pom文件)
作用:使用聚合工程可以将多个工程编组,通过对聚合工程进行构建,实现对所包含的模块进行同步构建
当工程中某个模块发生更新(变更)时,必须保障工程中与已更新模块关联的模块同步更新,此时可以使用聚合工程来解决批量模块同步构建的问题
新建一个模块,里面只有一个pom.xml,之前的打包方式都是jar之类的,这里为pom的打包方式
管理的话用module标签,路径是以pom.xml开始,往上找一级,标签里的顺序不会影响效果,因为他们是按照依赖关系构建,比如a依赖b,就会先构建b,先把没有依赖的构建
1 2 3 4 5 6 7 8 9 10 11 <groupId > com.itheima</groupId > <artifactId > maven 01_parent</artifactId > <version > 1.0-SNAPSHOT</version > <packaging > pom</packaging > <modules > <module > ../maven_02_ssm</module > <module > ../maven_03_pojo</module > <module > ../maven_04_dao</module > </modules >
继承:
概念:继承描述的是两个工程间的关系,与java中的继承相似,子工程可以继承父工程中的配置信息,常见于依赖关系的继承
作用:
这里我们让1为父工程,其他为子工程,继承关系要在子类中描述,现在在2的pom.xml里加入以下代码,relativePath是用来快速定位父工程路径的
1 2 3 4 5 6 7 <parent > <groupId > com.itheima</groupId > <artifactId > maven 0l parent</artifactId > <version > 1.0-SNAPSHOT</version > <relativePath > ../maven 01 parent/pom.xml</relativePath > </parent >
然后把子工程中的坐标都放到父工程里统一管理
如果2和3都要用到junit,而4不用,那可以在1里定义依赖管理,这里的不会让子工程都会继承,而是提供可选依赖资源
1 2 3 4 5 6 7 8 9 10 11 <dependencyManagement > <dependencies > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > </dependencies > </dependencyManagement >
子工程就还是写dependency,但是不要加版本,因为它用的是父工程里的版本。
1 2 3 4 5 <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <scope > test</scope)</dependency >
子工程中使用父工程中的可选依赖时,仅需要提供群组id和项目id,无需提供版本,版本由父工程统一提供,避免版本冲突。子工程中还可以定义父工程中没有定义的依赖关系
聚合与继承的区别
作用
相同点:
聚合与继承的pom.xm1文件打包方式均为pom,可以将两种关系制作到同一个pom文件中
聚合与继承均属于设计型模块,并无实际的模块内容
不同点:
聚合是在当前模块中配置关系,聚合可以感知到参与聚合的块有哪些
继承是在子模块中配置关系,父模块无法感知哪些子模块继承了自己
属性 前面的管理版本号还是有点麻烦,在java中学过用变量来定义版本号,maven的属性就是干这个的
1 2 3 4 5 6 7 8 9 10 11 12 <dependencys > <dependency > <groupId > org.springframework</groupId > <artifactId > spring-core</artifactId > <version > ${spring.version}</version > </dependency > </dependencys > <properties > <spring.version > 5.2.10.RELEASE</spring.version > </properties >
既然有了属性,能不能把jdbc.properties里的数据也做统一管理呢,也可以,同样是在下面标签里加上原本的数据库地址,但是只能在这个pom文件里使用,不能在配置文件里使用,如果想扩大范围可以使用build标签配置资源文件,加上路径和开启过滤规则,不过这个方法并不常用
1 2 3 4 5 6 7 8 9 10 11 12 <properties > <jdbc.url > jdbc:mysql://127.0.0.1:3306/ssm db</jdbc,url> </properties > <build > <resources > <resource > <directory > ../maven 02 ssm/src/main/resources</directory > <filtering > true</filtering > </resource > </resources > </build >
如何查看效果,只需要把02这个工程打包成war包,看看里面的配置有没有加载上值就行,找到本地仓库maven_repository_com_itheima把里面的东西清空,然后在maven点install,如果打包提示必须要web.xml,就需要新建一个,因为war包最起码要有一个web.xml。然后查看war文件里webinf里classes找到配置文件,看有没有生效就行
但是这样不规范,因为这里只配置了02,要是0304都配就要写好多,但这里面只支持写一个directory,所以就要改变代码,${project.basedir}代表了当前项目所在的目录,而后面的020304会继承这个,就不会局限在哪一个工程了
1 <directory > ${project.basedir}/src/main/resources</directory >
这里是刚才的web.xml根本性处理方案,是如果没有这个文件,你不要给我报错。而只有02这个工程有这个插件,所以要在这里的pom.xml写,这下面的plugin是一个插件,里面写maven打war包的那个插件,坐标从maven>repository>org>apachemaven >plugins这个路径里找,org.apache.maven.plugins这个就是。后面要写对应的配置,这个标签的意思是如果遇到了没有web.xml是否报错,写false
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <build > <plugins > <plugin > <groupId > org.apache.tomcat.maven</groupId > <artifactId > tomcat7-maven-plugin</artifactId > <version > 2.1</version > <configuration > <port > 80</port > <path > /</path > </configuration > </plugin > <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-war-plugin</artifactId > <version > 3.2.3</version > <configuration > <failonMissingWebXml > false<fail0nMissingWebXml > </configuration > </plugin > </plugins > </build >
上面用到的${project.basedir}是maven内置的系统属性
版本管理
工程版本:
SNAPSHOT(快照版本)
项目开发过程中临时输出的版本,称为快照版本
快照版本会随着开发的进展不断更新
RELEASE(发布版本)
项目开发到进入阶段里程碑后,向团队外部发布较为稳定的版本,这种版本所对应的构件文件是稳定的,即便进行功能的后续开发,也不会改变当前发布版本内容,这种版本称为发布版本
发布版本
多环境配置与应用 maven提供配置多种环境的设定,帮助开发者使用过程中快速切换环境
在01父工程里配置多环境开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <profiles > <profile > <id > env_depk/id> <properties > <jdbc.url > jdbc:mysq1://127.0.0.1:3306/ssm_db</jdbc.url > </properties > <activation > <activeByDefault > true</activeByDefault > </activation > </profile > <profile > <id > env_pro/id> <properties > <jdbc.url > jdbc:mysq1://127.2.2.1:3306/ssm_db</jdbc.url > </properties > </profile > <profile > <id > env_depk/id> <properties > <jdbc.url > jdbc:mysq1://127.0.0.1:3306/ssm_db</jdbc.url > </properties > </profile > </profiles >
上面设置默认启动环境太麻烦了,也可以换种方式,点击这里,输入执行指令mvn install -P env_test,-p代表指定环境,运行就可以了
跳过测试 点一下这个按钮,test就变成了灰色的
但是这个会全部跳过,能不能指定跳过哪些呢。在build里写,而测试是一个插件,所以要在插件标签里写配置,里面是跳过测试。然后比如某个测试用例不参与,其他的都参与,就要把
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <build > <resources > <resource > <directory > ${project.basedir}/src/main/resources</directory > <filtering > true</filtering)</resource > </resources > <plugins > <plugin > <artifactId > maven-surefire-plugin</artifactId > <version > 2.12.4</version > <configuration > <skipTests > true</skipTests > <excludes > <exclude > **/BookServiceTest.java</exclude > </excludes > </configuration > </plugin > </plugins > </build >
用指令就是mvn package -D skip
私服
私服是一台独立的服务器,用于解决团队内部的资源共享与资源同步问题
Nexus
下载完去bin目录运行cmd,然后执行nexus.exe /run nexus
访问地址是localhost:8081,用户名admin,密码在他提供的路径里找到admin.password里
修改基础配置信息
安装路径下etc目录中nexus-default.properties文件保存有nexus基础配置信息,例如默认访问端口
修改服务器运行配置信息
安装路径下bin目录中nexus.vmoptions文件保存有nexus服务器启动对应的配置信息,例如默认占用内存空间
现在idea打包到本地仓库,然后本地仓库上传到私服,但这样的话需要本地仓库配置访问权限,也就是访问私服的用户名和密码,还要告诉上传的位置,下载也是同理
找到maven的conf里的setting.xml,找到servers标签,复制一个server标签然后修改
1 2 3 4 5 6 <server > <id > 私服中的服务器id名称</id > <username > admin</username > <password > admin</password > </server >
在仓库里找到这个选项,点击创建仓库,然后选maven2(hosted),然后写上名字itheima-snapshot,下面的version policy要选同样的snapshot,其他的不动直接创建
然后还缺一个配置,就是当前项目要发布到哪一个私服仓库中,在01pom文件里配置,一个是正式版,一个是快照版
1 2 3 4 5 6 7 8 9 10 11 <distributionManagement > <repository > <id > itheima-release</id > <url > http://localhost:8081/repository/itheima-release/</ur1 > </repository > <snapshotRepository > <id > itheima-snapshot</id > <url > http://localhost:8081/repository/itheima-snapshot/</url > </snapshotRepository > </distributionManagement >
然后在maven选项中选择deploy,这个是上传到私服,本地的是用install,上传完后查看仓库,发现release里没有东西但snapshot有,这是因为在01pom文件里的版本号叫snapshot,要改成release
1 2 3 4 <groupId > com.itheima</groupId > <artifactId > maven 01 parent</artifactId > <version > 1.0-RELEASE</version > <packaging > pom</packaging >
下面这个地方可以换取阿里的坐标用来下载
SpringBoot SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程
入门案例 创建时选择 然后选择好jdk版本,按照下图设置,name随便写,描述随便写,包名只要前面的
“+id); return “hello , spring boot!”; }
1 2 3 4 5 6 7 8 点开Application查看代码,意思就是运行这个东西,然后把Application传进来,再加个参数就行了 ~~~java @springBootApplication public class Application { public static void main(string[] args){ SpringApplication.run(Application.class,args);
让后点开pom,把里面的name标签和description删掉,往上有个parent标签,这个是boot的核心继承,下面那个是前面点web所产生的东西
1 2 3 4 5 6 7 8 9 10 11 12 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.5.0</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency >
基于idea开发SpringBoot程序需要确保联网且能够加载到程序框架结构
Springboot项目快速启动 如果前端调试程序需要链接后端,但不在同一台电脑上,后端可以使用maven的package进行打包,这个包在target里,把打好的jar包发给前端就行,运行方式是在这个文件目录启动cmd,然后输入java -jar 包名.jar就行了,而且需要的其他jar包也会一起打包好
jar支持命令行启动需要依赖maven插件支持,请确认打包时是否具有SpringBoot对应的maven插件
Springboot概述 SpringBoot是由Pivota1团队提供的全新框架,其设计目的是用来简化Spring应用的初始搭建以及开发过程
在上面的parent标签里,点击spring-boot-starter-parent进去可以看到这个又继承了spring-boot-dependencies,这里程序就已经准备好了要用的资源的版本管理,只需要写上需要的依赖不加版本就可以
下面这个是帮我们一次性写了若干依赖,意思是起步依赖,点进去都可以看到
1 2 3 4 5 <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-test</artifactId > </dependency >
starter
SpringBoot中常见项目名称,定义了当前项目使用的所有项目坐标,以达到减少依赖配置的目的
parent
所有SpringBoot项目要继承的项目,定义了若干个坐标版本号(依赖管理,而非依赖),以达到减少依赖冲突的目的
spring-boot-starter-parent(2.5.0)与 spring-boot-starter-parent(2.4.6)共计57处坐标版本不同
实际开发
使用任意坐标时,仅书写GAV中的G和A,V由SpringBoot提供
如发生坐标错误,再指定version(要小心版本冲突)
上面这些都是辅助功能
springboot启动方式
这里我们想换成jetty服务器,不用tomcat,在pom里的org.springframework.boot里面是有tomcat的,并且是源码不能随意更改不能用可选依赖,所以我们这里用到之前学过的排除依赖,然后再加上jetty的依赖就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <exclusions > <exclusion > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-tomcat</artifactId > </exclusion > </exclusions > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-jetty</artifactId > </dependency >
Jetty比Tomcat更轻量级,可扩展性更强(相较于Tomcat),谷歌应用引擎(GAE)已经全面切换为Jetty
基础配置 修改服务器端口
如果三个文件都有的情况下,properties为主启动文件,然后是yml最后是yaml
SpringBoot核心配置文件名为application SpringBoot内置属性过多,且所有属性集中在一起修改,在使用时,通过提示键+关键字修改属性
yaml
YAML(YAML Ain’t Markup Language),一种数据序列化格式
优点:
容易阅读
容易与脚本语言交互
以数据为核心,重数据轻格式
YAML文件扩展舎
yaml语法规则
大小写敏感
属性层级关系使用多行描述,每行结尾使用冒号结束
使用缩进表示层级关系,同层级左侧对齐,只允许使用空格(不允许使用Tab键)
属性值前面添加空格(属性名与属性值之间使用冒号+空格作为分隔)
表示注释
核心规则:数据前面要加空格与冒号隔开
yaml数组数据
数组数据在数据书写位置的下方使用减号作为数据开始符号,每行书写一个数据,减号与数据间空格分隔
1 2 3 4 5 6 enterprise: tel:4006184000 subject: - Java - 前端 - 大数据
yaml数据读取 可以通过下面的方式读取上面yml文件的数据
1 2 3 4 5 6 7 8 9 10 @RestController @RequestMapping("/books") public class Bookcontroller {@Value("${enterprise.tel}") private Integer tel;@Value("${enterprise.subject[0]}") private Integer subject;
也可以使用加载环境信息全部获取
1 2 3 4 5 6 7 8 9 @RestController @RequestMapping(©v"/books") public class Bookcontroller { @Autowired private Environment environment; @GetMapping("/{id}") public String getById (@PathVariable Integer id) { System.out.println(environment.getProperty("lesson" ));
第三种方式是比较常用的,先定义一个实体类,内容和yml里的东西一一对应,要想加载这里的信息,得先让Spring控制这里,添加@Component把他加入bean,第二步要告诉从配置中读取属性,@ConfigurationProperties,里面写上前缀
1 2 3 4 5 @Component @ConfigurationProperties(prefix = "enterprise") public class Enterprise {private String tel;private String[] subject;
然后再放到上面的控制类里
1 2 3 4 5 6 @Autowired private Enterprise enterprise,@GetMapping("/{id}") public String getById (@PathVariable Integer id) { System.out.println(enterprise);
自定义对象封装数据警告解决方案
把这个加在依赖里就可以
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-configuration-processor</artifactId > <optional > true</optional > </dependency >
多环境开发配置 分隔环境在yml里用三个横线分隔,区分什么是开发测试的话用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 spring: profiles: active: test --- spring: profiles: dev server: port:80 --- spring: profiles: pro server: port: 81 --- spring: profiles: test server: port: 82
现在推荐的写法:用哪个都行
1 2 3 4 5 6 7 8 9 10 11 12 spring: profiles: active: dev --- spring: config: activate: on-profile: dev server: port:80
properties的写法:
1 2 spring.profiles.active =dev
dev在哪写呢?新建一个application-dev.properties,里面写上server.port=80,这样会读取
多环境命令行启动参数设置 前端人员在启动打好的jar包时可以通过命令来更改环境,但是在打包前一定要先执行clean,有中文注释的时候需要打开idea设置,输入encoding,把所有的编码改成utf-8
启动时先在jar包所在的文件夹里启动cmd,然后输入java -jar springboot.jar –spring.profiles.active=test,也可以临时修改参数,比如
java -jar springboot.jar –spring.profiles.active=test –server.port=88
多环境开发兼容问题 开发的时候jar最终要在服务器上运行,而maven是用来最终打包,所以要以maven的profile为主,boot为辅
这里准备了数据,在pom里有设置多环境开发,yml里设置的是,这时候运行打好的jar包,发现最终运行的端口是80,说明yml里的dev在生效,properties里想要生效的也没有告诉yml,怎么让yml知道呢,在pom里把每个环境都设上<properties.active>,把yml里active后面改成${profile.avtive}。
上面配置的pom属性只能在pom文件里用,不会干预配置文件,想要干预配置文件就要加个下面的插件,配置里的useDefaultDelimiters设置为true,然后再设置一下字符集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 <plugins > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-resources-plugin</artifactId > <version > 3.2.0</version > <configuraton > <encoding > UTF-8</encoding > <useDefaultDelimiters > true</useDefaultDelimiters > </configuraton > <configuration > <useDefaultDelimiters > true</useDefaultDelimiters > </configuration > </plugin > <profiles > <profile > <id > dev</id > <properties > <properties > dev</properties > </properties > </profile > <profile > <id > pro</id > <properties > <properties > pro</properties > </properties > <activation > <activeByDefault > true</activeByDefault > </activation > </profile > <profile > <id > test</id > <properties > <properties > test</properties > </properties > </profile > </profiles >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 spring: profiles: active: ${profile.avtive} --- spring: profiles:dev server: port:80 --- spring: profiles: pro server: port: 81 --- spring: profiles: test server: port: 82
配置文件分类 SpringBoot中4级配置文件 1级:file :config/application.yml【最高】 2级:file :application.yml 3级:classpath:config/application.yml 4级:classpath:application.yml【最低】 作用: 1级与2级留做系统打包后设置通用属性 3级与4级用于系统开发阶段设置通用属性
整合第三方技术
上面两个类其实都是统一的名字,如果都一样的话是不是就不用写了,boot给做了整合。随便弄了个Service,然后到测试类去做测试,下面的方法是不重要的可以随意修改,@SpringBootTest里面整合了运行器,在这里没有写需要加载的配置文件,但是有没有加载?加载了,在Springboote7TestApplication这个类(运行的那个)就起到了加载配置类的作用,他会把他所在包及其子包全部扫描一遍
1 2 3 4 5 6 7 8 @SpringBootTest class springboot07TestApplicationTests {@Autowired private BookService bookService;@Test public void save () {bookService.save();
如果就是换地方了,可以通过指定参数把类名指定上就行了
@SpringBootTest(class = Springboot07TestApplication.class)
基于SpringBoot整合mybatis 创建一个新模块,把mybatis framework依赖加上,还有mysql的
然后准备下面代码
1 2 3 4 5 public class Book {private Integer id;private string name;private string type;private string description;
1 2 3 public interface BookDao {@Select("select *from tbl_book where id = #{id}") public Book getById (Integer id) ;
还有个Application.yml,serverTimezone=UTC是设置时区,这个是boot2.4.2以前的会报错,后面的就不用加了
1 2 3 4 5 6 spring: datasource: driver-class-name:com.mysql.cj.jdbc.Driver url:jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTC username:root password: root
然后到测试类,
1 2 3 4 5 6 7 8 @SpringBootTest class springboote8MybatisApplicationTests {@Autowired private BookDao bookDao;@Test void testGetById () {Book book = bookDao.getById(1 );System.out.println(book);
运行时提示错误的创建了一个bean,依赖的bookDao加载失败了,因为这样配BookDao这是一个接口,那我们的对象是什么?是通过自动代理得到的实现类,而Spring怎么知道这个类要做自动代理呢,之前是@MapperScanner然后里面是包名,这里只需要写@Mapper
如果需要其他的数据库源,就先加载对应的依赖,然后在上的yml最后面加上
1 type: com.alibaba.druid.pool.DruidDatasource
案例:基于boot的ssm整合案例 之前的config包全部删掉,domain不需要变化,dao里需要加一个@Mapper,作用是让boot扫描到这里并且生成自动代理,Service包不需要动,controller也不需要,然后在resources包里把Application.properties后缀改成yml,然后把jdbc.properties里的东西放到yml里,测试类就是上面那些
mybatis plus MyBatisPlus简介 MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率
SpringBoot整合MyBatis开发过程(复习)
创建SpringBoot工程
勾选配置使用的技术(mybatis、mysql)
设置dataSource相关属性(JDBC参数)
定义数据层接口映射配置
新建一个模块选择mysql,如果没有mybatisplus就手动加,再加一个druid,然后在yml里配置数据库,新建一个UserDao的接口,继承BaseMapper就可以获得所有需要的查询方法
1 2 3 4 5 6 @Mapper public interface UserDao {public User getById (Long id) ;@Mapper public interface UserDao extends BaseMapper <User>{}
1 2 3 4 5 6 7 8 @SpringBootTest class Mybatispluse1QuickstartApplicationTests {@Autowired private UserDao userDao;@Test void testGetAll () {List<User>userList = userDao.selectList(null ); System.out.println(userList);
MyBatisPlus特性:
无侵入:只做增强不做改变,不会对现有工程产生影响
强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作
支持 Lambda:编写查询条件无需担心字段写错
支持主键自动生成
内置分页插件
标准数据层开发
新增,但是id是按照它的规则生成的
1 2 3 4 5 6 7 8 9 @Test void testSave () {User user = new User ();user.setName("黑马程序员" ); user.setPassword("itheima" ), user.setAge(12 ); user.setTel("4006184080" ) userDao.insert(user); }
按id删除
1 2 3 @Test void testDelete () {userDao.deleteById(1401856123725713409L );
改,这里以前要做个判断,哪些字段为空就不更改,不为空的才更改,现在他直接自动判断
1 2 3 4 5 6 @Test void testUpdate () {User user = new User ();user.setId(1L ); user.setName("Tom666" ); userDao.updateById(user);
查
1 2 3 4 @Test void testGetById () {User user = userDao.selectById(2L );System.out.println(user);
然后pojo对象可以用lombok来快速生成setget方法,也可以用@Data,这个包含以下所有除了两个构造方法的所有方法
1 2 3 4 5 6 7 8 9 10 11 12 @Setter @Getter @Tostring @NoArgsConstructor @AllArgsConstructor @EqualsAndHashCode public class User {private Long id;private string name;private string password;private Integer age;private string tel;
分页功能 selectPage要的是什么,是一个Ipage对象,那就new一个,怎么设置页码,page后面的1代表第一页,2代表有多少条
1 2 3 4 5 6 7 8 9 @Test void testGetByPage () {IPage page = new Page ( 1 ,2 );userDao.selectPage(page,queryWrapper: null ); System.out.println("当前页码值:" +page.getcurrent()); System.out.println("每页显示数:" +page.getsize()); System.out.println("一共多少页:" +page.getPages()); System.out.println("一共多少条数据:" +page.getTotal()); System.out.println("数据:" +page.getRecords());
但执行时发现他显示的是一共0页,一共0条数据,然后把数据全部查询出来,正常的查询语句应该是select *from user ????? limit 1,2.,就是要在后面加上limit进行分页,但这里不是aop,是mybatisplus的拦截器,用于配置分页功能
新建一个MyConfig
1 2 3 4 5 6 7 8 9 @Configuration public class Mpconfig {@Bean public MybatisPlusInterceptor mpInterceptor () {MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor ();mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); return mpInterceptor;
然后要被加载,一种是在Application类里写上@Import加载进去,另一种是直接加@Configuration
想看sql语句可以开启mypluls日志,要在yml配置以下,调程序用的一般不开启
1 2 3 4 mybatis-plus: configuration: log-impl:org.apache,ibatis.logging.stdout.stdoutImpl
DQL编程控制 MyBatisPlus将书写复杂的SQL查询条件进行了封装,使用编程的形式完成查询条件的组合
如果要关闭mybatisplus的图标显示可以在yml设置如下
1 2 3 mybatis-plus: global-config: banner: false
关闭springboot的是
1 2 3 spring: main: banner-mode:off
条件查询的使用:queryWrapper是专门做查询封装条件的,后面的lt是对应html的<,也就是小于号,这样就查询出年龄小于18的,大于号用gt
1 2 3 4 5 6 7 @Test void testGetA11 () {QueryWrapper qw= new QueryWrapper (); qw.lt("age" ,18 ); List<User>userList =userDao.selectList(qw); System.out.println(userList);
方式二,里面的是user类里的age属性的意思,注意要指定泛型
1 2 3 4 5 QueryWrapper<User> qw= new QueryWrapper <User>(); qw.lambda().lt(user::getAge, 18 ); List<User>userList =userDao.selectList(qw); System.out.println(userList);
方式三(推荐)
1 2 3 4 5 LambdaQueryWrapper<User>lqw=new LambdaQueryWrapper <User>(); lqw.lt(User::getAge,val:10 ); List<User>userList = userDao.selectList(lqw); System.out.println(userList);
null值处理 在购物的时候会有些没选择的值,这里就成了空值,这应该怎么处理呢
新建一个query包,新建UserQuery类,这是封装User类的,继承user,然后分析user中有哪些东西可能会有上下限或者范围,比如数值型和日期型。那么这里的age2就用来描述年龄的上限,user里的age就描述年龄的下限
1 2 3 @Data public class UserQuery extends User {private Integer age2;
然后到test中,模拟年龄下限为空时会发生什么,结果是不成功,以前判断空是要加个if(null != uq.getAge()),但这样写要写很多个if,
1 2 3 4 5 6 7 8 9 10 11 UserQuery uq = new UserQuery ();uq.setAge2(30 ); LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper <User>(); lqw.lt(User: :getAge, uq.getAge2()); lqw.gt(User::getAge, uq.getAge()); List<User> userList = userDao.selectList(lqw); System.out.println(userList) ;
lt的值可以传个boolean
可以改造成下面的代码
1 2 3 4 5 6 7 LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper <User>(); lqw.lt(User: :getAge, uq.getAge2()); lqw.lt(null != uq.getAge2(),User::getAge,uq.getAge2()); lqw.lt(null != uq.getAge(),User::getAge,uq.getAge()); List<User> userList = userDao.selectList(lqw); System.out.println(userList) ;
查询投影 就是设置查询结果的样子。下面这个是查全集,然后设置只显示其中某一些字段
1 2 3 4 5 6 7 8 9 LambdaQueryWrapper<User>lqw= new LambdaQueryWrapper <User>(); lqw.select(User::getId,User::getName,User: :getAge); QueryWrapper<User> lqw = new QueryWrapper <User>(); lqw.select("id" ,"name" ,"age" ); List<User>userList =userDao.selectList(lqw); System.out.println(userList);
现在要求查一下count,就是统计有多少数据。单纯用lqw.select(“count(*)”);的时候最终查询是个null,因为现在包装的是放在user对象里,放不进去,不能用selectList这个方法,用selectMaps,其中String代表key,Object代表value,最后输出的是count(*)=6,但是看着很怪,起个别名叫count
1 2 3 4 5 6 7 QueryWrapper<User> lqw = new QueryWrapper <User>(); lqw.select("count(*) as count, tel" ); lqw.groupBy("tel" ); List<Map<String, Object>> userList = userDao.selectMaps(lqw); System.out.println(userList);
查询条件 范围匹配(>、=、between)模糊匹配(like) 空判定(null)包含性匹配(in)分组(group)排序(order)
先看等匹配:eq就是equals
1 2 3 4 5 6 7 LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper <User>(); lqw.eq(User::getName,"Jerry" ).eq(User::getPassword,"jerry" ) ; User loginUser = userDao.select0ne(lqw);System.out.println(loginUser);
范围查询,lt 是不带等号的,le是带等号的,gt是不带ge是带
1 2 3 4 5 LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper <User>(); lqw.between(User::getAge,10 , 30 ); List<User> userList = userDao.selectList(lqw); System.out.println(userList) ;
模糊匹配
1 2 3 4 5 LambdaQueryWrapper<User> 1qw = new LambdaQueryWrapper <User>(); lqw.likeRight(User::getName,"J" ); List<User> userList = userDao.selectList(lqw); System.out.println(userList) ;
更多查询条件设置参看https://mybatis.plus/guide/wrapper.html#abstractwrapper
字段映射与表明映射
问题二:编码中添加了数据库中未定义的属性
1 2 3 @TableField(exist = false) private Integer online;
名称:@TableField 类型:属性注解 位置:模型类属性定义上方 作用:设置当前属性对应的数据库表中的字段关系
相关属性 value:设置数据库表字段名称 exist:设置属性在数据库表字段中是否存在,默认为true。此属性无法与value合井使用
问题三:采用默认查询开放了更多的字段查看权限
1 2 3 public class User {@TableField(value="pwd",select = false) private String password;
select:设置属性是否参与查询,此属性与select()映射配置不冲突
问题四:表名与编码开发设计不同步,比如表明叫tb1_user
名称:@TableName 类型:类注解 位置:模型类定义上方 作用:设置当前类对应与数据库表关系范例:
1 2 3 @TableName("tbl_user") public class User {private Long id;
相关属性:value:设置数据库表名称
id生成策略
不同的表应用不同的id生成策略
日志:自增(1,2,3,4,.)
购物订单:特殊规则(FQ23948AK3843)
外卖单:关联地区日期等信息(1004202003143491)
关系表:可省略id
名称:@TableId
类型:属性注解
位置:模型类中用于表示主键的属性定义上方
作用:设置当前类中主键属性的生成策略
范例:
1 2 3 - public class User { - @TableId(type = IdType.AUTo) - private Long id;
相关属性
value:设置数据库主键名称
type:设置主键属性的生成策略,值参照IdType枚举值,有很多种
设置为AUTO后要去数据库看一下表结构,在表的选项把自动递增的值改小一点,他会自动识别最大值
设置为INPUT后要去数据库把自增策略取消,现在要求必须传id,而id需要自己指定,比如user.serId(666L)
AUTO(0):使用数据库id自增策略控制id生成
NONE(1):不设置id生成策略
INPUT(2):用户手工输入id
ASSIGN_ID(3):雪花算法生成id(可兼容数值型与字符串型)
ASSIGN_UUID(4):以UUID生成算法作为id生成策略
雪花算法是生成一个串,是一个由64位二进制组成的,是一个long值
也可以弄个全局设置,在yml里加上
1 2 3 4 mybatis-plus: global-config: db-config: id-type:assign_id
多记录操作 用这个方法deleteBatchIds
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test void testDelete () {List<Long> list = new ArrayList <>(); list.add(1402551342481838081L ); list.add(1402553134049501186L ); list.add(1402553619611430913L ); userDao.deleteBatchIds(list); List<Long> list = new ArrayList <>(); list.add(1402551342481838081L ) ; list.add(1402553134049501186L ); list.add(1402553619611430913L ); userDao.selectBatchIds();
逻辑删除 删除操作业务问题:业务数据从数据库中丢弃
逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,比如设置1就是删除了,0就是没删,但都还保留着
先在数据库添加deleted字段,默认值为0,然后到User类加个逻辑删除字段,标记当前记录是否被删除
1 2 3 @TableLogic(value = "0" ,delval = "1") private Integer deleted;
执行后的sql语句:
1 UPDATE tbl_user SET deleted= 1 WHERE id= ? AND deleted= 0
这个也有在配置中搞定,第一个是用哪个字段做通用的逻辑,第二个是没有删除的东西标记是几,第三个是被逻辑删除的字段对应的值是几
乐观锁 业务并发现象带来的问题:秒杀
要现在数据库加个字段用来标记当前谁在操作这条数据,然后在user里加下面字段
1 2 @Version private Integer version;
这个锁的原理是比如一个sql语句update set abc=1,如果我有锁就where version = 1,但如果别人拿到了也是1,那么效果就达不到了,所以修改的时候就变成version=version+1,这样不断的累加就会不一样
在之前的分页里mp是通过拦截器帮我们把sql语句里加上,下面第二个拦截器是之前分页的拦截器
1 2 3 4 5 6 7 8 9 10 11 @Configuration public class MpConfig {@Bean public MybatisPlusInterceptor mpInterceptor () { MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor (); mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor ()); mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor ()); return mpInterceptor;
在使用这个的时候要记得加上version值,不提供就没有锁机制。下面这个是test类
输出的结果是
1 2 UPDATE tb1_user SET name= ?, version= ? WHERE id= ? AND version= ? AND deleted= 0 Parameters: Jock666(String), 2 (Integer ), 3 (Long), 1 (Integer )
如果不用锁,就是下面代码,这里面是有version的,可以不从前台拿到version但是要保证这里有version
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 User user = userDao.selectById(3L );user.setName("Jock888" ); 模拟一下两个用户同时操作 User user = userDao.selectById(3L );User user2 = userDao.selectById(3L );user2.setName("Jock aaa" ); userDao.updateById(user2) ; user.setName("Jock bbb" ); userDao.updateById(user);
也就是上面的执行完version已经是4了,下面的就不成立,所以执行失败
代码生成器
其实有些信息是可以读取数据库获取的
我们新建一个项目,pom里面新添两个坐标
1 2 3 4 5 6 7 8 9 <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-generator</artifactId > <version > 3.4.1</version > </dependency > <dependency > <groupId > org.apache.velocity</groupId > <artifactId > velocity-engine-core</artifactId > <version > 2.3</version > </dependency >
然后新建一个类,先创建代码生成器的对象,然后执行代码生成器
1 2 3 4 5 6 7 8 9 10 11 12 13 public class Generator {public static void main (string[]args) {AutoGenerator autoGenerator = new AutoGenerator (); DataSourceConfig dataSource = new DataSourceConfig ();dataSource.setDriverName("com.mysql.cj.jdbc.Driver" ); dataSource.setUrl("jdbc:mysql://localhost:3306/mybatisplus_db?serverTimezone=UTc" ); dataSource.setUsername("root" ); dataSource.setPassword("root" ); autoGenerator.setDataSource(dataSource); autoGenerator.execute();
执行后就生成了一些文件,文件名叫conf,但是生成的路径不太对,就可以配置一些东西
1 2 3 4 5 6 7 8 9 10 GlobalConfig globalConfig = new GlobalConfig ();globalconfig.setoutputDir(System.getProperty("user.dir" )+"/mybatisplus_04_generator/src/main/java" ); globalconfig.setOpen(false ); globalConfig.setAuthor("黑马程序员" ); globalConfig.setFileoverride(true ); globalConfig.setMapperName("%sDao" ); globalConfig.setIdType(IdType.ASSIGN_ID); autoGenerator.setGlobalConfig(globalConfig);
然后是包名相关配置,创建出来的实体类不叫domain,叫entity,dao包叫mapper
1 2 3 4 5 6 PackageConfig packageInfo = new PackageConfig ();packageInfo.setParent("com.itheima”);//设置生成的包名,与代码所在位置不冲突,二者叠加组成完整路径 packageInfo.setEntity(" domain");//设置实体类包名 packageInfo.setMapper(" dao");//设置数据层包名 autoGenerator.setPackageInfo(packageInfo);
然后是策略配置
1 2 3 4 5 6 7 8 9 10 11 StrategyConfig strategyConfig = new StrategyConfig ();strategyConfig.setInclude("tbl_user" ); strategyConfig.setTablePrefix("tbl_" ); strategyConfig.setRestControllerStyle(true ); strategyConfig.setEntityLombokModel(true ); strategyConfig.setVersionFieldName("version" ); autoGenerator.setStrategy(strategyConfig); autoGenerator.execute();
运行后controller里的注解就自动加上@RestController,然后User里加上了@Data,多了个@EquealsAndHashCode(callSuper = false),这个是生成的equals和hashcode是否继承父类的equals和hashcode方法,然后下面的deleted和version都加上了注解