精华内容
下载资源
问答
  • Python+Vue+Django前后端分离项目实战

    万人学习 2020-02-12 09:38:05
    学生管理系统v4.0开发,项目包含了如下几个内容:项目总体介绍、基本功能演示、Vuejs初始化、Element UI的使用、在Django中实现针对数据增删改查接口、在Vuejs中实现前端增删改查调用、实现文件的上传...
  • 4.12.18 跟踪数据库特性使用的统计数据 118 4.12.19 OEM Grid Control 120 4.12.20 管理Management Agent 122 4.12.21 OMS(Oracle Management Service,Oracle服务管理) 123 4.12.22 连接到Grid Control...
  • 3、我们获取到当前线程要执行sqldao类信息放在ThreadLocal对象里面,等到选择路由时候拿出来使用。 一、我们为什么要这么做? 在实际高并发项目中,单库压力非常大。这个时候需要引入数据库主从结构。...

    目录

    一、我们为什么要这么做?

    二、我们该怎么做?

    三、用到的技术

    四、使用

    1、首先定义一个拦截器,在我们数据库操作之前进行拦截

    2、然后在xml配置文件里面定义切面

    3、我们获取到当前线程要执行sql的dao类的信息后放在ThreadLocal对象里面,等到选择路由的时候拿出来使用。


    一、我们为什么要这么做?

    1. 在实际的高并发项目中,单库的压力非常大。这个时候需要引入数据库主从结构。(如果是分库分表或者是数据库集群,又另一说了)。
    2. 由于微服务没有拆分完全,或者压根就一个单应用,需要访问多个数据

    二、我们该怎么做?

    以前的做法可能是,在我们配置文件定义多个datasource,然后在dao中根据增、删、改、查去选择不同的datasource。这样做的话,可能就需要我们在代码里面硬编码选择数据源的过程,这样做显然不够友好,会产生很多冗余,重复的代码。

    那有没有可以让我们的程序自动去选择路由数据源,而我代码中还是像以前那样,只关心业务逻辑,至于怎么选,选什么全都交给框架去实现。这样做的话,是不是瞬间感觉到代码清晰了很多了。那下面我们就来一步一步的实现看看。

    三、用到的技术

    1. spring提供的 AbstractRoutingDataSource 类。该类就是在多数据源下,会根据determineCurrentLookupKey() 这个方法返回的路由key,在动态选择数据源
    2. spring AOP。需要在执行sql的方法前拦截请求,把该线程请求的方法,类名,参数等信息设置到线程局部变量(ThreadLocal)里面,然后determineCurrentLookupKey这个方法就可以根据线程的数据动态的选择数据源了。
    3. spring-boot-autoConfiguration。可以自动装载配置的bean。不需要再手工的去写配置文件

    整体过程

     

    四、使用

    1、首先定义一个拦截器,在我们数据库操作之前进行拦截

    一下完整的所有代码都在我的gitee上,详见:https://gitee.com/terry2870/hp-springboot

     

    定义 类 DAOMethodInterceptorHandle 实现 MethodInterceptor接口,完整代码如下

    package com.hp.springboot.database.interceptor;
    
    import org.aopalliance.intercept.MethodInterceptor;
    import org.aopalliance.intercept.MethodInvocation;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
    import com.hp.springboot.database.bean.DAOInterfaceInfoBean.DBDelayInfo;
    import com.hp.springboot.threadprofile.profile.ThreadProfile;
    
    /**
     * 
     * 描述:执行数据库操作之前拦截请求,记录当前线程信息
     * 之所以用抽象类,是因为可以扩展选择持久层框架。可以选择mybatis或jdbcTemplate,又或者hibernate
     * 作者:黄平
     * 时间:2018年4月11日
     */
    public abstract class DAOMethodInterceptorHandle implements MethodInterceptor {
    
    	private static Logger log = LoggerFactory.getLogger(DAOMethodInterceptorHandle.class);
    	
    	/**
    	 * 存放当前执行线程的一些信息
    	 */
    	private static ThreadLocal<DAOInterfaceInfoBean> routeKey = new ThreadLocal<>();
    	
    	/**
    	 * 最大数据库查询时间(超过这个时间,就会打印一个告警日志)
    	 */
    	private static final long MAX_DB_DELAY_TIME = 10L;
    	
    	/**
    	 * 获取dao操作的对象,方法等
    	 * @param invocation
    	 * @return
    	 */
    	public abstract DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation);
    	
    	/**
    	 * 获取当前线程的数据源路由的key
    	 */
    	public static DAOInterfaceInfoBean getRouteDAOInfo() {
    		return routeKey.get();
    	}
    	
    	@Override
    	public Object invoke(MethodInvocation invocation) throws Throwable {
    		//获取dao的操作方法,参数等信息,并设置到线程变量里
    		this.setRouteDAOInfo(getDAOInterfaceInfoBean(invocation));
    		
    		//设置进入查询,记录线程执行时长
    		entry();
    		Object obj = null;
    		try {
    			//执行实际方法
    			obj = invocation.proceed();
    			return obj;
    		} catch (Exception e) {
    			throw  e;
    		} finally {
    			//退出查询
    			exit();
    			
    			//避免内存溢出,释放当前线程的数据
    			this.removeRouteDAOInfo();
    		}
    	}
    	
    	/**
    	 * 进入查询
    	 */
    	private void entry() {
    		DAOInterfaceInfoBean bean = getRouteDAOInfo();
    		//加入到我们的线程调用堆栈里面,可以统计线程调用时间
    		ThreadProfile.enter(bean.getMapperNamespace(), bean.getStatementId());
    		DBDelayInfo delay = bean.new DBDelayInfo();
    		delay.setBeginTime(System.currentTimeMillis());
    		bean.setDelay(delay);
    	}
    	
    	/**
    	 * 结束查询
    	 */
    	private void exit() {
    		DAOInterfaceInfoBean bean = getRouteDAOInfo();
    		DBDelayInfo delay = bean.getDelay();
    		delay.setEndTime(System.currentTimeMillis());
    		ThreadProfile.exit();
    		//输出查询数据库的时间
    		if (delay.getEndTime() - delay.getBeginTime() >= MAX_DB_DELAY_TIME) {
    			log.warn("execute db expire time. {}", delay);
    		}
    		
    	}
    	
    	/**
    	 * 绑定当前线程数据源路由的key 使用完成后必须调用removeRouteKey()方法删除
    	 */
    	private void setRouteDAOInfo(DAOInterfaceInfoBean key) {
    		routeKey.set(key);
    	}
    
    	/**
    	 * 删除与当前线程绑定的数据源路由的key
    	 */
    	private void removeRouteDAOInfo() {
    		routeKey.remove();
    	}
    }
    

    这里通过getDAOInterfaceInfoBean这个方法获取当前线程调用的方法,参数,签名等一些信息。我这里提供了mybatis的实现。如下

    /**
     * 
     */
    package com.hp.springboot.mybatis.interceptor;
    
    import org.aopalliance.intercept.MethodInvocation;
    import org.apache.commons.lang3.ArrayUtils;
    import org.springframework.util.ClassUtils;
    
    import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
    import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;
    
    /**
     * @author huangping
     * Jul 14, 2020
     */
    public class MyBatisDAOMethodInterceptorHandle extends DAOMethodInterceptorHandle {
    
    	@Override
    	public DAOInterfaceInfoBean getDAOInterfaceInfoBean(MethodInvocation invocation) {
    		DAOInterfaceInfoBean bean = new DAOInterfaceInfoBean();
    		
    		// 获取当前类的信息(由于我们使用的mybatis,这里获取到的是spring的代理类信息)
    		Class<?> clazz = invocation.getThis().getClass();
    		
    		// 这里获取的才是我们定义的dao接口对象
    		Class<?>[] targetInterfaces = ClassUtils.getAllInterfacesForClass(clazz, clazz.getClassLoader());
    		
    		// 获取该类的父类(该操作暂时没用使用到)
    		Class<?>[] parentClass = targetInterfaces[0].getInterfaces();
    		if (ArrayUtils.isNotEmpty(parentClass)) {
    			bean.setParentClassName(parentClass[0]);
    		}
    		
    		// 设置类名信息
    		bean.setClassName(targetInterfaces[0]);
    		
    		// 设置方法的类信息
    		bean.setMapperNamespace(targetInterfaces[0].getName());
    		
    		// 设置方法名
    		bean.setStatementId(invocation.getMethod().getName());
    		
    		// 设置方法参数
    		bean.setParameters(invocation.getArguments());
    		return bean;
    	}
    }
    

    2、然后在xml配置文件里面定义切面

    这样,拦截器就生效了

     


    3、我们获取到当前线程要执行sql的dao类的信息后放在ThreadLocal对象里面,等到选择路由的时候拿出来使用。

    好,我们定义一个自动路由,执行步骤如下

    1. 服务启动时,加载数据库配置信息,读取主从数据库配置信息
    2. 所有数据源信息加载到 AbstractRoutingDataSource 的 targetDataSources中,以供后面动态选择
    3. 执行sql前,动态选择数据源路由

    先选择是哪个数据库,再选择主从。动态选择路由步骤:

    1. 获取前面拦截器拦截的当前线程执行的方法信息
    2. 按照dao的className,从数据源中获取数据源(就是databases.yml里面有没有单独为该dao配置数据源)
    3. 如果有,则使用指定的数据源;如果没有,则使用默认(也就是第一个数据源)数据源
    4. 由于前面获取的只是数据源的前缀,下面我们还要获取真正的数据源对应的key
    5. 根据查询的方法名称,判断走读库,还是写库。(这里面会考虑方法前有没有加ForceMaster的注解)
    6. 返回最终真实的数据源的key,获取到真实的Datasource

    对应代码如下:

    /**
     * 
     */
    package com.hp.springboot.database.datasource;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.regex.Pattern;
    
    import org.apache.commons.collections4.CollectionUtils;
    import org.apache.commons.lang3.BooleanUtils;
    import org.apache.commons.lang3.RandomUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
    
    import com.hp.springboot.database.bean.DAOInterfaceInfoBean;
    import com.hp.springboot.database.bean.DatabaseConfigProperties;
    import com.hp.springboot.database.bean.DatabaseConfigProperties.DatabaseConfig;
    import com.hp.springboot.database.bean.DynamicDatasourceBean;
    import com.hp.springboot.database.datasource.pool.AbstConnectionPoolFactory;
    import com.hp.springboot.database.enums.ConnectionPoolFactoryEnum;
    import com.hp.springboot.database.exception.DataSourceNotFoundException;
    import com.hp.springboot.database.exception.DynamicDataSourceRouteException;
    import com.hp.springboot.database.interceptor.DAOMethodInterceptorHandle;
    import com.hp.springboot.database.interceptor.ForceMasterInterceptor;
    
    /**
     * 描述:动态路由选择数据源
     * 作者:黄平
     * 时间:2018年4月1日
     */
    public class DynamicDatasource extends AbstractRoutingDataSource {
    
    	
    	private static Logger log = LoggerFactory.getLogger(DynamicDatasource.class);
    	
    	/**
    	 * 存放所有的dao对应的数据源的key
    	 * key=dao名称,value=databaseName
    	 * 多数据源时,根据database.yml中的配置,先找有没有该dao指定的数据源,如果有,则使用指定的数据源,如果找不到,则使用第一个(也就是主数据源)数据源
    	 */
    	private static Map<String, String> databaseNameMap = new HashMap<>();
    	
    	/**
    	 * 存放所有的数据源主从的个数
    	 * master_databaseName,10
    	 * slave_databaseName,20
    	 */
    	private static Map<String, Integer> databaseIPCountMap = new HashMap<>();
    	
    	/**
    	 * 默认的数据源名称
    	 */
    	private static String DEFAULT_DATABASE_NAME = "";
    	
    	/**
    	 * master数据源名称的前缀
    	 */
    	private static final String MASTER_DS_KEY_PREX = "master_";
    	
    	/**
    	 * slave数据源名称的前缀
    	 */
    	private static final String SLAVE_DS_KEY_PREX = "slave_";
    	
    	/**
    	 * 匹配查询语句
    	 */
    	private static Pattern select = Pattern.compile("^select.*");
    	
    	/**
    	 * 匹配更新语句
    	 */
    	private static Pattern update = Pattern.compile("^update.*");
    	
    	/**
    	 * 匹配插入语句
    	 */
    	private static Pattern insert = Pattern.compile("^insert.*");
    	
    	/**
    	 * 匹配删除语句
    	 */
    	private static Pattern delete = Pattern.compile("^delete.*");
    	
    	/**
    	 * 数据库配置信息
    	 */
    	private DatabaseConfigProperties databaseConfigProperties;
    	
    	public DynamicDatasource() {}
    	
    	public DynamicDatasource(DatabaseConfigProperties databaseConfigProperties) {
    		this.databaseConfigProperties = databaseConfigProperties;
    	}
    	
    	@Override
    	public void afterPropertiesSet() {
    		//设置targetDataSources 值
    		if (databaseConfigProperties == null || CollectionUtils.isEmpty(databaseConfigProperties.getDatabaseConfigList())) {
    			// 没有数据库配置信息,直接抛异常,启动失败
    			log.error("set DynamicDatasource error. with databaseConfigProperties is null.");
    			throw new DynamicDataSourceRouteException("DynamicDatasource route error. with databaseConfigProperties is null");
    		}
    		try {
    			Map<Object, Object> targetDataSources = new HashMap<>();
    			
    			// 使用哪种类型的连接池(可以dbcp,Druid等等)
    			AbstConnectionPoolFactory connectionPool = ConnectionPoolFactoryEnum.getConnectionPoolFactory(databaseConfigProperties.getPoolName());
    			DynamicDatasourceBean dynamicDatasourceBean = null;
    			String databaseName = null;
    			
    			// 循环遍历数据库配置信息
    			for (DatabaseConfig databaseConfig : databaseConfigProperties.getDatabaseConfigList()) {
    				if (databaseConfig.getServers() == null || CollectionUtils.isEmpty(databaseConfig.getServers().getMaster())) {
    					// 没有配置数据库ip信息或没有配置主库,直接抛异常,启动失败
    					log.error("init database error. with masterUrls is empty.");
    					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
    				}
    				
    				databaseName = databaseConfig.getDatabaseName();
    				dynamicDatasourceBean = connectionPool.getDynamicDatasource(databaseConfig);
    				
    				if (dynamicDatasourceBean == null || CollectionUtils.isEmpty(dynamicDatasourceBean.getMasterDatasource())) {
    					log.error("init database error. with masterUrls is empty.");
    					throw new DynamicDataSourceRouteException("masterUrls is empty. with databaseConfig is: " + databaseConfig);
    				}
    				
    				//设置master
    				for (int i = 0; i < dynamicDatasourceBean.getMasterDatasource().size(); i++) {
    					// 设置到自动路由的map中
    					targetDataSources.put(buildMasterDatasourceKey(databaseName, i), dynamicDatasourceBean.getMasterDatasource().get(i));
    				}
    				//设置master有几个数据源
    				databaseIPCountMap.put(buildMasterDatasourceKey(databaseName, -1), dynamicDatasourceBean.getMasterDatasource().size());
    				
    				//设置slave
    				if (CollectionUtils.isNotEmpty(dynamicDatasourceBean.getSlaveDatasource())) {
    					for (int i = 0; i < dynamicDatasourceBean.getSlaveDatasource().size(); i++) {
    						targetDataSources.put(buildSlaveDatasourceKey(databaseName, i), dynamicDatasourceBean.getSlaveDatasource().get(i));
    					}
    					//设置slave有几个数据源
    					databaseIPCountMap.put(buildSlaveDatasourceKey(databaseName, -1), dynamicDatasourceBean.getSlaveDatasource().size());
    				}
    				
    				//默认数据源
    				if (StringUtils.isEmpty(DEFAULT_DATABASE_NAME)) {
    					// databases.yml的节点 databaseConfigList 下的第一个数据源就是主数据源
    					DEFAULT_DATABASE_NAME = databaseName;
    				}
    				
    				//处理dao(这里就是多数据源自动路由使用)
    				dealDAOS(databaseConfig.getDaos(), databaseName);
    			}
    			
    			super.setTargetDataSources(targetDataSources);
    			super.afterPropertiesSet();
    		} catch (Exception e) {
    			log.error("deal DynamicDatasource error.", e);
    		}
    	}
    
    	@Override
    	protected Object determineCurrentLookupKey() {
    		// 获取当前线程的信息
    		DAOInterfaceInfoBean daoInfo = DAOMethodInterceptorHandle.getRouteDAOInfo();
    		if (daoInfo == null) {
    			//如果没有获取到拦截信息,则取主数据库
    			log.warn("determineCurrentLookupKey error. with daoInfo is empty.");
    			//return null;
    			// 由于上面没有设置defaultTargetDataSource,所以这里需要new一个对象出来空对象,下面会自动在主库中随机选择一个
    			daoInfo = new DAOInterfaceInfoBean();
    		}
    		
    		//按照dao的className,从数据源中获取数据源
    		String mapperNamespace = daoInfo.getMapperNamespace();
    		String databaseName = databaseNameMap.get(mapperNamespace);
    		if (StringUtils.isEmpty(databaseName)) {
    			//如果没有,则使用默认数据源
    			databaseName = DEFAULT_DATABASE_NAME;
    		}
    		
    		// 根据数据源的key前缀,获取真实的数据源的key
    		// 这里考虑了在代码里面的注解(ForceMaster)
    		String result = getDatasourceByKey(databaseName, getForceMaster(daoInfo));
    		log.debug("-------select route datasource with statementId={} and result is {}", (daoInfo.getMapperNamespace() + "." + daoInfo.getStatementId()), result);
    		return result;
    	}
    	
    	/**
    	* @Title: getForceMaster  
    	* @Description: 获取是否  forceMaster
    	* @param daoInfo
    	* @return
    	 */
    	private boolean getForceMaster(DAOInterfaceInfoBean daoInfo) {
    		if (ForceMasterInterceptor.getForceMaster()) {
    			//有设置forceMaster
    			return true;
    		}
    		
    		return getMasterOrSlave(daoInfo);
    	}
    	
    	/**
    	* @Title: getMasterOrSlave  
    	* @Description: 根据方法名,判断走读库还是写库
    	* 这里约定我们的dao里面的方法命名
    	* 查询方法:selectXXX
    	* 新增方法:insertXXX
    	* 更新方法:insertXXX
    	* 删除方法:deleteXXX
    	* 如果不符合这个规范,则默认路由到master库
    	* @param daoInfo
    	* @return
    	 */
    	private boolean getMasterOrSlave(DAOInterfaceInfoBean daoInfo) {
    		//根据方法名称去判断
    		boolean fromMaster = false;
    		//获取用户执行的sql方法名
    		String statementId = daoInfo.getStatementId();
    		if (StringUtils.isEmpty(statementId)) {
    			//没有获取到方法,走master
    			return true;
    		}
    		statementId = statementId.toLowerCase();
    		if (select.matcher(statementId).matches()) {
    			// 如果是查询语句,这里,随机取主从
    			int i = RandomUtils.nextInt(0, 2);
    			fromMaster = BooleanUtils.toBoolean(i);
    		} else if (update.matcher(statementId).matches() || insert.matcher(statementId).matches() || delete.matcher(statementId).matches()) {
    			// 更新,插入,删除使用master数据源
    			fromMaster = true;
    		} else {
    			//如果statemenetId不符合规范,则告警,并且使用master数据源
    			log.warn("statement id {}.{} is invalid, should be start with select*/insert*/update*/delete*. ", daoInfo.getMapperNamespace(), daoInfo.getStatementId());
    			fromMaster = true;
    		}
    		return fromMaster;
    	}
    	
    	/**
    	* @Title: getDatasourceByKey  
    	* @Description: 随机获取路由
    	* @param databaseName
    	* @param fromMaster
    	* @return
    	 */
    	private String getDatasourceByKey(String databaseName, boolean fromMaster) {
    		String datasourceKey = null;
    		Integer num = null;
    		if (fromMaster) {
    			datasourceKey = buildMasterDatasourceKey(databaseName, -1);
    			num = databaseIPCountMap.get(datasourceKey);
    			if (num == null) {
    				//没找到,直接抛出异常
    				log.error("datasource not found with databaseName= {}", databaseName);
    				throw new DataSourceNotFoundException(databaseName);
    			}
    		} else {
    			datasourceKey = buildSlaveDatasourceKey(databaseName, -1);
    			num = databaseIPCountMap.get(datasourceKey);
    			if (num == null) {
    				//没有配置从库,则路由到主库
    				return getDatasourceByKey(databaseName, true);
    			}
    		}
    		
    		int random = 0;
    		if (num == 1) {
    			//如果就只有一个数据源,则就选择它
    			random = 0;
    		} else {
    			//随机获取一个数据源
    			random = RandomUtils.nextInt(0, num);
    		}
    		return fromMaster ? buildMasterDatasourceKey(databaseName, random) : buildSlaveDatasourceKey(databaseName, random);
    	}
    	
    	/**
    	* @Title: dealDAOS  
    	* @Description: dao处理
    	* @param daoList
    	* @param databaseName
    	 */
    	private void dealDAOS(List<String> daoList, String databaseName) {
    		if (CollectionUtils.isEmpty(daoList)) {
    			return;
    		}
    		for (String dao : daoList) {
    			databaseNameMap.put(dao, databaseName);
    		}
    	}
    
    	/**
    	* @Title: buildMasterDatasourceKey  
    	* @Description: 获取主数据源的key
    	* @param databaseName
    	* @param index
    	* @return
    	 */
    	private String buildMasterDatasourceKey(String databaseName, int index) {
    		StringBuilder sb = new StringBuilder(MASTER_DS_KEY_PREX).append(databaseName);
    		if (index >= 0) {
    			sb.append("_").append(index);
    		}
    		return sb.toString();
    	}
    	
    	/**
    	 * 获取从数据源的key
    	 * @param databaseName
    	 * @param index
    	 * @return
    	 */
    	private String buildSlaveDatasourceKey(String databaseName, int index) {
    		StringBuilder sb = new StringBuilder(SLAVE_DS_KEY_PREX).append(databaseName);
    		if (index >= 0) {
    			sb.append("_").append(index);
    		}
    		return sb.toString();
    	}
    
    }
    

    这样,我们就根据方法名称,自动路由到对应的数据库中。我们的业务代码还是跟原来一样写,对我们业务代码几乎没有侵入性。

     

    你以为这样就大功告成了吗?或许上面在大部分情况下都已经没有问题了。但是,敲黑板,敲黑板,我们还没有考虑到事务的情况

    我们正常如果要加事务的时候,就会在方法前加上@Transactional注解。如下所示:

    由于Transactional是加在方法上的,所以在进入这个test()方法之前,spring就要选择好数据源,但是我们DAOMethodInterceptorHandle拦截器拦截的是DAO的方法,dao的方法在service里面,所以这个时候你会发现,spring执行determineCurrentLookupKey()这个方法会获取不到数据源(当然,我这里面写了,默认返回了第一个数据源),因为我们还没有执行拦截器设置线程变量。所以,这个时候如果再执行sql,就会报错(准确的说,执行非默认数据库下的sql会报错)。

    这个时候如果我们想使用非默认数据库下,而且又要有事务控制,这里就会报错。那问题来了,怎么解决呢?

    我们正常使用自动路由的时候,是首先进入拦截器设置线程变量,再进入动态路由选择数据源。但是加了Transactional注解后,选择动态路由的步骤被强行提前到了service方法前了。所以这个时候是获取不到线程变量的。所以,在执行非默认数据库下,而且又要有事务控制,就会报错。

    既然我们已经知道了执行逻辑和顺序,那问题就迎刃而解了。只要我们再加一个注解(跟Transactional平行),强制设置数据库。(不要妄想在一个事务下查询不同的数据库)

    好,那我们加个注解 @UseDatabase

    package com.hp.springboot.database.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 描述:强制使用数据库
     * 作者:黄平
     * 时间:2021-1-7
     */
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value = {ElementType.METHOD})
    public @interface UseDatabase {
    
    	/**
    	* @Title: value  
    	* @Description: 数据名称
    	* @return
    	 */
    	String value() ;
    }
    

    解析UseDatabase注解:

    package com.hp.springboot.database.interceptor;
    
    import java.lang.reflect.Method;
    
    import org.apache.commons.lang3.StringUtils;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.core.Ordered;
    import org.springframework.core.annotation.Order;
    
    import com.hp.springboot.database.annotation.UseDatabase;
    import com.hp.springboot.database.exception.DatabaseNotSetException;
    
    /**
     * 描述:强制使用数据
     * 作者:黄平
     * 时间:2021-1-7
     */
    @Aspect
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class UseDatabaseInterceptor {
    
    	private static Logger log = LoggerFactory.getLogger(UseDatabaseInterceptor.class);
    	
    	private static final ThreadLocal<String> USE_DATABASE = new InheritableThreadLocal<>();
    	
    	/**
    	* @Title: around  
    	* @Description: 设置强制走的数据库
    	* @param joinPoint
    	* @return
    	* @throws Throwable
    	 */
    	@Around("@annotation(com.hp.springboot.database.annotation.UseDatabase)")
    	public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    		log.debug("start before");
    		
    		//方法签名
    		MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    		
    		Method method = methodSignature.getMethod();
    		
    		// 获取UseDatabase注解
    		UseDatabase useDatabase = method.getAnnotation(UseDatabase.class);
    		
    		// 设置的数据库名称
    		String databaseName = useDatabase.value();
    		
    		if (StringUtils.isEmpty(databaseName)) {
    			// 没有指定数据库名称,报错
    			log.error("UseDatabase error. with databaseName is empty");
    			throw new DatabaseNotSetException();
    		}
    		
    		// 设置到线程变量中
    		USE_DATABASE.set(databaseName);
    		Object obj = null;
    		try {
    			obj = joinPoint.proceed();
    			return obj;
    		} catch (Exception e) {
    			throw e;
    		} finally {
    			log.debug("start after");
    			USE_DATABASE.remove();
    		}
    	}
    	
    	/**
    	* @Title: getForceMaster  
    	* @Description: 获取设置的哪个数据库
    	* @return
    	 */
    	public static String getDatabaseName() {
    		return USE_DATABASE.get();
    	}
    }
    

    该拦截器类来解析我们加了UseDatabase注解的方法,并且把数据库名称设置到线程变量里面

    注意这里加了

    加了Order注解,并且设置注解顺序为最高优先级,来设置该拦截器执行顺序。不然这个拦截器如果在Transactional拦截器后面执行那就杯具了。

    好,那我们的自动路由的determineCurrentLookupKey方法要做修改了

    当然,我们可以把这些包装在一个 springboot-starter里面,这样外面使用的话,直接maven依赖一下就可以了

    ok大功告成!也解决事务问题。但是有一点要特别注意的,在@Transactional注解的方法内,不要去访问不同的数据库。

    最后配上对应的databases.yml文件,该文件默认放在src/main/resources下面

    hp:
      springboot:
        database:
          expression: "execution(* com.test.dal..*.*(..))"
          poolName: DBCP
          databaseConfigList:
            - databaseName: test
              servers:
                master:
                  - ${database.test.master.url}
                slave:
                  - 127.0.0.22:3307
                  - 127.0.0.33:3308
              username: root
              password: 123456
        
            - databaseName: sys
              servers:
                master:
                  - 127.0.0.1:3306
                slave:
                  - 127.0.0.2:3301
                  - 127.0.0.3:3302
              username: root
              password: 123456
              daos:
                - com.test.dal.ISysConfigDAO
    

     

     

    完整的代码可以见我的gitee:https://gitee.com/terry2870/hp-springboot

    第一次写博客,如果有写的不好的,或者建议,意见都欢迎大家给我留言!

     

    展开全文
  • 大家把仓库clone到本地,不要全部导入IDE中,根据自己学习章节,一节节案例进行导入比较好,之前第一版代码就是全部放一起,还有一些是自研发框架,所以依赖会有问题,第二版按章节进行分类,这样代码少,...
  • 我们就尝试使用 Docker 来解决开发环境统一问题。 构建一个通用 Linux 镜像,里面包含了前端开发基本环境,包括 python,nodejs 等等... 前端只要给定源码文件就行了。就这样暂时统一了开发...
  • 文件在线聊天系统

    2013-10-28 13:40:08
    主要功能及使用说明: 支持纯缓存模式,同时支持聊天记录定期自动储存功能;...只有一个文件,喜欢怎么移植都可以,兼容所有其他网站系统,不需要任何数据库支持,全缓存模式,高速、安全、实用性强。
  • 数据库索引原理和使用准则 数据库锁机制 数据库事务特性与隔离级别详解 SQL优化详解 基于代理的数据库分库分表框架 Mycat实践 其他 UML大科普:一文梳理清 14种 UML图 工具和软件 【置顶1】熬10天夜,肝...
  • MySQL数据库新建 xboot 数据库,配置文件已开启ddl自动生成表结构但无初始数据,请记得运行导入xboot.sql文件(当报错找不到Quartz相关表时请设置数据库忽略大小写或额外重新导入quartz.sql) 修改配置文件 ...
  • 还是继续使用oracle+mysql,但是每天定时抽取数据方式得做更改,比如,每隔一小时从oracle 吐到mysql,而且oracle转mysql 写一个简单中间件(或者有什么成熟工具),直接进行表与表之间数据同步。...
  • 在ASP.Net课程一开始,不是直接教学员怎么拖ASP.Net控件进行快速开发,而是通过ashx模式开发原始动态网站,让学员明白“请求—处理—响应模型”、“Http协议、Http无状态”、“c#代码渲染生成浏览器端...
  • <ol><li>基于模板渲染动态页面</li><li>基于 AJAX 前后端分离</li><li>基于 Node.js 前端工程化</li><li>基于 Node.js 全栈开发</li></ol> <p><a name="315dd60e"></a></p> 基于模板渲染动态页面 在早起...
  • 前面一篇主要是介绍了怎么完成axios请求测试以及mock.js模拟真实数据请求,前后端分离的实际应用还是得与后端进行交互,下面是关于怎么样与java后台进行交互介绍。 快速开始⚡ 我么将要完成Mybatis框架集成,并...

    前言

    前面一篇主要是介绍了怎么完成axios请求测试以及mock.js模拟真实数据请求,前后端分离的实际应用还是得与后端进行交互,下面是关于怎么样与java后台进行交互的介绍。

    快速开始⚡

    我么将要完成Mybatis框架的集成,并编写查询语句查询出MySQL数据库的数据,通过controller层的API暴露给前端,从而实现数据的交互。

    首先,完成SpringBoot项目的创建,添加mybatis、spring-web、spring-configuration、mysql、lombok依赖。

    项目目录

    创建完成后,项目目录如下:

    img

    一、添加依赖

    pom.xml

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    

    二、编写yaml配置文件

    将根目录下的application.properties重命名为application.yml,然后进行配置:

    server:
      port: 8080
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
        username: root
        password: 123456
        driver-class-name: com.mysql.cj.jdbc.Driver
    # 数据库驱动
    # 此處驱动有两个
    # com.mysql.jdbc.Driver
    # com.mysql.cj.jdbc.Driver
    # MySQL5用的驱动url是com.mysql.jdbc.Driver,
    # MySQL6以后用的是com.mysql.cj.jdbc.Driver。
    # 使用何种驱动,根据安装MySQL的版本而定
    # 下面是Mybatis下划线转驼峰
    mybatis:
      configuration:
        map-underscore-to-camel-case: true
    
    

    注意:这里如果添加了Mybatis而不进行数据库连接池的配置,将会报出异常!

    三、编写主类文件

    User.java

    package com.deepinsea.cors.entity;
    
    import lombok.Data;
    
    /**
     * @author 南街北巷
     * @data 2021/1/6 15:51
     */
    @Data
    public class User {
    
        private int id;
        private String name;
        private int age;
    }
    

    **注意:**我们应该事先创建好MySQL数据库,设置库、表编码为utf8mb64(为什么MySQL不用utf-8?),然后根据数据库设置的字段编写实体类!

    UserMapper.java

    package com.deepinsea.cors.mapper;
    
    import com.deepinsea.cors.entity.User;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    import org.springframework.stereotype.Repository;
    
    import java.util.List;
    
    /**
     * @author 南街北巷
     * @data 2021/1/6 15:52
     */
    @Mapper
    @Repository
    public interface UserMapper {
    
        /**
         * 根据id查询User
         * @param id
         * @return
         */
        @Select("select * from user where id = #{id}")
        User findById(int id);
    
        /**
         * 查询所有用户
         * @return
         */
        @Select("select * from user")
        List<User> findAll();
    }
    

    HelloController.java

    package com.deepinsea.cors.controller;
    
    import com.deepinsea.cors.entity.User;
    import com.deepinsea.cors.mapper.UserMapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.List;
    
    /**
     * @author 南街北巷
     * @data 2021/1/6 14:44
     * @RestController 等同于 @Controller+@ResponseBody
     */
    @RestController
    @RequestMapping("/user")
    public class HelloController {
    
        @Autowired
        private UserMapper userMapper;
    
        @PostMapping("/getBy/{id}")
        public User findById(@PathVariable int id){
            User user = userMapper.findById(id);
            return user;
        }
    
        @PostMapping("/getAll")
        public List<User> getAll(){
            List<User> users = userMapper.findAll();
            return users;
        }
    }
    

    四、测试使用

    下面以请求APIhttp://localhost:8080/user/getBy/2为例进行测试:

    img

    测试以后,发现返回JSON字符串成功(需要自定义序列化配置、异常、枚举可以自行配置,这里仅快速集成)!

    好了,到了这里,数据库 => 后端的数据传递也完成了!可以正式进行后端 => 前端的数据交互了:

    前后端交互

    现在是正式集成进行后端与前端的跨域请求,因此我们需要把注释的portproxyTable打开:

    端口

    port: 8081 # 后端端口为8080
    

    代理配置

      proxyTable: {
            // 跨域配置
            // 修改配置后如果项目启动404,记得重启项目(类似于redis,webpack是依赖配置文件启动的)
            // 错误日志:xhr.js?ec6c:177 POST http://localhost:8081/user/getAll 404 (Not Found)
            '/api': {
              target: 'http://localhost:8080/',
              changeOrigin: true,
              secure: false,  // 如果是https接口,需要配置这个参数
              pathRewrite: {
                '^/api': ''
              }
            }
        },
    

    上面的配置相当于将前端的请求代理为:

    http://localhost:8081/api => http://localhost:8080
    

    上面将原后端增加一个/api路径参数,是为了隐藏真实的后端API地址,但是对于前端请求真实的API地址没有用。因此 ,我们需要在main.js中(或者vue.config.js)设置前端跨域代理的基本路径,这样就不用编写地址时每次都加一个/api了:

    axios.defaults.baseURL = '/api' // 设置跨域代理基本路径
    

    因为使用axios进行过测试(后面可以集成vuex状态管理器对axios进行二次封装),只需要将Home.vue中的测试API访问地址更改为后端的API地址即可:

    <script>
    // @ is an alias to /src
    import HelloWorld from '@/components/HelloWorld.vue'
    import MockTest from '@/components/MockTest.vue' // 这里不能使用./AxiosTest.vue的方式引入
      name: 'Home',
      components: { // 局部注册(引入并注册)
        HelloWorld,
        MockTest
      },
      methods: {
        mockTest () {
          this.$axios.post('/user/getAll') // 'https://v1.alapi.cn/api/music/search?   keyword=我爱你'
            .then(res => {
              console.log(res) // 在控制台打印响应数据(包括响应头与响应体)
            })
            .catch(err => {
              console.log(err)
            })
        }
      }
    </script>
    

    注意:也可以在**created()mounted()**生命周期函数里面添加该方法,将会自动执行。

    测试使用

    跨域配置完成了,启动项目:

    img

    可以看到,成功返回了后端的数据!

    到这里基本可以结束了,下面是关于数组遍历出对象显示到前端的部分(主要为原理深究)

    读取对象数组数据

    1. 使用foreach遍历读取

    首先,一般遍历Javascript遍历实现数据的传递都是通过document.getElementById()方法进行单向数据传递,但是对于已经默认进行了双向绑定的vue对象来说——即:在data()中声明的对象,进行数据的传递往往要考虑一个数据的状态变化性(动态变化性)。

    因为获取到axios响应体的数据一般是通过this指针传递给vue对象的,因此this指向的对象发生变化时全局的对象的值也会随之发生改变(this相当于指针),这将会导致数组使用for循环以及其他各种遍历方式非钩子函数,遍历时新数组对象都会将原来的数组对象覆盖:

    下面是使用for循环遍历等各种数组测试

    Home.vue

    <template>
      <div class="home">
        <Hello-World />
        <Mock-Test />
    	<button @click="userTest">试试</button>
      <!-- 下面两种方式进行遍历都可以,index是数组下标0,1,2...,item是id序号1,2,3... -->
      <!-- 当两种标签遍历方式都存在时,只用item的遍历会报key重复错误(应该是DOM结构冲突),
           因此最好使用item和index同时定位对象的遍历方式 -->
      <p>{{userList}}</p>
      <!-- <p v-for="item in userList" :key="item.id">{{item}}</p>
      <p v-for="(item,index) in userList" :key="index">{{item.id}}</p> -->
    
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from '@/components/HelloWorld.vue'
    import MockTest from '@/components/MockTest.vue' // 这里不能使用./AxiosTest.vue的方式引入
    
    export default {
      name: 'Home',
      components: {
        HelloWorld,
        MockTest
      },
      data () {
        return {
          userList: '' // 注意: 这里为对象数组时参数时定义为userList:[](适用于单个对象)的话会默认产生一个[]符号,
        }
      },
      // created () { // 或created: function(),为生命周期函数created,也可以是mounted—已挂载,可在页面加载时自动执行方法
      //   this.userTest()
      // },
      methods: {
        userTest () {
          this.$axios.post('/user/getAll') // 'https://v1.alapi.cn/api/music/search?keyword=我爱你'
            .then(res => {
              // 响应体获取测试
              // console.log(res)
              // console.log(res.data) // 这里采用逐步分层取值的方式测试
              // console.log(res.data[0])
              // console.log(res.data[0].name)
              // console.log(res.data[res.data[0].id].name) // 嵌套取值
              // console.log(res.data.length)
    
              // var _this = this// 这句话的位置需要注意
              // console.log(this)// 依然是这个方法的对象
              // console.log(_this) // 这个变成了原来的对象
    
              // 循环取值测试(因为this的指针变化问题,不可行,因为this无论是否处于严格模式下都引用全局对象。)
              for (var i = 0; i < res.data.length; i++) { // 遍历出所有对象
                // 将对象合并(for循环遍历赋值、Object.assign()、序列化、浅拷贝、深拷贝(不会覆盖对象)、foreach遍历(测试成功))
                // 1.遍历赋值
                // this.userList = res.data[i] // 测试发现只有id为2(即:第二个对象)对象的值,说明userList的this指向被修改了
                // console.log(this.userList)
                // 2.Object.assign()
                // var objList = Object.assign({}, res.data[i]) // 参数表示,测试时也可以直接用常数表示
                // console.log(objList)
                // this.userList >= objList
                // 3.序列化
                // const obj = JSON.parse(JSON.stringify(res.data[i]))
                // this.userList = obj
                // console.log(obj)
                // 浅拷贝与深拷贝需要下载JQuery,暂时没有尝试
              }
    
              // 4.foreach遍历测试
              // var list = ['750', '1080', '1125', '1242', '1242']
              // var data = []
              // Object.keys(list).forEach(key => {
              //   let item = {
              //     val: list[key]
              //   }
              //   data.push(item)
              // })
              // console.log(list)
              // 正式使用
              var list = res.data
              var data = []
              Object.keys(list).forEach(key => { // 因为这里使用了ES6语法中的 => 钩子函数,因此将不会
                data.push(list[key])
              })
              this.userList = data
              console.log(data)
    
              // this.userList = res.data
              // console.log(res.data)
              // axios的response采用默认参数data存放数据,这里只需要传data的值就行了,
              // 否则要从一个加了其他参数的多重JSON对象中取值,无异于自己设计一个JSON序列化工具
              // 注意:这里res.data前不需要加this指针,因为箭头函数没有自己的this(因此本身是局部变量),
              // 箭头函数里面的this是对象的指针(定义时的全局变量),如果加了this那么将获取不到res.data的值
            })
            .catch(err => {
              console.log(err)
            })
        }
      }
    }
    </script>
    

    使用for循环里面的遍历赋值与浅拷贝都会导致最终数组元素的值发生改变,导致对象数组遍历出对象再合并出现对象覆盖的现象:

    img

    但是使用foreach遍历搭配钩子函数就能避免这个问题:

     // 4.foreach遍历测试
          // var list = ['750', '1080', '1125', '1242', '1242']
          // var data = []
          // Object.keys(list).forEach(key => {
          //   let item = {
          //     val: list[key]
          //   }
          //   data.push(item)
          // })
          // console.log(list)
          // 正式使用
          var list = res.data
          var data = []
          Object.keys(list).forEach(key => { // 因为这里使用了ES6语法中的 => 钩子函数,因此将不会
            data.push(list[key])
          })
          this.userList = data
          console.log(data)
    

    2. 使用v-for读取

    对此,vue2.0提供了默认的数组遍历方法(当然,数组还存在下标属性更新不了以及数组长度不可响应式改变的问题,具体参考issues;这些在vue3.0中使用proxy代替Object.defineProperty()时得到了解决),下面是关于vue的数组遍历方法v-for的使用:

    Home.vue

    <template>
      <div class="home">
        <Hello-World />
        <Mock-Test />
    	<button @click="userTest">试试</button>
      <!-- 下面两种方式进行遍历都可以,index是数组下标0,1,2...,item是id序号1,2,3... -->
      <!-- 当两种标签遍历方式都存在时,只用item的遍历会报key重复错误(应该是DOM结构冲突),
           因此最好使用item和index同时定位对象的遍历方式 -->
      <p>{{userList}}</p>
      <p v-for="item in userList" :key="item.id">{{item}}</p>
      <!-- 使用单item作为参数的方式最好不要获取详细到具体key的值,否则会提示错误,因此这种适合整个对象的获取 -->
      <!-- <p v-for="(item,index) in userList" :key="index">{{item.id}}</p> -->
    
      </div>
    </template>
    
    <script>
    // @ is an alias to /src
    import HelloWorld from '@/components/HelloWorld.vue'
    import MockTest from '@/components/MockTest.vue' // 这里不能使用./AxiosTest.vue的方式引入
    
    export default {
      name: 'Home',
      components: {
        HelloWorld,
        MockTest
      },
      data () {
        return {
          userList: '' // 注意: 这里为对象数组时参数时定义为userList:[](适用于单个对象)的话会默认产生一个[]符号,
        }
      },
      // created () { // 或created: function(),为生命周期函数created,也可以是mounted—已挂载,可在页面加载时自动执行方法
      //   this.userTest()
      // },
      methods: {
        userTest () {
          this.$axios.post('/user/getAll') // 'https://v1.alapi.cn/api/music/search?keyword=我爱你'
            .then(res => {
              // 响应体获取测试
              // console.log(res)
              // console.log(res.data) // 这里采用逐步分层取值的方式测试
              // console.log(res.data[0])
              // console.log(res.data[0].name)
              // console.log(res.data[res.data[0].id].name) // 嵌套取值
              // console.log(res.data.length)
    
              // var _this = this// 这句话的位置需要注意
              // console.log(this)// 依然是这个方法的对象
              // console.log(_this) // 这个变成了原来的对象
    
              // 循环取值测试(因为this的指针变化问题,不可行,因为this无论是否处于严格模式下都引用全局对象。)
              for (var i = 0; i < res.data.length; i++) { // 遍历出所有对象
                // 将对象合并(for循环遍历赋值、Object.assign()、序列化、浅拷贝、深拷贝(不会覆盖对象)、foreach遍历(测试成功))
                // 1.遍历赋值
                // this.userList = res.data[i] // 测试发现只有id为2(即:第二个对象)对象的值,说明userList的this指向被修改了
                // console.log(this.userList)
                // 2.Object.assign()
                // var objList = Object.assign({}, res.data[i]) // 参数表示,测试时也可以直接用常数表示
                // console.log(objList)
                // this.userList >= objList
                // 3.序列化
                // const obj = JSON.parse(JSON.stringify(res.data[i]))
                // this.userList = obj
                // console.log(obj)
                // 浅拷贝与深拷贝需要下载JQuery,暂时没有尝试
              }
    
              // 4.foreach遍历测试
              // var list = ['750', '1080', '1125', '1242', '1242']
              // var data = []
              // Object.keys(list).forEach(key => {
              //   let item = {
              //     val: list[key]
              //   }
              //   data.push(item)
              // })
              // console.log(list)
              // 正式使用
              // var list = res.data
              // var data = []
              // Object.keys(list).forEach(key => { // 因为这里使用了ES6语法中的 => 钩子函数,因此将不会
              //   data.push(list[key])
              // })
              // this.userList = data
              // console.log(data)
    
              this.userList = res.data
              console.log(res.data)
              // axios的response采用默认参数data存放数据,这里只需要传data的值就行了,
              // 否则要从一个加了其他参数的多重JSON对象中取值,无异于自己设计一个JSON序列化工具
              // 注意:这里res.data前不需要加this指针,因为箭头函数没有自己的this(因此本身是局部变量),
              // 箭头函数里面的this是对象的指针(定义时的全局变量),如果加了this那么将获取不到res.data的值
            })
            .catch(err => {
              console.log(err)
            })
        }
      }
    }
    </script>
    

    启动项目,成功获取到数据:

    img

    前后端数据请求成功!

    小结

    前后端打通了,下面就是项目UI框架的集成了,冲冲冲💪💪

    我是Java白羊🐏,一只想做全栈的小羊,感谢大家的观看ヾ(◍°∇°◍)ノ゙!

    展开全文
  • asp.net知识库

    2015-06-18 08:45:45
    怎么在ASP.NET 2.0中使用Membership asp.net 2.0-实现数据访问(1) ASP.NET 2.0 新特性 .NET 2.0里使用强类型数据创建多层应用 在MastPage中引用脚本资源 2.0正式版中callback一些变化+使用示例(ASP.NET 2.0)...
  • 精通Oracle PL/SQL--详细书签版

    热门讨论 2012-08-21 13:06:28
    这一章说明了为什么PL/SQL几乎总是数据库编程正确工具,但也探讨了PL/SQL并不适合于哪些场合,这时需要创新性地使用SQL来完全避免过程式代码。..  第2章:全部打包。包不只是过程逻辑组合,它们具有很多优势,...
  • 该项目是校园资讯管理系统,前后端分离,本文只针对前端进行阐述,前端使用swagger接口与后端交互数据即可,主要实现功能无非是对数据增删改查。 配置环境 部署数据库 连接服务器 上传jar包到服务器上 查看...

    项目介绍

    该项目是校园资讯管理系统,前后端分离,本文只针对前端进行阐述,前端使用swagger接口与后端交互数据即可,主要实现的功能无非是对数据的增删改查。

    配置环境

    • 部署数据库
    • 连接服务器
    • 上传jar包到服务器上
    • 查看swagger接口文档

    搭建项目框架

    项目框架比较繁琐,尚不说明,重点讨论子页面怎么写

    封装js文件

    本项目中我们自己写了两个js文件

    • http.js
      这一文件主要是对ajax进行了封装,封装成get方法,postForm还有postJSON方法,在我们需要与后台交互数据时,调用对应的方法即可,就不用了每次都写ajax了。例如:
    let myAjax = {
      get(url, params) {
        return new Promise((resovle, reject) => {
          $.ajax({
            url: baseURL + url,
            method: 'get',
            data: qs.stringify(params),
            headers: {
              'Content-Type': 'application/x-www-form-urlencoded;charset=utf8',
              'Authorization': sessionStorage.getItem('token')
            },
            success(res) {
              resovle(res)
            },
            error(err) {
              reject(err);
            }
          })
        });
      },
     }
    
    • api.js
      这一文件是将swagger上的接口路径进行封装,提前写好就不用了每一次都去swagger文档中复制接口路径了,例如:
    let api = {
        CATEGORY_FINDALL: '/category/findAll',
        CATEGORY_SAVEORUPDATE: '/category/saveOrUpdate',
        CATEGORY_DELETEBYID: '/category/deleteById',
        CATEGORY_BATCHDELETE: '/category/batchDelete',
        BASEUSER_PAGEQUERY: '/baseUser/pageQuery'
    };
    

    写子页面(重点)

    思路

    把页面基本元素布置好---->(查)将信息显示到页面上---->(删)删除信息---->(改)(增)添加和修改信息【共用一个模态框】

    过程

    • html+css

    这一部分没什么好说的,内容和样式按需求来设计

    • 数据模型
    	// 数据模型
      var categoryDataModel = {
        // 所有类别数据
        categoryData: [],
        // 当前修改的对象
        currentCategory: {},
        // 模态框的标题
        dialogTitle: '新增分类信息',
      };
    

    我们把接收到的数据存放到我们写好的数据模型中,在下方调用的时候,方便编写代码,能让思路清晰一点

    • categoryData存放后台发过来的所有数据,主要是用在findAll()方法中

    • currentCategory存放的是当前要修改的对象

    • dialogTitle 这一个存放的是模态框的标题,这里的内容放到模态框中进行阐述

    1. 封装模态框
    // 封装 处理模态框的标题,追加option,设置表单控件内容,显示模态框
      function changeDialogTitle() {
        $('.dialog-title').text(categoryDataModel.dialogTitle);
        $('[name=parentId]').empty();
        $('[name=parentId]').append($(` <option value="">请选择</option>`)); //开单引号
        // 过滤一级栏目作为父栏目
        let result = categoryDataModel.categoryData.filter(item => !item.parentId);
        result.forEach(item => {
          $('[name=parentId]').append($(`<option value="${item.id}">${item.name}</option>`));
        });
        $('[name=name]').val(categoryDataModel.currentCategory.name);
        $('[name=parentId]').val(categoryDataModel.currentCategory.parentId);
        $('[name=no]').val(categoryDataModel.currentCategory.no);
        $('[name=description]').val(categoryDataModel.currentCategory.description);
        // 显示模态框
        $('.dialog').fadeIn();
      }
    

    模态框是一个挺重要的一个部分,它涉及到了添加和修改这两个功能,当然我们可以让新增有新增的模态框,修改有修改的模态框,但这样代码量特别多,为了减少代码量,提高复用性,我们可以封装一个新增和修改都可以使用的模态框。新增模态框和修改模态框的区别在于模态框的标题不同,input中显示的内容不同,还有提交给后台数据的不同,我们来一一谈论一下

    一、标题不同,让用户区分所使用的模态框是哪一种功能的,如果后台修改接口和新增接口是分开的,那么我们就可以用if语句判断模态框标题是新增还是修改,以此调用不同的接口实现不同的功能
    二、后台提交的数据不同,如果新增和修改的功能是同一个接口,那我们要判断模态框提交的数据中是否有id值,有就更改该id的数据,没有就是新增一条数据。在这里,我们使用到了数据模型当中的currentCategory来存对象值,点击新增时我们会清空currentCategory,传数据时id为空,后台判断为新增;点击修改时,将改对象值赋给currentCategory,带有id值,传给后台后判断为修改。以此来实现新增和修改两个不同的功能
    三、input中的内容不同,如上一条所说,在点击新增时清空input,即currentCategory中的数据,在点击修改时,将改对象值显示在input当中,并把值赋给currentCategory。
    1. 写一个FindAll()方法【查,刷新页面】
      findAll()其实是调用查询接口让后台把所有数据传上来,但为了方面我们在增删改后刷新页面,显示新的数据,我们把清除数据(防止数据累加)还有显示数据写到一个方法中,那他实现的功能就是获取页面初始信息还有刷新页面,在增删改中调用findAll()方法即可。
    // 获取全部类别信息
      findAll();
    
      function findAll() {
        myAjax.get(api.CATEGORY_FINDALL, null).then((res) => {
          // console.log(res);
          // 清空节点
          $('.categoryManage tbody').empty();
          // 将数据设置到数据模型中
          categoryDataModel.categoryData = res.data;
          let arr = res.data || [];
          if (arr.length > 0) {
            arr.forEach((item, index) => {
              $('.categoryManage tbody').append($(`
            <tr>
              <td>${index + 1}</td>
              <td>${item.name}</td>
              <td>${item.no}</td>
              <td>${item.parentId || '--'}</td>
              <td>${item.description || '--'}</td>
              <td>
                <i data-item='${JSON.stringify(item)}' class="fa fa-edit" title="编辑"></i>
                <i data-id="${item.id}" class="fa fa-trash-o" title="删除"></i>
              </td>
            </tr>
            `));
            })
          }
        })
      }
    
    1. 写增删改查点击事件

    查就是在打开页面时调用findAll()即可,删则绑定按钮,通过id调用myAjax对应的删除接口即可删除,改和查就绑定对应的按钮,调用模态框,更改模态框标题即可

    展开全文
  • C#基类库(苏飞版)

    2014-05-16 23:11:45
    1.把上传的文件转换为字节数组 2.流转化为字节数组 2.上传文件根据FileUpload控件上传 3.把Byte流上传到指定目录并保存为文件 复制代码 UpLoadFiles 页面专用类 19.时间操作类 DateFormat 返回每月的第一天和最后一...
  • 每个微服务都有自己独立的数据库,那么后台管理联合查询怎么处理?这应该是大家会普遍遇到一个问题,有三种处理方案。 1、严格按照微服务划分来做,微服务相互独立,各微服务数据库也独立,后台需要展示数据时...
  • 很简单,就是你先打开一个你以前写过的项目,进去看看你电脑能轻松打开的项目的这两个版本号分别是多少,然后再用记事本打开从网上下载下来对应的文件,修改版本号为你本地已有的版本号,最后再打开项目。...
  • 全面解析mall中使用的数据库表结构 mall数据库表结构概览 商品模块数据库表解析(一) 商品模块数据库表解析(二) 订单模块数据库表解析(一) 订单模块数据库表解析(二) 订单模块数据库表解析(三) 营销模块...
  • 手把手教你怎么使用云服务器 带你了解什么是Push消息推送 人在家中坐,班从天上来「小程序推送」 Java发送邮件时,必须要一个配置! fastjson学习笔记 本地文件自动同步到GitHub 为什么PUSH推送经常出事故? 三歪...
  • 二十三种设计模式【PDF版】

    热门讨论 2011-05-30 14:13:49
    可扩展的使用 JDBC针对不同的数据库编程,Facade提供了一种灵活实现. 设计模式之 Composite(组合) 就是将类用树形结构组合成一个单位.你向别人介绍你是某单位,你是单位中一个元素,别人和你做买卖,相当于 和...
  • 数据库设置:前台数据库链接地址修改在Ft_conn.asp文件中,后台数据库链接地址修改在admin/Ft_conn_admin.asp文件中; 网站基本参数设置在Ft_config.asp文件中,在后台通过FSO功能进行实时修改,如果你网站空间不...
  • 34、重新优化会员和管理功能,使用同一个入口,指定某一会员为管理员的功能,获取登录后的管理权利; 35、支持热门关键词功能,自动记录搜索字词,后台可设置热门关键; 36、支持网站在线设置; 37、提供帖子自动...
  • 4、重新优化会员和管理功能,使用同一个入口,指定某一会员为管理员的功能,获取登录后的管理权利; 5、支持热门关键词功能,自动记录搜索字词,后台可设置热门关键; 6、支持网站在线设置; 7、提供帖子自动锁定...
  • 注意:根目录有空数据库,替换asaidata文件夹中的数据库即可删除搜索调试中数据,替换,需要从后台添加12+产品,及其他信息方能完美显示前台页面。 ★★★★★★★★★★ 二、系统优势 1、专业美工操刀,...
  • 虽然说学习ASP.NET不需要任何ASP基础,但是我觉得如果大家ASP不会,还是先看一下【十天学会ASP教程】,大家所需要了解不是ASP程序怎么写,而是怎么构建服务器,怎么使用HTML表单,同时对SQL语句有一个基础和理解...
  • 修改dataHost和schema对应的连接信息,user,pay 垂直切分后的配置如下所示: schema 是实际逻辑库的配置,user,pay分别对应两个逻辑库,多个schema代表多个逻辑库。 dataNode是逻辑库对应的分片,如果配置多个分片...

空空如也

空空如也

1 2
收藏数 40
精华内容 16
关键字:

数据库分离后的文件怎么使用