在C层直接实现业务逻辑,这将导致: 1、不同的controller之间,无法共享通用的业务逻辑,比如:折扣计算,反作弊判定,这必然是不合理的。 2、业务逻辑升级,需直接在原代码上做修改兼容,导致controller代码不断膨胀复杂。 3、远程服务协议或者调用方式升级,需要找到所有controller里的调用点,逐一修改。 4、DAO发生替换(比如从oracle迁移mysql),需要找到所有controller里的调用点,逐一修改。
在M层(DAO+model或者ActiveRecord,下面以model泛指)里实现业务逻辑,这将导致: 1、model承担了过多的业务逻辑,导致业务逻辑升级需要修改model,然而model的职责并不是业务,这是很矛盾的。 2、调用一个model中的业务代码没有问题,但是遇到跨表事务又该由哪个model管理呢? 3、业务逻辑实现在model中,如果model发生变更,那么里面写的业务逻辑也得粘贴复制到新的model中,这就是耦合的代价。
有了service层的出现,在C层可以灵活的替换Service保持高度的简洁,而M层保持职责单一仅仅为Service提供数据,Service层则实现所有复杂的业务逻辑与通用的业务逻辑。
Service层的职责
根据上面的分析,Service夹在C层和M层中间,从逻辑上大致划分为3大类: 1、model层的Service:也就是封装每个model与业务相关的通用数据接口,比如:查询订单。(我认为:访问远程服务获取数据也应该归属于这一类Service) 2、中间的Service:封装通用的业务逻辑,比如:计算订单折扣(会用到1中的Service)。 3、controller层的Service:基于1、2中的Service进一步封装对外接口的用户业务逻辑,当然也不排斥直接访问DAO而不使用上述2个Service(不建议)。
在实践中,应该会很自然的用到这三类Service,在了解了这些概念之后再进行代码设计,就不会对Service的职责产生困惑了,自然也对MVC有了新的认识。
关于抽象1、Controller里调用"controller侧的Service"直接完成业务处理,意味着Controller依赖了具体是哪个Service类。 2、Service里调用"DAO/AR"实现数据库的访问,意味着Service依赖了具体是拿个"DAO/AR"类。 3、Service里调用Service,意味着Service依赖了具体是拿个Service类。 为了解除这种耦合,在Web领域一般采用的都是IOC依赖注入来实现【依赖反转】,JAVA和PHP都可以基于反射实现这个能力,各个MVC框架都有相似的实现。
关于service中的代码很简单的问题:对于我这样的初学者,可以发现大部分时候都是在service里直接调用dao,service里面就new一个dao类对象调用,其他有意义的事没做,也不明白有这个有什么用,其实业务才是整个代码中的重中之重。
为什么说service层是最重要的?
我们都知道,标准主流现在的编程方式都是采用MVC综合设计模式,MVC本身不属于设计模式的一种,它描述的是一种结构,最终目的达到解耦,解耦说的意思是你更改某一层代码,不会影响我其他层代码;
现在我们写代码都是:表示层调用控制层,控制层调用业务层,业务层调用数据访问层,初期也许都是new对象去调用下一层,这样写是不对的;
后期我们在学习框架的时候:在业务层中是不应该含有具体对象,最多只能有引用,如果有具体对象存在,就耦合了。当那个对象不存在,我还要修改业务的代码,这不符合逻辑。好比主板上内存坏了,我换内存,没必要连主板一起换。我不用知道内存是哪家生产,不用知道多大容量,只要是内存都可以插上这个接口使用,这就是MVC的意义;
举例理解service
1、ATM机器上,你插入银行卡要给 小红 转账,1 元 【界面】 转账 进入转账界面 输入对方的卡号 输入转账金额 确认按钮 【controller】可以一键生成的代码 获取对方卡号,获取转账金额 获取当前登录用户(session会话跟踪) 【service】需要程序员根据业务需求自行编写,设计规则 //jdbc事务 public void zz(String fromNo,String toNo,int money ){ //判断 对方卡号存不存在 对方卡的状态对不对 你的余额够不够 updateMoney(toNo,1); //停电了.... updateMoney(fromNo,-1); 给你发短信 给对方发短信 给你的微信公众号发消息 添加转账记录..... } 【dao】可以一键生成的 public void updateMoney(String cardNo,int money); 2、注册账号: 【界面】 填写你的账号 输入两次密码 手机验证码可选的(越来越多,短信不能随便发了) 【controller】 servlet(springMVC) try{ 接收数据 String 数据封装,数据类型转换,封装位实体对象 调用--》service 显示结果--》请求转发/重定向到页面 输出的是json }catch(Exception e){ //对异常进行显示即可 String msg = e.getMessage(); request.setAttr("msg",msg); //请求转发到注册jsp,jsp ${msg} //如果前端是ajax //response.getWrite().print("{result:false,msg:“+msg+”}"); } 【service】业务逻辑 public void register(Account account){ //判断,非空判断,长度判断,密码安全性判断【前端不是有js判断,前端判断对程序员无效】 throw new RuntimeException("密码长度不足6位"); //两次密码一致性判断 throw new RuntimeException("两次密码不一致"); //用户名or手机号是否被注册判断.... //密码进行加密储存--》 MD5【前端传输数据的时候就已经进行MD5的加密了】 //调用dao保存数据 //日志,xxx 什么 时间 xxx 注册了,什么 ip //发送短信 } 【dao】 public Account queryByName(String name){} public void save(Account account){}1、增加mybatis的依赖、mysql jar包的依赖; 2、编写mybatis-config.xml核心配置文件(是mybatis运行的基础,mybatis所有的组件都是基于配置文件运行的); 3、编写model实体类; 4、编写mapper.xml映射文件; 5、使用mybatis提供的API,对数据库进行操作【接口、类】;
import com.alibaba.fastjson.JSON; import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import org.junit.Test; import java.io.IOException; import java.io.InputStream; public class MyTest1 { @Test public void selectOne() throws IOException { //加载核心配置文件 InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml"); //获取sqlSession工厂 SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(resourceAsStream); //获取sqlSession会话 SqlSession sqlSession = factory.openSession(); //进行CRUD Object o = sqlSession.selectOne("com.oracle.mapper.AccountMapper.selectOne", 2); //输出结果 System.out.println(JSON.toJSONString(o, true)); /*{ "aid":2, "aname":"zhangcuishan", "anikename":"张翠山", "apass":"shanshan" }*/ } }读取文件:
@Test public void show(){ InputStream resourceAsStream = MyTest1.class.getClass().getResourceAsStream("/mybatis-config.xml"); System.out.println(resourceAsStream); InputStream resourceAsStream1 = MyTest1.class.getClassLoader().getResourceAsStream("mybatis-config.xml"); System.out.println(resourceAsStream1); } java.io.BufferedInputStream@28c97a5 java.io.BufferedInputStream@6659c656对于读法一:类的字节码文件对象.getResourceAsStream()
如果此时只有文件名:"mybatis-config.xml" 从当前类所在的包查找文件; 如果此时文件名前加反斜杠:"/mybatis-config.xml" 从classes根目录下寻找;
对于读法二:类的字节码文件对象.类加载器对象.getResourceAsStream()
这个是直接从classes根目录查找文件
最终,Resourses类给我们提供了Resources.getResourceAsStream("mybatis-config.xml");方法,他也是从classes根目录下寻找文件;mybatis读取核心配置文件源码分析1、SqlSessionFactoryBuilder从XML中构建SqlSessionFactory:
String resource = "org/mybatis/example/mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);2、SqlSessionFactoryBuilder用来创建SqlSessionFactory实例,一旦创建了SqlSessionFactory,就不再需要它了,也就是说:一旦构建了sqlsessionfactory,builder的作用就达到了, builder生命周期就是方法范围;
public SqlSessionFactory build(InputStream inputStream) { return build(inputStream, null, null); }3、调用:
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties)4、源码如下:
/** * 根据配置创建SqlSessionFactory * @param inputStream 配置文件输入流 * @param environment 环境名称 * @param properties 外部配置 * @return */ public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); //parser.parse()读取配置文件返回configuration //build()根据返回的configuration创建SqlSessionFactory return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }5、最终创建DefaultSqlSessionFactory实例
public SqlSessionFactory build(Configuration config) { return new DefaultSqlSessionFactory(config); }6、其中:
XMLConfigBuilder与Configuration:
7、XMLConfigBuilder的方法parse()
public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; //读取mybatis-config.xml配置信息,"configuration"是根结点 parseConfiguration(parser.evalNode("/configuration")); return configuration; }8、XMLConfigBuilder的方法
parseConfiguration(XNode root) /** * 读取配置文件组装configuration * @param root 配置文件的configuration节点 */ private void parseConfiguration(XNode root) { try { //issue #117 read properties first propertiesElement(root.evalNode("properties")); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectionFactoryElement(root.evalNode("reflectionFactory")); settingsElement(root.evalNode("settings")); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } 设计模式从SqlSessionFactory和Configuration的创建过程中可以看出它们都使用了建造者模式;SqlSessionFactory 接口是一个重量级的对象,他的作用是获取sqlsession,应该被设计为系统唯一。
public interface SqlSessionFactory { SqlSession openSession();//使用最多的 SqlSession openSession(boolean autoCommit); SqlSession openSession(Connection connection); SqlSession openSession(TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType); SqlSession openSession(ExecutorType execType, boolean autoCommit); SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level); SqlSession openSession(ExecutorType execType, Connection connection); Configuration getConfiguration(); }平时写代码的时候怎么获取唯一的对象呢?以创建SqlSessionFactory为例:
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; public class MyBatisUtil { private MyBatisUtil() { } //避免线程不可见性 //保证这个值改变之后,能够快速的刷回主存 // 可以保证内存可见性,但是不能保证原子性问题 private volatile static SqlSessionFactory sqlSessionFactory; public static SqlSessionFactory getSqlSessionFactory() throws IOException { //double lock if (sqlSessionFactory == null) { synchronized (MyBatisUtil.class) { if (sqlSessionFactory == null) { try { //在这里初始化即可 InputStream resourceAsStream = Resources.getResourceAsStream("mybatis-config.xml"); sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream); } catch (IOException e) { System.err.println("初始化SqlSessionFactory异常" + e.getMessage()); } } } } return sqlSessionFactory; } }1、这里加锁以及volatile关键字,都是为了防止出现多次创建SqlSessionFactory,以上代码使用了double checked locking;
2、执行流程是:代码最开始判断该对象是否为空,如果不为空的话,进来的这个线程持有了这把类锁,然后第二次判断是否对象为空,如果不为空的话,就创建唯一的该对象,这个创建出来的对象最终被返回去;
3、为什么要判断两次非空呢?如果第一个线程先持有了这把锁,进去之后创建好了该对象,第二个线程在创建对象之前就判断是否为空,结果是空,他进去了第一次的if判断,等待第一个线程释放锁之后,第二个线程持有了这把锁,他再次判断该对象是否为空,如果不为空,就不创建该对象了;
总结一句话:【执行双重检查是因为,如果多个线程同时了通过了第一次检查,并且其中一个线程首先通过了第二次检查并实例化了对象,那么剩余通过了第一次检查的线程就不会再去实例化对象。】
4、为什么要加volatile关键字,假如没有这个关键字的存在,我们分析代码会发现有一个很大的隐患:实例化对象的那行代码实际上可以分为三个步骤:
分配内存空间初始化对象将对象指向刚分配的内存空间但是有些编译器为了性能的原因,可能会将第二步和第三步进行重排序,顺序就成了:
在这种情况下,T7时刻线程B对sqlSessionFactory的访问,访问的是一个初始化未完成的对象。使用了volatile关键字后,重排序被禁止,所有的写(write)操作都将发生在读(read)操作之前。通过上面的工具类可以获取sqlSessionFactory工厂,编写dao层代码如下:
import com.oracle.model.Account; import com.oracle.test.MyBatisUtil; import org.apache.ibatis.session.SqlSession; import java.io.IOException; import java.util.List; public class AccountDao { public List<Account> queryAll() throws IOException { SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession(); try { return sqlSession.selectList("com.oracle.mapper.AccountMapper.selectAll"); } finally { sqlSession.close(); } } public Account queryById(Integer aid) throws IOException { SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession(); try { return sqlSession.selectOne("com.oracle.mapper.AccountMapper.selectOne",aid); } finally { sqlSession.close(); } } public void save(Account account) throws IOException { SqlSession sqlSession = MyBatisUtil.getSqlSessionFactory().openSession(); try { sqlSession.insert("com.oracle.mapper.AccountMapper.insert",account); sqlSession.commit(); } finally { sqlSession.close(); } } }非常明显的可以看到,这里面的代码非常简洁,没有任何与数据库交互的代码;此时可以通过工厂创建的sqlSession会话对象直接进行CRUD操作,并返回结果;
SqlSession是和数据库的一次会话,本质 connection+statement ;生命周期应该是方法内的,不能放在全局,类的属性都不行;而且特别还要注意用完即关闭,否则连接资源将会耗尽;测试类:
public class AccountService { public static void main(String[] args) throws IOException { AccountDao accountDao = new AccountDao(); //查询单条数据 System.out.println(JSON.toJSONString(accountDao.queryById(2), true)); Account account = new Account(); account.setAid(null); account.setAname("赵六"); account.setApass("zhaoliu"); account.setAnikename("小赵"); //插入数据 accountDao.save(account); //查询所有数据 List<Account> accounts = accountDao.queryAll(); System.out.println(JSON.toJSONString(accounts, true)); } }1、编写db.properties文件:
db.driverClass=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://127.0.0.1:3306/db2020?serverTimezone=UTC&characterEncoding=utf-8 db.username=root db.password=1234562、修改mybatis-config.xml核心配置文件:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--根节点configuration配置--> <configuration> <properties resource="db.properties"></properties> <environments default="development"> <!--指的是其中一个数据源--> <environment id="development"> <!--事务管理JDBC--> <transactionManager type="JDBC"/> <!--数据源配置,type类型是pooloed连接池--> <dataSource type="POOLED"> <!--数据库连接的驱动--> <property name="driver" value="${db.driverClass}"/> <property name="url" value="${db.url}"/> <property name="username" value="${db.username}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <!--加载mapper映射文件--> <mappers> <!--mapper文件的全路径,注意使用/分割--> <mapper resource="com/oracle/mapper/AccountMapper.xml"/> </mappers> </configuration>其余代码都和之前的一样;
需要注意的是在properties文件中, key 最好是以文件名为前缀,因为spring框架有配置文件key叫做 username ,或者一些莫名的框架有同样的key,造成程序中获取值有误;并且,绝大多数的配置文件对于XML节点元素的编写顺序有要求,比如properties标签不能放在environments后面;这里我们取出键值对中的值使用${键名}(后面介绍);映射一个实体类:
mybatis-config.xml文件:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <!--根节点configuration配置--> <configuration> <properties resource="db.properties"></properties> <typeAliases> <!--type是类型 alias是别名--> <!--在mapper.xml中 <select id="selectAll" resultType="account"> --> <typeAlias type="com.oracle.model.Account" alias="account"/> </typeAliases> <environments default="development"> <!--指的是其中一个数据源--> <environment id="development"> <!--事务管理JDBC--> <transactionManager type="JDBC"/> <!--数据源配置,type类型是pooloed连接池--> <dataSource type="POOLED"> <!--数据库连接的驱动--> <property name="driver" value="${db.driverClass}"/> <property name="url" value="${db.url}"/> <property name="username" value="${db.username}"/> <property name="password" value="${db.password}"/> </dataSource> </environment> </environments> <!--加载mapper映射文件--> <mappers> <!--mapper文件的全路径,注意使用/分割--> <mapper resource="com/oracle/mapper/AccountMapper.xml"/> </mappers> </configuration>AccountMapper.xml文件:
<select id="selectOne" resultType="account"> select aid,aname,apass,a_nikename as anikename from account where aid = #{id} </select>映射整个包:
部分代码:
<typeAliases> <!--能不能对整个model包进行别名设置--> <!--包下所有的类的别名都是类的名字,且不区分大小写--> <package name="com.oracle.model"/> </typeAliases>这样整个包下的类都可以直接写类名,并且不区分大小写;
如果mapper文件很多,且程序中编写了大量的SQL代码写起来很费劲,难以维护,后面采用mapper代理开发;
MyBatis中#{ }和${ }都可以用来动态传递参数,补全SQL语句,它们之间有没有什么区别和联系?
【不同点】:
#{ }会根据参数的类型,自动匹配是否增加'' ,实质就是利用 PreparedStatement 进行sql的执行; ${ }只是替换,也就是说,你传递 String类型,也直接替换,不会拼接'';
${value}:表示要拼接的是简单类型参数;
注意: 1、如果参数为简单类型时,${}里面的参数名称必须为 value; 2、如果参数为对象类型(如Student类型),传递的参数必须是对象的属性名(name); 3、${}会引起 SQL 注入,一般情况下不推荐使用。但是有些场景必须使用${},比如:order by ${colname} <select id="findUserByName" parameterType="String" resultType="model.User"> SELECT * FROM USER WHERE username like '%${value}%';Java代码:
@Test public void likeSelect() { Account account = new Account(); account.setAname("赵"); List<Account> list = sqlSession.selectList("com.oracle.mapper.AccountMapper.likeSelect1", account); System.out.println(JSON.toJSONString(list, true)); }XML文件:
<select id="likeSelect1" resultType="account"> select aid,aname,apass,a_nikename as anikename from account where aname like '%#{aname}%' </select> <select id="likeSelect2" resultType="account"> select aid,aname,apass,a_nikename as anikename from account where aname like '%${aname}%' </select> 两个模糊查询,第一个使用的是#{},第二个使用的是${},但是执行的结果是第一个报错,第二个能顺利执行;1、原因是在第一个模糊查询中,aname字段会自动被识别为一个字符串,并加上了单引号,此时SQL语句变成了:
select aid,aname,apass,a_nikename as anikename from account where aname like '%'赵'%'因此一定是执行错误;
2、第二个SQL语句中,虽然aname字段是一个字符串,但是它不会被添加单引号,此时的SQL语句为:
select aid,aname,apass,a_nikename as anikename from account where aname like '%赵%'因此顺利执行;
【使用的频率】:
#{ }使用的多 ; ${ }模糊查询,最大的缺点sql注入的问题;比如我们的Java代码这样写:
@Test public void likeSelect() { Account account = new Account(); account.setAname(" ' or 1=1 -- ' "); List<Account> list = sqlSession.selectList("com.oracle.mapper.AccountMapper.likeSelect2", account); System.out.println(JSON.toJSONString(list, true)); } select aid,aname,apass,a_nikename as anikename from account where aname like '%'or 1=1-- '%'看上面的SQL语句,可以发现,这个条件查询任何时候都会成立,因为1=1恒成立,最后的两个–之后的内容全变成了SQL的注释;因此会出现SQL注入的问题;
怎么解决SQL注入问题:
我们可以使用#{}占位,然后Java代码这样写:
@Test public void likeSelect() { Account account = new Account(); account.setAname("%赵%"); List<Account> list = sqlSession.selectList("com.oracle.mapper.AccountMapper.likeSelect1", account); System.out.println(JSON.toJSONString(list, true)); }#{ }的本质:
#{ }使用的是OGNL表达式获取对象中的数据;
<insert id="insert" > insert into account(aname,apass,a_nikename) values (#{aname},#{apass},#{anikename}) </insert> session.insert("com.oracle.mapper.AccountMapper.insert",account);以上两个代码结合,就是传递了Account对象,但是在SQL运行的时候,获取到了对象的属性值,这种从对象中获取属性值的方式,是通过OGNL对象图表达式语言完成的。
ONGL表达式详解
【得到插入数据的主键值】:
比如说qq号,注册qq号,可能qq号是数据库的主键值。 在比如插入类别和商品,注意一次性插入,插入类别后返回主键值;
@Test public void insert() { Account account = new Account(); account.setAname("田七"); account.setApass("tianqi"); account.setAnikename("xiaoqi"); sqlSession.insert("com.oracle.mapper.AccountMapper.insert", account); System.out.println(account.getAid()); //14 } <!--下面的insert语句中 #{} 中写的是对象的属性--> <!--useGeneratedKeys 使用插入后的主键值--> <!--keyProperty将主键值映射到哪一属性上--> <insert id="insert" useGeneratedKeys="true" keyProperty="aid"> insert into account(aname,apass,a_nikename) values (#{aname},#{apass},#{anikename}) </insert>注意:MySQL数据库与SQL Servier数据库都支持主键设置为自动增长,因此我们才能获取主键的值,但是Oracle数据库不支持主键自动增长,他是怎么获取主键的值呢?
MySQL数据库与SQL Servier数据库以及Oracle数据库获取自增长键的值详解