一.搭建 1.前端
npm install
npm run serve
2.後端
老生常談的配置,修改mysql與redis即可。
二.業務功能介紹
功能上jeecgboot主要提供了系列的代碼生成器、模闆頁面、報表頁面。
1.報表功能
主要提供報表的相關操作。提供了積木報表插件,可以自定義資料報表、圖形報表。并将報表挂載到菜單上。
2.線上開發
也就是代碼生成器,可以可視化的在頁面上建立資料庫表,并通過資料庫表生成前背景代碼。減少業務代碼開發的時間。
3.系統管理
使用者管理、角色管理、機構管理、消息管理等基礎子產品。
4.系統監控
主要負責各種日志、監控的統一處理。
5.頁面元件樣式
常見案例、詳情頁、結果頁、異常頁、清單頁、表單頁主要提供了樣式頁面與控件頁面示例。在開發過程中如果需要模闆直接複制代碼即可。詳情請
三.背景架構介紹 1.概括
其中報表和代碼生成器沒有提供源碼,如果有興趣可以自行檢視jar包源碼。
2.架構核心包jeecg-boot-base
jeecg-boot-base包括了下文的幾個部分。
1.接口包jeecg-boot-base-api 1.對外接口jeecg-system-cloud-api
使用feign+hystrix實作了服務間調用加熔斷,單機環境并沒有使用。
2.服務内接口jeecg-system-local-api
該包提供了下文使用的常用方法接口。僅提供了接口并無其他配置。
2.核心配置包jeecg-boot-base-core 1.通用類common 1.api
其中為通用接口與通用傳回對象。
1.Result
其中Result為所有類的傳回實體,這樣能夠通過code編碼和message擷取是否成功和成功/失敗的資訊。此類是常用的架構設計
2.aspect
為項目的自定義注解,使用了AOP的切面方式實作,這裡就不詳細說了,比較簡單都可以看懂。
3.constant
存放着枚舉類與常量池,這裡不多說了。
4.es
為操作es的通用類,主要是配置es連接配接和查詢時動态拼接and/or的方法。
5.exception
exception為自定義的異常類。
1.JeecgBootExceptionHandler
這裡詳細說一下JeecgBootExceptionHandler,該類也是常見的架構設計之一,核心為@RestControllerAdvice、@ExceptionHandler。當業務代碼中沒有對異常攔截時,該類會自動攔截異常,并資料log日志。是以某些日志在該類配置後,就不需要在每個接口中都捕獲這個異常了。
6.handler
為下文規範提供了接口類。沒有其他特别說明。
7.system類
這裡主要說controller、entity、service等業務代碼的父類
1.JeecgController
是以controller的父類,提供了導入導出的功能。還可以在裡面擴充分頁、排序、常用調用方法等,這樣就可以避免相同的代碼多次添加。這也是架構設計中常用的技巧。
2.JeecgEntity
将通用字段如id、建立人、修改人、建立時間、修改時間等字段統一封裝在一個實體中,使用其他實體繼承。這也是架構設計中常用的技巧。
3.service
主要提供Mybatis-plus提供的curd方法。
8.utli
提供了一大波的工具類,如果在工作中需要,直接複制使用。
2.通用配置類config 1.mybatis
1.MybatisInterceptor
MybatisInterceptor這裡主要說MybatisInterceptor,該類負責在mybatis執行語句前,攔截并擷取參數,将建立人、建立時間等字元動态插入。這裡上部分核心代碼。
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
String sqlId = mappedStatement.getId();
log.debug("------sqlId------" + sqlId);
//擷取sql類型是插入還是修改
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//擷取插入參數
Object parameter = invocation.getArgs()[1];
if (parameter == null) {
return invocation.proceed();
}
if (SqlCommandType.INSERT == sqlCommandType) {
LoginUser sysUser = this.getLoginUser();
//通過反射擷取入參的類
Field[] fields = oConvertUtils.getAllFields(parameter);
for (Field field : fields) {
log.debug("------field.name------" + field.getName());
try {
//将建立人資訊動态加入
if ("createBy".equals(field.getName())) {
field.setAccessible(true);
Object local_createBy = field.get(parameter);
field.setAccessible(false);
if (local_createBy == null || local_createBy.equals("")) {
if (sysUser != null) {
// 登入人賬号
field.setAccessible(true);
field.set(parameter, sysUser.getUsername());
field.setAccessible(false);
}
}
}
}
2.MybatisPlusSaasConfig
該類主要負責多租戶,什麼是多租戶呢?
多租戶:就是多個公司/客戶公用一套系統/資料庫,這就需要保證資料的權限。
該場景比較少不詳細說明。
2.oss
主要從application-dev.yml擷取到上傳的路徑與配置。
3.shiro
安全架構主要有兩個目标:認證與鑒權。
認證:判斷使用者名密碼是否正确。
鑒權:判斷使用者是否有權限通路該接口。
這裡本文着重講解,如果遇到shiro相關應用,可以項目直接移植使用。
1.CustomShiroFilterFactoryBean
該類主要負責解決資源中文路徑問題。這裡有個通用的解決方式。
建立類內建ShiroFilterFactoryBean方法,并重寫核心方法createInstance(),并在注入時,注入建立的類CustomShiroFilterFactoryBean,這樣就達到的以往重新源碼的功能。因為spring提供的功能都是用該思想,是以修改源碼的地方就原來越少了,都可以使用該方式實作。
2.JwtFilter
同上文,複寫BasicHttpAuthenticationFilter的驗證登入使用者的方法,在執行登入接口後判斷使用者是否正确。
3.ResourceCheckFilter
負責鑒權使用,判斷目前使用者是否有權限通路。
//表示是否允許通路 ,如果允許通路傳回true,否則false;
@Override
protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
//擷取目前url
String url = getPathWithinApplication(servletRequest);
log.info("目前使用者正在通路的 url => " + url);
return subject.isPermitted(url);
}
//onAccessDenied:表示當通路拒絕時是否已經處理了; 如果傳回 true 表示需要繼續處理; 如果傳回 false
@Override
protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.sendRedirect(request.getContextPath() + this.errorUrl);
// 傳回 false 表示已經處理,例如頁面跳轉啥的,表示不在走以下的攔截器了(如果還有配置的話)
return false;
}
4.ShiroRealm
主要負責擷取使用者所有的菜單權限,并提供token的一系列方法。
//擷取所有菜單權限集合
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
}
//驗證使用者輸入的賬号和密碼是否正确
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
}
//校驗token的有效性
public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
}
//重新整理token有效時間
public boolean jwtTokenRefresh(String token, String userName, String passWord) {
}
//清除目前使用者的權限認證緩存
@Override
public void clearCache(PrincipalCollection principals) {
super.clearCache(principals);
}
5.ShiroConfig
此為shiro的核心配置類,大多數寫法都是固定寫法。
public class ShiroConfig {
@Value("${jeecg.shiro.excludeUrls}")
private String excludeUrls;
@Resource
LettuceConnectionFactory lettuceConnectionFactory;
@Autowired
private Environment env;
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
CustomShiroFilterFactoryBean shiroFilterFactoryBean = new CustomShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 攔截器
Map<string, string=""> filterChainDefinitionMap = new linkedHashMap<string, string="">();
if(oConvertUtils.isNotEmpty(excludeUrls)){
String[] permissionUrl = excludeUrls.split(",");
for(String url : permissionUrl){
filterChainDefinitionMap.put(url,"anon");
}
}
// 配置不會被攔截的連結 順序判斷 也就是不同通過token通路的位址
filterChainDefinitionMap.put("/sys/cas/client/validateLogin", "anon"); /
// 添加自己的過濾器并且取名為jwt
Map<string, filter=""> filterMap = new HashMap<string, filter="">(1);
//如果cloudServer為空 則說明是單體 需要加載跨域配置【微服務跨域切換】
Object cloudServer = env.getProperty(CommonConstant.CLOUD_SERVER_KEY);
//前文定義的過濾器
filterMap.put("jwt", new JwtFilter(cloudServer==null));
shiroFilterFactoryBean.setFilters(filterMap);
//
ex.printStackTrace();
}
}
}
}
public interface MqListener<t> {
default void handler(T map, Channel channel) {
}
}
</t></string,></string,></string,></string,>
2.RabbitMqClient
主要在隊列初始化時實作隊列的初始化,而是否初始化根據使用時的@RabbitListener、@RabbitComponent判斷。
public interface MqListener<t> {
default void handler(T map, Channel channel) {
}
}
@Bean
public void initQueue() {
//擷取帶RabbitComponent注解的類
Map<string, object=""> beansWithRqbbitComponentMap = this.applicationContext.getBeansWithAnnotation(RabbitComponent.class);
Class<!--? extends Object--> clazz = null;
//循環map
for (Map.Entry<string, object=""> entry : beansWithRqbbitComponentMap.entrySet()) {
log.info("初始化隊列............");
//擷取到執行個體對象的class資訊
clazz = entry.getValue().getClass();
Method[] methods = clazz.getMethods();
//判斷是否有RabbitListener注解
RabbitListener rabbitListener = clazz.getAnnotation(RabbitListener.class);
//類上有注解 就建立隊列
if (ObjectUtil.isNotEmpty(rabbitListener)) {
createQueue(rabbitListener);
}
//方法上有注解 就建立隊列
for (Method method : methods) {
RabbitListener methodRabbitListener = method.getAnnotation(RabbitListener.class);
if (ObjectUtil.isNotEmpty(methodRabbitListener)) {
createQueue(methodRabbitListener);
}
}
}
}
private void createQueue(RabbitListener rabbitListener) {
String[] queues = rabbitListener.queues();
//建立交換機
DirectExchange directExchange = createExchange(DelayExchangeBuilder.DELAY_EXCHANGE);
rabbitAdmin.declareExchange(directExchange);
//建立隊列
if (ObjectUtil.isNotEmpty(queues)) {
for (String queueName : queues) {
Properties result = rabbitAdmin.getQueueProperties(queueName);
if (ObjectUtil.isEmpty(result)) {
Queue queue = new Queue(queueName);
addQueue(queue);
Binding binding = BindingBuilder.bind(queue).to(directExchange).with(queueName);
rabbitAdmin.declareBinding(binding);
log.info("建立隊列:" + queueName);
}else{
log.info("已有隊列:" + queueName);
}
}
}
}
</string,></string,></t>
3.RabbitMqConfig
為消息隊列的常用配置方式。這裡不多描述。
4.event
這個包主要是為使用mq發送消息使用,多類别的消息會實作JeecgBusEventHandler類,而baseApplicationEvent通過消息類型傳入的不同的參數選擇合适的業務類發送消息。
5.DelayExchangeBuilder
為延時隊列的交換機聲明與綁定。
2.jeecg-boot-starter-lock
1.如何使用分布式鎖
使用時有兩種方式,一種是使用注解方式,一種是使用redisson提供的API。
@Scheduled(cron = "0/5 * * * * ?")
@JLock(lockKey = CloudConstant.REDISSON_DEMO_LOCK_KEY1)
public void execute() throws InterruptedException {
log.info("執行execute任務開始,休眠三秒");
Thread.sleep(3000);
System.out.println("=======================業務邏輯1=============================");
Map map = new baseMap();
map.put("orderId", "BJ0001");
rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map);
//延遲10秒發送
map.put("orderId", "NJ0002");
rabbitMqClient.sendMessage(CloudConstant.MQ_JEECG_PLACE_ORDER, map, 10000);
log.info("execute任務結束,休眠三秒");
}
public DemoLockTest() {
}
//@Scheduled(cron = "0/5 * * * * ?")
public void execute2() throws InterruptedException {
if (redissonLock.tryLock(CloudConstant.REDISSON_DEMO_LOCK_KEY2, -1, 6000)) {
log.info("執行任務execute2開始,休眠十秒");
Thread.sleep(10000);
System.out.println("=======================業務邏輯2=============================");
log.info("定時execute2結束,休眠十秒");
redissonLock.unlock(CloudConstant.REDISSON_DEMO_LOCK_KEY2);
} else {
log.info("execute2擷取鎖失敗");
}
}
2.RepeatSubmitAspect
通過公平鎖判斷是否是多次點選按鈕。
@Around("pointCut(jRepeat)")
public Object repeatSubmit(ProceedingJoinPoint joinPoint,JRepeat jRepeat) throws Throwable {
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
if (Objects.nonNull(jRepeat)) {
// 擷取參數
Object[] args = joinPoint.getArgs();
// 進行一些參數的處理,比如擷取訂單号,操作人id等
StringBuffer lockKeyBuffer = new StringBuffer();
String key =getValueBySpEL(jRepeat.lockKey(), parameterNames, args,"RepeatSubmit").get(0);
// 公平加鎖,lockTime後鎖自動釋放
boolean isLocked = false;
try {
isLocked = redissonLockClient.fairLock(key, TimeUnit.SECONDS, jRepeat.lockTime());
// 如果成功擷取到鎖就繼續執行
if (isLocked) {
// 執行程序
return joinPoint.proceed();
} else {
// 未擷取到鎖
throw new Exception("請勿重複送出");
}
} finally {
// 如果鎖還存在,在方法執行完成後,釋放鎖
if (isLocked) {
redissonLockClient.unlock(key);
}
}
}
return joinPoint.proceed();
}
3.DistributedLockHandler
該類主要是jLock的切面類,通過jLock注解參數,判斷需要加鎖的類型,同時加鎖的方法也不相同。
//jLock切面,進行加鎖
@SneakyThrows
@Around("@annotation(jLock)")
public Object around(ProceedingJoinPoint joinPoint, JLock jLock) {
Object obj = null;
log.info("進入RedisLock環繞通知...");
RLock rLock = getLock(joinPoint, jLock);
boolean res = false;
//擷取逾時時間
long expireSeconds = jLock.expireSeconds();
//等待多久,n秒内擷取不到鎖,則直接傳回
long waitTime = jLock.waitTime();
//執行aop
if (rLock != null) {
try {
if (waitTime == -1) {
res = true;
//一直等待加鎖
rLock.lock(expireSeconds, TimeUnit.MILLISECONDS);
} else {
res = rLock.tryLock(waitTime, expireSeconds, TimeUnit.MILLISECONDS);
}
if (res) {
obj = joinPoint.proceed();
} else {
log.error("擷取鎖異常");
}
} finally {
if (res) {
rLock.unlock();
}
}
}
log.info("結束RedisLock環繞通知...");
return obj;
}
//通過參數判斷加鎖類型
@SneakyThrows
private RLock getLock(ProceedingJoinPoint joinPoint, JLock jLock) {
//擷取key
String[] keys = jLock.lockKey();
if (keys.length == 0) {
throw new RuntimeException("keys不能為空");
}
//擷取參數
String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
LockModel lockModel = jLock.lockModel();
if (!lockModel.equals(LockModel.MULTIPLE) && !lockModel.equals(LockModel.REDLOCK) && keys.length > 1) {
throw new RuntimeException("參數有多個,鎖模式為->" + lockModel.name() + ".無法鎖定");
}
RLock rLock = null;
String keyConstant = jLock.keyConstant();
//判斷鎖類型
if (lockModel.equals(LockModel.AUTO)) {
if (keys.length > 1) {
lockModel = LockModel.REDLOCK;
} else {
lockModel = LockModel.REENTRANT;
}
}
//根據不同的鎖類型執行不同的加鎖方式
switch (lockModel) {
case FAIR:
rLock = redissonClient.getFairLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0));
break;
case REDLOCK:
List<rlock> rLocks = new ArrayList<>();
for (String key : keys) {
List<string> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);
for (String s : valueBySpEL) {
rLocks.add(redissonClient.getLock(s));
}
}
RLock[] locks = new RLock[rLocks.size()];
int index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonRedLock(locks);
break;
case MULTIPLE:
rLocks = new ArrayList<>();
for (String key : keys) {
List<string> valueBySpEL = getValueBySpEL(key, parameterNames, args, keyConstant);
for (String s : valueBySpEL) {
rLocks.add(redissonClient.getLock(s));
}
}
locks = new RLock[rLocks.size()];
index = 0;
for (RLock r : rLocks) {
locks[index++] = r;
}
rLock = new RedissonMultiLock(locks);
break;
case REENTRANT:
List<string> valueBySpEL = getValueBySpEL(keys[0], parameterNames, args, keyConstant);
//如果spel表達式是數組或者LIST 則使用紅鎖
if (valueBySpEL.size() == 1) {
rLock = redissonClient.getLock(valueBySpEL.get(0));
} else {
locks = new RLock[valueBySpEL.size()];
index = 0;
for (String s : valueBySpEL) {
locks[index++] = redissonClient.getLock(s);
}
rLock = new RedissonRedLock(locks);
}
break;
case READ:
rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).readLock();
break;
case WRITE:
rLock = redissonClient.getReadWriteLock(getValueBySpEL(keys[0], parameterNames, args, keyConstant).get(0)).writeLock();
break;
}
return rLock;
}
</string></string></string></rlock>
4.RedissonLockClient
redisson用戶端,提供了一大波方法,請自行檢視。
public class RedissonLockClient {
@Autowired
private RedissonClient redissonClient;
@Autowired
private RedisTemplate<string, object=""> redisTemplate;
public RLock getLock(String lockKey) {
return redissonClient.getLock(lockKey);
}
public boolean tryLock(String lockName, long expireSeconds) {
return tryLock(lockName, 0, expireSeconds);
}
</string,>
5.core包
主要通過application.yml配置檔案擷取redis連接配接類型,通過根據該參數動态的選擇政策類,連接配接redis。
public class RedissonManager {
public Redisson getRedisson() {
return redisson;
}
//Redisson連接配接方式配置工廠
static class RedissonConfigFactory {
private RedissonConfigFactory() {
}
private static volatile RedissonConfigFactory factory = null;
public static RedissonConfigFactory getInstance() {
if (factory == null) {
synchronized (Object.class) {
if (factory == null) {
factory = new RedissonConfigFactory();
}
}
}
return factory;
}
//根據連接配接類型創建連接配接方式的配置
Config createConfig(RedissonProperties redissonProperties) {
Preconditions.checkNotNull(redissonProperties);
Preconditions.checkNotNull(redissonProperties.getAddress(), "redis位址未配置");
RedisConnectionType connectionType = redissonProperties.getType();
// 聲明連接配接方式
RedissonConfigStrategy redissonConfigStrategy;
if (connectionType.equals(RedisConnectionType.SENTINEL)) {
redissonConfigStrategy = new SentinelRedissonConfigStrategyImpl();
} else if (connectionType.equals(RedisConnectionType.CLUSTER)) {
redissonConfigStrategy = new ClusterRedissonConfigStrategyImpl();
} else if (connectionType.equals(RedisConnectionType.MASTERSLAVE)) {
redissonConfigStrategy = new MasterslaveRedissonConfigStrategyImpl();
} else {
redissonConfigStrategy = new StandaloneRedissonConfigStrategyImpl();
}
Preconditions.checkNotNull(redissonConfigStrategy, "連接配接方式建立異常");
return redissonConfigStrategy.createRedissonConfig(redissonProperties);
}
}
}
//政策實作,此類是指定redis的連接配接方式是哨兵。
public class SentinelRedissonConfigStrategyImpl implements RedissonConfigStrategy {
@Override
public Config createRedissonConfig(RedissonProperties redissonProperties) {
Config config = new Config();
try {
String address = redissonProperties.getAddress();
String password = redissonProperties.getPassword();
int database = redissonProperties.getDatabase();
String[] addrTokens = address.split(",");
String sentinelAliasName = addrTokens[0];
// 設定redis配置檔案sentinel.conf配置的sentinel别名
config.useSentinelServers().setMasterName(sentinelAliasName);
config.useSentinelServers().setDatabase(database);
if (StringUtils.isNotBlank(password)) {
config.useSentinelServers().setPassword(password);
}
// 設定哨兵節點的服務IP和端口
for (int i = 1; i < addrTokens.length; i++) {
config.useSentinelServers().addSentinelAddress(GlobalConstant.REDIS_CONNECTION_PREFIX+ addrTokens[i]);
}
log.info("初始化哨兵方式Config,redisAddress:" + address);
} catch (Exception e) {
log.error("哨兵Redisson初始化錯誤", e);
e.printStackTrace();
}
return config;
}
}
6.jeecg-cloud-module
這裡詳細的說一下jeecg-cloud-gateway,因為其他的都是開源項目沒下載下傳即用。
jeecg-cloud-system-start為封裝start的使用方法,上文已經介紹了。
1.jeecg-cloud-gateway
1.GatewayRoutersConfiguration
當固定的幾個路由,有特殊化的執行方法。
2.RateLimiterConfiguration
主要配置限流,與application.yml一起使用,下文配置含義是,發送過來的請求隻能容納redis-rate-limiter.burstCapacity的配置(3次)多餘的會全部丢棄(限流),每秒消費redis-rate-limiter.replenishRate(1次)。
3.FallbackController
熔斷的執行方法。
4.GlobalAccessTokenFilter
全局攔截器,在調用其他服務時,将使用者資訊放在請求頭中。
5.SentinelFilterContextConfig
使Sentinel鍊路流控模式生效,固定寫法。
6.HystrixFallbackHandler、SentinelBlockRequestHandler
在降級/限流時,将異常資訊轉換成json傳回給前台。
7.LoderRouderHandler
動态重新整理路由。
8.MySwaggerResourceProvider、SwaggerResourceController
将swagger位址統一管理起來
9.DynamicRouteLoader、DynamicRouteService
DynamicRouteLoader:通過application.yml判斷從nacos/redis中擷取路由資訊,并實作動态的加載。
DynamicRouteService:為底層處理路由的API。
四.總體感想
文章到這裡差不多就接近尾聲了,大多數功能附帶着代碼都講述了一遍。在功能上來說,jeecg提供了很多常用功能,如rabbitMq封裝、積木報表、代碼生成器等。這些在日常工作中有很大的機率碰上,如果有以上需求,可以來架構中直接複制粘貼即可。
但是在格式規範上,如出入參的規範,代碼的寫法,代碼的格式化等方面,并不是特别統一,且沒有嚴格規範。總體來說非常适合做私活與畢業設計,同時也是最早一批開源的前後端項目腳手架,爆贊。